Container images¶
Two reusable workflows cover container work. reusable-docker-lint-build.yaml lints the
Dockerfile and dry-builds it for pull-request feedback. reusable-docker-publish.yaml
builds an image, pushes it to a registry, and attests its build provenance.
It builds linux/amd64 only unless the caller passes platforms.
Usage¶
name: Release Deliver Docker
on:
release:
types: [published]
permissions:
contents: read
jobs:
publish:
permissions:
contents: read
packages: write
id-token: write
attestations: write
uses: nolte/gh-plumbing/.github/workflows/reusable-docker-publish.yaml@<tag>
with:
image_name: my-service
context: "."
dockerfile: Dockerfile
platforms: linux/amd64,linux/arm64
secrets:
token: ${{ secrets.GITHUB_TOKEN }}
The calling job must grant id-token and attestations
Without them the workflow does not start. It does not fall back to
publishing an image without provenance, and it does not run with a reduced
token: GitHub reports startup_failure with zero jobs.
That failure looks like nothing happened at all, and it happens during a release, so it is easy to miss. Grant both before bumping to a version of the reusable that attests.
Both scopes belong on the job, not on the workflow. id-token is a
signing identity, and widening one beyond the job that needs it is what
spec/project/github-actions-best-practices §H forbids.
Build provenance¶
The publish workflow records how each image was built, using GitHub's own
attestation mechanism through
actions/attest-build-provenance.
The platform generates and signs the record independently of the build.
That independence is the point. A build that attests to its own integrity
can't detect its own compromise, which is why spec/project/continuous-delivery/
§C requires the pipeline rather than the build to produce the record. Earlier
versions of this workflow used BuildKit's provenance output, which is
generated by the build being attested.
Verify a published image with:
For a package that isn't public, the gh login needs the read:packages
scope or the command can't reach the image.
Verify against the tag or the index digest, not a per-architecture one
A multi-architecture build publishes an image index, and every tag points at
that index. The attestation names the index digest, which is what the
workflow also emits as its digest output.
Individual per-architecture manifests inside the index carry their own
digests, and docker buildx imagetools inspect shows them. No attestation
exists for those, so verifying one reports nothing found even though the
image is attested. Integrity still covers them through the index; only
discoverability does not.
An attestation records origin, not safety
It tells you which workflow, commit and runner produced an image. It makes no claim that the image is free of vulnerabilities. Security findings stay with the scanning stages.
Tags¶
The metadata step publishes an immutable type=sha tag and, for releases,
type=semver tags.
It sets latest on two paths, not one: a non-prerelease release publish, and
a workflow_dispatch fired against a non-prerelease type=semver tag ref. The second
path is easy to forget and has already moved latest unexpectedly once, in
nolte/reachy-mini-mcp v0.1.1.
Three of the published tags move and two don't:
| Tag | Moves? |
|---|---|
type=sha |
no |
type=semver |
no |
latest |
yes, on either path above |
type=ref,event=branch |
yes, on every push to that branch |
type=ref,event=pr |
yes, on every push to that pull request |
A moving tag is a convenience alias for humans and never a deployment
reference. Deployments consume the index digest or a type=semver tag.
Dry builds¶
reusable-docker-lint-build.yaml builds without pushing, so it produces no
digest and no attestation. The lint and the build run independently, so one
run reports both a Dockerfile finding and a build failure rather than hiding
the second behind the first.