fix(build,ci): auto-commit embedded refresh on beta cuts; pin chart to HEAD

Closes the long-standing chart-bump race that required a manual rebase
on every beta cut. Three coordinated changes:

  build (top-level): broaden the existing stable-only "fold embedded
  artifacts into a release commit" block to also fire on beta cuts.
  Same idempotency check; new commit message ("chore(embedded): cut
  v<X.Y.Z>-beta") derived via _coordinated_next_stable. Tagging stays
  stable-only (channels are mutable mirrors and never get tags). Beta
  cuts now produce exactly one commit on main; HEAD always carries
  the bytes the binary will serve.

  shared/build-lib.sh: drop the SHA from alpha/beta channel labels.
  Embedding HEAD's SHA in the bytes the SHA identifies created a
  feedback loop — each auto-commit advanced HEAD, which shifted the
  SHA in the next run's versions.txt, which triggered another
  embedded commit, ad infinitum. Channel labels now read
  "v<X.Y.Z>-<channel> · <date>" — version + date is enough; SHA
  traceability lives in the chart's appVersion (full SHA) and the
  binary's --version output. Plain dev builds keep the timestamp +
  -dirty fingerprint since they don't commit. Stable cuts already
  use a clean version-only label.

  .forgejo/scripts/notify-chart-bump.sh: pin the chart's appVersion
  to `git rev-parse HEAD` instead of the SHA in versions.txt. The
  build's auto-commit now ensures HEAD == "the commit containing the
  embedded bytes the binary will bake," so HEAD is the substantively
  correct anchor. The previous versions.txt read pinned one commit
  too early (the source-side commit, before the embed refresh
  committed) — every beta cut required a manual chart-rebase to
  point at the embed commit. With both halves landed, the cycle is
  zero-touch: ./build beta + git push → auto-bump CI fires → chart
  appVersion at correct SHA → dev image bakes the right bytes.

Verification: ran ./build beta twice on the same source state. First
run produced one commit; second run printed "no embedded changes to
commit (re-run on same source state)" and made no commit. The label
SHA-loop bug is fixed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
ZDDC 2026-05-05 20:48:09 -05:00
parent 4e6473a5cd
commit 8f07b47a0b
3 changed files with 75 additions and 47 deletions

View file

@ -60,37 +60,27 @@ case "$CHANNEL" in
PAT=$(echo "${LATEST_STABLE#zddc-server-v}" | cut -d. -f3) PAT=$(echo "${LATEST_STABLE#zddc-server-v}" | cut -d. -f3)
NEXT_STABLE="$MAJ.$MIN.$((PAT + 1))" NEXT_STABLE="$MAJ.$MIN.$((PAT + 1))"
# Use the SHA baked into the embedded files (third field of # Pin to HEAD. The dev pipeline's Dockerfile fetches this SHA
# versions.txt: "<tool>=<version> · <date> · <sha>"), NOT # via `git fetch --depth=1 origin <sha>` and runs `go build`
# `git rev-parse HEAD`. This matters because `./build beta` # against it; //go:embed at build time bakes whatever
# runs locally at HEAD=N, then the operator commits the # zddc/internal/apps/embedded/* and zddc/internal/handler/{form,
# generated embed files as N+1; the embed label encodes N # tables}.html are at THAT commit.
# while git HEAD on push is N+1. If we used N+1 here, the #
# chart's appVersion (N+1) wouldn't match the build label # Since `./build beta` (build:952-995) now auto-commits the
# users see in the served website (N) — confusing on its # regenerated embedded artifacts before push, HEAD always
# face when triaging "is this image current?". Reading from # contains the bytes the binary will serve. Earlier this script
# versions.txt guarantees they line up. # read the SHA from embedded/versions.txt to keep the served
VERSIONS_FILE="zddc/internal/apps/embedded/versions.txt" # HTML's build label cosmetically matched to the chart's
SHORT_SHA=$(awk -F' · ' '/^[a-z]+=/ { print $NF; exit }' "$VERSIONS_FILE" \ # appVersion — but that read pinned the chart at the source-
| tr -d '[:space:]') # side commit (HEAD-1), which is the commit BEFORE the
if [ -z "$SHORT_SHA" ]; then # embedded refresh. The Dockerfile would then bake the previous
echo "::error::could not parse SHA from $VERSIONS_FILE" >&2 # cut's bytes. Manual chart-rebases were required on every beta
cat "$VERSIONS_FILE" >&2 # cut. HEAD is the right anchor: substantively correct, even
exit 1 # if the build-label SHA in the served HTML is one commit
fi # behind cosmetically (operators triaging "is this image
# Expand the short SHA from versions.txt to its full 40-char # current?" should compare chart appVersion to the running
# form. The chart's downstream Dockerfile fetches the SHA via # binary's `--version` output, not the HTML footer).
# `git fetch --depth=1 origin <sha>` against Forgejo, and FULL_SHA=$(git rev-parse HEAD)
# Forgejo's uploadpack.allowAnySHA1InWant only matches FULL
# SHAs — a 7-char abbreviation returns "couldn't find remote
# ref". Resolving here (where we have the full local clone
# via actions/checkout@v4 fetch-depth: 0) keeps the chart's
# downstream consumers free of git plumbing.
FULL_SHA=$(git rev-parse "$SHORT_SHA" 2>/dev/null)
if [ -z "$FULL_SHA" ]; then
echo "::error::could not resolve $SHORT_SHA to a full SHA via git rev-parse" >&2
exit 1
fi
TARGET_VERSION="${NEXT_STABLE}-beta-${FULL_SHA}" TARGET_VERSION="${NEXT_STABLE}-beta-${FULL_SHA}"
BRANCHES="develop" BRANCHES="develop"
TRIGGER_DESC="ZDDC beta cut" TRIGGER_DESC="ZDDC beta cut"

57
build
View file

@ -949,35 +949,64 @@ if [ -n "$RELEASE_CHANNEL" ]; then
verify_channel_links "$RELEASES_DIR" verify_channel_links "$RELEASES_DIR"
fi fi
# --- Release commit + tag (stable cut only) ------------------------------- # --- Embedded commit (stable + beta cuts) ---------------------------------
# On a stable cut, fold the regenerated embedded artifacts into a release # On both stable and beta cuts, fold the regenerated embedded artifacts
# commit, then place all seven tool tags at that new commit. This is the # into a single commit on main. Two reasons:
# fix for the previous tag-before-commit bug that left tags pointing at
# alpha-dirty source-side commits, baking alpha labels into prod binaries.
# #
# Idempotent: if there are no embedded changes, no commit is made; tags # 1. Stable: the next tag block needs HEAD to point at the bytes the
# are still verified to be at HEAD. # stable binary will serve. Without this commit, tags would land on
if [ "$RELEASE_CHANNEL" = "stable" ]; then # the source-side commit (with alpha-dirty embedded/*) and prod
# images compiled from `git checkout zddc-server-vX.Y.Z` would
# ship alpha bytes. (Original justification — preserved.)
#
# 2. Beta: the dev pipeline pins the chart's appVersion to a SHA
# (.forgejo/scripts/notify-chart-bump.sh reads HEAD). For that
# pin to point at a SHA where embedded/* matches what the binary
# will serve, HEAD has to advance past the source-side commit.
# Without this commit, the chart pin lags one commit and the dev
# image bakes the previous beta cut's bytes — exactly the failure
# mode that required manual chart-rebases on the v0.0.16-beta cuts.
#
# Idempotent: if there are no embedded changes, no commit is made.
if [ "$RELEASE_CHANNEL" = "stable" ] || [ "$RELEASE_CHANNEL" = "beta" ]; then
echo "" echo ""
echo "=== Release commit + tag ===" echo "=== Embedded commit ==="
# Stage the artifacts that are part of the release. dist/ is # Stage the artifacts that are part of the release. dist/ is
# gitignored everywhere — none of the tools' dist/<tool>.html files # gitignored everywhere — none of the tools' dist/<tool>.html files
# are tracked. The release commit only carries the bake-in artifacts # are tracked. The release commit only carries the bake-in artifacts
# that the binary needs at //go:embed time + the embedded form # that the binary needs at //go:embed time + the embedded form +
# template. # tables templates.
git -C "$SCRIPT_DIR" add "$EMBED_DIR/" \ git -C "$SCRIPT_DIR" add "$EMBED_DIR/" \
"$SCRIPT_DIR/zddc/internal/handler/form.html" \ "$SCRIPT_DIR/zddc/internal/handler/form.html" \
"$SCRIPT_DIR/zddc/internal/handler/tables.html" "$SCRIPT_DIR/zddc/internal/handler/tables.html"
if ! git -C "$SCRIPT_DIR" diff --cached --quiet; then if ! git -C "$SCRIPT_DIR" diff --cached --quiet; then
git -C "$SCRIPT_DIR" commit -m "release: v${RELEASE_VERSION} lockstep" if [ "$RELEASE_CHANNEL" = "stable" ]; then
echo " release commit created" git -C "$SCRIPT_DIR" commit -m "release: v${RELEASE_VERSION} lockstep"
else
# Beta cuts don't carry an explicit version — derive the
# next-stable target from the lockstep helper so the commit
# message reflects "cut v<X.Y.Z>-beta" the same way
# versions.txt and the release-output filenames do.
_embed_ver=$(_coordinated_next_stable)
git -C "$SCRIPT_DIR" commit -m "chore(embedded): cut v${_embed_ver}-beta"
fi
echo " embedded commit created at HEAD: $(git -C "$SCRIPT_DIR" rev-parse --short HEAD)"
else else
echo " no embedded changes to commit (re-run on same source state)" echo " no embedded changes to commit (re-run on same source state)"
fi fi
fi
# Tag the seven artifacts at HEAD. Pre-flight already validated that # --- Release tag (stable cut only) ----------------------------------------
# Beta channels never get tags — channel mirrors are by-design moving
# targets. Tags only exist for stable, where they pin a specific
# X.Y.Z to an immutable commit (the release commit produced above).
if [ "$RELEASE_CHANNEL" = "stable" ]; then
echo ""
echo "=== Release tag ==="
# Tag the nine artifacts at HEAD. Pre-flight already validated that
# any pre-existing tag is in HEAD's history, so this is safe. # any pre-existing tag is in HEAD's history, so this is safe.
_head=$(git -C "$SCRIPT_DIR" rev-parse HEAD) _head=$(git -C "$SCRIPT_DIR" rev-parse HEAD)
for _t in archive transmittal classifier mdedit landing form tables browse zddc-server; do for _t in archive transmittal classifier mdedit landing form tables browse zddc-server; do

View file

@ -187,8 +187,17 @@ compute_build_label() {
alpha | beta) alpha | beta)
channel="$_arg" channel="$_arg"
_date=$(date -u +"%Y-%m-%d") _date=$(date -u +"%Y-%m-%d")
_sha=$(git -C "$root_dir" rev-parse --short=7 HEAD 2>/dev/null || echo "unknown") # No SHA in the channel-cut label — the build's auto-commit
build_label="v${_next_stable}-${channel} · ${_date} · ${_sha}" # block (build:971-995) advances HEAD when committing the
# regenerated embedded artifacts, which would shift the SHA
# and trigger the next run to commit again ad infinitum.
# Cosmetic SHA traceability is now via the chart's
# appVersion (full SHA, written by notify-chart-bump.sh on
# push) and the running binary's `--version` output —
# both of which are accurate where the embedded label
# SHA was not (it always pointed at HEAD-before-commit,
# off by one).
build_label="v${_next_stable}-${channel} · ${_date}"
_emit_build_label_sidecar "$_tool" _emit_build_label_sidecar "$_tool"
return 0 return 0
;; ;;