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]>
96 lines
3.1 KiB
Bash
Executable File
96 lines
3.1 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Decide which images CI should build. Prints GITHUB_OUTPUT lines:
|
|
#
|
|
# 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
|
|
# 2. tag push (<image>/vX.Y.Z) -> the image named in the tag
|
|
# 3. shared build plumbing changed -> every image
|
|
# 4. anything else -> images with changed files
|
|
#
|
|
# When the diff base is unknown (first push, force-push, shallow clone) this builds
|
|
# everything. Rebuilding too much is the safe direction to fail.
|
|
set -euo pipefail
|
|
cd "$(dirname "$0")/.."
|
|
|
|
# A change to any of these can affect how every image is built.
|
|
SHARED_PATHS='^(hack/|Makefile|\.hadolint\.yaml|\.gitea/workflows/)'
|
|
|
|
all_images() {
|
|
local dockerfile
|
|
for dockerfile in images/*/Dockerfile; do
|
|
# Guards against the glob staying literal when there are no images.
|
|
[ -f "$dockerfile" ] || continue
|
|
basename "$(dirname "$dockerfile")"
|
|
done
|
|
}
|
|
|
|
emit() {
|
|
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
|
|
}
|
|
|
|
require_image() {
|
|
[ -f "images/$1/Dockerfile" ] || {
|
|
echo "no such image: images/$1/Dockerfile does not exist" >&2
|
|
exit 1
|
|
}
|
|
}
|
|
|
|
# 1. Explicit request via workflow_dispatch.
|
|
case "${DISPATCH_IMAGE:-}" in
|
|
'') ;;
|
|
all) emit "$(all_images)" 'workflow_dispatch: all' ;;
|
|
*)
|
|
require_image "$DISPATCH_IMAGE"
|
|
emit "$DISPATCH_IMAGE" "workflow_dispatch: $DISPATCH_IMAGE"
|
|
;;
|
|
esac
|
|
|
|
# 2. Release tag: <image>/vX.Y.Z.
|
|
if [ "${GITHUB_REF_TYPE:-}" = tag ]; then
|
|
image=${GITHUB_REF_NAME%/*}
|
|
require_image "$image"
|
|
emit "$image" "tag ${GITHUB_REF_NAME}"
|
|
fi
|
|
|
|
# 3 and 4 both need a usable diff base.
|
|
base=${BASE_SHA:-}
|
|
if [ -z "$base" ] || [[ $base =~ ^0+$ ]] || ! git cat-file -e "${base}^{commit}" 2>/dev/null; then
|
|
emit "$(all_images)" 'diff base unavailable, building everything'
|
|
fi
|
|
|
|
changed=$(git diff --name-only "$base" HEAD)
|
|
|
|
if printf '%s\n' "$changed" | grep -qE "$SHARED_PATHS"; then
|
|
emit "$(all_images)" 'shared build plumbing changed'
|
|
fi
|
|
|
|
# Map changed paths back to image names, dropping any that no longer exist so a
|
|
# deleted image directory does not fail the build.
|
|
selected=$(
|
|
printf '%s\n' "$changed" |
|
|
sed -n 's#^images/\([^/]*\)/.*#\1#p' |
|
|
sort -u |
|
|
while IFS= read -r i; do
|
|
[ -f "images/$i/Dockerfile" ] && echo "$i"
|
|
done
|
|
)
|
|
|
|
emit "$selected" 'changed paths'
|