mirror of
https://github.com/hanzoai/ci.git
synced 2026-10-27 09:19:34 +00:00
A branch build published `sha-<short7>` and nothing else, so an image only earned a semver when a human remembered to cut a git tag. That is why 14 of the fleet's 117 universe pins name a commit instead of a release — not drift, just the only tag CI ever offered them. bin/imgver is that number, derived and never typed, monotonic against two floors: the repo's own manifest (package.json / Cargo.toml / VERSION / pyproject.toml, or an explicit `version:`) and the highest semver already at the registry for that image. max + a patch, so one tag can never come to cover two digests — deriving from the manifest alone re-publishes the same number until someone edits the file, and a node on imagePullPolicy: IfNotPresent never picks up the second one. universe's images.yml learned that rule on iam-secret-sync; this is that rule, for every repo. It is a SCRIPT, not inline shell, because the fleet has two build front doors: this reusable, and the hand-rolled .hanzo/workflows/deploy.yml that 11 repos carry instead of importing it. Both need the identical number, and written twice it would be right twice and then wrong once. .github/actions/imgver is the composite action those 11 call; build.yml calls the same script. Also: the semver tag is proven resolvable before the run goes green (buildx can exit 0 while the manifest is not yet servable, and a pin onto a phantom tag is an ImagePullBackOff), and the run summary prints tag and digest TOGETHER — universe pins repo:tag@digest and the kubelet honours the digest, so a new tag beside an old digest reports the new version while serving the old bytes. 22 cases in bin/imgver_test.sh, wired into hanzo.yml's test gate so it runs. Both build.yml copies (.github/ for GitHub, .hanzo/ for the forge) stay byte-identical. Co-authored-by: Hanzo Dev <[email protected]>
94 lines
4.6 KiB
Bash
Executable File
94 lines
4.6 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# imgver — the version an image build publishes. One implementation, every caller.
|
|
#
|
|
# imgver <image-repo> [context-dir] # e.g. imgver ghcr.io/hanzoai/iam .
|
|
#
|
|
# We don't ship shas. A build that cannot name a version is a broken build, so
|
|
# this exits non-zero rather than letting a caller fall back to sha-<short>.
|
|
#
|
|
# WHY THIS IS A SCRIPT AND NOT INLINE SHELL: the fleet has two build front doors
|
|
# — hanzoai/ci's build.yml (imported by repos with a hanzo.yml) and the
|
|
# hand-rolled .hanzo/workflows/deploy.yml that 11 repos carry instead. Both need
|
|
# the identical number. Written twice it would be right twice and then wrong
|
|
# once, which is how `sha-<short>` became the only tag those 11 repos ever
|
|
# published.
|
|
#
|
|
# THE NUMBER IS DERIVED, NEVER TYPED, and monotonic against two floors:
|
|
# declared — the repo's own manifest (package.json / Cargo.toml / VERSION /
|
|
# pyproject.toml, or $IMGVER_VERSION to name it outright). The
|
|
# human's say: bump the minor there and the series jumps there.
|
|
# published — the highest semver already at the registry for THIS image.
|
|
# max(declared, published) + a patch is what stops one name from ever covering
|
|
# two digests. Deriving from the manifest alone re-publishes the same number on
|
|
# every push until someone edits the file, and a node running
|
|
# imagePullPolicy: IfNotPresent never picks up the second one. universe's
|
|
# images.yml learned that on iam-secret-sync; this is that rule, everywhere.
|
|
#
|
|
# ENV: GH_PAT (or GITHUB_TOKEN) to read the registry floor; IMGVER_PUBLISHED to
|
|
# supply it directly (a registry this cannot read, and the test seam).
|
|
# Without either, only the manifest carries the series.
|
|
set -euo pipefail
|
|
|
|
repo="${1:?usage: imgver <image-repo> [context-dir]}"
|
|
ctx="${2:-.}"
|
|
|
|
semver='^[0-9]+\.[0-9]+\.[0-9]+$'
|
|
|
|
# ---- declared ---------------------------------------------------------------
|
|
declared="${IMGVER_VERSION:-}"
|
|
case "$declared" in
|
|
# The "<file>:<command printing it>" shape hanzo.yml's client lane already uses.
|
|
*:*) declared=$(bash -c "${declared#*:}" 2>/dev/null || true) ;;
|
|
esac
|
|
if [ -z "$declared" ]; then
|
|
# The image's own context first, then the repo root: a monorepo's web/ or api/
|
|
# carries the version of the thing being built, not the workspace stub.
|
|
for d in "$ctx" .; do
|
|
[ -d "$d" ] || continue
|
|
if [ -f "$d/package.json" ]; then
|
|
declared=$(jq -r '.version // ""' "$d/package.json" 2>/dev/null || true)
|
|
elif [ -f "$d/Cargo.toml" ]; then
|
|
declared=$(sed -n '/^\[\(workspace\.\)\?package\]/,/^\[/p' "$d/Cargo.toml" \
|
|
| sed -n 's/^version *= *"\([^"]*\)".*/\1/p' | head -1)
|
|
elif [ -f "$d/VERSION" ]; then
|
|
declared=$(tr -d ' \n' < "$d/VERSION")
|
|
elif [ -f "$d/pyproject.toml" ]; then
|
|
declared=$(sed -n 's/^version *= *"\([^"]*\)".*/\1/p' "$d/pyproject.toml" | head -1)
|
|
fi
|
|
[ -n "$declared" ] && break
|
|
done
|
|
fi
|
|
declared="${declared#v}"
|
|
# A workspace stub (0.0.0) or a placeholder is not a version anyone declared.
|
|
[ "$declared" = "0.0.0" ] && declared=""
|
|
echo "$declared" | grep -qE "$semver" || declared=""
|
|
|
|
# ---- published --------------------------------------------------------------
|
|
# The GitHub Packages API, not the registry v2 tags list: an anonymous ghcr pull
|
|
# token can fetch a manifest by name but returns an EMPTY tag list, so a v2 read
|
|
# would silently report "nothing published" and restart the series at 0.
|
|
published="${IMGVER_PUBLISHED:-}"
|
|
tok="${GH_PAT:-${GITHUB_TOKEN:-}}"
|
|
if [ -z "$published" ] && [ -n "$tok" ] && [ "${repo#ghcr.io/}" != "$repo" ]; then
|
|
org="${repo#ghcr.io/}"; pkg="${org#*/}"; org="${org%%/*}"
|
|
published=$(curl -fsSL -H "Authorization: Bearer $tok" \
|
|
-H "Accept: application/vnd.github+json" \
|
|
"https://api.github.com/orgs/${org}/packages/container/${pkg}/versions?per_page=100" 2>/dev/null \
|
|
| jq -r '.[].metadata.container.tags[]?' 2>/dev/null \
|
|
| sed 's/^v//' | grep -E "$semver" | sort -V | tail -1 || true)
|
|
fi
|
|
|
|
# ---- the number -------------------------------------------------------------
|
|
max=$(printf '%s\n%s\n' "$declared" "$published" | grep -E "$semver" | sort -V | tail -1 || true)
|
|
if [ -z "$max" ]; then
|
|
echo "imgver: no version for $repo. Declare one — a package.json/Cargo.toml/VERSION/pyproject.toml under '$ctx', or IMGVER_VERSION. We don't ship shas." >&2
|
|
exit 1
|
|
elif [ "$max" = "$declared" ] && [ "$declared" != "$published" ]; then
|
|
ver="$declared" # the human bumped it — publish exactly that
|
|
else
|
|
ver="${max%.*}.$(( ${max##*.} + 1 ))" # already out there — next patch
|
|
fi
|
|
|
|
echo "imgver $repo: declared=${declared:-none} published=${published:-none} -> $ver" >&2
|
|
echo "$ver"
|