From 21e961d4f02674070658df225c26405302d9d2ef Mon Sep 17 00:00:00 2001 From: vishnu-prasad-wdt Date: Fri, 2 Oct 2026 12:39:06 +0530 Subject: [PATCH] Add kaniko-build: shared action to build and push container images 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 --- .forgejo/workflows/selftest.yml | 23 +++++++ README.md | 106 ++++++++++++++++++++++++++++++++ kaniko-build/Dockerfile | 9 +++ kaniko-build/action.yml | 68 ++++++++++++++++++++ kaniko-build/entrypoint.sh | 82 ++++++++++++++++++++++++ kaniko-build/test/Dockerfile | 5 ++ 6 files changed, 293 insertions(+) create mode 100644 .forgejo/workflows/selftest.yml create mode 100644 README.md create mode 100644 kaniko-build/Dockerfile create mode 100644 kaniko-build/action.yml create mode 100755 kaniko-build/entrypoint.sh create mode 100644 kaniko-build/test/Dockerfile diff --git a/.forgejo/workflows/selftest.yml b/.forgejo/workflows/selftest.yml new file mode 100644 index 0000000..07a8d8c --- /dev/null +++ b/.forgejo/workflows/selftest.yml @@ -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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..abff4ed --- /dev/null +++ b/README.md @@ -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. diff --git a/kaniko-build/Dockerfile b/kaniko-build/Dockerfile new file mode 100644 index 0000000..966c6f6 --- /dev/null +++ b/kaniko-build/Dockerfile @@ -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"] diff --git a/kaniko-build/action.yml b/kaniko-build/action.yml new file mode 100644 index 0000000..0056cab --- /dev/null +++ b/kaniko-build/action.yml @@ -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 diff --git a/kaniko-build/entrypoint.sh b/kaniko-build/entrypoint.sh new file mode 100755 index 0000000..ae09a54 --- /dev/null +++ b/kaniko-build/entrypoint.sh @@ -0,0 +1,82 @@ +#!/busybox/sh +# scotty-git shared action: build and push a container image with Kaniko. +# Inputs arrive as INPUT_ 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 diff --git a/kaniko-build/test/Dockerfile b/kaniko-build/test/Dockerfile new file mode 100644 index 0000000..1f09101 --- /dev/null +++ b/kaniko-build/test/Dockerfile @@ -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"]