Add kaniko-build: shared action to build and push container images
All checks were successful
Self-test kaniko-build / build-without-push (push) Successful in 7s

Lets any repository on git.scotty-world.com build a Docker/OCI image from its
checked-out code and push it to the registry it chooses (scotty-git's own
container registry, Artifact Registry, Docker Hub, GHCR, ...).

Kaniko instead of docker build: the runner is a shared, long-lived VM, so
giving workflows the Docker daemon would mean root on that machine and image
layers left on its disk. Kaniko builds inside the step's own container: no
root, no privileged mode, nothing left behind. Kaniko v1.23.2 is pinned by
digest.

The action stores no credentials; each repository passes its own secret.
A self-test workflow builds a small image (without pushing) on every change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Vishnu Prasd P S 2026-10-02 12:39:06 +05:30
commit 21e961d4f0
6 changed files with 293 additions and 0 deletions

View file

@ -0,0 +1,23 @@
name: Self-test kaniko-build
# Builds a tiny image with the action in this repo, without pushing, so a broken change to
# kaniko-build/ shows up here before any user's workflow picks it up.
on:
push:
branches: [main]
pull_request:
workflow_dispatch:
jobs:
build-without-push:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build the test image (no push)
uses: ./kaniko-build
with:
context: kaniko-build/test
dockerfile: kaniko-build/test/Dockerfile
push: "false"
build_args: |
GREETING=selftest

106
README.md Normal file
View file

@ -0,0 +1,106 @@
# scotty-git actions
Shared Forgejo Actions that any repository on **git.scotty-world.com** can use.
| Action | What it does |
|---|---|
| [`kaniko-build`](#kaniko-build) | Build a container image from your repository and push it to a registry |
Actions on this server are referenced by their **full URL**. Short names such as `actions/checkout`
still resolve to `data.forgejo.org`, as usual.
---
## kaniko-build
Builds a Docker/OCI image from the repository your job checked out, using
[Kaniko](https://github.com/GoogleContainerTools/kaniko), and pushes it to the registry you choose.
**Why Kaniko and not `docker build`:** the scotty-git runner is a shared, long-lived machine. Giving
workflows the Docker daemon would hand every workflow root access to that machine and leave image
layers on its disk. Kaniko builds inside the step's own container instead: no root, no privileged
mode, nothing left behind. The build happens the same way as on a GitHub-hosted runner; only the
engine differs.
### Quick start: push to scotty-git's own container registry
No cloud account needed. Create a token at **Settings → Applications** with the
`write:package` scope, and save it in your repository as the secret `REGISTRY_TOKEN`
(**Settings → Actions → Secrets**). Then:
```yaml
# .forgejo/workflows/image.yml
name: Build image
on:
push:
branches: [main]
jobs:
image:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: https://git.scotty-world.com/scotty-git/actions/kaniko-build@v1
with:
destination: git.scotty-world.com/${{ github.repository }}:${{ github.sha }}
username: ${{ github.actor }}
password: ${{ secrets.REGISTRY_TOKEN }}
```
The image appears under your repository's **Packages** tab.
### Google Artifact Registry
Use a service account that can push to **your** repository only (role `Artifact Registry Writer`
granted on that one repository, nothing project-wide). Save its JSON key as a secret in **your**
repository, never at organisation or instance level:
```yaml
- uses: https://git.scotty-world.com/scotty-git/actions/kaniko-build@v1
with:
destination: asia-south1-docker.pkg.dev/my-project/my-repo/my-app:${{ github.sha }}
gcp_key_json: ${{ secrets.GCP_AR_PUSH_KEY }}
cache_repo: asia-south1-docker.pkg.dev/my-project/my-repo/my-app-cache
```
### Docker Hub, GitHub Container Registry, others
```yaml
with:
destination: docker.io/my-user/my-app:1.2.3
username: my-user
password: ${{ secrets.DOCKERHUB_TOKEN }}
```
### Inputs
| Input | Default | Description |
|---|---|---|
| `destination` | | Image reference(s), separated by commas or new lines. Required unless `push` is `false`. |
| `context` | `.` | Build context, relative to the repository root. |
| `dockerfile` | `Dockerfile` | Dockerfile path, relative to the repository root. |
| `push` | `true` | `false` builds without pushing, e.g. to check that a PR still builds. |
| `registry` | host of the first destination | Registry the credentials are for. |
| `username` / `password` | | Registry login. Always pass the password from a secret. |
| `gcp_key_json` | | Google service-account key JSON, instead of username/password. |
| `cache_repo` | | Registry repository for the layer cache. Speeds up rebuilds; stored in the registry, not on the runner. |
| `build_args` | | One `KEY=VALUE` per line. |
| `extra_args` | | Extra Kaniko flags, space-separated (advanced). |
**Output:** `digest`, the sha256 digest of the pushed image.
### Good to know
- **Check out first.** The action builds whatever `actions/checkout` put in the workspace.
- **Credentials stay yours.** This action stores no credentials. Each repository passes its own
secret, and the login exists only inside the step's container for the length of the build.
- **The runner is shared.** Image builds take a few minutes on a small machine and other jobs
queue behind them. Use `cache_repo` to keep rebuilds short.
- **The Kaniko version is pinned** (v1.23.2, by digest) in `kaniko-build/Dockerfile`, so a build
tool update can never change your builds without a reviewed change here.
- **Versions:** use `@v1` to get compatible fixes. Breaking changes get a new major tag.
### Changing this action
Open a pull request. The `Self-test kaniko-build` workflow builds a small test image with your
change before it can affect anyone's workflow.

9
kaniko-build/Dockerfile Normal file
View file

@ -0,0 +1,9 @@
# Kaniko v1.23.2 (the last release of the original Google project), pinned by digest so the
# build tool can never change underneath a workflow. The -debug variant includes a busybox
# shell, which the entrypoint needs for the credential setup.
FROM gcr.io/kaniko-project/executor@sha256:c3109d5926a997b100c4343944e06c6b30a6804b2f9abe0994d3de6ef92b028e
# Kept under /kaniko: Kaniko never overwrites that directory while it unpacks the base image.
COPY entrypoint.sh /kaniko/scotty-git-entrypoint.sh
ENTRYPOINT ["/busybox/sh", "/kaniko/scotty-git-entrypoint.sh"]

68
kaniko-build/action.yml Normal file
View file

@ -0,0 +1,68 @@
name: Kaniko build and push
description: >-
Build a container image from the checked-out repository with Kaniko and push it to any
registry. Needs no Docker daemon, no root on the runner and no privileged mode, and leaves
nothing behind on the runner's disk.
author: scotty-git
inputs:
destination:
description: >-
Image reference(s) to push, separated by commas or new lines,
e.g. "git.scotty-world.com/my-org/my-app:1.2.3". Required unless push is "false".
required: false
default: ""
context:
description: Build context, relative to the repository root.
required: false
default: "."
dockerfile:
description: Path to the Dockerfile, relative to the repository root.
required: false
default: "Dockerfile"
push:
description: '"false" builds the image but does not push it (useful for PR checks).'
required: false
default: "true"
registry:
description: >-
Registry host the credentials below are for, e.g. "git.scotty-world.com",
"asia-south1-docker.pkg.dev", "ghcr.io". Defaults to the host of the first destination.
required: false
default: ""
username:
description: Registry username. Use with password.
required: false
default: ""
password:
description: Registry password or token. Pass it from a secret, never as plain text.
required: false
default: ""
gcp_key_json:
description: >-
Google service-account key JSON for Artifact Registry, instead of username/password.
Pass it from a secret.
required: false
default: ""
cache_repo:
description: >-
Registry repository for Kaniko's layer cache, e.g. "git.scotty-world.com/my-org/my-app-cache".
Makes rebuilds fast without storing anything on the runner. Leave empty for no cache.
required: false
default: ""
build_args:
description: Build arguments, one KEY=VALUE per line.
required: false
default: ""
extra_args:
description: Extra Kaniko executor flags, space-separated (advanced).
required: false
default: ""
outputs:
digest:
description: The sha256 digest of the pushed image (empty when push is "false").
runs:
using: docker
image: Dockerfile

82
kaniko-build/entrypoint.sh Executable file
View file

@ -0,0 +1,82 @@
#!/busybox/sh
# scotty-git shared action: build and push a container image with Kaniko.
# Inputs arrive as INPUT_<NAME> environment variables (see action.yml).
# Never prints credentials. The registry login only exists inside this short-lived container.
set -eu
fail() { printf '::error::%s\n' "$*"; exit 1; }
DESTINATION=${INPUT_DESTINATION:-}
CONTEXT=${INPUT_CONTEXT:-.}
DOCKERFILE=${INPUT_DOCKERFILE:-Dockerfile}
PUSH=${INPUT_PUSH:-true}
REGISTRY=${INPUT_REGISTRY:-}
USERNAME=${INPUT_USERNAME:-}
PASSWORD=${INPUT_PASSWORD:-}
GCP_KEY_JSON=${INPUT_GCP_KEY_JSON:-}
CACHE_REPO=${INPUT_CACHE_REPO:-}
BUILD_ARGS=${INPUT_BUILD_ARGS:-}
EXTRA_ARGS=${INPUT_EXTRA_ARGS:-}
# ---- Source: the repository the job already checked out ----
WS=${GITHUB_WORKSPACE:-}
if [ -z "$WS" ] || [ ! -d "$WS" ]; then fail "No checked-out workspace found. Add 'uses: actions/checkout@v4' before this step."; fi
if [ ! -d "$WS/$CONTEXT" ]; then fail "Build context '$CONTEXT' does not exist in the repository."; fi
if [ ! -f "$WS/$DOCKERFILE" ]; then fail "Dockerfile '$DOCKERFILE' not found in the repository."; fi
# ---- Destinations ----
DESTS=$(printf '%s' "$DESTINATION" | tr ',\n' ' ')
if [ "$PUSH" = "true" ] && [ -z "$(printf '%s' "$DESTS" | tr -d ' ')" ]; then
fail "'destination' is required when push is true."
fi
# ---- Registry login (written only inside this container) ----
mkdir -p /kaniko/.docker
if [ -z "$REGISTRY" ]; then
for d in $DESTS; do REGISTRY=${d%%/*}; break; done
if [ -z "$REGISTRY" ] && [ -n "$CACHE_REPO" ]; then REGISTRY=${CACHE_REPO%%/*}; fi
fi
AUTH=""
if [ -n "$GCP_KEY_JSON" ]; then
AUTH=$(printf '_json_key:%s' "$GCP_KEY_JSON" | base64 | tr -d '\n')
elif [ -n "$USERNAME" ] && [ -n "$PASSWORD" ]; then
AUTH=$(printf '%s:%s' "$USERNAME" "$PASSWORD" | base64 | tr -d '\n')
elif [ -n "$USERNAME" ] || [ -n "$PASSWORD" ]; then
fail "Set both 'username' and 'password', or neither."
fi
if [ -n "$AUTH" ]; then
if [ -z "$REGISTRY" ]; then fail "Could not tell which registry the credentials are for; set 'registry'."; fi
printf '{"auths":{"%s":{"auth":"%s"}}}\n' "$REGISTRY" "$AUTH" > /kaniko/.docker/config.json
echo "Logged in to $REGISTRY for this build."
fi
# ---- Executor flags ----
set -- --context "dir://$WS/$CONTEXT" --dockerfile "$WS/$DOCKERFILE" --cleanup
if [ "$PUSH" = "true" ]; then
for d in $DESTS; do set -- "$@" --destination "$d"; done
set -- "$@" --digest-file /kaniko/digest
else
set -- "$@" --no-push
for d in $DESTS; do set -- "$@" --destination "$d"; done
fi
if [ -n "$CACHE_REPO" ]; then set -- "$@" --cache=true --cache-repo "$CACHE_REPO"; fi
OLDIFS=$IFS
IFS='
'
for a in $BUILD_ARGS; do
if [ -n "$a" ]; then set -- "$@" --build-arg "$a"; fi
done
IFS=$OLDIFS
# shellcheck disable=SC2086 # extra_args is deliberately split into separate flags
if [ -n "$EXTRA_ARGS" ]; then set -- "$@" $EXTRA_ARGS; fi
echo "Building $DOCKERFILE (context: $CONTEXT)$( [ "$PUSH" = "true" ] && echo " -> $DESTS" || echo ", not pushing")"
/kaniko/executor "$@"
# ---- Output ----
if [ "$PUSH" = "true" ] && [ -f /kaniko/digest ]; then
DIGEST=$(cat /kaniko/digest)
echo "Pushed: $DIGEST"
if [ -n "${GITHUB_OUTPUT:-}" ]; then echo "digest=$DIGEST" >> "$GITHUB_OUTPUT"; fi
fi
exit 0

View file

@ -0,0 +1,5 @@
# Tiny image used by the self-test workflow to prove kaniko-build works on the runner.
FROM alpine:3.20
ARG GREETING=hello
RUN echo "$GREETING from kaniko-build" > /built-by-kaniko.txt
CMD ["cat", "/built-by-kaniko.txt"]