Readable job names, latest tracks main, and an AGENTS.md

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]>
This commit is contained in:
2026-08-09 12:05:23 -04:00
co-authored by Claude Opus 5
parent 7a81afec65
commit b888f09eb1
8 changed files with 336 additions and 130 deletions
+15 -16
View File
@@ -1,8 +1,15 @@
#!/usr/bin/env bash
# Decide which images CI should build. Prints GITHUB_OUTPUT lines:
#
# images=["node-agent",...] JSON array consumed by the build job's matrix
# any=true|false whether there is anything to build at all
# images=node-agent other-image space-separated, fed straight to build-images.sh
# any=true|false whether there is anything to build at all
#
# Space-separated rather than a JSON matrix on purpose. Gitea resolves a job's matrix
# when it parses the workflow — before `needs` outputs exist — so a dynamic
# `fromJSON(needs...)` matrix yields one job whose name is the raw, uninterpolated
# expression. See jobparser.nameWithMatrix in the Gitea source, and
# https://github.com/go-gitea/gitea/issues/28207. Looping inside one job gives an
# honest job name and collapsible per-image log sections instead.
#
# Rules, first match wins:
# 1. workflow_dispatch naming one image -> that image
@@ -27,21 +34,13 @@ all_images() {
done
}
# Names on stdin -> ["a","b"]. Built by hand so the runner needs no jq.
as_json() {
local out='' name
while IFS= read -r name; do
[ -n "$name" ] || continue
out="${out:+$out,}\"$name\""
done
printf '[%s]' "$out"
}
emit() {
local names=$1 reason=$2 any=false
[ -n "$names" ] && any=true
echo "selected (${reason}): ${names:-<none>}" >&2
printf 'images=%s\n' "$(printf '%s\n' "$names" | as_json)"
local names=$1 reason=$2 any=false flat
# Collapse the newline-separated list onto one line for the workflow output.
flat=$(printf '%s\n' "$names" | tr '\n' ' ' | sed -e 's/ */ /g' -e 's/^ //' -e 's/ $//')
[ -n "$flat" ] && any=true
echo "selected (${reason}): ${flat:-<none>}" >&2
printf 'images=%s\n' "$flat"
printf 'any=%s\n' "$any"
exit 0
}