# action/image-build Composite Gitea Action that builds a container image with buildx and optionally runs a smoke test. **Does not push** — pair with `action/image-push` to publish. Splitting build from push lets a PR workflow run `image-build` without push or deploy side effects while `main` runs the full build → push → deploy chain. A PR build that pulls a private base image still needs a registry token limited to the `read:package` capability; public-base builds need no token. The action logs in as `ci-bot`, so the token must be issued to that account. ## Private Go module builds `tools/private-modules/` owns the stdlib-only offline generator and verifier for native Go file-proxy bundles. Consumers export exact committed source bytes so their first check needs no Git, network, module dependency, or credential. Changes belong here; consumers must not edit their generated copies. From this checkout, export a reviewed full commit ID into a service checkout: ```sh python3 tools/export-private-modules.py /path/to/service --revision FULL_COMMIT_ID python3 tools/export-private-modules.py /path/to/service --revision FULL_COMMIT_ID --check ``` The export includes source revision/path/digest metadata and a SHA256 lock. Service Make and Docker builds run `sha256sum -c tools/private-modules.sha256` before `GO111MODULE=off GOTOOLCHAIN=local GOFLAGS= go run tools/private-modules.go`. Reviewers can reproduce `--check` from that pinned source commit. The local hash check detects accidental drift; Git review establishes the trusted source pin. Populate selected private versions through the configured authenticated Go client once, then run the generated command with `-write` to copy unchanged `.info`, `.mod`, and `.zip` cache artifacts into `third_party/go-proxy`. The generator itself enforces offline resolution and a local toolchain. It preserves the complete original archives, including any licenses and notices. Normal Go downloads still validate their content against the consumer's `go.sum`. The generated proxy directory has an exact build-only nested `go.mod` marker. Native Go module packaging excludes that directory from provider releases, so downstream consumers never recursively bundle another provider's build inputs. The checker requires the marker, and a native local-Git archive test proves the exclusion without permitting Git network protocols. Consumer resolution uses `GONOPROXY=none`, `GONOSUMDB=code.fritzlab.net`, `GOFLAGS=-mod=readonly`, `GOTOOLCHAIN=local`, and `GOPROXY=file:///absolute/service/third_party/go-proxy,https://proxy.golang.org`. Run the checker before and after downloads; its exact go.mod/go.sum fingerprint rejects stale dependency closures. Public modules stay on the public proxy. Registry publication remains a separate authenticated operation. ## Usage ```yaml - uses: actions/checkout@v4 - uses: https://code.fritzlab.net/action/image-build@v1 with: image: code.fritzlab.net/fritzlab/chrony smoke-test: docker run --rm --entrypoint /usr/sbin/chronyd $IMAGE -v ``` The image is built and tagged as `:` in the runner's local Docker daemon. Subsequent steps (e.g. `action/image-push`) can reference the same tag. ### Private bases in PRs Pass `token` only when the PR head is trusted, and limit `ci-bot` package access to the private base images that the build requires. Never expose an organization-wide package reader to a contributor-controlled Dockerfile: it can pull and disclose any package that the account can read. ```yaml with: token: ${{ secrets.PACKAGE_READ_TOKEN }} # caller-chosen secret name ``` The token must be issued to `ci-bot` with `read:package` capability. Tokens from other accounts fail because the action's registry username is fixed. Omit the input for public bases. ## Inputs | Name | Required | Default | Description | |---|---|---|---| | `image` | yes | — | Full image name without tag (e.g. `code.fritzlab.net/fritzlab/chrony`). | | `context` | no | `.` | Docker build context. | | `dockerfile` | no | `Dockerfile` (in context) | Path to the Dockerfile, relative to the context (or absolute under `$GITHUB_WORKSPACE`). Use for monorepos where the build context is the repo root but the Dockerfile lives in a subdir, e.g. `dockerfile: api/Dockerfile`. | | `build-args` | no | — | Multiline `KEY=VALUE` build args. Visible in `docker history` — never put secrets here. | | `secrets` | no | — | Multiline `id=VALUE` BuildKit secrets (`--secret`). For tokens the build needs (e.g. a ci-bot token to `go mod download` a private module) that must not leak into layers. Reference with `RUN --mount=type=secret,id=`. | | `smoke-test` | no | — | Shell command run after build. `$IMAGE` is set to `:`. Non-zero exit fails the action. | | `token` | no | — | `ci-bot` access token with `read:package` capability. Required to pull a private base image; omit for public bases. | ## Outputs | Name | Description | |---|---| | `tag` | Numeric tag assigned (= `github.run_number`). | ## Private module contract validation The private-module contract workflow runs its Go race tests in the digest-pinned standard Go compiler image in `tests/private-modules.Dockerfile`. The Fritzlab runner doesn't provide GCC. Only `tools/private-modules` enters that build context; the test runs with networking and module downloads disabled. The public compiler image must already be cached or obtainable through the existing image pull path. This check doesn't publish an image or change runner configuration. ```sh python3 -m unittest discover -s tests docker build --network none --file tests/private-modules.Dockerfile tools/private-modules ``` ## Smoke test patterns Override entrypoint for a binary that expects no args: ```yaml smoke-test: docker run --rm --entrypoint /usr/sbin/chronyd $IMAGE -v ``` Run a help command that returns non-zero: ```yaml smoke-test: docker run --rm $IMAGE --help || true ``` Multiple checks chained: ```yaml smoke-test: | docker run --rm $IMAGE --version docker run --rm --entrypoint /bin/sh $IMAGE -c 'test -x /usr/local/bin/myapp' ```