fix(build): commit embedded artifacts before tagging; alpha never bakes in

Two related fixes to the lockstep release flow + the project invariant
that prod must always run stable bytes (and dev only ever beta-or-stable).

1) tag-after-commit ordering. `./build release X.Y.Z` previously
   regenerated zddc/internal/apps/embedded/* with stable labels but
   tagged BEFORE folding those changes in. The tag landed on the
   source-side commit (alpha-dirty embedded), and the operator was
   expected to commit the embedded changes as a follow-up — which got
   dropped in practice, leaving prod binaries with alpha-dirty bytes
   baked in. (See the v0.0.9 re-anchor in the immediately preceding
   commit for the manifestation.)

   Refactor:
   - _promote_stable / promote_zddc_server in shared/build-lib.sh
     no longer call `git tag`. They keep their pre-flight check
     (now: tag must be in HEAD's history rather than == HEAD, since
     HEAD will advance after the release commit).
   - Top-level ./build adds a new "Release commit + tag" block at
     the end of stable cuts: stages the regenerated embedded files,
     makes a `release: vX.Y.Z lockstep` commit, and tags all seven
     artifacts at the new commit. Idempotent — no commit if there
     are no changes.

2) bake-in invariant. Plain `./build` and `./build alpha` now
   leave zddc/internal/apps/embedded/ untouched — the binary keeps
   shipping whatever the last beta or stable cut wrote. `./build
   beta` and `./build release` are the only paths that update
   embedded bytes. Active dev iteration uses tool/dist/<tool>.html
   directly; the binary's embedded copy is the default fallback,
   not a workbench.

Verification on this commit:
  ./build       → embedded mtime unchanged, no "M" lines for embedded/
  ./build alpha → embedded mtime unchanged, no "M" lines for embedded/

Docs updated to match in CLAUDE.md "Things that bite" + AGENTS.md
"Releasing — lockstep" + the leading help text in ./build itself.
This commit is contained in:
ZDDC 2026-05-03 16:44:39 -05:00
parent b15382ba9d
commit 8dbd002727
4 changed files with 151 additions and 59 deletions

View file

@ -213,12 +213,22 @@ Format: `trackingNumber_revision (status) - title.extension`
| `zddc-server_<X>.html` | generated stub page | per-version / per-channel; lists the four platform downloads. This is what the matrix-cell link points at — one stub fans out to four binaries | | `zddc-server_<X>.html` | generated stub page | per-version / per-channel; lists the four platform downloads. This is what the matrix-cell link points at — one stub fans out to four binaries |
| `index.html` | regenerated by `build.sh` | matrix table, one column per tool, one row per release | | `index.html` | regenerated by `build.sh` | matrix table, one column per tool, one row per release |
**Single point of truth.** `./build release` is the canonical lockstep cut. It seeds `dist/release-output/` from `/srv/zddc/releases/` (so cascades and the verifier see a complete world), forwards each HTML tool's build with the agreed version, then `promote_zddc_server` (in `shared/build-lib.sh`) copies the freshly cross-compiled binaries into `dist/release-output/` with the matching symlink chain, then `write_zddc_server_stubs_all` regenerates every stub page, then `build_releases_index` rewrites the index, then `verify_channel_links` asserts nothing dangles. `./deploy --releases` then publishes the bundle. **Single point of truth.** `./build release` is the canonical lockstep cut. It seeds `dist/release-output/` from `/srv/zddc/releases/` (so cascades and the verifier see a complete world), forwards each HTML tool's build with the agreed version, then `promote_zddc_server` (in `shared/build-lib.sh`) copies the freshly cross-compiled binaries into `dist/release-output/` with the matching symlink chain, then `write_zddc_server_stubs_all` regenerates every stub page, then `build_releases_index` rewrites the index, then `verify_channel_links` asserts nothing dangles. **Then** the top-level build folds the regenerated `zddc/internal/apps/embedded/*` files into a `release: vX.Y.Z lockstep` commit and tags all seven artifacts at that commit. `./deploy --releases` then publishes the bundle.
- **Stable** (`./build release` or `--release X.Y.Z`): Writes per-version HTML for the six HTML tools + per-version binaries for zddc-server (real bytes, immutable). Refreshes 5 symlinks per HTML tool + 5 symlinks per zddc-server platform → the new version. Tags all seven: `<tool>-v<X.Y.Z>`. Cascade: stable cut means beta and alpha both reset to stable for every tool. Skips silently if source for an HTML tool hasn't changed since the latest stable tag (the binary always builds). - **Stable** (`./build release` or `--release X.Y.Z`): Writes per-version HTML for the six HTML tools + per-version binaries for zddc-server (real bytes, immutable). Refreshes 5 symlinks per HTML tool + 5 symlinks per zddc-server platform → the new version. Updates `zddc/internal/apps/embedded/*` to stable-labeled bytes, makes a release commit, tags all seven (`<tool>-v<X.Y.Z>`) **at that commit** so binaries built from the tag embed clean stable bytes. Cascade: stable cut means beta and alpha both reset to stable for every tool.
- **Beta** (`./build beta`): Overwrites `<tool>_beta.html` with dist bytes for each HTML tool, and `zddc-server_beta_<platform>` with each platform's binary. Cascade: `<tool>_alpha.html``<tool>_beta.html` and `zddc-server_alpha_<platform>``zddc-server_beta_<platform>` (symlinks). No tag. - **Beta** (`./build beta`): Overwrites `<tool>_beta.html` with dist bytes for each HTML tool, and `zddc-server_beta_<platform>` with each platform's binary. Updates `zddc/internal/apps/embedded/*` to beta-labeled bytes (the dev image picks them up via `ZDDC_REF=main`). Cascade: `<tool>_alpha.html``<tool>_beta.html` and `zddc-server_alpha_<platform>``zddc-server_beta_<platform>` (symlinks). No tag.
- **Alpha** (`./build`): Overwrites only the alpha mirrors, all seven tools. No tag, no other side-effects. - **Alpha** (`./build alpha`): Overwrites only the alpha mirrors in `dist/release-output/`, all seven tools. **Does NOT update `zddc/internal/apps/embedded/`** — the project invariant is that alpha is never baked into the binary. No tag, no other side-effects.
- **Plain dev builds** (`./build` with no arg): produce `tool/dist/<tool>.html` for HTML tools and `zddc/dist/zddc-server-<platform>` binaries; do NOT touch `dist/release-output/` or the live site. Use it to iterate without affecting deployable state. - **Plain dev builds** (`./build` with no arg): produce `tool/dist/<tool>.html` for HTML tools and `zddc/dist/zddc-server-<platform>` binaries; do NOT touch `dist/release-output/`, the live site, or `embedded/`. Use it to iterate without affecting deployable state.
**Bake-in invariant** — what zddc-server's binary embeds via `//go:embed` from `zddc/internal/apps/embedded/`:
| Image | `ZDDC_REF` | Embeds |
|---|---|---|
| Prod (Dockerfile.prod, BMCD) | `stable` (latest tag) | Stable-labeled bytes from the tagged release commit |
| Dev (Dockerfile, devshell) | `main` | Beta or stable bytes — whatever the last beta/stable cut wrote |
| Local dev iteration | n/a | Use `tool/dist/<tool>.html` directly; binary's embedded copy lags |
**Alpha is never baked in.** Active dev work uses the tool's local dist HTML opened directly in a browser; the binary's embedded copy is the "default fallback" served when no `.zddc apps:` override exists, and only ever holds beta or stable bytes.
On-page `{{BUILD_LABEL}}` format (HTML tools only — zddc-server's version comes from the binary itself): On-page `{{BUILD_LABEL}}` format (HTML tools only — zddc-server's version comes from the binary itself):

View file

@ -65,7 +65,8 @@ No lint/typecheck/format commands exist for the HTML tools — vanilla JS + POSI
- **`dist/` is gitignored.** `tool/dist/<tool>.html` is the canonical built artifact for testing and as the source for `--release` writes. `dist/release-output/` is the local-only release bundle written by `./build alpha|beta|release`. Never hand-edit a `dist/` file. - **`dist/` is gitignored.** `tool/dist/<tool>.html` is the canonical built artifact for testing and as the source for `--release` writes. `dist/release-output/` is the local-only release bundle written by `./build alpha|beta|release`. Never hand-edit a `dist/` file.
- **Build vs deploy are separate verbs.** `./build` and `./build alpha|beta|release` produce artifacts under `dist/release-output/`. Nothing escapes the source tree until the operator runs `./deploy`, which rsyncs into `/srv/zddc/` (Caddy's bind-mount). This decouples local iteration from live state. - **Build vs deploy are separate verbs.** `./build` and `./build alpha|beta|release` produce artifacts under `dist/release-output/`. Nothing escapes the source tree until the operator runs `./deploy`, which rsyncs into `/srv/zddc/` (Caddy's bind-mount). This decouples local iteration from live state.
- **Channel/release cuts seed from live state.** Before running per-tool promote, `./build alpha|beta|release` clears `dist/release-output/` and copies `/srv/zddc/releases/` into it (preserving symlinks). The cut then mutates the channels being cut on top. Result: `dist/release-output/` is always a complete intended-live snapshot, the verifier sees a complete world, and `./deploy --releases` (rsync `--delete-after`) replaces live state cleanly. - **Channel/release cuts seed from live state.** Before running per-tool promote, `./build alpha|beta|release` clears `dist/release-output/` and copies `/srv/zddc/releases/` into it (preserving symlinks). The cut then mutates the channels being cut on top. Result: `dist/release-output/` is always a complete intended-live snapshot, the verifier sees a complete world, and `./deploy --releases` (rsync `--delete-after`) replaces live state cleanly.
- **Lockstep releases.** Every release cut bumps all seven artifacts (6 HTML tools + zddc-server) to the same version, even if a tool didn't change. The coordinated next-stable target is `max(latest tag across all tools) + 1`. Per-tool independent versions are no longer the norm — `./build release` is the canonical path. Workflow: alpha = active dev, beta = ready for general testing, stable = ready to ship. - **Lockstep releases.** Every release cut bumps all seven artifacts (6 HTML tools + zddc-server) to the same version, even if a tool didn't change. The coordinated next-stable target is `max(latest tag across all tools) + 1`. Per-tool independent versions are no longer the norm — `./build release` is the canonical path. Workflow: alpha = active dev, beta = ready for general testing, stable = ready to ship. Stable cuts atomically (1) regenerate `zddc/internal/apps/embedded/` with stable-labeled bytes, (2) make a `release: vX.Y.Z lockstep` commit, (3) tag all seven artifacts at that commit. Tags ALWAYS point at a clean release commit — never at a source-side commit with alpha-dirty embedded files. (Fixed in May 2026; see git log around the v0.0.9 re-anchor.)
- **Bake-in invariant.** What zddc-server's binary embeds via `//go:embed`: prod images (built from `ZDDC_REF=stable`) ship the latest stable cut's bytes. Dev images (built from `ZDDC_REF=main`) ship whatever the last beta-or-stable cut wrote — no alpha. **Alpha is never baked in.** Active dev iteration uses `tool/dist/<tool>.html` opened directly, not the binary's embedded copy. The `./build` (no arg) and `./build alpha` paths intentionally leave `embedded/` untouched.
- **Release artifact layout** (in `dist/release-output/`, mirrored to `/srv/zddc/releases/`). HTML tools: per-version `<tool>_v<X.Y.Z>.html` (real immutable files) + partial-version pins (`<tool>_v<X.Y>.html`, `_v<X>.html`) + channel mirrors (`<tool>_{stable,beta,alpha}.html`) — all symlinks except per-version. zddc-server: `zddc-server_v<X.Y.Z>_<platform>` per-version binaries (raw bytes, no LFS), `_v<X.Y>_<platform>` / `_v<X>_<platform>` / `_<channel>_<platform>` symlinks, plus `zddc-server_<X>.html` stub pages that surface the four platform downloads in one matrix-cell link. Same cascade rule for both: stable cut → beta + alpha both reset to stable; beta cut → alpha cascades to beta. - **Release artifact layout** (in `dist/release-output/`, mirrored to `/srv/zddc/releases/`). HTML tools: per-version `<tool>_v<X.Y.Z>.html` (real immutable files) + partial-version pins (`<tool>_v<X.Y>.html`, `_v<X>.html`) + channel mirrors (`<tool>_{stable,beta,alpha}.html`) — all symlinks except per-version. zddc-server: `zddc-server_v<X.Y.Z>_<platform>` per-version binaries (raw bytes, no LFS), `_v<X.Y>_<platform>` / `_v<X>_<platform>` / `_<channel>_<platform>` symlinks, plus `zddc-server_<X>.html` stub pages that surface the four platform downloads in one matrix-cell link. Same cascade rule for both: stable cut → beta + alpha both reset to stable; beta cut → alpha cascades to beta.
- **No tags for alpha/beta.** Channel URLs are stable URLs by design — appending counter tags would defeat the purpose. The on-page label encodes `<date> · <sha>` for traceability. Stable cuts get clean `<tool>-vX.Y.Z` tags for every tool (six tags per cut, all sharing the same X.Y.Z). - **No tags for alpha/beta.** Channel URLs are stable URLs by design — appending counter tags would defeat the purpose. The on-page label encodes `<date> · <sha>` for traceability. Stable cuts get clean `<tool>-vX.Y.Z` tags for every tool (six tags per cut, all sharing the same X.Y.Z).
- **Pre-release semver in the on-page label.** Plain dev builds and `--release alpha|beta` cuts embed `vX.Y.Z-{alpha,beta}` in `{{BUILD_LABEL}}` where X.Y.Z is the next-stable target. Plain dev adds a full timestamp + `-dirty` marker; `--release alpha|beta` is date-only. - **Pre-release semver in the on-page label.** Plain dev builds and `--release alpha|beta` cuts embed `vX.Y.Z-{alpha,beta}` in `{{BUILD_LABEL}}` where X.Y.Z is the next-stable target. Plain dev adds a full timestamp + `-dirty` marker; `--release alpha|beta` is date-only.

128
build
View file

@ -5,16 +5,29 @@ set -eu
# #
# ./build dev build: assemble tool dist/, cross-compile # ./build dev build: assemble tool dist/, cross-compile
# zddc-server binaries. Nothing else is touched # zddc-server binaries. Nothing else is touched
# — no release artifacts produced, no deploy. # — no release artifacts produced, no deploy,
# and zddc/internal/apps/embedded/ is left alone
# (binary will embed whatever the last beta or
# stable cut committed there).
# ./build alpha cut alpha: produce a complete release bundle # ./build alpha cut alpha: produce a complete release bundle
# in dist/release-output/ (cascades nothing). # in dist/release-output/ (cascades nothing).
# ./build beta cut beta (cascades alpha → beta). # Like dev, embedded/ is NOT updated — the
# invariant is that alpha labels are never baked
# into the binary.
# ./build beta cut beta (cascades alpha → beta). Updates
# embedded/ with beta-labeled tool HTMLs and
# commits them — the dev image (which builds
# from main) ships those bytes.
# ./build release cut coordinated stable (cascades alpha + beta # ./build release cut coordinated stable (cascades alpha + beta
# → new stable; tags all six tools). # → new stable; updates embedded/ with stable
# labels, makes a release commit, tags all
# seven tools at that commit). Prod images
# (which build from the latest stable tag)
# ship those bytes.
# ./build release X.Y.Z same, explicit version. # ./build release X.Y.Z same, explicit version.
# ./build help this message. # ./build help this message.
# #
# Lockstep: every channel/release cut bumps all six tools (5 HTML + # Lockstep: every channel/release cut bumps all seven tools (6 HTML +
# zddc-server) together. Coordinated next-stable = max(latest tag) + 1. # zddc-server) together. Coordinated next-stable = max(latest tag) + 1.
# #
# Channel/release cuts write a complete intended-live snapshot to # Channel/release cuts write a complete intended-live snapshot to
@ -23,6 +36,14 @@ set -eu
# to rsync the snapshot into /srv/zddc/. The snapshot is built by seeding # to rsync the snapshot into /srv/zddc/. The snapshot is built by seeding
# from the current live state (so cascades and the verifier see a # from the current live state (so cascades and the verifier see a
# complete world), then mutating the channel(s) being cut on top. # complete world), then mutating the channel(s) being cut on top.
#
# Bake-in invariant (what zddc-server's binary embeds via //go:embed):
# - prod image (Dockerfile.prod, ZDDC_REF=stable): always stable bytes
# - dev image (Dockerfile, ZDDC_REF=main): stable OR beta bytes
# (whatever last beta/
# stable cut wrote)
# - alpha is NEVER baked in. Active dev iteration happens via the tool's
# local dist/<tool>.html, not via the binary's embedded copy.
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
@ -158,29 +179,33 @@ cp "$SCRIPT_DIR/form/dist/form.html" "$SCRIPT_DIR/zddc/dist/web/
echo "Wrote zddc/dist/web/{index,archive,transmittal,classifier,mdedit,form}.html" echo "Wrote zddc/dist/web/{index,archive,transmittal,classifier,mdedit,form}.html"
# Mirror the five cascade-served HTMLs into the apps embed source dir so the # Mirror the five cascade-served HTMLs into the apps embed source dir so the
# next `go build` of zddc-server picks them up via //go:embed. Files are # next `go build` of zddc-server picks them up via //go:embed. ONLY happens
# checked into git as empty placeholders; the build always overwrites them # on a beta or stable cut — that's the project invariant: alpha labels are
# with the fresh dist/ output. # never baked into the binary, beta labels go to the dev image (which builds
# from main), and stable labels go to prod (which builds from the latest
# stable tag). Plain `./build` and `./build alpha` leave the embedded files
# untouched; whatever the last beta/stable cut committed remains in place.
EMBED_DIR="$SCRIPT_DIR/zddc/internal/apps/embedded" EMBED_DIR="$SCRIPT_DIR/zddc/internal/apps/embedded"
mkdir -p "$EMBED_DIR" if [ "$RELEASE_CHANNEL" = "beta" ] || [ "$RELEASE_CHANNEL" = "stable" ]; then
cp "$SCRIPT_DIR/landing/dist/index.html" "$EMBED_DIR/index.html" mkdir -p "$EMBED_DIR"
cp "$SCRIPT_DIR/archive/dist/archive.html" "$EMBED_DIR/archive.html" cp "$SCRIPT_DIR/landing/dist/index.html" "$EMBED_DIR/index.html"
cp "$SCRIPT_DIR/transmittal/dist/transmittal.html" "$EMBED_DIR/transmittal.html" cp "$SCRIPT_DIR/archive/dist/archive.html" "$EMBED_DIR/archive.html"
cp "$SCRIPT_DIR/classifier/dist/classifier.html" "$EMBED_DIR/classifier.html" cp "$SCRIPT_DIR/transmittal/dist/transmittal.html" "$EMBED_DIR/transmittal.html"
cp "$SCRIPT_DIR/mdedit/dist/mdedit.html" "$EMBED_DIR/mdedit.html" cp "$SCRIPT_DIR/classifier/dist/classifier.html" "$EMBED_DIR/classifier.html"
echo "Populated $EMBED_DIR/ for //go:embed" cp "$SCRIPT_DIR/mdedit/dist/mdedit.html" "$EMBED_DIR/mdedit.html"
echo "Populated $EMBED_DIR/ for //go:embed"
# The form renderer lives next to its handler (no cascade needed — it's a # The form renderer lives next to its handler (no cascade needed — it's a
# fixed renderer, not a per-folder-override tool). # fixed renderer, not a per-folder-override tool).
cp "$SCRIPT_DIR/form/dist/form.html" "$SCRIPT_DIR/zddc/internal/handler/form.html" cp "$SCRIPT_DIR/form/dist/form.html" "$SCRIPT_DIR/zddc/internal/handler/form.html"
echo "Populated zddc/internal/handler/form.html for //go:embed" echo "Populated zddc/internal/handler/form.html for //go:embed"
# Assemble the embedded versions manifest from the per-tool .label sidecars # Assemble the embedded versions manifest from the per-tool .label sidecars
# written by shared/build-lib.sh's compute_build_label. The Go side reads # written by shared/build-lib.sh's compute_build_label. The Go side reads
# this via //go:embed in internal/apps/versions.go and surfaces it in # this via //go:embed in internal/apps/versions.go and surfaces it in
# `zddc-server --version` output and the startup log line. # `zddc-server --version` output and the startup log line.
VERSIONS_FILE="$EMBED_DIR/versions.txt" VERSIONS_FILE="$EMBED_DIR/versions.txt"
{ {
echo "# Generated by build.sh — do not edit. One <app>=<build label> per line." echo "# Generated by build.sh — do not edit. One <app>=<build label> per line."
for _tool in archive transmittal classifier mdedit landing form; do for _tool in archive transmittal classifier mdedit landing form; do
_label_file="$BUILD_LABELS_DIR/${_tool}.label" _label_file="$BUILD_LABELS_DIR/${_tool}.label"
@ -191,8 +216,9 @@ VERSIONS_FILE="$EMBED_DIR/versions.txt"
fi fi
printf '%s=%s\n' "$_tool" "$_label" printf '%s=%s\n' "$_tool" "$_label"
done done
} > "$VERSIONS_FILE" } > "$VERSIONS_FILE"
echo "Wrote $VERSIONS_FILE" echo "Wrote $VERSIONS_FILE"
fi
rm -rf "$BUILD_LABELS_DIR" rm -rf "$BUILD_LABELS_DIR"
# Cross-compiled zddc-server binaries for Linux/macOS/Windows. Always built # Cross-compiled zddc-server binaries for Linux/macOS/Windows. Always built
@ -686,6 +712,56 @@ if [ -n "$RELEASE_CHANNEL" ]; then
verify_channel_links "$RELEASES_DIR" verify_channel_links "$RELEASES_DIR"
fi fi
# --- Release commit + tag (stable cut only) -------------------------------
# On a stable cut, fold the regenerated embedded artifacts into a release
# commit, then place all seven tool tags at that new commit. This is the
# 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
# are still verified to be at HEAD.
if [ "$RELEASE_CHANNEL" = "stable" ]; then
echo ""
echo "=== Release commit + tag ==="
# Stage the artifacts that are part of the release. mdedit's dist
# file is the only force-tracked dist artifact today; the others
# are gitignored and intentionally not committed.
git -C "$SCRIPT_DIR" add "$EMBED_DIR/" \
"$SCRIPT_DIR/zddc/internal/handler/form.html"
if [ -f "$SCRIPT_DIR/mdedit/dist/mdedit.html" ]; then
git -C "$SCRIPT_DIR" add -f "$SCRIPT_DIR/mdedit/dist/mdedit.html"
fi
if ! git -C "$SCRIPT_DIR" diff --cached --quiet; then
git -C "$SCRIPT_DIR" commit -m "release: v${RELEASE_VERSION} lockstep"
echo " release commit created"
else
echo " no embedded changes to commit (re-run on same source state)"
fi
# Tag the seven artifacts at HEAD. Pre-flight already validated that
# any pre-existing tag is in HEAD's history, so this is safe.
_head=$(git -C "$SCRIPT_DIR" rev-parse HEAD)
for _t in archive transmittal classifier mdedit landing form zddc-server; do
_tag="${_t}-v${RELEASE_VERSION}"
if git -C "$SCRIPT_DIR" rev-parse -q --verify "refs/tags/$_tag" >/dev/null; then
_existing=$(git -C "$SCRIPT_DIR" rev-list -n 1 "$_tag")
if [ "$_existing" = "$_head" ]; then
echo " $_tag already at HEAD"
continue
fi
# Pre-flight in promote_release confirmed _existing is an
# ancestor of HEAD. Move it forward to HEAD (the release commit).
git -C "$SCRIPT_DIR" tag -f "$_tag" "$_head" >/dev/null
echo " $_tag advanced to release commit"
else
git -C "$SCRIPT_DIR" tag "$_tag" "$_head"
echo " $_tag created at release commit"
fi
done
fi
echo "" echo ""
echo "=== Build done ===" echo "=== Build done ==="
echo "" echo ""

View file

@ -333,7 +333,12 @@ promote_release() {
esac esac
} }
# Stable cut: per-version file + 5 symlinks + git tag. # Stable cut: per-version file + 5 symlinks. Tagging is centralized in
# the top-level ./build (it commits embedded artifacts FIRST, then tags
# at the new commit — see "Release commit + tag" block at the bottom of
# the script). _promote_stable historically created tags itself, but
# that placed them on the source-side commit before embedded files were
# folded in, leaving prod binaries with alpha-dirty bytes baked in.
_promote_stable() { _promote_stable() {
_t="$1" _t="$1"
_ver="$2" _ver="$2"
@ -358,22 +363,22 @@ _promote_stable() {
echo " $_sym$_versioned" echo " $_sym$_versioned"
done done
# Pre-flight check only: if the tag already exists pointing at a
# commit that is NOT an ancestor of HEAD, the operator needs to
# resolve manually before this cut can complete cleanly.
_tag="${_t}-v${_ver}" _tag="${_t}-v${_ver}"
if git -C "$root_dir" rev-parse -q --verify "refs/tags/$_tag" >/dev/null; then if git -C "$root_dir" rev-parse -q --verify "refs/tags/$_tag" >/dev/null; then
_existing=$(git -C "$root_dir" rev-list -n 1 "$_tag") _existing=$(git -C "$root_dir" rev-list -n 1 "$_tag")
_head=$(git -C "$root_dir" rev-parse HEAD) _head=$(git -C "$root_dir" rev-parse HEAD)
if [ "$_existing" != "$_head" ]; then if [ "$_existing" != "$_head" ] \
echo "promote_release: tag $_tag exists at $_existing but HEAD is $_head" >&2 && ! git -C "$root_dir" merge-base --is-ancestor "$_existing" "$_head"; then
echo "promote_release: tag $_tag exists at $_existing which is not in HEAD's history" >&2
echo " manual intervention required before re-running" >&2
return 1 return 1
fi fi
echo "(tag $_tag already at HEAD)"
else
git -C "$root_dir" tag "$_tag"
echo "tagged $_tag"
fi fi
echo "Released ${_t} v${_ver} (stable)" echo "Released ${_t} v${_ver} (stable; tagging deferred to top-level build)"
echo " publish git tag with: git push origin $_tag"
} }
# Alpha/beta cut: overwrite mutable channel mirror; cascade alpha → beta # Alpha/beta cut: overwrite mutable channel mirror; cascade alpha → beta
@ -596,21 +601,21 @@ promote_zddc_server() {
done done
done done
# Tag the commit so the binary set is reproducible. # Pre-flight tag check only — actual tagging happens in the
# top-level ./build after embedded artifacts are committed.
# See _promote_stable for the same rationale.
_tag="zddc-server-v${_ver}" _tag="zddc-server-v${_ver}"
if git -C "$root_dir" rev-parse -q --verify "refs/tags/$_tag" >/dev/null; then if git -C "$root_dir" rev-parse -q --verify "refs/tags/$_tag" >/dev/null; then
_existing=$(git -C "$root_dir" rev-list -n 1 "$_tag") _existing=$(git -C "$root_dir" rev-list -n 1 "$_tag")
_head=$(git -C "$root_dir" rev-parse HEAD) _head=$(git -C "$root_dir" rev-parse HEAD)
if [ "$_existing" != "$_head" ]; then if [ "$_existing" != "$_head" ] \
echo "promote_zddc_server: tag $_tag exists at $_existing but HEAD is $_head" >&2 && ! git -C "$root_dir" merge-base --is-ancestor "$_existing" "$_head"; then
echo "promote_zddc_server: tag $_tag exists at $_existing which is not in HEAD's history" >&2
echo " manual intervention required before re-running" >&2
return 1 return 1
fi fi
echo "(tag $_tag already at HEAD)"
else
git -C "$root_dir" tag "$_tag"
echo "tagged $_tag"
fi fi
echo "Released zddc-server v${_ver} (stable)" echo "Released zddc-server v${_ver} (stable; tagging deferred to top-level build)"
;; ;;
alpha | beta) alpha | beta)
# Mutable channel mirror per platform; cascade alpha → beta on # Mutable channel mirror per platform; cascade alpha → beta on