ZDDC/build
ZDDC 6167e99f3a chore: simplify CLI to ./build / ./build beta / ./build release
Renames build.sh → build and replaces the --release flag form with
subcommands:

  ./build                  cut alpha (default; active dev iteration)
  ./build beta             cut beta  (cascades alpha → beta)
  ./build release          cut stable (coordinated next version)
  ./build release X.Y.Z    cut stable at explicit version
  ./build help

The contract shift: there's no longer a "plain dev build that doesn't
touch channels" at the top level. Every full-stack build is a publish
action — running ./build IS active dev iteration, which is what alpha
already meant. To iterate on one tool without writing to the website
worktree, use the per-tool sh tool/build.sh (unchanged).

Output continues to land in ${ZDDC_DEPLOY_RELEASES_DIR:-$HOME/src/zddc-website/releases}
and nothing is pushed automatically. Commit + push the website branch
yourself when you want to publish. Stable cuts still tag locally on
main; tags push separately too.

Behind the scenes: the export of ZDDC_DEPLOY_RELEASES_DIR is moved
above the per-tool build.sh invocations so children inherit it. The
prior "if RELEASE_CHANNEL else write_zddc_server_stubs_all" branch is
collapsed since RELEASE_CHANNEL is always set under the new CLI.

Docs (CLAUDE.md, AGENTS.md, ARCHITECTURE.md, zddc/README.md) updated
to reference ./build everywhere; the per-tool sh tool/build.sh refs
stay (they're a separate, narrower entry point).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 08:11:10 -05:00

651 lines
30 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
set -eu
# build — lockstep release driver for ZDDC.
#
# Three subcommands. The first arg names a channel and every invocation
# is a publish action — alpha is the default since "alpha = active dev
# iteration" and that's what running the build IS.
#
# ./build cut alpha (default; active dev iteration)
# ./build beta cut beta (cascades alpha → beta)
# ./build release cut stable (coordinated next version;
# cascades alpha + beta → stable)
# ./build release X.Y.Z cut stable at explicit version
# ./build help this message
#
# Lockstep: every cut bumps all six tools (5 HTML + zddc-server)
# together. The coordinated next-stable version is
# max(latest tag across all tools) + 1.
#
# Output goes to ${ZDDC_DEPLOY_RELEASES_DIR:-$HOME/src/zddc-website/releases}
# — the website branch's worktree, which is what Caddy serves. Nothing
# is pushed automatically; commit + push the website branch yourself
# when you want the changes mirrored to Codeberg.
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
# Source build-lib.sh once at the top level so the helpers it provides
# (promote_zddc_server, write_zddc_server_stubs_all, verify_channel_links,
# _coordinated_next_stable) are in scope. Each tool's build.sh sources it
# again — that's a no-op on already-defined functions.
root_dir="$SCRIPT_DIR"
. "$SCRIPT_DIR/shared/build-lib.sh"
# --- Parse subcommand ------------------------------------------------------
RELEASE_CHANNEL=""
RELEASE_VERSION=""
case "${1:-alpha}" in
alpha)
RELEASE_CHANNEL="alpha"
;;
beta)
RELEASE_CHANNEL="beta"
;;
release)
RELEASE_CHANNEL="stable"
if [ -n "${2:-}" ]; then
_validate_semver "$2"
RELEASE_VERSION="$2"
echo "=== Lockstep stable release — explicit version: v$RELEASE_VERSION ==="
else
RELEASE_VERSION=$(_coordinated_next_stable)
echo "=== Lockstep stable release — coordinated version: v$RELEASE_VERSION ==="
fi
;;
help | -h | --help)
sed -n '4,22p' "$0" | sed 's/^# \{0,1\}//'
exit 0
;;
*)
echo "build: unknown subcommand '$1'. Try './build help'." >&2
exit 1
;;
esac
# Per-tool argument list. The single-tool build.sh scripts still take
# the legacy `--release [channel|version]` form, so translate.
if [ "$RELEASE_CHANNEL" = "stable" ]; then
TOOL_RELEASE_ARGS="--release $RELEASE_VERSION"
else
TOOL_RELEASE_ARGS="--release $RELEASE_CHANNEL"
fi
# Deploy directory for release artifacts. The website lives in the
# orphan `website` branch served by Caddy from a fixed path; this dir
# is the worktree of that branch (default ~/src/zddc-website/releases).
# Override with $ZDDC_DEPLOY_RELEASES_DIR for testing or alternate
# deploy targets. Exported so child per-tool build.sh invocations see
# the same path.
export ZDDC_DEPLOY_RELEASES_DIR="${ZDDC_DEPLOY_RELEASES_DIR:-$HOME/src/zddc-website/releases}"
RELEASES_DIR="$ZDDC_DEPLOY_RELEASES_DIR"
mkdir -p "$RELEASES_DIR"
echo "=== Building ZDDC tools ==="
# Each tool's compute_build_label writes a sidecar `<tool>.label` here so
# we can assemble zddc/internal/apps/embedded/versions.txt below.
BUILD_LABELS_DIR="$SCRIPT_DIR/zddc/internal/apps/embedded/.labels"
rm -rf "$BUILD_LABELS_DIR"
mkdir -p "$BUILD_LABELS_DIR"
export BUILD_LABELS_DIR
# shellcheck disable=SC2086 # intentional word-splitting on TOOL_RELEASE_ARGS
sh "$SCRIPT_DIR/transmittal/build.sh" $TOOL_RELEASE_ARGS
sh "$SCRIPT_DIR/archive/build.sh" $TOOL_RELEASE_ARGS
sh "$SCRIPT_DIR/classifier/build.sh" $TOOL_RELEASE_ARGS
sh "$SCRIPT_DIR/mdedit/build.sh" $TOOL_RELEASE_ARGS
sh "$SCRIPT_DIR/landing/build.sh" $TOOL_RELEASE_ARGS
echo ""
echo "=== Assembling zddc/dist/web/ ==="
# All five tool HTMLs ship inside the server bundle. landing and archive call
# server APIs (GET / for the project list, directory listings for archive) and
# are useless without zddc-server. transmittal, classifier, and mdedit are
# pure client-side tools but are still bundled — the server uses these copies
# as the embedded fallback (//go:embed in internal/apps/embedded/) when both
# the cache is empty AND the upstream is unreachable.
mkdir -p "$SCRIPT_DIR/zddc/dist/web"
cp "$SCRIPT_DIR/landing/dist/index.html" "$SCRIPT_DIR/zddc/dist/web/index.html"
cp "$SCRIPT_DIR/archive/dist/archive.html" "$SCRIPT_DIR/zddc/dist/web/archive.html"
cp "$SCRIPT_DIR/transmittal/dist/transmittal.html" "$SCRIPT_DIR/zddc/dist/web/transmittal.html"
cp "$SCRIPT_DIR/classifier/dist/classifier.html" "$SCRIPT_DIR/zddc/dist/web/classifier.html"
cp "$SCRIPT_DIR/mdedit/dist/mdedit.html" "$SCRIPT_DIR/zddc/dist/web/mdedit.html"
echo "Wrote zddc/dist/web/{index,archive,transmittal,classifier,mdedit}.html"
# Mirror the same five HTMLs into the Go embed source dir so the next
# `go build` of zddc-server picks them up via //go:embed. Files are checked
# into git as empty placeholders; the build always overwrites them with the
# fresh dist/ output.
EMBED_DIR="$SCRIPT_DIR/zddc/internal/apps/embedded"
mkdir -p "$EMBED_DIR"
cp "$SCRIPT_DIR/landing/dist/index.html" "$EMBED_DIR/index.html"
cp "$SCRIPT_DIR/archive/dist/archive.html" "$EMBED_DIR/archive.html"
cp "$SCRIPT_DIR/transmittal/dist/transmittal.html" "$EMBED_DIR/transmittal.html"
cp "$SCRIPT_DIR/classifier/dist/classifier.html" "$EMBED_DIR/classifier.html"
cp "$SCRIPT_DIR/mdedit/dist/mdedit.html" "$EMBED_DIR/mdedit.html"
echo "Populated $EMBED_DIR/ for //go:embed"
# 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
# this via //go:embed in internal/apps/versions.go and surfaces it in
# `zddc-server --version` output and the startup log line.
VERSIONS_FILE="$EMBED_DIR/versions.txt"
{
echo "# Generated by build.sh — do not edit. One <app>=<build label> per line."
for _tool in archive transmittal classifier mdedit landing; do
_label_file="$BUILD_LABELS_DIR/${_tool}.label"
if [ -f "$_label_file" ]; then
_label=$(cat "$_label_file")
else
_label=""
fi
printf '%s=%s\n' "$_tool" "$_label"
done
} > "$VERSIONS_FILE"
echo "Wrote $VERSIONS_FILE"
rm -rf "$BUILD_LABELS_DIR"
# Cross-compiled zddc-server binaries for Linux/macOS/Windows. Always built
# inside docker.io/golang:1.24-alpine via podman (or docker), matching the
# helm/zddc-server-prod chart's `buildImage` so dev binaries are byte-for-byte
# what production gets. The build container is downloaded on first run.
echo ""
echo "=== Building zddc-server binaries (containerized) ==="
mkdir -p "$SCRIPT_DIR/zddc/dist"
# Pick a container runtime. Both work; podman is preferred (rootless default).
GO_RUNNER=""
if command -v podman >/dev/null 2>&1; then
GO_RUNNER=podman
elif command -v docker >/dev/null 2>&1; then
GO_RUNNER=docker
else
echo "error: neither podman nor docker is available — cannot build zddc-server binaries." >&2
echo " Install podman (preferred) or docker. zddc-server build is containerized as policy." >&2
exit 1
fi
GO_BUILD_IMAGE="${ZDDC_GO_BUILD_IMAGE:-docker.io/golang:1.24-alpine}"
# Cache the Go module + build cache across runs via named volumes that
# persist between container invocations. Second build is fast.
GO_MOD_VOL="${ZDDC_GO_MOD_VOL:-zddc-go-mod}"
GO_BUILD_VOL="${ZDDC_GO_BUILD_VOL:-zddc-go-cache}"
# Compute the binary's own version. On a stable cut, hard-code the
# coordinated version so the binary embeds the same string the rest of the
# release cycle has agreed on. Otherwise fall back to git describe (clean
# tag, or tag-N-gSHA[-dirty] for in-flight commits).
if [ -n "$RELEASE_VERSION" ]; then
ZDDC_BINARY_VERSION="$RELEASE_VERSION"
else
ZDDC_BINARY_VERSION=$(git -C "$SCRIPT_DIR" describe --tags --dirty --match 'zddc-server-v*' 2>/dev/null || true)
if [ -z "$ZDDC_BINARY_VERSION" ]; then
_sha=$(git -C "$SCRIPT_DIR" rev-parse --short=7 HEAD 2>/dev/null || echo unknown)
if ! git -C "$SCRIPT_DIR" diff --quiet HEAD 2>/dev/null; then
_sha="${_sha}-dirty"
fi
ZDDC_BINARY_VERSION="dev-${_sha}"
fi
fi
echo " binary version: $ZDDC_BINARY_VERSION"
# Single container invocation, multiple cross-compile targets inside a
# `for` loop — avoids paying image-startup overhead 4×.
"$GO_RUNNER" run --rm \
-v "$SCRIPT_DIR:/src:Z" \
-v "${GO_MOD_VOL}:/go/pkg/mod" \
-v "${GO_BUILD_VOL}:/root/.cache/go-build" \
-w /src/zddc \
-e GOFLAGS=-mod=mod \
-e CGO_ENABLED=0 \
-e ZDDC_BINARY_VERSION="$ZDDC_BINARY_VERSION" \
"$GO_BUILD_IMAGE" \
sh -c '
set -e
for target in linux/amd64 darwin/amd64 darwin/arm64 windows/amd64; do
os="${target%/*}"; arch="${target#*/}"
out="zddc-server-${os}-${arch}"
case "$os" in windows) out="${out}.exe" ;; esac
echo " building $out"
GOOS="$os" GOARCH="$arch" \
go build -trimpath \
-ldflags="-s -w -X main.version=${ZDDC_BINARY_VERSION}" \
-o "dist/$out" ./cmd/zddc-server
done
'
# --- Promote zddc-server release artifacts ---------------------------------
# Copy the freshly cross-compiled binaries to the website worktree's
# releases/ under their canonical names + symlinks. promote_zddc_server
# also re-runs write_zddc_server_stubs_all internally, so the matrix-cell
# stub pages get regenerated in the same call.
echo ""
echo "=== Promoting zddc-server $RELEASE_CHANNEL release ==="
promote_zddc_server "$RELEASE_CHANNEL" "$RELEASE_VERSION" "$RELEASES_DIR" "$SCRIPT_DIR/zddc/dist"
# Latest stable version, by following archive_stable.html → versioned target.
# Returns "" if no stable cut exists yet (bootstrap state). All HTML tools
# move in lockstep so any one of them is a valid probe; archive is canonical.
_latest_stable_version() {
_link="$RELEASES_DIR/archive_stable.html"
[ -L "$_link" ] || return 0
_target=$(readlink "$_link")
# archive_v0.0.8.html → 0.0.8
_v="${_target#archive_v}"
_v="${_v%.html}"
case "$_v" in
[0-9]*.[0-9]*.[0-9]*) echo "$_v" ;;
esac
}
# Channel "active" iff the channel mirror is real bytes rather than a
# symlink → stable. Used to surface alpha/beta in the dropdown only when
# they meaningfully differ from stable. Probes archive (HTML lockstep
# representative); zddc-server's probe is its per-platform binary.
_channel_is_active() {
_ch="$1" # alpha | beta
_f="$RELEASES_DIR/archive_${_ch}.html"
[ -L "$_f" ] && return 1 # symlink → tracks stable, not "active"
[ -f "$_f" ] && return 0
return 1
}
# Regenerate website/releases/index.html as the action-first install
# guide (not a matrix). The page guides users to either self-host the
# server or download individual tools, with one version dropdown that
# rewires every download link via JS. The default static state always
# uses latest-stable URLs so the page works fully without JS.
build_releases_index() {
_out="$RELEASES_DIR/index.html"
mkdir -p "$RELEASES_DIR"
_latest=$(_latest_stable_version)
if [ -z "$_latest" ]; then
_latest="0.0.0"
fi
# All distinct stable versions across every tool, descending. Same
# awk that the prior matrix used — proven across the tool naming.
_all_versions=$(
find "$RELEASES_DIR" -maxdepth 1 -type f \( \
-name 'archive_v*.html' -o -name 'transmittal_v*.html' \
-o -name 'classifier_v*.html' -o -name 'mdedit_v*.html' \
-o -name 'landing_v*.html' \
-o -name 'zddc-server_v*_linux-amd64' \
\) 2>/dev/null \
| awk -F/ '{
n = split($NF, parts, "_v");
if (n < 2) next;
v = parts[2];
sub(/\.html$/, "", v);
sub(/_linux-amd64$/, "", v);
if (v ~ /^[0-9]+\.[0-9]+\.[0-9]+$/) print v;
}' \
| sort -Vu \
| sort -Vr
)
_alpha_active="0"; _channel_is_active alpha && _alpha_active="1"
_beta_active="0"; _channel_is_active beta && _beta_active="1"
{
cat <<HEAD
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Download ZDDC</title>
<meta name="description" content="Self-host the ZDDC server, or download individual tools. Pin a version your project trusts; your archive's tools are yours.">
<meta name="theme-color" content="#2a5a8a">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="../css/style.css">
</head>
<body>
<header class="site-header">
<div class="container header-content">
<a href="/" class="brand">
<svg class="brand-logo" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" aria-hidden="true">
<rect width="64" height="64" rx="12" fill="#1e3a5f"/>
<g fill="#fff">
<rect x="14" y="18" width="36" height="7"/>
<polygon points="43,25 50,25 21,43 14,43"/>
<rect x="14" y="43" width="36" height="7"/>
</g>
</svg>
<span class="brand-name">ZDDC</span>
</a>
<nav class="header-nav">
<a href="/" class="nav-link">Home</a>
<a href="../reference.html" class="nav-link">Docs</a>
<a href="index.html" class="nav-link active">Download</a>
</nav>
</div>
</header>
<section class="hero">
<div class="container">
<h1>Download ZDDC</h1>
<p class="hero-subtitle">Pick how you want to use it. Pick the version you want. Every link below points at a real, immutable file you can save into your archive — your tools, your version, forever.</p>
</div>
</section>
<main class="container" style="margin-bottom: var(--spacing-2xl);">
<div class="version-picker-bar">
<label for="version-picker">Showing</label>
<select id="version-picker">
HEAD
# Channels — selectable directly so users can copy the channel-
# mirror URLs (e.g. archive_stable.html) for bookmarks. stable is
# the default. The label tells the truth about the channel's
# current state: when stable is set, show which version it points
# at; when alpha/beta is just a symlink to stable, mark as
# "tracks stable" so picking it isn't surprising.
printf ' <optgroup label="Channels (mutable URLs)">\n'
if [ -n "$_latest" ] && [ "$_latest" != "0.0.0" ]; then
printf ' <option value="stable" selected>stable — currently v%s</option>\n' "$_latest"
else
printf ' <option value="stable" selected>stable</option>\n'
fi
if [ "$_beta_active" = "1" ]; then
printf ' <option value="beta">beta — general testing</option>\n'
else
printf ' <option value="beta">beta — tracks stable</option>\n'
fi
if [ "$_alpha_active" = "1" ]; then
printf ' <option value="alpha">alpha — active dev</option>\n'
else
printf ' <option value="alpha">alpha — tracks stable</option>\n'
fi
printf ' </optgroup>\n'
# Pinned per-version, latest first. These are the immutable URLs
# for reproducibility. No "(current stable)" suffix because the
# stable channel above already covers that.
printf ' <optgroup label="Pinned versions (immutable URLs)">\n'
printf '%s\n' "$_all_versions" | while read -r _v; do
[ -n "$_v" ] || continue
printf ' <option value="v%s">v%s</option>\n' "$_v" "$_v"
done
printf ' </optgroup>\n'
cat <<'PICKER_END'
</select>
<span class="picker-hint">Changes every download link below.</span>
</div>
<!-- ───────────── Path A — Self-host the server ───────────── -->
<section class="card" style="background: var(--color-bg-subtle); border: 1px solid var(--color-border); border-radius: var(--radius-md); padding: var(--spacing-lg) var(--spacing-xl); margin-top: var(--spacing-lg);">
<h2 style="margin-top:0;">Path A — Self-host the server</h2>
<p>One small Go binary. <strong>All five tools are baked in</strong> via <code>//go:embed</code>; the server picks the right one for each folder of your archive. Adds ACL via <code>.zddc</code> files, the virtual <code>.archive</code> document index, and SSO header passthrough. Stop the server and the directory is still a perfectly valid ZDDC archive — the server is convenience, not lock-in.</p>
PICKER_END
# Render the download UI only when zddc-server has been published
# at least once. Until then, show an honest "not yet released"
# placeholder rather than dangling download buttons.
_zs_published="0"
if [ -e "$RELEASES_DIR/zddc-server_stable_linux-amd64" ]; then
_zs_published="1"
fi
if [ "$_zs_published" = "1" ]; then
# Default href is the channel-mirror URL (zddc-server_stable_<plat>)
# because "stable" is the dropdown's selected option. Picking a
# pinned version from the dropdown rewrites these to the
# immutable per-version URL via the IIFE.
printf ' <a class="dl-primary"\n'
printf ' data-tool="zddc-server"\n'
printf ' data-platform="linux-amd64"\n'
printf ' href="zddc-server_stable_linux-amd64"\n'
printf ' id="dl-primary-binary">\n'
printf ' <span class="dl-icon">⬇</span>\n'
printf ' <span>Download <span id="dl-primary-platlabel">for Linux (x86_64)</span></span>\n'
printf ' </a>\n'
printf ' <span class="dl-primary-meta" id="dl-primary-meta">zddc-server_stable_linux-amd64</span>\n'
printf ' <div class="dl-secondary-row" id="dl-others">\n'
printf ' <span>Other platforms:</span>\n'
for _entry in "linux-amd64|Linux (x86_64)" \
"darwin-amd64|macOS (Intel)" \
"darwin-arm64|macOS (Apple Silicon)" \
"windows-amd64|Windows (x86_64)"; do
_plat="${_entry%%|*}"
_label="${_entry#*|}"
_suffix=""
case "$_plat" in *windows*) _suffix=".exe" ;; esac
printf ' <a data-tool="zddc-server" data-platform="%s" href="zddc-server_stable_%s%s">%s</a>\n' \
"$_plat" "$_plat" "$_suffix" "$_label"
done
printf ' </div>\n'
cat <<'PATH_A_END'
<p style="margin-top: var(--spacing-md); font-size: 0.92rem; color: var(--color-text-muted);">
After download: <code>chmod +x</code> the file, set <code>ZDDC_ROOT=/path/to/archive</code>, run.
Need a different platform? <a href="https://codeberg.org/VARASYS/ZDDC">Build from source</a> at the matching tag.
</p>
</section>
PATH_A_END
else
# Bootstrap state: no zddc-server stable cut yet.
cat <<'PATH_A_BOOTSTRAP'
<p style="margin-top: var(--spacing-md); padding: var(--spacing-md); background: var(--color-bg); border-left: 3px solid var(--color-accent); border-radius: var(--radius-sm); color: var(--color-text);">
<strong>Not yet published.</strong> The first lockstep release publishes binaries here. Until then, build from source: <code>git clone</code> and <code>(cd zddc && go build ./cmd/zddc-server)</code>. Once <code>sh build.sh --release</code> runs, this card auto-populates with download buttons for every platform.
</p>
</section>
PATH_A_BOOTSTRAP
fi
cat <<'PATH_B_OPEN'
<!-- ───────────── Path B — Standalone tool HTMLs ───────────── -->
<section class="card" style="border: 1px solid var(--color-border); border-radius: var(--radius-md); padding: var(--spacing-lg) var(--spacing-xl); margin-top: var(--spacing-xl);">
<h2 style="margin-top:0;">Path B — Standalone tools</h2>
<p>Every tool is a single self-contained HTML file. <strong>Open it locally and point it at a folder on your disk</strong> — no install, no server, no account. Same on-disk layout the server uses. Use one tool, use all five, mix and match — there is no orchestration to set up.</p>
<div class="grid-4" style="margin-top: var(--spacing-md);">
PATH_B_OPEN
# Tool cards — reuse home page's .tool-card vocabulary
for _entry in "archive|Archive Browser|Browse and download from a ZDDC archive." \
"transmittal|Transmittal Creator|Build, sign, and verify transmittal packages." \
"classifier|Classifier|Rename loose files to ZDDC convention." \
"mdedit|Markdown Editor|Edit project markdown files in place." \
"landing|Landing|Project picker for multi-project servers."; do
_t="${_entry%%|*}"
_rest="${_entry#*|}"
_name="${_rest%%|*}"
_desc="${_rest#*|}"
# Default href is the stable-channel mirror; the dropdown
# rewires these per selection.
printf ' <a class="tool-card" data-tool="%s" href="%s_stable.html">\n' "$_t" "$_t"
printf ' <span class="tool-card__title">%s</span>\n' "$_name"
printf ' <span class="tool-card__desc">%s</span>\n' "$_desc"
printf ' <span class="tool-card__link">Download &rarr;</span>\n'
printf ' </a>\n'
done
cat <<'PATH_B_END'
</div>
</section>
<!-- ───────────── Pinning empowerment narrative ───────────── -->
<section class="card" style="border: 1px solid var(--color-border); border-radius: var(--radius-md); padding: var(--spacing-lg) var(--spacing-xl); margin-top: var(--spacing-xl);">
<h2 style="margin-top:0;">Your version, forever</h2>
<p>Your server may run v0.0.8 next month and v0.1.0 the month after. <strong>Your project doesn't have to follow.</strong> If you depend on a specific behavior in <code>archive</code> v0.0.5, save that version into your archive — the next server upgrade can't take it away from you. Two ways to do it:</p>
<div class="grid-2" style="margin-top: var(--spacing-md);">
<div class="pin-card">
<h3>Drop a copy into your archive</h3>
<p>Save the tool's HTML at the path the server would serve it from. The server's resolution order picks up real files <em>first</em> — before any cascade or embedded fallback.</p>
PATH_B_END
printf ' <pre>curl -o MyProject/archive.html \\\n https://zddc.varasys.io/releases/archive_v%s.html</pre>\n' "$_latest"
cat <<'PIN_MID'
<p>Now <code>MyProject/archive.html</code> is yours. The server serves your bytes; nothing about a future <code>--release</code> can change them.</p>
</div>
<div class="pin-card">
<h3>Pin via <code>.zddc</code></h3>
<p>Less invasive — no copies in your archive, just a small config entry telling the server which version to fetch and cache. Closer-to-leaf wins, so subprojects can pin further.</p>
PIN_MID
printf ' <pre># MyProject/.zddc\napps:\n archive: v%s</pre>\n' "$_latest"
cat <<'PIN_END'
<p>Server fetches once on first hit, caches under <code>_app/</code>, falls through to the embedded copy if the fetch fails.</p>
</div>
</div>
<p class="pin-note">Your archive's tools are <strong>yours</strong>. The server is convenience; deletion of the server doesn't break your archive — every per-version download above is a real, immutable static file. Save what you trust.</p>
</section>
<!-- ───────────── Channels explainer ───────────── -->
<section class="card" style="border: 1px solid var(--color-border); border-radius: var(--radius-md); padding: var(--spacing-lg) var(--spacing-xl); margin-top: var(--spacing-xl); margin-bottom: var(--spacing-xl);">
<h2 style="margin-top:0;">Channels</h2>
<p>Three channels, applied in lockstep across all tools. Pre-release channels exist to soak changes; <strong>stable</strong> is what production runs.</p>
<div class="channel-explainer">
<div>
<h4 class="alpha">alpha</h4>
<p>Active dev iteration. Rebuilds without notice. Look here for the very latest.</p>
</div>
<div>
<h4 class="beta">beta</h4>
<p>Ready for general testing. Has soaked through alpha. Still mutable — pin to a versioned URL for reproducibility.</p>
</div>
<div>
<h4 class="stable">stable</h4>
<p>Ready to ship. Every per-version file is immutable; <code>_stable</code> follows the latest cut. Channel cuts cascade: stable cut resets beta and alpha to track stable.</p>
</div>
</div>
</section>
</main>
<footer class="site-footer">
<div class="container footer-content">
<span>ZDDC is open source — <a href="https://codeberg.org/VARASYS/ZDDC">codeberg.org/VARASYS/ZDDC</a></span>
</div>
</footer>
<script>
(function() {
// Platform auto-detect: choose the most likely binary for this user's
// OS on first paint. Promotes that platform to the primary CTA; the
// other three render in the secondary row. UA-sniffing is good
// enough — wrong guesses fall through to the always-visible
// "Other platforms" row below.
var ua = navigator.userAgent || '';
var detected = 'linux-amd64'; // sensible default
var platLabel = 'Linux (x86_64)';
if (/Macintosh|Mac OS X/.test(ua)) {
// Apple Silicon vs Intel — UA hints aren't reliable, prefer arm64
// since modern Macs are predominantly arm64. Users on Intel can
// pick from "Other platforms".
detected = 'darwin-arm64';
platLabel = 'macOS (Apple Silicon)';
} else if (/Windows/.test(ua)) {
detected = 'windows-amd64';
platLabel = 'Windows (x86_64)';
}
var primary = document.getElementById('dl-primary-binary');
var primaryLabel = document.getElementById('dl-primary-platlabel');
var primaryMeta = document.getElementById('dl-primary-meta');
var others = document.getElementById('dl-others');
function isChannel(v) {
return v === 'stable' || v === 'beta' || v === 'alpha';
}
function platBinaryName(slug, plat) {
// slug is a channel name ("stable") or a pinned version ("v0.0.8").
// The on-disk name uses the slug as-is in both cases since the
// channel-mirror filenames are zddc-server_<channel>_<plat> and
// per-version are zddc-server_v<X.Y.Z>_<plat>.
var suf = (plat.indexOf('windows') === 0) ? '.exe' : '';
return 'zddc-server_' + slug + '_' + plat + suf;
}
function htmlAssetName(tool, slug) {
return tool + '_' + slug + '.html';
}
// Promote the detected platform to the primary CTA. The secondary
// row keeps all four; the matching one is hidden to avoid showing
// the same download twice.
if (primary) {
primary.dataset.platform = detected;
if (primaryLabel) primaryLabel.textContent = 'for ' + platLabel;
}
if (others) {
others.querySelectorAll('a[data-platform="' + detected + '"]').forEach(function(a) {
a.style.display = 'none';
});
}
// Single source of truth: the dropdown's current value drives every
// download link's href. Static markup ships with the stable-channel
// mirror (`<tool>_stable.html`, `zddc-server_stable_<plat>`) so the
// page works fully without JS — the JS just keeps things in sync
// when the user picks a different channel or pins a version.
var picker = document.getElementById('version-picker');
if (!picker) return;
function rewire(slug) {
// slug ∈ {"stable", "beta", "alpha"} | "v<X.Y.Z>". Every link with
// a data-tool attribute is a download URL the dropdown owns.
document.querySelectorAll('[data-tool]').forEach(function(a) {
var tool = a.dataset.tool;
var plat = a.dataset.platform || '';
if (tool === 'zddc-server') {
a.href = plat ? platBinaryName(slug, plat) : ('zddc-server_' + slug + '.html');
} else {
a.href = htmlAssetName(tool, slug);
}
});
if (primary && primaryMeta) {
primaryMeta.textContent = primary.getAttribute('href');
}
}
picker.addEventListener('change', function() { rewire(picker.value); });
// Run rewire once on load to apply the platform-detection result
// (the static href for the primary button is for linux-amd64; on a
// non-linux client, that needs to flip to the detected platform).
rewire(picker.value);
})();
</script>
</body>
</html>
PIN_END
} > "$_out"
echo "Wrote $_out"
}
echo ""
echo "=== Building releases/index.html ==="
build_releases_index
echo ""
echo "=== Verifying channel links ==="
verify_channel_links "$RELEASES_DIR"
echo ""
echo "=== All tools built successfully ==="
echo ""
echo "Cut: $RELEASE_CHANNEL"
if [ -n "$RELEASE_VERSION" ]; then
echo "Version: v$RELEASE_VERSION"
echo ""
echo "Tags created locally on main (push when ready):"
for _t in archive transmittal classifier mdedit landing zddc-server; do
echo " ${_t}-v${RELEASE_VERSION}"
done
echo " git push origin main && git push origin --tags"
fi
echo ""
echo "Artifacts written to $RELEASES_DIR/"
echo " cd $(dirname "$RELEASES_DIR") && git status # to review the deploy"
echo " cd $(dirname "$RELEASES_DIR") && git add -A && git commit && git push origin website"
echo " ↑ commits + pushes the website branch when you're ready to publish"