ZDDC/shared/build-lib.sh
ZDDC db68dc9eec
All checks were successful
Notify chart dev on beta cut / notify-chart-dev (push) Successful in 5s
build(channels): correct wordlist path for tool-side build invocations
shared/build-lib.sh is sourced from each tool's build.sh with
root_dir pointing at the tool directory, so paths to other shared/
artifacts have to be \$root_dir/../shared/<file>. The slug helper
incorrectly used \$root_dir/shared/build-words.txt, which only
resolves correctly when the helper is invoked from the repo root
(e.g. via the smoke test `root_dir=\$PWD sh -c …`). In a real
./build run, the helper found the wordlist missing and silently
fell back to the short-SHA — the previous beta cut shipped
"v0.0.17-beta · 2026-05-07 · 1dafc97" instead of the intended
slug.

Fix: align the path with the existing shared/favicon.svg pattern.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 12:12:46 -05:00

793 lines
34 KiB
Bash
Executable file
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/bin/sh
# =============================================================================
# ZDDC shared build helpers — sourced by each tool's build.sh
#
# Usage in a tool build.sh:
# root_dir=$(cd "$(dirname "$0")" && pwd)
# . "$root_dir/../shared/build-lib.sh"
#
# Provides:
# ensure_exists <path> — abort with error if file missing
# concat_files <file ...> — cat each relative path under $root_dir
# build_timestamp — ISO UTC timestamp string, set at source time;
# used as build_label for dev builds
# escape_js_close_tags <in> <out>
# — copy <in> to <out> with all '</' rewritten as
# '<\/' so the HTML parser cannot misread the
# inlined JS as containing a closing </script>.
# The JS engine treats \/ as a regular slash,
# so runtime behaviour is unchanged.
# compute_build_label <tool> [--release [<channel-or-version>]]
# — sets globals: build_label, build_version,
# is_release, is_red, channel.
# See "Channels and release args" below.
# promote_release <tool> — for stable / alpha / beta, copy the dist
# HTML into the release-output bundle
# (default $root_dir/../dist/release-output;
# override $ZDDC_DEPLOY_RELEASES_DIR). Stable
# cuts write the immutable per-version file +
# refresh five symlinks (_v<X.Y>, _v<X>,
# _stable, _beta, _alpha) and tag
# <tool>-v<X.Y.Z>. Alpha/beta cuts
# overwrite the channel mirror in place
# and cascade alpha → beta. No git tags
# for alpha/beta cuts. The bundle is a
# complete intended-live snapshot — the
# top-level ./build seeds it from
# /srv/zddc/releases/ before per-tool
# promote runs, then ./deploy --releases
# rsyncs it back. See ARCHITECTURE.md
# "Channels" for the full table.
#
# Channels and release args:
# <none> dev build, tool/dist/ only, label
# "v<next-stable>-alpha · <ts> · <sha>[-dirty]" (red).
# No release-output side-effect. To produce a deployable
# bundle, re-run with `--release alpha`.
# --release stable, auto-bump patch from latest tag (or 0.0.1).
# Writes per-version file + symlinks; tags vX.Y.Z.
# --release X.Y.Z stable, explicit version.
# --release alpha alpha channel cut at HEAD;
# label "v<next-stable>-alpha · <date> · <sha>" (red).
# Overwrites <tool>_alpha.html. No tag.
# --release beta beta channel; label "v<next-stable>-beta · <date> · <sha>".
# Overwrites <tool>_beta.html. Cascades <tool>_alpha.html
# → <tool>_beta.html (symlink). No tag.
# --release <other> error.
# =============================================================================
# Abort if root_dir is not set by the caller
if [ -z "${root_dir:-}" ]; then
echo "build-lib.sh: root_dir must be set before sourcing this file" >&2
exit 1
fi
# NOTE: there's no Codeberg release-asset publication path anymore. All
# release artifacts (HTML tools + zddc-server binaries) materialize in
# dist/release-output/ via the lockstep ./build, then ./deploy rsyncs
# them to /srv/zddc/ on the deploy host. The deprecated zddc/release.sh
# is now a no-op guard that prints a redirection message.
# Fail hard on any missing source file
ensure_exists() {
_path="$1"
if [ ! -f "$_path" ]; then
echo "error: missing file: $_path" >&2
exit 1
fi
}
# Concatenate files listed as positional args, each relative to root_dir
concat_files() {
for _rel do
ensure_exists "$root_dir/$_rel"
cat "$root_dir/$_rel"
printf '\n'
done
}
# ISO UTC build timestamp — set once when this file is sourced
build_timestamp=$(date -u +"%Y-%m-%d %H:%M:%S")
# Read shared/favicon.svg, base64-encode it, and assemble a data: URI suitable
# for inlining into a <link rel="icon" type="image/svg+xml" href="..."> tag.
# Set once at source time so every tool's build.sh can pick it up via the
# $favicon_data_uri global. Editing shared/favicon.svg auto-propagates to all
# tools on the next build.
_favicon_path="$root_dir/../shared/favicon.svg"
if [ -f "$_favicon_path" ]; then
favicon_data_uri="data:image/svg+xml;base64,$(base64 -w 0 "$_favicon_path")"
else
favicon_data_uri=""
fi
# Rewrite '</script' (case-insensitive) in JS as '<\/script' so the HTML parser
# cannot mistake string contents for a closing </script> tag. Per the HTML5 spec
# only </script terminates a <script> block — other tags like </div> are safe
# inside a script's text content. Narrowly targeting </script avoids corrupting
# regex literals like /</g whose trailing letter is a flag, not a tag name.
# The JS engine treats '\/' the same as '/' inside a string, so behaviour is
# unchanged. See ARCHITECTURE.md "HTML Embedding Safety".
escape_js_close_tags() {
sed 's#</\([sS][cC][rR][iI][pP][tT]\)#<\\/\1#g' "$1" > "$2"
}
# Validate that $1 is a strict X.Y.Z numeric version, where each component
# is a non-empty numeric string. Exits with an error if not.
_validate_semver() {
_v="$1"
_bad() {
echo "error: invalid release argument: '$_v' (expected: alpha, beta, or X.Y.Z stable version)" >&2
exit 1
}
_v1="${_v%%.*}"
_rest="${_v#*.}"
[ "$_rest" = "$_v" ] && _bad
_v2="${_rest%%.*}"
_v3="${_rest#*.}"
{ [ "$_v3" = "$_rest" ] || [ "$_v3" != "${_v3%.*}" ]; } && _bad
case "$_v1" in '' | *[!0-9]*) _bad ;; esac
case "$_v2" in '' | *[!0-9]*) _bad ;; esac
case "$_v3" in '' | *[!0-9]*) _bad ;; esac
}
# Walk backwards from HEAD until a non-auto-commit is found, return the
# resolved git ref (e.g. "HEAD~2"). Auto-commits are recognised by their
# canonical commit-message prefixes:
#
# - "chore(embedded): cut v<X.Y.Z>-beta" (beta auto-commit, build:993)
# - "release: v<X.Y.Z> lockstep" (stable auto-commit, build:986)
#
# Used by the build-label helpers to derive a stable identifier (short
# SHA, three-word slug) from the underlying source state. The source
# ref is invariant across the auto-commit step (HEAD shifts when
# embedded bytes change), so a re-run on the same source state produces
# the same identifiers and no spurious commit.
#
# Defensive cap: stops walking after 32 commits and returns whatever
# ref was reached.
_source_commit_ref() {
_i=0
_ref="HEAD"
while [ "$_i" -lt 32 ]; do
_msg=$(git -C "$root_dir" log -1 --format=%s "$_ref" 2>/dev/null || echo "")
case "$_msg" in
"chore(embedded): cut v"* | "release: v"*" lockstep")
_ref="${_ref}~1"
_i=$((_i + 1))
continue
;;
esac
break
done
echo "$_ref"
}
# Three-word slug derived deterministically from the source commit's
# full SHA. Same source state → same slug; survives embedded auto-
# commits by deferring to _source_commit_ref.
#
# Three 4-hex-char chunks of the SHA are taken modulo the wordlist
# length (shared/build-words.txt). 283 words × 3 = 22M combinations,
# enough that a tester eyeballing two beta builds can tell at a glance
# whether they're running the same source.
#
# Format: "word-word-word" (lowercase, hyphen-separated).
_source_commit_slug() {
_ref=$(_source_commit_ref)
_full_sha=$(git -C "$root_dir" rev-parse "$_ref" 2>/dev/null || echo "0000000000000000")
# build-lib.sh is sourced from each tool's build.sh as
# "$root_dir/../shared/build-lib.sh"; the wordlist lives next to
# this file in the same shared/ directory.
_words_file="$root_dir/../shared/build-words.txt"
if [ ! -f "$_words_file" ]; then
# Fall back to plain short SHA if the wordlist is missing
# (e.g. running from a checkout that predates this feature).
git -C "$root_dir" rev-parse --short=7 "$_ref" 2>/dev/null || echo "unknown"
return
fi
_wc=$(awk 'END{print NR}' "$_words_file")
_h1=$(echo "$_full_sha" | cut -c 1-4)
_h2=$(echo "$_full_sha" | cut -c 5-8)
_h3=$(echo "$_full_sha" | cut -c 9-12)
_n1=$(( (0x$_h1 % _wc) + 1 ))
_n2=$(( (0x$_h2 % _wc) + 1 ))
_n3=$(( (0x$_h3 % _wc) + 1 ))
_w1=$(awk -v n="$_n1" 'NR==n{print; exit}' "$_words_file")
_w2=$(awk -v n="$_n2" 'NR==n{print; exit}' "$_words_file")
_w3=$(awk -v n="$_n3" 'NR==n{print; exit}' "$_words_file")
echo "${_w1}-${_w2}-${_w3}"
}
# Compute build label and channel. Reads positional args:
# compute_build_label <tool_name> [--release [<channel-or-version>]]
# Sets global variables:
# build_label — text rendered into the page's {{BUILD_LABEL}} slot
# build_version — bare semver string (stable releases only)
# is_release — "1" for any --release invocation, else "0"
# is_red — "1" if the label should render red+bold (dev/alpha/beta), else "0"
# channel — "stable" / "alpha" / "beta" / "" (dev)
#
# Versioning: pre-release semver. The next-stable target is computed from
# the latest clean tool-vX.Y.Z tag (patch-bump). Plain builds and
# `--release alpha`/`--release beta` carry the next-stable target as a
# pre-release suffix in the on-page label so users can see which stable
# the alpha/beta is working toward. Stable releases write a clean
# vX.Y.Z label and tag.
#
# HTML tools do NOT tag alpha/beta cuts (consistent with current
# behavior — alpha and beta artifacts are mutable files, not immutable
# per-build snapshots). The label distinguishes plain dev builds from
# explicit channel cuts via the timestamp granularity (full ts + dirty
# marker for plain builds vs. date-only for `--release alpha|beta`).
compute_build_label() {
_tool="$1"
_flag="${2:-}"
_arg="${3:-}"
is_release=0
is_red=1
channel=""
build_version=""
# Compute the next-stable target once for label inclusion.
_next_stable=$(_next_stable_for_tool "$_tool")
if [ "$_flag" != "--release" ]; then
# Plain builds are dev builds — labeled as the alpha channel because
# that's what the next formal cut would produce, but no Codeberg upload
# happens until `--release alpha` is invoked. Full timestamp (granular
# than date) and -dirty marker distinguish iterative dev builds from
# formal `--release alpha` cuts (which stamp date-only).
_sha=$(git -C "$root_dir" rev-parse --short=7 HEAD 2>/dev/null || echo "unknown")
if ! git -C "$root_dir" diff --quiet HEAD 2>/dev/null; then
_sha="${_sha}-dirty"
fi
channel="alpha"
build_label="v${_next_stable}-alpha · ${build_timestamp} · ${_sha}"
_emit_build_label_sidecar "$_tool"
return 0
fi
is_release=1
case "$_arg" in
alpha | beta)
channel="$_arg"
_date=$(date -u +"%Y-%m-%d")
# Three-word slug derived from the *source* SHA — see
# _source_commit_slug. Two builds carrying the same slug
# were cut from the same source state, so a tester can
# glance at the on-page label and confirm "yes, this is
# the cobalt-otter-meadow build I was emailed about" without
# having to look up a SHA. The slug is invariant across
# the embedded auto-commit step (build:971-995) so a re-cut
# on unchanged source produces the same slug, no spurious
# commit. Full source SHA is available via the binary's
# `--version` output and the chart appVersion for any case
# where exact provenance matters.
_slug=$(_source_commit_slug)
build_label="v${_next_stable}-${channel} · ${_date} · ${_slug}"
_emit_build_label_sidecar "$_tool"
return 0
;;
'')
# Stable cut, auto-bump patch.
build_version="$_next_stable"
;;
*)
_validate_semver "$_arg"
build_version="$_arg"
;;
esac
channel="stable"
is_red=0
build_label="v${build_version}"
_emit_build_label_sidecar "$_tool"
}
# Write the resolved build label to a sidecar file the top-level build.sh
# reads to assemble zddc/internal/apps/embedded/versions.txt. No-op when
# BUILD_LABELS_DIR is not set in the env (tools built standalone).
_emit_build_label_sidecar() {
if [ -z "${BUILD_LABELS_DIR:-}" ]; then
return 0
fi
mkdir -p "$BUILD_LABELS_DIR"
printf '%s\n' "$build_label" > "$BUILD_LABELS_DIR/$1.label"
}
# Tools that participate in the lockstep release. Source of truth — used
# by helpers that enumerate "all release artifacts" (matrix render,
# coordinated next-stable, channel-link verifier).
ZDDC_RELEASE_TOOLS="archive transmittal classifier mdedit landing form tables browse zddc-server"
# Compute the next-stable target for a single tool — patch-bump of its own
# latest <tool>-vX.Y.Z tag. Used by compute_build_label so a tool's
# alpha/beta on-page label still reads against its own history (e.g. an
# alpha cut for a tool that's been quiet still labels itself targeting that
# tool's next stable, even when the lockstep convention is in force).
_next_stable_for_tool() {
_t="$1"
_latest=$(git -C "$root_dir" tag --list "${_t}-v*" 2>/dev/null \
| grep -E "^${_t}-v[0-9]+\.[0-9]+\.[0-9]+\$" \
| sed "s|^${_t}-v||" \
| sort -V \
| tail -1)
[ -n "$_latest" ] || _latest="0.0.0"
_major="${_latest%%.*}"
_rest="${_latest#*.}"
_minor="${_rest%%.*}"
_patch="${_rest#*.}"
echo "${_major}.${_minor}.$((_patch + 1))"
}
# Compute the coordinated next-stable target across every release artifact
# (5 HTML tools + zddc-server). Used by the top-level build.sh on
# `--release` (no explicit version) to enforce lockstep — every tool cuts
# at the same version even if it hasn't changed. Picks max(latest tag
# across all tools) + patch bump, so a tool at v0.0.2 jumps straight to
# wherever the leader is + 1 the first time the lockstep rule fires.
_coordinated_next_stable() {
_max="0.0.0"
for _t in $ZDDC_RELEASE_TOOLS; do
_latest=$(git -C "$root_dir" tag --list "${_t}-v*" 2>/dev/null \
| grep -E "^${_t}-v[0-9]+\.[0-9]+\.[0-9]+\$" \
| sed "s|^${_t}-v||" \
| sort -V \
| tail -1)
[ -n "$_latest" ] || continue
# sort -V picks the larger of two semvers
_max=$(printf '%s\n%s\n' "$_max" "$_latest" | sort -V | tail -1)
done
_major="${_max%%.*}"
_rest="${_max#*.}"
_minor="${_rest%%.*}"
_patch="${_rest#*.}"
echo "${_major}.${_minor}.$((_patch + 1))"
}
# Promote a built dist file to the release-output bundle. Reads from caller
# scope: $channel ("stable" / "alpha" / "beta"), $build_version (stable only),
# $output_html, $root_dir. Bundle path resolves from $ZDDC_DEPLOY_RELEASES_DIR
# (default $root_dir/../dist/release-output).
#
# Stable cuts:
# 1. Skip if source unchanged since latest stable tag.
# 2. Copy dist HTML → <bundle>/<tool>_v<X.Y.Z>.html (immutable).
# 3. Refresh symlinks: _v<X.Y>, _v<X>, _stable, _beta, _alpha all → the
# new versioned file. Cascade rule: stable cut means beta and alpha
# reset to stable (no active dev on either downstream channel).
# 4. Tag the commit <tool>-v<X.Y.Z>.
#
# Alpha/beta cuts:
# 1. Overwrite <bundle>/<tool>_<channel>.html with dist HTML
# (replaces a symlink with real bytes if one was there).
# 2. For beta: cascade <tool>_alpha.html → <tool>_beta.html (symlink),
# since alpha defaults to beta when no active alpha.
# 3. No tag — channel URLs are stable URLs by design; counters defeat
# that. The on-page label encodes <date> · <sha> for traceability.
#
# Plain dev builds (no --release): never call promote_release.
#
# No Codeberg upload — HTML tools live in git. zddc-server's release.sh
# handles binary uploads to Codeberg directly (different distribution model).
promote_release() {
_tool="$1"
# The top-level `./build` exports $ZDDC_DEPLOY_RELEASES_DIR pointing
# at $SCRIPT_DIR/dist/release-output. Single-tool standalone
# invocations fall back to the same default — no inheritance from a
# parent build run.
_releases_dir="${ZDDC_DEPLOY_RELEASES_DIR:-$root_dir/../dist/release-output}"
mkdir -p "$_releases_dir"
if [ ! -d "$_releases_dir" ]; then
echo "promote_release: $_releases_dir not found" >&2
return 1
fi
case "$channel" in
stable)
if [ -z "$build_version" ]; then
echo "promote_release: stable channel but no build_version" >&2
return 1
fi
# Lockstep: every release cut writes per-version files for
# every tool, even when a tool's source hasn't changed since
# its last tag. The bytes are identical (build is deterministic
# at the same source), so the overwrite is a no-op on disk;
# but the symlink chain (_v<X.Y>, _v<X>, _stable, _beta, _alpha)
# gets advanced to the new version, which is the actual goal.
#
# The previous "skip if no source changes since $_latest" check
# was a relic of per-tool independent versioning. It broke
# CI re-cuts at a tag commit (HEAD == latest tag → diff empty
# → skip → dist/release-output/ stays seeded at the previous
# version → deploy publishes the previous version).
_promote_stable "$_tool" "$build_version" "$_releases_dir"
;;
alpha | beta)
_promote_channel "$_tool" "$channel" "$_releases_dir"
;;
*)
echo "promote_release: unknown channel '$channel'" >&2
return 1
;;
esac
}
# 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() {
_t="$1"
_ver="$2"
_rdir="$3"
_major="${_ver%%.*}"
_rest="${_ver#*.}"
_minor="${_rest%%.*}"
_versioned="${_t}_v${_ver}.html"
cp "$output_html" "$_rdir/$_versioned"
echo "Wrote $_rdir/$_versioned"
# Refresh the 5 symlinks. Cascade: stable cut → beta + alpha both
# reset to stable (no active dev on either downstream channel).
for _sym in "${_t}_v${_major}.${_minor}.html" \
"${_t}_v${_major}.html" \
"${_t}_stable.html" \
"${_t}_beta.html" \
"${_t}_alpha.html"; do
ln -sfn "$_versioned" "$_rdir/$_sym"
echo " $_sym$_versioned"
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}"
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")
_head=$(git -C "$root_dir" rev-parse HEAD)
if [ "$_existing" != "$_head" ] \
&& ! 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
fi
fi
echo "Released ${_t} v${_ver} (stable; tagging deferred to top-level build)"
}
# Alpha/beta cut: overwrite mutable channel mirror; cascade alpha → beta
# on a beta cut (alpha defaults to beta when no active alpha).
_promote_channel() {
_t="$1"
_ch="$2"
_rdir="$3"
_file="${_t}_${_ch}.html"
# Replace symlink (if present) with real bytes by removing first;
# cp -f follows symlinks and would overwrite the symlink target.
rm -f "$_rdir/$_file"
cp "$output_html" "$_rdir/$_file"
echo "Wrote $_rdir/$_file"
if [ "$_ch" = "beta" ]; then
ln -sfn "$_file" "$_rdir/${_t}_alpha.html"
echo " ${_t}_alpha.html → $_file"
fi
echo "Released ${_t} ${_ch}"
}
# Platforms zddc-server is cross-compiled for. The first three are
# extension-less (Linux/macOS); Windows gets .exe. The build always emits
# all four; the matrix cell's stub page links each by its <platform> tag.
ZDDC_SERVER_PLATFORMS="linux-amd64 darwin-amd64 darwin-arm64 windows-amd64"
# Display label for the stub-page download list. Keeps the binary-asset
# names canonical even if we later add e.g. linux-arm64.
_zddc_server_platform_label() {
case "$1" in
linux-amd64) echo "Linux (x86_64)" ;;
darwin-amd64) echo "macOS (Intel)" ;;
darwin-arm64) echo "macOS (Apple Silicon)" ;;
windows-amd64) echo "Windows (x86_64)" ;;
*) echo "$1" ;;
esac
}
# Resolve a zddc-server binary's filename for one (version, platform).
# Returns the bare name (no path); ".exe" suffix on windows.
_zddc_server_binary_name() {
_ver_or_chan="$1"
_plat="$2"
_suffix=""
case "$_plat" in *windows*) _suffix=".exe" ;; esac
if echo "$_ver_or_chan" | grep -qE '^v[0-9]'; then
# Per-version asset, e.g. zddc-server_v0.0.8_linux-amd64
printf 'zddc-server_%s_%s%s' "$_ver_or_chan" "$_plat" "$_suffix"
else
# Channel mirror, e.g. zddc-server_stable_linux-amd64
printf 'zddc-server_%s_%s%s' "$_ver_or_chan" "$_plat" "$_suffix"
fi
}
# Write the small HTML index page that becomes the matrix cell's link for
# a zddc-server release. Lists each platform binary with a download link.
# $1 — release directory (absolute)
# $2 — slug (e.g. v0.0.8, v0.0, stable, beta, alpha)
# $3 — display label (e.g. "v0.0.8", "stable channel")
write_zddc_server_stub() {
_rdir="$1"
_slug="$2"
_label="$3"
_out="$_rdir/zddc-server_${_slug}.html"
{
cat <<HEAD
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>zddc-server ${_label} — ZDDC</title>
<link rel="stylesheet" href="../css/style.css">
<style>
.dl-table { width: 100%; border-collapse: collapse; margin: 1rem 0; }
.dl-table th, .dl-table td { text-align: left; padding: 0.5rem 0.75rem; border-bottom: 1px solid var(--color-border); }
.dl-table a { color: var(--color-primary); text-decoration: none; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0.9rem; }
.dl-table a:hover { text-decoration: underline; }
.breadcrumb { color: var(--color-text-muted); margin-bottom: 1rem; font-size: 0.9rem; }
.breadcrumb a { color: var(--color-text-muted); }
</style>
</head>
<body>
<main class="container" style="max-width: 720px; margin: 2rem auto;">
<p class="breadcrumb"><a href="/">home</a> &nbsp;/&nbsp; <a href="index.html">releases</a> &nbsp;/&nbsp; zddc-server ${_label}</p>
<h1>zddc-server &mdash; ${_label}</h1>
<p>Cross-compiled binaries. Download for your platform, mark executable, and run with <code>ZDDC_ROOT=/path/to/archive ./zddc-server</code>.</p>
<table class="dl-table">
<thead><tr><th>Platform</th><th>Download</th></tr></thead>
<tbody>
HEAD
for _plat in $ZDDC_SERVER_PLATFORMS; do
_bin=$(_zddc_server_binary_name "$_slug" "$_plat")
_plabel=$(_zddc_server_platform_label "$_plat")
printf ' <tr><td>%s</td><td><a href="%s">%s</a></td></tr>\n' "$_plabel" "$_bin" "$_bin"
done
cat <<'TAIL'
</tbody>
</table>
<p style="font-size: 0.9rem; color: var(--color-text-muted);">Need a different platform? Build from source: <code>(cd zddc &amp;&amp; go build -o zddc-server ./cmd/zddc-server)</code> from the repo at the matching tag.</p>
</main>
</body>
</html>
TAIL
} > "$_out"
}
# Refresh every zddc-server stub page based on what's currently in the
# release-output bundle. Driven by the existing per-version binary files +
# symlinks that the release flow already maintains; just emits the HTML
# wrappers for them. Safe to run on every cut (idempotent).
#
# $1 — releases dir (absolute)
write_zddc_server_stubs_all() {
_rdir="$1"
# Every per-version stable binary that exists. We index off
# linux-amd64 specifically since all four platforms ship in lockstep
# — if the linux build is missing the version is incomplete anyway.
for _bin in "$_rdir"/zddc-server_v*_linux-amd64; do
[ -e "$_bin" ] || continue
_name=$(basename "$_bin")
# zddc-server_vX.Y.Z_linux-amd64 → vX.Y.Z
_slug=$(echo "$_name" | sed -E 's/^zddc-server_(v[^_]+)_linux-amd64$/\1/')
# Skip partial-version pins (vX.Y, vX) — these are written
# separately below from symlink resolution.
case "$_slug" in
v*.*.*) write_zddc_server_stub "$_rdir" "$_slug" "$_slug" ;;
esac
done
# Partial-version + channel stubs follow the symlink chain. If the
# symlink resolves to a real binary, write the stub; otherwise skip.
for _slug in stable beta alpha; do
_probe="$_rdir/zddc-server_${_slug}_linux-amd64"
if [ -e "$_probe" ]; then
write_zddc_server_stub "$_rdir" "$_slug" "${_slug} channel"
fi
done
# vX.Y and vX partial pins — derive the slug list from the per-version
# binaries so we only emit pages we actually have artifacts for.
_all_versions=$(find "$_rdir" -maxdepth 1 -name 'zddc-server_v*_linux-amd64' \
| sed -E 's|^.*/zddc-server_(v[0-9]+\.[0-9]+\.[0-9]+)_linux-amd64$|\1|' \
| sort -Vu)
if [ -n "$_all_versions" ]; then
# vX.Y pins — pick the highest patch within each X.Y, then make
# sure the symlink and stub exist.
echo "$_all_versions" | sed -E 's|^v([0-9]+\.[0-9]+)\.[0-9]+$|\1|' | sort -Vu | while read -r _xy; do
_probe="$_rdir/zddc-server_v${_xy}_linux-amd64"
if [ -e "$_probe" ]; then
write_zddc_server_stub "$_rdir" "v${_xy}" "v${_xy}"
fi
done
# vX pins.
echo "$_all_versions" | sed -E 's|^v([0-9]+)\..*$|\1|' | sort -Vu | while read -r _x; do
_probe="$_rdir/zddc-server_v${_x}_linux-amd64"
if [ -e "$_probe" ]; then
write_zddc_server_stub "$_rdir" "v${_x}" "v${_x}"
fi
done
fi
}
# Promote a freshly-cross-compiled set of zddc-server binaries to the
# release-output bundle. Called by the top-level ./build on a release cut.
#
# $1 — channel ("stable" | "alpha" | "beta")
# $2 — version (X.Y.Z; required for stable; ignored for alpha/beta but
# passed through so labels can include the next-stable target)
# $3 — releases dir (absolute)
# $4 — dist dir holding cross-compiled binaries (absolute)
promote_zddc_server() {
_ch="$1"
_ver="$2"
_rdir="$3"
_dist="$4"
# Verify all four binaries exist before doing anything destructive.
for _plat in $ZDDC_SERVER_PLATFORMS; do
_suffix=""
case "$_plat" in *windows*) _suffix=".exe" ;; esac
_src="$_dist/zddc-server-${_plat}${_suffix}"
if [ ! -f "$_src" ]; then
echo "promote_zddc_server: missing source binary $_src" >&2
return 1
fi
done
case "$_ch" in
stable)
if [ -z "$_ver" ]; then
echo "promote_zddc_server: stable cut requires version" >&2
return 1
fi
_major="${_ver%%.*}"
_rest="${_ver#*.}"
_minor="${_rest%%.*}"
# Per-version: copy each binary to its immutable name + refresh
# the partial-version + channel symlinks. Mirrors the HTML-tool
# cascade: stable cut → beta + alpha both reset to stable.
for _plat in $ZDDC_SERVER_PLATFORMS; do
_suffix=""
case "$_plat" in *windows*) _suffix=".exe" ;; esac
_src="$_dist/zddc-server-${_plat}${_suffix}"
_versioned="zddc-server_v${_ver}_${_plat}${_suffix}"
cp "$_src" "$_rdir/$_versioned"
echo "Wrote $_rdir/$_versioned"
for _sym in "zddc-server_v${_major}.${_minor}_${_plat}${_suffix}" \
"zddc-server_v${_major}_${_plat}${_suffix}" \
"zddc-server_stable_${_plat}${_suffix}" \
"zddc-server_beta_${_plat}${_suffix}" \
"zddc-server_alpha_${_plat}${_suffix}"; do
ln -sfn "$_versioned" "$_rdir/$_sym"
done
done
# 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}"
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")
_head=$(git -C "$root_dir" rev-parse HEAD)
if [ "$_existing" != "$_head" ] \
&& ! 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
fi
fi
echo "Released zddc-server v${_ver} (stable; tagging deferred to top-level build)"
;;
alpha | beta)
# Mutable channel mirror per platform; cascade alpha → beta on
# a beta cut.
for _plat in $ZDDC_SERVER_PLATFORMS; do
_suffix=""
case "$_plat" in *windows*) _suffix=".exe" ;; esac
_src="$_dist/zddc-server-${_plat}${_suffix}"
_file="zddc-server_${_ch}_${_plat}${_suffix}"
rm -f "$_rdir/$_file"
cp "$_src" "$_rdir/$_file"
echo "Wrote $_rdir/$_file"
if [ "$_ch" = "beta" ]; then
ln -sfn "$_file" "$_rdir/zddc-server_alpha_${_plat}${_suffix}"
fi
done
echo "Released zddc-server ${_ch}"
;;
*)
echo "promote_zddc_server: unknown channel '$_ch'" >&2
return 1
;;
esac
# Refresh every stub page (covers the new release plus any pre-existing).
write_zddc_server_stubs_all "$_rdir"
}
# Verify every channel link for every release tool exists and resolves.
# Runs at the end of every build. Fails the build if anything is dangling.
# Channel verification covers both HTML tools (one .html per channel) and
# zddc-server (one stub HTML + four binaries per channel).
#
# Bootstrap-friendly: if zddc-server has no per-version artifacts at all
# (i.e. no release has been cut yet under the new lockstep model), the
# zddc-server entries are skipped with a heads-up rather than failing. The
# first stable cut materializes them.
verify_channel_links() {
_rdir="$1"
_missing=0
_verified=0
for _t in archive transmittal classifier mdedit landing form tables browse; do
for _ch in stable beta alpha; do
_f="$_rdir/${_t}_${_ch}.html"
if [ -e "$_f" ]; then
_verified=$((_verified + 1))
else
echo " MISSING: ${_t}_${_ch}.html" >&2
_missing=$((_missing + 1))
fi
done
done
# zddc-server's stable cut anchors the channel chain (cascade rule:
# stable cut → alpha + beta both reset to stable). Until stable
# exists, the verifier runs in bootstrap mode and skips — alpha/beta
# cuts in isolation are valid bootstrap state but have no cascade
# fallback target yet.
_zs_stable_exists=$(find "$_rdir" -maxdepth 1 -name 'zddc-server_stable_linux-amd64' -print -quit 2>/dev/null)
if [ -z "$_zs_stable_exists" ]; then
echo " (zddc-server stable not yet cut — run 'sh build.sh --release' to anchor the channel chain)"
else
for _ch in stable beta alpha; do
_f="$_rdir/zddc-server_${_ch}.html"
if [ -e "$_f" ]; then
_verified=$((_verified + 1))
else
echo " MISSING: zddc-server_${_ch}.html" >&2
_missing=$((_missing + 1))
fi
for _plat in $ZDDC_SERVER_PLATFORMS; do
_suffix=""
case "$_plat" in *windows*) _suffix=".exe" ;; esac
_f="$_rdir/zddc-server_${_ch}_${_plat}${_suffix}"
if [ -e "$_f" ]; then
_verified=$((_verified + 1))
else
echo " MISSING: zddc-server_${_ch}_${_plat}${_suffix}" >&2
_missing=$((_missing + 1))
fi
done
done
fi
if [ "$_missing" -gt 0 ]; then
echo "channel-link verification: $_missing missing artifact(s)" >&2
return 1
fi
echo "channel-link verification: $_verified link(s) ok"
}