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:
Executable
+121
@@ -0,0 +1,121 @@
|
||||
#!/usr/bin/env bash
|
||||
# Build, smoke-test and — in CI, on a publishing ref — push images.
|
||||
#
|
||||
# Usage: build-images.sh <image>...
|
||||
#
|
||||
# How an image gets tagged is a property of the git ref, not of the image, so one
|
||||
# run treats every image it builds the same way:
|
||||
#
|
||||
# tag <image>/vX.Y.Z -> :vX.Y.Z :vX.Y :vX published release
|
||||
# push to main -> :latest :main-<sha> latest tracks main
|
||||
# pull request -> :pr-<n> built, never pushed
|
||||
# anywhere else -> :dev built, never pushed (local default)
|
||||
#
|
||||
# `latest` has exactly one owner (main), so a release tag and a main build can never
|
||||
# race to define it.
|
||||
#
|
||||
# Uses plain `docker build` rather than buildx with a container driver: it loads
|
||||
# straight into the local image store so the smoke test can run before anything is
|
||||
# published, and it produces a plain manifest with no attestations, which Gitea's
|
||||
# registry rejects.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
REGISTRY=${REGISTRY:-gitea.libretechconsulting.com}
|
||||
NAMESPACE=${NAMESPACE:-rmcguire}
|
||||
|
||||
[ $# -gt 0 ] || {
|
||||
echo "usage: ${0##*/} <image>..." >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
sha=$(git rev-parse --short=7 HEAD)
|
||||
revision=$(git rev-parse HEAD)
|
||||
created=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||
|
||||
push=false
|
||||
if [ "${GITHUB_REF_TYPE:-}" = tag ]; then
|
||||
mode="release"
|
||||
version=${GITHUB_REF_NAME##*/}
|
||||
[[ $version =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]] || {
|
||||
echo "tag '${GITHUB_REF_NAME}' is not of the form <image>/vX.Y.Z" >&2
|
||||
exit 1
|
||||
}
|
||||
push=true
|
||||
elif [ "${GITHUB_EVENT_NAME:-}" = pull_request ]; then
|
||||
mode="pr"
|
||||
version="pr-${PR_NUMBER:-0}"
|
||||
elif [ "${GITHUB_REF_NAME:-}" = main ]; then
|
||||
mode="main"
|
||||
version="main-$sha"
|
||||
push=true
|
||||
else
|
||||
mode="local"
|
||||
version=dev
|
||||
fi
|
||||
|
||||
tags_for() {
|
||||
local repo="$REGISTRY/$NAMESPACE/$1" n
|
||||
case $mode in
|
||||
release)
|
||||
n=${version#v}
|
||||
printf '%s\n' "$repo:$version" "$repo:v${n%.*}" "$repo:v${n%%.*}"
|
||||
;;
|
||||
main) printf '%s\n' "$repo:latest" "$repo:$version" ;;
|
||||
*) printf '%s\n' "$repo:$version" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Collapsible sections in the Actions log; plain headers when run locally.
|
||||
group() {
|
||||
if [ -n "${GITHUB_ACTIONS:-}" ]; then
|
||||
echo "::group::$*"
|
||||
else
|
||||
echo "==> $*"
|
||||
fi
|
||||
}
|
||||
endgroup() {
|
||||
if [ -n "${GITHUB_ACTIONS:-}" ]; then
|
||||
echo "::endgroup::"
|
||||
fi
|
||||
}
|
||||
|
||||
echo "mode=$mode version=$version push=$push images=$*"
|
||||
|
||||
for image in "$@"; do
|
||||
[ -f "images/$image/Dockerfile" ] || {
|
||||
echo "no such image: images/$image/Dockerfile does not exist" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
mapfile -t tags < <(tags_for "$image")
|
||||
tag_args=()
|
||||
for tag in "${tags[@]}"; do tag_args+=(--tag "$tag"); done
|
||||
|
||||
group "build $image"
|
||||
docker build "${tag_args[@]}" \
|
||||
--build-arg "VERSION=$version" \
|
||||
--build-arg "REVISION=$revision" \
|
||||
--build-arg "CREATED=$created" \
|
||||
"images/$image"
|
||||
endgroup
|
||||
|
||||
if [ -x "images/$image/test.sh" ]; then
|
||||
group "test $image"
|
||||
"images/$image/test.sh" "${tags[0]}"
|
||||
endgroup
|
||||
else
|
||||
echo "note: $image has no test.sh, nothing to smoke-test"
|
||||
fi
|
||||
|
||||
if [ "$push" = true ]; then
|
||||
group "push $image"
|
||||
for tag in "${tags[@]}"; do
|
||||
echo "pushing $tag"
|
||||
docker push "$tag"
|
||||
done
|
||||
endgroup
|
||||
fi
|
||||
done
|
||||
|
||||
echo "done: $* ($mode)"
|
||||
@@ -1,54 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Work out how one image should be tagged, and whether it should be published.
|
||||
# Prints GITHUB_OUTPUT lines:
|
||||
#
|
||||
# tags=<multiline> every ref to build, one per line
|
||||
# primary=<ref> the ref the smoke test runs against
|
||||
# version=<string> org.opencontainers.image.version
|
||||
# created=<rfc3339> org.opencontainers.image.created
|
||||
# push=true|false whether these refs get published
|
||||
#
|
||||
# Tag scheme:
|
||||
# tag <image>/vX.Y.Z -> :vX.Y.Z :vX.Y :vX :latest published release
|
||||
# push to main -> :edge :main-<sha> tip of main
|
||||
# pull request -> :pr-<n> built, never published
|
||||
# anything else -> :dev-<sha> built, never published
|
||||
set -euo pipefail
|
||||
|
||||
image=${1:?usage: docker-tags.sh <image>}
|
||||
repo="${REGISTRY:?REGISTRY is not set}/${NAMESPACE:?NAMESPACE is not set}/${image}"
|
||||
|
||||
sha=$(git rev-parse --short=7 HEAD)
|
||||
created=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||
push=false
|
||||
|
||||
if [ "${GITHUB_REF_TYPE:-}" = tag ]; then
|
||||
version=${GITHUB_REF_NAME##*/}
|
||||
[[ $version =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]] || {
|
||||
echo "tag '${GITHUB_REF_NAME}' is not of the form <image>/vX.Y.Z" >&2
|
||||
exit 1
|
||||
}
|
||||
n=${version#v}
|
||||
tags=("$repo:$version" "$repo:v${n%.*}" "$repo:v${n%%.*}" "$repo:latest")
|
||||
push=true
|
||||
elif [ "${GITHUB_EVENT_NAME:-}" = pull_request ]; then
|
||||
version="pr-${PR_NUMBER:-0}"
|
||||
tags=("$repo:$version")
|
||||
elif [ "${GITHUB_REF_NAME:-}" = main ]; then
|
||||
version="edge-$sha"
|
||||
tags=("$repo:edge" "$repo:main-$sha")
|
||||
push=true
|
||||
else
|
||||
version="dev-$sha"
|
||||
tags=("$repo:$version")
|
||||
fi
|
||||
|
||||
printf 'version %s, push %s, tags: %s\n' "$version" "$push" "${tags[*]}" >&2
|
||||
|
||||
echo "tags<<__TAGS__"
|
||||
printf '%s\n' "${tags[@]}"
|
||||
echo "__TAGS__"
|
||||
echo "primary=${tags[0]}"
|
||||
echo "version=$version"
|
||||
echo "created=$created"
|
||||
echo "push=$push"
|
||||
+15
-16
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user