#!/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 — abort with error if file missing # concat_files — 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 # — copy to with all '. # The JS engine treats \/ as a regular slash, # so runtime behaviour is unchanged. # compute_build_label [--release []] # — sets globals: build_label, build_version, # is_release, is_red, channel. # See "Release args" below. # promote_release — for stable cuts, copy the dist HTML into # the release-output bundle (default # $root_dir/../dist/release-output; # override $ZDDC_DEPLOY_RELEASES_DIR). # Writes the immutable per-version file # _v.html plus the canonical # symlink .html pointing at it. # Tagging is centralized in the top-level # ./build (after the embedded commit). # Beta cuts produce NO public artifact — # they are an internal SHA snapshot for # the BMC dev chart pipeline; the # embedded/* regeneration + chore commit # in the top-level ./build is the actual # artifact (chart appVersion pins to that # SHA, Dockerfile fetches it from git). # # Release args: # dev build, tool/dist/ only, label # "v-dev · · [-dirty]" (red). # No release-output side-effect. # --release stable cut, auto-bump patch from latest tag (or 0.0.1). # Writes _v.html + .html symlink; # tagged later by ./build. # --release X.Y.Z stable cut, explicit version. # --release beta internal SHA snapshot for the BMC dev chart. Build # label is "v-beta · · "; # no public artifact, no tag. The top-level ./build # regenerates zddc/internal/apps/embedded/ + commits. # --release 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 } # Mirror the conversion templates from a canonical source dir into a build embed # dir — go:embed can't follow symlinks, so the bytes must be a real copy under the # Go package. Copies every *.html, drops stale destination *.html the source no # longer has, and verifies byte-identity. Guarded at test time by # convert.TestEmbeddedTemplatesMatchSource. Usage: sync_pandoc_templates sync_pandoc_templates() { _src="$1" _dst="$2" if [ ! -d "$_src" ]; then echo "error: missing template source dir: $_src" >&2 exit 1 fi mkdir -p "$_dst" # Drop destination templates the source no longer provides. for _f in "$_dst"/*.html; do [ -e "$_f" ] || continue if [ ! -f "$_src/$(basename "$_f")" ]; then rm -f "$_f" fi done # Copy + verify each source template. for _f in "$_src"/*.html; do [ -e "$_f" ] || continue cp "$_f" "$_dst/$(basename "$_f")" if ! cmp -s "$_f" "$_dst/$(basename "$_f")"; then echo "error: template sync mismatch: $_f" >&2 exit 1 fi done echo "Synced templates: $_src -> $_dst" } # 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 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 ' tag. Per the HTML5 spec # only block — other tags like are safe # inside a script's text content. Narrowly targeting "$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: 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-beta" (beta auto-commit, build:993) # - "release: v 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" } # Short SHA of the underlying source commit (skipping past embedded # auto-commits — see _source_commit_ref). Same source state → same SHA # even after a `chore(embedded): cut …` commit has landed on top. _source_commit_short_sha() { _ref=$(_source_commit_ref) git -C "$root_dir" rev-parse --short=7 "$_ref" 2>/dev/null || echo "unknown" } # Compute build label and channel. Reads positional args: # compute_build_label [--release []] # 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/beta), else "0" # channel — "stable" / "beta" / "dev" # # Versioning: pre-release semver. The next-stable target is computed from # the latest clean tool-vX.Y.Z tag (patch-bump). Plain dev builds and # `--release beta` carry the next-stable target as a pre-release suffix # in the on-page label so users can see which stable the snapshot is # working toward. Stable releases write a clean vX.Y.Z label and tag. # # HTML tools do NOT tag beta cuts — beta produces no public artifact # (the chart pins by SHA via appVersion). Plain dev builds and beta # cuts share the same on-page label format (full UTC timestamp + short # source SHA). A plain dev build may carry a "-dirty" SHA suffix when # the working tree has uncommitted changes; release cuts don't. 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 iteration — tool/dist/ only, no release # output. The label includes the next-stable target so a developer # opening the local dist file can see which version-in-progress # they're looking at. Full timestamp + dirty marker distinguish # iterative dev builds from formal cuts. _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="dev" build_label="v${_next_stable}-dev · ${build_timestamp} · ${_sha}" _emit_build_label_sidecar "$_tool" return 0 fi is_release=1 case "$_arg" in beta) channel="beta" # Internal SHA snapshot for the BMC dev chart. The chart's # appVersion gets set to "-beta-" and the # Dockerfile parses the suffix to fetch this SHA from git. # _source_commit_short_sha walks past any `chore(embedded): # cut …` auto-commit at HEAD so a re-cut on unchanged # source produces the same SHA. _sha=$(_source_commit_short_sha) build_label="v${_next_stable}-beta · ${build_timestamp} · ${_sha}" _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). ZDDC_RELEASE_TOOLS="archive transmittal classifier landing form tables browse zddc-server" # Compute the next-stable target for a single tool — patch-bump of its own # latest -vX.Y.Z tag. Used by compute_build_label so a tool's # on-page label reads against its own history (e.g. a beta 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" / "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. Copy dist HTML → /_v.html (immutable). # 2. Refresh canonical symlink: /.html → the new versioned file. # 3. Tag the commit -v (centralized in the top-level ./build). # # Beta cuts: # No public artifact. The chart's Dockerfile fetches the source at the # SHA pinned in chart appVersion and compiles its own binary; the # embedded/* regeneration + chore commit in the top-level ./build is # the actual snapshot. # # Plain dev builds (no --release): never call promote_release. 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 canonical symlink .html advances to the new # version, which is the actual goal. _promote_stable "$_tool" "$build_version" "$_releases_dir" ;; beta) # Internal SHA snapshot for the BMC dev chart. No public # artifact: the chart fetches the source at the SHA via git, # the embedded/* regeneration + chore commit (in the top- # level ./build) IS the artifact. echo " ${_tool}: beta is internal (no public artifact)" ;; *) echo "promote_release: unknown channel '$channel'" >&2 return 1 ;; esac } # Stable cut: per-version immutable file + canonical symlink. 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 stale bytes # baked in. _promote_stable() { _t="$1" _ver="$2" _rdir="$3" _versioned="${_t}_v${_ver}.html" _canonical="${_t}.html" cp "$output_html" "$_rdir/$_versioned" echo "Wrote $_rdir/$_versioned" ln -sfn "$_versioned" "$_rdir/$_canonical" echo " $_canonical → $_versioned" # Companion .sig symlink so `curl .sig` resolves. The # actual .sig file is written by sign_release_artifacts; this # symlink points there. ln -sfn "${_versioned}.sig" "$_rdir/${_canonical}.sig" # 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)" } # 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 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 (slug, platform). # Returns the bare name (no path); ".exe" suffix on windows. Empty slug # means the canonical "current stable" symlink (zddc-server_); # non-empty slug is a per-version asset (zddc-server_v_). _zddc_server_binary_name() { _slug="$1" _plat="$2" _suffix="" case "$_plat" in *windows*) _suffix=".exe" ;; esac if [ -z "$_slug" ]; then printf 'zddc-server_%s%s' "$_plat" "$_suffix" else printf 'zddc-server_%s_%s%s' "$_slug" "$_plat" "$_suffix" fi } # Write the small HTML index page that becomes the entry point for a # zddc-server release. Lists each platform binary with a download link. # $1 — release directory (absolute) # $2 — slug ("" for canonical "current stable", or "v0.0.8" per-version) # $3 — display label (e.g. "current stable", "v0.0.8") write_zddc_server_stub() { _rdir="$1" _slug="$2" _label="$3" if [ -z "$_slug" ]; then _out="$_rdir/zddc-server.html" else _out="$_rdir/zddc-server_${_slug}.html" fi { cat < zddc-server ${_label} — ZDDC

zddc-server — ${_label}

Cross-compiled binaries. Download for your platform, mark executable, and run with ZDDC_ROOT=/path/to/archive ./zddc-server.

HEAD for _plat in $ZDDC_SERVER_PLATFORMS; do _bin=$(_zddc_server_binary_name "$_slug" "$_plat") _plabel=$(_zddc_server_platform_label "$_plat") printf ' \n' "$_plabel" "$_bin" "$_bin" done cat <<'TAIL'
PlatformDownload
%s%s

Need a different platform? Build from source: (cd zddc && go build -o zddc-server ./cmd/zddc-server) from the repo at the matching tag.

TAIL } > "$_out" } # Refresh every zddc-server stub page based on what's currently in the # release-output bundle: one per-version stub per zddc-server_v*_* # binary set, plus a canonical zddc-server.html if the latest-stable # symlinks are in place. Indexed off linux-amd64 since all four # platforms ship in lockstep. # # $1 — releases dir (absolute) write_zddc_server_stubs_all() { _rdir="$1" # Per-version stubs (immutable). for _bin in "$_rdir"/zddc-server_v*_linux-amd64; do [ -e "$_bin" ] || continue _name=$(basename "$_bin") _slug=$(echo "$_name" | sed -E 's/^zddc-server_(v[^_]+)_linux-amd64$/\1/') case "$_slug" in v*.*.*) write_zddc_server_stub "$_rdir" "$_slug" "$_slug" ;; esac done # Canonical stub (follows the latest-stable symlink). Probes the # linux-amd64 canonical name; if it exists, the platform symlinks # are in place and we can write the entry page. if [ -e "$_rdir/zddc-server_linux-amd64" ]; then write_zddc_server_stub "$_rdir" "" "current stable" fi } # Promote a freshly-cross-compiled set of zddc-server binaries to the # release-output bundle. Called by the top-level ./build on a stable # release cut. Beta cuts produce no public artifact (the chart's # Dockerfile compiles from source at the SHA pinned in appVersion). # # $1 — channel ("stable" | "beta") # $2 — version (X.Y.Z; required for stable; ignored for beta) # $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 # Per-version immutable + canonical per-platform symlink. 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}" _canonical="zddc-server_${_plat}${_suffix}" cp "$_src" "$_rdir/$_versioned" echo "Wrote $_rdir/$_versioned" ln -sfn "$_versioned" "$_rdir/$_canonical" echo " $_canonical → $_versioned" # Companion .sig symlink — see _promote_stable for the # same pattern. ln -sfn "${_versioned}.sig" "$_rdir/${_canonical}.sig" 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)" ;; beta) # Internal SHA snapshot — the chart's Dockerfile fetches the # source at that SHA and compiles its own binary. No public # binary is published. echo " zddc-server: beta is internal (no public artifact)" ;; *) echo "promote_zddc_server: unknown channel '$_ch'" >&2 return 1 ;; esac # Refresh stub pages (per-version + canonical). write_zddc_server_stubs_all "$_rdir" }