Job names in the Gitea UI rendered as the raw matrix expression
("${{ fromJSON(needs.select.outputs.images) }}"). Gitea resolves a job's matrix
when it PARSES the workflow, before `needs` outputs exist, so matrix.image was
interpolated against an unresolved matrix. There is no way to win with a dynamic
matrix: jobparser.nameWithMatrix interpolates a name containing "${{ }}", and
appends "(<values>)" to one that doesn't — either way the raw expression shows.
See go-gitea/gitea#28207.
So drop the matrix. build.yaml now runs one job, "Build changed images", that
loops over the selected images and emits ::group:: markers, giving a collapsible
section per image. Also: every step has an explicit static name, because Gitea
does not interpolate expressions in names either ("Log in to ${{ env.REGISTRY }}"
rendered literally).
Tag scheme, per review: `latest` now tracks main rather than the newest release,
and `edge` is gone — it's an Alpine/Traefik convention, not a broad standard, and
`main-<sha>` already covers "a specific commit". `latest` has exactly one owner
so a release tag and a main build can't race to define it. Release tags remain
immutable `:vX.Y.Z`/`:vX.Y`/`:vX` for pinning. This also means the ToolHive
manifest's `:latest` resolves as soon as this lands on main, with no release tag
needed first.
hack/docker-tags.sh folded into hack/build-images.sh, which is now the whole
pipeline — tag, build, smoke-test, push — shared by `make build` and CI. It uses
plain `docker build` instead of buildx, dropping setup-buildx-action: it loads
into the local store so the test runs pre-publish, and emits a plain manifest
with no attestations for Gitea's registry.
Verified in a simulated runner (repo in a docker volume, socket mounted, real
runner image): select + build + group markers, all four tag modes, and the
multi-image loop with a scratch second image.
AGENTS.md records the conventions and, importantly, the three Gitea gotchas that
all look fine locally: no bind-mounting the workspace into a sibling container,
no dynamic matrix, no expressions in names.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
images
Custom container images, published to the public
rmcguire registry
on Gitea.
Each image is self-contained in its own directory under images/: a Dockerfile,
a README.md, and an optional test.sh.
Images
| Image | Base | Purpose |
|---|---|---|
node-agent |
node:22-alpine |
Node 22 plus a GNU shell, network and Kubernetes toolchain — a general-purpose command execution environment for AI agents |
Pulling
docker pull gitea.libretechconsulting.com/rmcguire/node-agent:latest
The registry is public, so no pull secret is required.
Tags
| Tag | Points at | Set by |
|---|---|---|
latest |
the current tip of main |
push to main |
main-<sha> |
one specific commit on main |
push to main |
vX.Y.Z / vX.Y / vX |
an immutable released build | tag <image>/vX.Y.Z |
pr-<n> |
a pull request build — built, never published | pull request |
latest has exactly one owner — main — so a release tag and a main build can
never race to define it. Pin to vX.Y.Z when you want a build that will not move.
Releases are cut per image by pushing a tag named <image>/vX.Y.Z, which builds
and publishes that image alone:
git tag node-agent/v1.0.0 && git push origin node-agent/v1.0.0
Base images and package versions are deliberately not pinned — rebuilding is how upstream updates land, and the published tag is what pins things for whoever pulls it.
Adding an image
cp -r template images/my-image
- Edit
images/my-image/Dockerfile, or base it on an image already published here (FROM gitea.libretechconsulting.com/rmcguire/node-agent:latest). - Fill in
images/my-image/README.md. - Extend
images/my-image/test.shto assert whatever the image promises. make build IMAGE=my-image
CI needs no changes — it discovers every directory under images/ that contains
a Dockerfile. See AGENTS.md for the full conventions.
Local development
make help # list targets
make all # everything CI does
make build IMAGE=node-agent # build and smoke-test one image
CI
Two Gitea workflows, both of which just call the make and hack/ entry points
above, so a green make all locally means a green pipeline:
lint.yaml— repository layout,hadolint,shellcheck.build.yaml— works out which images changed, then builds, smoke-tests and (onmainor a release tag) publishes each one. Nothing is pushed until its smoke test passes.