mdedit/ is gone. Its functionality moved into browse's preview plugin
(browse/js/preview-markdown.js) — YAML front matter editing, outline,
and on-demand DOCX/HTML/PDF download all happen there. Browse is the
default_tool for working/ + reviewing/ as of the previous commit, so
existing URLs of the form /<project>/working land on browse without
operator action.
Removed:
• mdedit/ source tree (Toast UI app, CSS, JS, template, build.sh)
• zddc/internal/apps/embedded/mdedit.html (//go:embed blob)
• tests/mdedit.spec.js + the "mdedit" project in playwright.config.js
• mdedit entries in zddc/internal/apps/embed.go (//go:embed, var,
switch case in EmbeddedBytes)
• "mdedit" in zddc/internal/zddc/validate.go AppNames + the matching
error-message app list
• "mdedit.html" branch in zddc/internal/apps/handler.go MatchAppHTML
• mdedit case in tests (handler_test.go, validate_test.go,
zddchandler_test.go) — test fixtures now use browse/classifier
• mdedit from build (per-tool build.sh loop, tool-list literals,
composer cards) and shared/build-lib.sh ZDDC_RELEASE_TOOLS
• mdedit from freshen-channel's tool list and usage banner
• mdedit-specific paragraphs in AGENTS.md and ARCHITECTURE.md;
Markdown Editor section in ARCHITECTURE.md rewritten to point at
browse/js/preview-markdown.js
• mdedit from CLAUDE.md, README.md, zddc/README.md tool lists
Historical mdedit_v*.html / mdedit_v*.html.sig files in
/srv/zddc/releases/ on the deploy host are immutable history — they
stay where they are. The next ./build release cut will simply not
produce new mdedit_v* artifacts.
793 lines
34 KiB
Bash
Executable file
793 lines
34 KiB
Bash
Executable file
#!/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 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> / <a href="index.html">releases</a> / zddc-server ${_label}</p>
|
||
<h1>zddc-server — ${_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 && 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 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"
|
||
}
|