build: simplify to stable + exact-version (drop alpha/beta as public concepts)
Releases publish only two things per tool now: a current-stable
canonical symlink and an immutable per-version file. No more channel
mirrors (_stable/_beta/_alpha) and no more partial-version pins
(_v<X.Y>, _v<X>) — those were debt from a release model that never
matched the project's actual usage.
The `./build beta` verb stays, but narrowed: it's an internal SHA
snapshot for the BMC dev chart pipeline (chart's appVersion pins to
"<X.Y.Z>-beta-<sha>" and the chart Dockerfile fetches that SHA from
git). No public artifact on /srv/zddc/releases/. The embedded/* +
chore commit produced by `./build beta` is the actual snapshot.
`./build alpha` is removed entirely.
build/build-lib.sh:
- Drop alpha verb; narrow beta verb to embedded regen + chore commit
- promote_release: stable cut writes <tool>_v<X.Y.Z>.html + <tool>.html
symlink + <tool>.html.sig companion symlink; beta is a no-op
- promote_zddc_server: same shape — per-version binary +
per-platform canonical symlink (zddc-server_<plat>) + .sig symlink
- write_zddc_server_stub: singular; emits per-version stubs +
one canonical zddc-server.html for current stable
- Delete _promote_channel, verify_channel_links, _channel_is_active
- Seed-from-live now copies only per-version files + .sig + pubkey.pem
(the canonical symlinks get rewritten by this cut; old layout files
get cleaned by deploy's --delete-after)
- build_releases_index: dropdown simplified to "latest stable +
pinned versions"; channels-explainer section removed; tool cards +
CTA URLs point at canonical <tool>.html / zddc-server_<plat>;
composer emits "stable" sentinel for `apps:` entries
- Fix the acl:{allow:[...]} footgun in the apps_pubkey example
apps.go:
- isValidChannelOrVersion: accept only "stable" + exact X.Y.Z
(drop alpha/beta and partial pins v0.0/v0)
- normalizeChannel: same
- Resolve URL composition: stable → canonical <prefix>/<app>.html
(no _stable_ suffix), exact-version → <prefix>/<app>_v<X.Y.Z>.html
- Tests rewritten to match (beta/alpha replaced with v0.0.4 / stable;
a new TestParseSpec_RejectsLegacyChannelsAndPartialPins locks in
that the removed forms now error)
browse/build.sh: gate promote_release on $is_release like every other
tool's build.sh (longstanding inconsistency that errored under the new
promote_release case-statement).
freshen-channel: deleted (no channels to freshen).
Net: -254 lines, all green on full `go test ./...`. Dev build verified
via `./build` (no-arg) — new label format "v<next>-dev · <ts> · <sha>".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
784ed21a34
commit
bdd14609d1
9 changed files with 378 additions and 632 deletions
|
|
@ -116,4 +116,7 @@ echo "Wrote $output_html"
|
||||||
|
|
||||||
# Promote AFTER the dist file exists so promote_release can copy from
|
# Promote AFTER the dist file exists so promote_release can copy from
|
||||||
# $output_html. (The order matters — _promote_stable does cp $output_html ...)
|
# $output_html. (The order matters — _promote_stable does cp $output_html ...)
|
||||||
promote_release "$tool"
|
# Only fires on a release cut; plain dev builds leave release-output alone.
|
||||||
|
if [ "$is_release" = "1" ]; then
|
||||||
|
promote_release "$tool"
|
||||||
|
fi
|
||||||
|
|
|
||||||
348
build
348
build
|
|
@ -6,68 +6,68 @@ set -eu
|
||||||
# ./build dev build: assemble tool dist/, cross-compile
|
# ./build dev build: assemble tool dist/, cross-compile
|
||||||
# zddc-server binaries. Nothing else is touched
|
# zddc-server binaries. Nothing else is touched
|
||||||
# — no release artifacts produced, no deploy,
|
# — no release artifacts produced, no deploy,
|
||||||
# and zddc/internal/apps/embedded/ is left alone
|
# zddc/internal/apps/embedded/ is left alone
|
||||||
# (binary will embed whatever the last beta or
|
# (binary will embed whatever the last beta or
|
||||||
# stable cut committed there).
|
# stable cut committed there).
|
||||||
# ./build alpha cut alpha: produce a complete release bundle
|
# ./build beta internal SHA snapshot for the BMC dev chart.
|
||||||
# in dist/release-output/ (cascades nothing).
|
# Updates embedded/ with current tool HTMLs +
|
||||||
# Like dev, embedded/ is NOT updated — the
|
# makes a `chore(embedded): cut v<X.Y.Z>-beta`
|
||||||
# invariant is that alpha labels are never baked
|
# commit; the chart's appVersion pins to that
|
||||||
# into the binary.
|
# SHA via Dockerfile parsing. NO public
|
||||||
# ./build beta cut beta (cascades alpha → beta). Updates
|
# artifact in dist/release-output/.
|
||||||
# embedded/ with beta-labeled tool HTMLs and
|
# ./build release cut coordinated stable. Updates embedded/
|
||||||
# commits them — the dev image (which builds
|
# with stable-labeled bytes, makes a release
|
||||||
# from main) ships those bytes.
|
# commit, tags all 8 artifacts at that commit,
|
||||||
# ./build release cut coordinated stable (cascades alpha + beta
|
# writes <tool>_v<X.Y.Z>.html + <tool>.html
|
||||||
# → new stable; updates embedded/ with stable
|
# symlink for every tool and the zddc-server
|
||||||
# labels, makes a release commit, tags all
|
# per-platform binaries into
|
||||||
# seven tools at that commit). Prod images
|
# dist/release-output/.
|
||||||
# (which build from the latest stable tag)
|
|
||||||
# ship those bytes.
|
|
||||||
# ./build release X.Y.Z same, explicit version.
|
# ./build release X.Y.Z same, explicit version.
|
||||||
# ./build help this message.
|
# ./build help this message.
|
||||||
#
|
#
|
||||||
# Lockstep: every channel/release cut bumps all seven tools (6 HTML +
|
# Lockstep: every release cut bumps all 8 tools (7 HTML + zddc-server)
|
||||||
# zddc-server) together. Coordinated next-stable = max(latest tag) + 1.
|
# together. Coordinated next-stable = max(latest tag) + 1.
|
||||||
#
|
#
|
||||||
# Channel/release cuts write a complete intended-live snapshot to
|
# Stable release cuts write a complete intended-live snapshot to
|
||||||
# ${ZDDC_DEPLOY_RELEASES_DIR:-$SCRIPT_DIR/dist/release-output}. The build
|
# ${ZDDC_DEPLOY_RELEASES_DIR:-$SCRIPT_DIR/dist/release-output}. The build
|
||||||
# does NOT touch the live site — run `./deploy` (or `./deploy --releases`)
|
# does NOT touch the live site — run `./deploy --releases` to rsync the
|
||||||
# to rsync the snapshot into /srv/zddc/. The snapshot is built by seeding
|
# snapshot into /srv/zddc/. The snapshot is seeded from the current
|
||||||
# from the current live state (so cascades and the verifier see a
|
# live state's per-version files (so older immutable artifacts are
|
||||||
# complete world), then mutating the channel(s) being cut on top.
|
# preserved), then this cut's <tool>.html canonical symlinks + new
|
||||||
|
# per-version file are written on top.
|
||||||
#
|
#
|
||||||
# Bake-in invariant (what zddc-server's binary embeds via //go:embed):
|
# Bake-in invariant (what zddc-server's binary embeds via //go:embed):
|
||||||
# - prod image (Dockerfile.prod, ZDDC_REF=stable): always stable bytes
|
# - prod image (Dockerfile.prod): always stable bytes — chart's
|
||||||
# - dev image (Dockerfile, ZDDC_REF=main): stable OR beta bytes
|
# Dockerfile.prod fetches the source at the latest
|
||||||
# (whatever last beta/
|
# zddc-server-vX.Y.Z tag.
|
||||||
# stable cut wrote)
|
# - dev image (Dockerfile): stable OR beta-snapshot bytes — the
|
||||||
# - alpha is NEVER baked in. Active dev iteration happens via the tool's
|
# chart's appVersion is set to either "X.Y.Z" (stable)
|
||||||
# local dist/<tool>.html, not via the binary's embedded copy.
|
# or "X.Y.Z-beta-<sha>" (snapshot), and Dockerfile
|
||||||
|
# fetches that ref. Dev builds (`./build` no-arg) do
|
||||||
|
# NOT touch embedded/, so the binary's baked copy stays
|
||||||
|
# at whatever the last beta or stable cut wrote.
|
||||||
|
|
||||||
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
|
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
|
||||||
|
|
||||||
# Source build-lib.sh once at the top level so the helpers it provides
|
# 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,
|
# (promote_zddc_server, write_zddc_server_stubs_all,
|
||||||
# _coordinated_next_stable) are in scope. Each tool's build.sh sources it
|
# _coordinated_next_stable) are in scope. Each tool's build.sh sources
|
||||||
# again — that's a no-op on already-defined functions.
|
# it again — that's a no-op on already-defined functions.
|
||||||
root_dir="$SCRIPT_DIR"
|
root_dir="$SCRIPT_DIR"
|
||||||
. "$SCRIPT_DIR/shared/build-lib.sh"
|
. "$SCRIPT_DIR/shared/build-lib.sh"
|
||||||
|
|
||||||
# --- Parse subcommand ------------------------------------------------------
|
# --- Parse subcommand ------------------------------------------------------
|
||||||
# RELEASE_CHANNEL empty means dev mode (build only, no website worktree
|
# RELEASE_CHANNEL empty means dev mode (build only, no release output);
|
||||||
# writes); set means a channel/release cut that promotes to the website
|
# "beta" means an internal SHA snapshot (regenerate embedded/ + commit,
|
||||||
# worktree under $ZDDC_DEPLOY_RELEASES_DIR.
|
# no public artifact); "stable" means a coordinated release cut that
|
||||||
|
# writes to dist/release-output/.
|
||||||
RELEASE_CHANNEL=""
|
RELEASE_CHANNEL=""
|
||||||
RELEASE_VERSION=""
|
RELEASE_VERSION=""
|
||||||
|
|
||||||
case "${1:-dev}" in
|
case "${1:-dev}" in
|
||||||
dev|build)
|
dev|build)
|
||||||
# Dev build: tool dist/ + zddc-server binaries only. Touches
|
# Dev build: tool dist/ + zddc-server binaries only. Touches
|
||||||
# nothing in the website worktree.
|
# nothing in release-output.
|
||||||
;;
|
|
||||||
alpha)
|
|
||||||
RELEASE_CHANNEL="alpha"
|
|
||||||
;;
|
;;
|
||||||
beta)
|
beta)
|
||||||
RELEASE_CHANNEL="beta"
|
RELEASE_CHANNEL="beta"
|
||||||
|
|
@ -84,7 +84,7 @@ case "${1:-dev}" in
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
help | -h | --help)
|
help | -h | --help)
|
||||||
sed -n '4,22p' "$0" | sed 's/^# \{0,1\}//'
|
sed -n '4,30p' "$0" | sed 's/^# \{0,1\}//'
|
||||||
exit 0
|
exit 0
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
|
|
@ -112,32 +112,38 @@ export ZDDC_DEPLOY_RELEASES_DIR="${ZDDC_DEPLOY_RELEASES_DIR:-$SCRIPT_DIR/dist/re
|
||||||
RELEASES_DIR="$ZDDC_DEPLOY_RELEASES_DIR"
|
RELEASES_DIR="$ZDDC_DEPLOY_RELEASES_DIR"
|
||||||
mkdir -p "$RELEASES_DIR"
|
mkdir -p "$RELEASES_DIR"
|
||||||
|
|
||||||
# When cutting a channel/release, seed RELEASES_DIR from the current live
|
# On a stable cut, seed RELEASES_DIR from the current live site so the
|
||||||
# site so the resulting bundle is a complete intended-live snapshot, not
|
# resulting bundle is a complete intended-live snapshot, not a sparse
|
||||||
# a sparse one-channel diff. Two reasons:
|
# diff. The seed copies the immutable per-version files
|
||||||
# 1. Per-tool promote_release does cascade writes (beta cut → also
|
# (<tool>_v<X.Y.Z>.html, zddc-server_v<X.Y.Z>_<plat>) plus their .sig
|
||||||
# rewrites alpha to track beta; stable cut → resets alpha + beta).
|
# files. The cut then writes this version's new per-version files +
|
||||||
# The cascade itself is deterministic, but downstream artifacts that
|
# refreshes the canonical <tool>.html / zddc-server_<plat> symlinks on
|
||||||
# were NOT touched by this cut (e.g. older versioned files, the
|
# top. `./deploy --releases` (rsync --delete-after) wipes any stale
|
||||||
# other channel mirrors, partial-version symlinks) still need to be
|
# files in /srv/zddc/releases/ that aren't in the bundle.
|
||||||
# present in the bundle so `./deploy --releases` (rsync
|
#
|
||||||
# --delete-after) doesn't wipe them off the live site.
|
# We skip the seed for beta cuts (no public artifacts to produce).
|
||||||
# 2. verify_channel_links cross-checks the full release tree; it
|
# Bootstrap case (no live site yet, or empty live releases dir) is
|
||||||
# flags absent channels as missing. With seeding, a fresh
|
|
||||||
# `dist/release-output/` matches live state, the cut mutates on
|
|
||||||
# top, and the verifier sees a complete world.
|
|
||||||
# Bootstrap case (no live site yet, or live releases dir empty) is
|
|
||||||
# silently skipped — the very first stable cut populates everything.
|
# silently skipped — the very first stable cut populates everything.
|
||||||
if [ -n "$RELEASE_CHANNEL" ]; then
|
if [ "$RELEASE_CHANNEL" = "stable" ]; then
|
||||||
LIVE_RELEASES="${ZDDC_LIVE_DIR:-/srv/zddc}/releases"
|
LIVE_RELEASES="${ZDDC_LIVE_DIR:-/srv/zddc}/releases"
|
||||||
if [ -d "$LIVE_RELEASES" ] && [ -n "$(ls -A "$LIVE_RELEASES" 2>/dev/null)" ]; then
|
if [ -d "$LIVE_RELEASES" ] && [ -n "$(ls -A "$LIVE_RELEASES" 2>/dev/null)" ]; then
|
||||||
echo "=== Seeding $RELEASES_DIR from $LIVE_RELEASES ==="
|
echo "=== Seeding $RELEASES_DIR from $LIVE_RELEASES (per-version artifacts only) ==="
|
||||||
rm -rf "$RELEASES_DIR"
|
rm -rf "$RELEASES_DIR"
|
||||||
mkdir -p "$RELEASES_DIR"
|
mkdir -p "$RELEASES_DIR"
|
||||||
# cp -a preserves the symlink graph (channel mirrors +
|
# Copy per-version immutable files + their .sig sidecars. The
|
||||||
# _v<X.Y> / _v<X> partial-version pins) so cascade decisions
|
# canonical <tool>.html / zddc-server_<plat> symlinks will be
|
||||||
# downstream see the same world the live site has.
|
# rewritten by this cut and old channel/partial-pin files from
|
||||||
cp -a "$LIVE_RELEASES/." "$RELEASES_DIR/"
|
# the previous layout (if any still live there) will be cleaned
|
||||||
|
# up by deploy's --delete-after rsync.
|
||||||
|
find "$LIVE_RELEASES" -maxdepth 1 -type f \( \
|
||||||
|
-name '*_v*.html' -o \
|
||||||
|
-name '*_v*.html.sig' -o \
|
||||||
|
-name 'zddc-server_v*' \
|
||||||
|
\) -exec cp -a '{}' "$RELEASES_DIR/" \;
|
||||||
|
# Also seed the public key (it lives at the releases root).
|
||||||
|
if [ -f "$LIVE_RELEASES/pubkey.pem" ]; then
|
||||||
|
cp -a "$LIVE_RELEASES/pubkey.pem" "$RELEASES_DIR/"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
@ -183,11 +189,10 @@ echo "Wrote zddc/dist/web/{index,archive,transmittal,classifier,form,tables,brow
|
||||||
|
|
||||||
# Mirror the cascade-served HTMLs into the apps embed source dir so the
|
# Mirror the cascade-served HTMLs into the apps embed source dir so the
|
||||||
# next `go build` of zddc-server picks them up via //go:embed. ONLY happens
|
# next `go build` of zddc-server picks them up via //go:embed. ONLY happens
|
||||||
# on a beta or stable cut — that's the project invariant: alpha labels are
|
# on a beta or stable cut — beta cuts feed the dev image (chart pins by
|
||||||
# never baked into the binary, beta labels go to the dev image (which builds
|
# SHA to the embedded-commit), stable cuts feed the prod image (chart
|
||||||
# from main), and stable labels go to prod (which builds from the latest
|
# pins to the tag). Plain `./build` leaves embedded files untouched —
|
||||||
# stable tag). Plain `./build` and `./build alpha` leave the embedded files
|
# whatever the last beta or stable cut committed stays in place.
|
||||||
# untouched; whatever the last beta/stable cut committed remains in place.
|
|
||||||
EMBED_DIR="$SCRIPT_DIR/zddc/internal/apps/embedded"
|
EMBED_DIR="$SCRIPT_DIR/zddc/internal/apps/embedded"
|
||||||
if [ "$RELEASE_CHANNEL" = "beta" ] || [ "$RELEASE_CHANNEL" = "stable" ]; then
|
if [ "$RELEASE_CHANNEL" = "beta" ] || [ "$RELEASE_CHANNEL" = "stable" ]; then
|
||||||
mkdir -p "$EMBED_DIR"
|
mkdir -p "$EMBED_DIR"
|
||||||
|
|
@ -304,10 +309,12 @@ echo " binary version: $ZDDC_BINARY_VERSION"
|
||||||
'
|
'
|
||||||
|
|
||||||
# --- Sign release artifacts -----------------------------------------------
|
# --- Sign release artifacts -----------------------------------------------
|
||||||
# After a channel/release cut has populated $RELEASES_DIR with the actual
|
# After a stable cut has populated $RELEASES_DIR with the actual bytes
|
||||||
# bytes for this build, walk the dir and produce a detached Ed25519 .sig
|
# for this build, walk the dir and produce a detached Ed25519 .sig
|
||||||
# alongside every real artifact. Symlinks (channel mirrors, partial-version
|
# alongside every immutable per-version artifact. Canonical symlinks
|
||||||
# pins) skip — the .sig at the symlink's target is what counts.
|
# (<tool>.html, zddc-server_<plat>) skip — the .sig at the symlink's
|
||||||
|
# target is what counts, and a companion .sig symlink (also written
|
||||||
|
# by promote_release) chains the canonical .sig URL to that target.
|
||||||
#
|
#
|
||||||
# Operators verify with stdlib openssl:
|
# Operators verify with stdlib openssl:
|
||||||
#
|
#
|
||||||
|
|
@ -341,14 +348,15 @@ sign_release_artifacts() {
|
||||||
fi
|
fi
|
||||||
# Collect the artifact list first so the signing loop runs in this
|
# Collect the artifact list first so the signing loop runs in this
|
||||||
# shell (no subshell counter scope issue). find: real files only
|
# shell (no subshell counter scope issue). find: real files only
|
||||||
# (-P, the default), matching <tool>_v*.html, <tool>_<channel>.html,
|
# (-P, the default), matching <tool>_v*.html and
|
||||||
# and zddc-server_v*_<plat>(.exe). Excludes the index, stub pages,
|
# zddc-server_v*_<plat>(.exe). The canonical symlinks (<tool>.html /
|
||||||
# and any pre-existing .sig files.
|
# zddc-server_<plat>) don't get separate .sig files — verification
|
||||||
|
# follows the symlink to the immutable per-version file whose .sig
|
||||||
|
# is signed below. Excludes the index, stub pages, and pre-existing
|
||||||
|
# .sig files.
|
||||||
_list=$(find "$_dir" -maxdepth 1 -type f \( \
|
_list=$(find "$_dir" -maxdepth 1 -type f \( \
|
||||||
-name '*_v*.html' -o \
|
-name '*_v*.html' -o \
|
||||||
-name '*_stable.html' -o -name '*_beta.html' -o -name '*_alpha.html' -o \
|
-name 'zddc-server_v*' \
|
||||||
-name 'zddc-server_v*' -o \
|
|
||||||
-name 'zddc-server_stable_*' -o -name 'zddc-server_beta_*' -o -name 'zddc-server_alpha_*' \
|
|
||||||
\) ! -name '*.sig' ! -name 'index.html' ! -name 'zddc-server_*.html' 2>/dev/null)
|
\) ! -name '*.sig' ! -name 'index.html' ! -name 'zddc-server_*.html' 2>/dev/null)
|
||||||
|
|
||||||
_signed=0
|
_signed=0
|
||||||
|
|
@ -377,22 +385,22 @@ sign_release_artifacts() {
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- Promote zddc-server release artifacts ---------------------------------
|
# --- Promote zddc-server release artifacts ---------------------------------
|
||||||
# On a channel/release cut, copy the freshly cross-compiled binaries to
|
# On a stable cut, copy the freshly cross-compiled binaries to the
|
||||||
# the website worktree's releases/ under their canonical names +
|
# release-output bundle under their canonical names + symlinks.
|
||||||
# symlinks. promote_zddc_server also re-runs write_zddc_server_stubs_all
|
# promote_zddc_server also re-runs write_zddc_server_stubs_all
|
||||||
# internally, so the matrix-cell stub pages get regenerated in the same
|
# internally, so the per-version + canonical stub pages get regenerated
|
||||||
# call. On a plain dev build, skip — we don't touch the worktree.
|
# in the same call. Beta cuts produce no public binary artifact.
|
||||||
if [ -n "$RELEASE_CHANNEL" ]; then
|
if [ "$RELEASE_CHANNEL" = "stable" ]; then
|
||||||
echo ""
|
echo ""
|
||||||
echo "=== Promoting zddc-server $RELEASE_CHANNEL release ==="
|
echo "=== Promoting zddc-server stable release ==="
|
||||||
promote_zddc_server "$RELEASE_CHANNEL" "$RELEASE_VERSION" "$RELEASES_DIR" "$SCRIPT_DIR/zddc/dist"
|
promote_zddc_server "stable" "$RELEASE_VERSION" "$RELEASES_DIR" "$SCRIPT_DIR/zddc/dist"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Latest stable version, by following archive_stable.html → versioned target.
|
# Latest stable version, by following archive.html → versioned target.
|
||||||
# Returns "" if no stable cut exists yet (bootstrap state). All HTML tools
|
# 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.
|
# move in lockstep so any one of them is a valid probe; archive is canonical.
|
||||||
_latest_stable_version() {
|
_latest_stable_version() {
|
||||||
_link="$RELEASES_DIR/archive_stable.html"
|
_link="$RELEASES_DIR/archive.html"
|
||||||
[ -L "$_link" ] || return 0
|
[ -L "$_link" ] || return 0
|
||||||
_target=$(readlink "$_link")
|
_target=$(readlink "$_link")
|
||||||
# archive_v0.0.8.html → 0.0.8
|
# archive_v0.0.8.html → 0.0.8
|
||||||
|
|
@ -403,18 +411,6 @@ _latest_stable_version() {
|
||||||
esac
|
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
|
# Regenerate website/releases/index.html as the action-first install
|
||||||
# guide (not a matrix). The page guides users to either self-host the
|
# guide (not a matrix). The page guides users to either self-host the
|
||||||
# server or download individual tools, with one version dropdown that
|
# server or download individual tools, with one version dropdown that
|
||||||
|
|
@ -450,9 +446,6 @@ build_releases_index() {
|
||||||
| sort -Vr
|
| sort -Vr
|
||||||
)
|
)
|
||||||
|
|
||||||
_alpha_active="0"; _channel_is_active alpha && _alpha_active="1"
|
|
||||||
_beta_active="0"; _channel_is_active beta && _beta_active="1"
|
|
||||||
|
|
||||||
{
|
{
|
||||||
cat <<HEAD
|
cat <<HEAD
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
|
|
@ -503,39 +496,21 @@ build_releases_index() {
|
||||||
<select id="version-picker">
|
<select id="version-picker">
|
||||||
HEAD
|
HEAD
|
||||||
|
|
||||||
# Channels — selectable directly so users can copy the channel-
|
# "latest" — the canonical URL <tool>.html, a symlink that always
|
||||||
# mirror URLs (e.g. archive_stable.html) for bookmarks. stable is
|
# follows the most recently cut stable. Use this when you want
|
||||||
# the default. The label tells the truth about the channel's
|
# auto-updates. Default option so the page works fully without JS.
|
||||||
# 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
|
if [ -n "$_latest" ] && [ "$_latest" != "0.0.0" ]; then
|
||||||
printf ' <option value="stable" selected>stable — currently v%s</option>\n' "$_latest"
|
printf ' <option value="latest" selected>latest stable — currently v%s</option>\n' "$_latest"
|
||||||
else
|
else
|
||||||
printf ' <option value="stable" selected>stable</option>\n'
|
printf ' <option value="latest" selected>latest stable</option>\n'
|
||||||
fi
|
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
|
# Pinned per-version, latest first. Immutable URLs — pin one
|
||||||
# for reproducibility. No "(current stable)" suffix because the
|
# into your archive when you depend on a specific behavior.
|
||||||
# stable channel above already covers that.
|
|
||||||
printf ' <optgroup label="Pinned versions (immutable URLs)">\n'
|
|
||||||
printf '%s\n' "$_all_versions" | while read -r _v; do
|
printf '%s\n' "$_all_versions" | while read -r _v; do
|
||||||
[ -n "$_v" ] || continue
|
[ -n "$_v" ] || continue
|
||||||
printf ' <option value="v%s">v%s</option>\n' "$_v" "$_v"
|
printf ' <option value="v%s">v%s (pinned)</option>\n' "$_v" "$_v"
|
||||||
done
|
done
|
||||||
printf ' </optgroup>\n'
|
|
||||||
|
|
||||||
cat <<'PICKER_END'
|
cat <<'PICKER_END'
|
||||||
</select>
|
</select>
|
||||||
|
|
@ -552,24 +527,24 @@ PICKER_END
|
||||||
# at least once. Until then, show an honest "not yet released"
|
# at least once. Until then, show an honest "not yet released"
|
||||||
# placeholder rather than dangling download buttons.
|
# placeholder rather than dangling download buttons.
|
||||||
_zs_published="0"
|
_zs_published="0"
|
||||||
if [ -e "$RELEASES_DIR/zddc-server_stable_linux-amd64" ]; then
|
if [ -e "$RELEASES_DIR/zddc-server_linux-amd64" ]; then
|
||||||
_zs_published="1"
|
_zs_published="1"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "$_zs_published" = "1" ]; then
|
if [ "$_zs_published" = "1" ]; then
|
||||||
# Default href is the channel-mirror URL (zddc-server_stable_<plat>)
|
# Default href is the canonical per-platform URL
|
||||||
# because "stable" is the dropdown's selected option. Picking a
|
# (zddc-server_<plat>), a symlink that always points at the
|
||||||
# pinned version from the dropdown rewrites these to the
|
# latest stable. Picking a pinned version from the dropdown
|
||||||
# immutable per-version URL via the IIFE.
|
# rewrites these to the immutable per-version URL via JS.
|
||||||
printf ' <a class="dl-primary"\n'
|
printf ' <a class="dl-primary"\n'
|
||||||
printf ' data-tool="zddc-server"\n'
|
printf ' data-tool="zddc-server"\n'
|
||||||
printf ' data-platform="linux-amd64"\n'
|
printf ' data-platform="linux-amd64"\n'
|
||||||
printf ' href="zddc-server_stable_linux-amd64"\n'
|
printf ' href="zddc-server_linux-amd64"\n'
|
||||||
printf ' id="dl-primary-binary">\n'
|
printf ' id="dl-primary-binary">\n'
|
||||||
printf ' <span class="dl-icon">⬇</span>\n'
|
printf ' <span class="dl-icon">⬇</span>\n'
|
||||||
printf ' <span>Download <span id="dl-primary-platlabel">for Linux (x86_64)</span></span>\n'
|
printf ' <span>Download <span id="dl-primary-platlabel">for Linux (x86_64)</span></span>\n'
|
||||||
printf ' </a>\n'
|
printf ' </a>\n'
|
||||||
printf ' <span class="dl-primary-meta" id="dl-primary-meta">zddc-server_stable_linux-amd64</span>\n'
|
printf ' <span class="dl-primary-meta" id="dl-primary-meta">zddc-server_linux-amd64</span>\n'
|
||||||
|
|
||||||
printf ' <div class="dl-secondary-row" id="dl-others">\n'
|
printf ' <div class="dl-secondary-row" id="dl-others">\n'
|
||||||
printf ' <span>Other platforms:</span>\n'
|
printf ' <span>Other platforms:</span>\n'
|
||||||
|
|
@ -581,7 +556,7 @@ PICKER_END
|
||||||
_label="${_entry#*|}"
|
_label="${_entry#*|}"
|
||||||
_suffix=""
|
_suffix=""
|
||||||
case "$_plat" in *windows*) _suffix=".exe" ;; esac
|
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' \
|
printf ' <a data-tool="zddc-server" data-platform="%s" href="zddc-server_%s%s">%s</a>\n' \
|
||||||
"$_plat" "$_plat" "$_suffix" "$_label"
|
"$_plat" "$_plat" "$_suffix" "$_label"
|
||||||
done
|
done
|
||||||
printf ' </div>\n'
|
printf ' </div>\n'
|
||||||
|
|
@ -622,9 +597,9 @@ PATH_B_OPEN
|
||||||
_rest="${_entry#*|}"
|
_rest="${_entry#*|}"
|
||||||
_name="${_rest%%|*}"
|
_name="${_rest%%|*}"
|
||||||
_desc="${_rest#*|}"
|
_desc="${_rest#*|}"
|
||||||
# Default href is the stable-channel mirror; the dropdown
|
# Default href is the canonical symlink <tool>.html; the
|
||||||
# rewires these per selection.
|
# dropdown rewires these per selection.
|
||||||
printf ' <a class="tool-card" data-tool="%s" href="%s_stable.html">\n' "$_t" "$_t"
|
printf ' <a class="tool-card" data-tool="%s" href="%s.html">\n' "$_t" "$_t"
|
||||||
printf ' <span class="tool-card__title">%s</span>\n' "$_name"
|
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__desc">%s</span>\n' "$_desc"
|
||||||
printf ' <span class="tool-card__link">Download →</span>\n'
|
printf ' <span class="tool-card__link">Download →</span>\n'
|
||||||
|
|
@ -717,12 +692,12 @@ PIN_MID
|
||||||
</div>
|
</div>
|
||||||
<div class="pin-card">
|
<div class="pin-card">
|
||||||
<h3>Verify a download</h3>
|
<h3>Verify a download</h3>
|
||||||
<p>Each artifact has a matching <code class="inline">.sig</code> file alongside it (<code class="inline">archive_stable.html</code> → <code class="inline">archive_stable.html.sig</code>, etc.). Fetch both, then:</p>
|
<p>Each artifact has a matching <code class="inline">.sig</code> file alongside it (<code class="inline">archive.html</code> → <code class="inline">archive.html.sig</code>, etc.). Fetch both, then:</p>
|
||||||
<pre>curl -O https://zddc.varasys.io/releases/archive_stable.html
|
<pre>curl -O https://zddc.varasys.io/releases/archive.html
|
||||||
curl -O https://zddc.varasys.io/releases/archive_stable.html.sig
|
curl -O https://zddc.varasys.io/releases/archive.html.sig
|
||||||
openssl pkeyutl -verify -pubin -inkey pubkey.pem \
|
openssl pkeyutl -verify -pubin -inkey pubkey.pem \
|
||||||
-rawin -in archive_stable.html \
|
-rawin -in archive.html \
|
||||||
-sigfile archive_stable.html.sig</pre>
|
-sigfile archive.html.sig</pre>
|
||||||
<p style="font-size: 0.85rem;">Output is <code class="inline">Signature Verified Successfully</code> on a clean download. Any other output (or no output and a non-zero exit) means the bytes do not match the published signature — do not trust them.</p>
|
<p style="font-size: 0.85rem;">Output is <code class="inline">Signature Verified Successfully</code> on a clean download. Any other output (or no output and a non-zero exit) means the bytes do not match the published signature — do not trust them.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -745,7 +720,8 @@ ZDDC_ROOT=/srv/zddc ./zddc-server</pre>
|
||||||
<pre># <ZDDC_ROOT>/.zddc
|
<pre># <ZDDC_ROOT>/.zddc
|
||||||
admins: [you@yourcompany.com]
|
admins: [you@yourcompany.com]
|
||||||
acl:
|
acl:
|
||||||
allow: ["*@yourcompany.com"]
|
permissions:
|
||||||
|
'*@yourcompany.com': r
|
||||||
apps_pubkey: |
|
apps_pubkey: |
|
||||||
-----BEGIN PUBLIC KEY-----
|
-----BEGIN PUBLIC KEY-----
|
||||||
MCowBQYDK2VwAyEAXXaxIUIyOFnhD1eZs02nEt3xZ8izOi7bURFcpJ9iWZY=
|
MCowBQYDK2VwAyEAXXaxIUIyOFnhD1eZs02nEt3xZ8izOi7bURFcpJ9iWZY=
|
||||||
|
|
@ -756,25 +732,6 @@ apps_pubkey: |
|
||||||
<p style="margin-top: var(--spacing-md); font-size: 0.9rem; color: var(--color-text-muted);">When configured, the resolver fetches the <code class="inline">.sig</code> automatically on every URL-pinned <code class="inline">apps:</code> entry and rejects any unsigned or invalid-signature artifact, falling back to the embedded copy. Operators enforcing signature verification on locally-saved artifacts (Path A, "drop a copy into your archive") run the <code class="inline">openssl</code> verify command above as part of their save workflow.</p>
|
<p style="margin-top: var(--spacing-md); font-size: 0.9rem; color: var(--color-text-muted);">When configured, the resolver fetches the <code class="inline">.sig</code> automatically on every URL-pinned <code class="inline">apps:</code> entry and rejects any unsigned or invalid-signature artifact, falling back to the embedded copy. Operators enforcing signature verification on locally-saved artifacts (Path A, "drop a copy into your archive") run the <code class="inline">openssl</code> verify command above as part of their save workflow.</p>
|
||||||
</section>
|
</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>
|
</main>
|
||||||
|
|
||||||
<footer class="site-footer">
|
<footer class="site-footer">
|
||||||
|
|
@ -809,18 +766,21 @@ apps_pubkey: |
|
||||||
var primaryMeta = document.getElementById('dl-primary-meta');
|
var primaryMeta = document.getElementById('dl-primary-meta');
|
||||||
var others = document.getElementById('dl-others');
|
var others = document.getElementById('dl-others');
|
||||||
|
|
||||||
function isChannel(v) {
|
|
||||||
return v === 'stable' || v === 'beta' || v === 'alpha';
|
|
||||||
}
|
|
||||||
function platBinaryName(slug, plat) {
|
function platBinaryName(slug, plat) {
|
||||||
// slug is a channel name ("stable") or a pinned version ("v0.0.8").
|
// slug === "latest" → canonical symlink zddc-server_<plat>;
|
||||||
// The on-disk name uses the slug as-is in both cases since the
|
// slug === "v<X.Y.Z>" → immutable per-version zddc-server_v<X.Y.Z>_<plat>.
|
||||||
// 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' : '';
|
var suf = (plat.indexOf('windows') === 0) ? '.exe' : '';
|
||||||
|
if (slug === 'latest') {
|
||||||
|
return 'zddc-server_' + plat + suf;
|
||||||
|
}
|
||||||
return 'zddc-server_' + slug + '_' + plat + suf;
|
return 'zddc-server_' + slug + '_' + plat + suf;
|
||||||
}
|
}
|
||||||
function htmlAssetName(tool, slug) {
|
function htmlAssetName(tool, slug) {
|
||||||
|
// slug === "latest" → canonical symlink <tool>.html;
|
||||||
|
// slug === "v<X.Y.Z>" → immutable per-version <tool>_v<X.Y.Z>.html.
|
||||||
|
if (slug === 'latest') {
|
||||||
|
return tool + '.html';
|
||||||
|
}
|
||||||
return tool + '_' + slug + '.html';
|
return tool + '_' + slug + '.html';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -838,21 +798,25 @@ apps_pubkey: |
|
||||||
}
|
}
|
||||||
|
|
||||||
// Single source of truth: the dropdown's current value drives every
|
// Single source of truth: the dropdown's current value drives every
|
||||||
// download link's href. Static markup ships with the stable-channel
|
// download link's href. Static markup ships with the canonical
|
||||||
// mirror (`<tool>_stable.html`, `zddc-server_stable_<plat>`) so the
|
// URLs (`<tool>.html`, `zddc-server_<plat>`) so the page works
|
||||||
// page works fully without JS — the JS just keeps things in sync
|
// fully without JS — the JS just keeps things in sync when the
|
||||||
// when the user picks a different channel or pins a version.
|
// user pins a specific version.
|
||||||
var picker = document.getElementById('version-picker');
|
var picker = document.getElementById('version-picker');
|
||||||
if (!picker) return;
|
if (!picker) return;
|
||||||
|
|
||||||
function rewire(slug) {
|
function rewire(slug) {
|
||||||
// slug ∈ {"stable", "beta", "alpha"} | "v<X.Y.Z>". Every link with
|
// slug === "latest" | "v<X.Y.Z>". Every link with a data-tool
|
||||||
// a data-tool attribute is a download URL the dropdown owns.
|
// attribute is a download URL the dropdown owns.
|
||||||
document.querySelectorAll('[data-tool]').forEach(function(a) {
|
document.querySelectorAll('[data-tool]').forEach(function(a) {
|
||||||
var tool = a.dataset.tool;
|
var tool = a.dataset.tool;
|
||||||
var plat = a.dataset.platform || '';
|
var plat = a.dataset.platform || '';
|
||||||
if (tool === 'zddc-server') {
|
if (tool === 'zddc-server') {
|
||||||
a.href = plat ? platBinaryName(slug, plat) : ('zddc-server_' + slug + '.html');
|
if (plat) {
|
||||||
|
a.href = platBinaryName(slug, plat);
|
||||||
|
} else {
|
||||||
|
a.href = (slug === 'latest') ? 'zddc-server.html' : ('zddc-server_' + slug + '.html');
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
a.href = htmlAssetName(tool, slug);
|
a.href = htmlAssetName(tool, slug);
|
||||||
}
|
}
|
||||||
|
|
@ -891,13 +855,17 @@ apps_pubkey: |
|
||||||
var optionsHTML = picker.innerHTML;
|
var optionsHTML = picker.innerHTML;
|
||||||
selects.forEach(function(sel) {
|
selects.forEach(function(sel) {
|
||||||
sel.innerHTML = optionsHTML;
|
sel.innerHTML = optionsHTML;
|
||||||
sel.value = 'stable'; // default per-app
|
sel.value = 'latest'; // default per-app
|
||||||
});
|
});
|
||||||
|
|
||||||
function rebuild() {
|
function rebuild() {
|
||||||
|
// The picker uses "latest" as its sentinel; the .zddc apps:
|
||||||
|
// cascade resolves "stable" the same way (follow the upstream
|
||||||
|
// canonical URL), so emit "stable" for the YAML user.
|
||||||
var lines = ['apps:'];
|
var lines = ['apps:'];
|
||||||
selects.forEach(function(sel) {
|
selects.forEach(function(sel) {
|
||||||
lines.push(' ' + sel.dataset.app + ': ' + sel.value);
|
var val = (sel.value === 'latest') ? 'stable' : sel.value;
|
||||||
|
lines.push(' ' + sel.dataset.app + ': ' + val);
|
||||||
});
|
});
|
||||||
textarea.value = lines.join('\n') + '\n';
|
textarea.value = lines.join('\n') + '\n';
|
||||||
}
|
}
|
||||||
|
|
@ -934,9 +902,9 @@ PIN_END
|
||||||
echo "Wrote $_out"
|
echo "Wrote $_out"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Matrix index + verifier only run when we touched the website
|
# Sign artifacts + regenerate releases/index.html on stable cuts.
|
||||||
# worktree. Dev builds leave the worktree alone.
|
# Beta cuts produce no public artifact, so nothing to sign or index.
|
||||||
if [ -n "$RELEASE_CHANNEL" ]; then
|
if [ "$RELEASE_CHANNEL" = "stable" ]; then
|
||||||
echo ""
|
echo ""
|
||||||
echo "=== Signing release artifacts ==="
|
echo "=== Signing release artifacts ==="
|
||||||
sign_release_artifacts "$RELEASES_DIR"
|
sign_release_artifacts "$RELEASES_DIR"
|
||||||
|
|
@ -944,10 +912,6 @@ if [ -n "$RELEASE_CHANNEL" ]; then
|
||||||
echo ""
|
echo ""
|
||||||
echo "=== Building releases/index.html ==="
|
echo "=== Building releases/index.html ==="
|
||||||
build_releases_index
|
build_releases_index
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "=== Verifying channel links ==="
|
|
||||||
verify_channel_links "$RELEASES_DIR"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# --- Embedded commit (stable + beta cuts) ---------------------------------
|
# --- Embedded commit (stable + beta cuts) ---------------------------------
|
||||||
|
|
@ -956,12 +920,11 @@ fi
|
||||||
#
|
#
|
||||||
# 1. Stable: the next tag block needs HEAD to point at the bytes the
|
# 1. Stable: the next tag block needs HEAD to point at the bytes the
|
||||||
# stable binary will serve. Without this commit, tags would land on
|
# stable binary will serve. Without this commit, tags would land on
|
||||||
# the source-side commit (with alpha-dirty embedded/*) and prod
|
# the source-side commit (with stale embedded/*) and prod images
|
||||||
# images compiled from `git checkout zddc-server-vX.Y.Z` would
|
# compiled from `git checkout zddc-server-vX.Y.Z` would ship stale
|
||||||
# ship alpha bytes. (Original justification — preserved.)
|
# bytes. (Original justification — preserved.)
|
||||||
#
|
#
|
||||||
# 2. Beta: the dev pipeline pins the chart's appVersion to a SHA
|
# 2. Beta: the dev chart pipeline pins appVersion to a SHA. For that
|
||||||
# (.forgejo/scripts/notify-chart-bump.sh reads HEAD). For that
|
|
||||||
# pin to point at a SHA where embedded/* matches what the binary
|
# pin to point at a SHA where embedded/* matches what the binary
|
||||||
# will serve, HEAD has to advance past the source-side commit.
|
# will serve, HEAD has to advance past the source-side commit.
|
||||||
# Without this commit, the chart pin lags one commit and the dev
|
# Without this commit, the chart pin lags one commit and the dev
|
||||||
|
|
@ -1037,7 +1000,8 @@ if [ -z "$RELEASE_CHANNEL" ]; then
|
||||||
echo " tool/dist/*.html ready"
|
echo " tool/dist/*.html ready"
|
||||||
echo " zddc/dist/zddc-server-* binaries ready"
|
echo " zddc/dist/zddc-server-* binaries ready"
|
||||||
echo ""
|
echo ""
|
||||||
echo "To cut alpha into a deployable bundle: ./build alpha"
|
echo "For an internal SHA snapshot (BMC dev chart): ./build beta"
|
||||||
|
echo "To cut a stable release: ./build release"
|
||||||
else
|
else
|
||||||
echo "Cut: $RELEASE_CHANNEL"
|
echo "Cut: $RELEASE_CHANNEL"
|
||||||
if [ -n "$RELEASE_VERSION" ]; then
|
if [ -n "$RELEASE_VERSION" ]; then
|
||||||
|
|
|
||||||
|
|
@ -1,99 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
# =============================================================================
|
|
||||||
# freshen-channel — rebuild a tool's alpha or beta channel from its current
|
|
||||||
# stable tag, so users tracking that channel are never on code older than
|
|
||||||
# current stable.
|
|
||||||
#
|
|
||||||
# Usage:
|
|
||||||
# ./freshen-channel <tool> <channel>
|
|
||||||
# tool archive | transmittal | classifier | browse | landing | form | tables
|
|
||||||
# channel alpha | beta
|
|
||||||
#
|
|
||||||
# Why this exists:
|
|
||||||
# Stable releases do NOT automatically clobber alpha/beta files (see
|
|
||||||
# AGENTS.md "Channel discipline" rule 4). After cutting stable v0.0.5,
|
|
||||||
# users pinned to alpha may be on an older build than current stable —
|
|
||||||
# that violates the stale-channel rule. Run this to drag alpha (or
|
|
||||||
# beta) forward to whatever stable currently is.
|
|
||||||
#
|
|
||||||
# What it does:
|
|
||||||
# 1. Finds the latest <tool>-v* tag.
|
|
||||||
# 2. Creates a temporary git worktree at that tag — does NOT touch
|
|
||||||
# your current branch or working tree.
|
|
||||||
# 3. Runs <tool>/build.sh --release <channel> inside the worktree.
|
|
||||||
# 4. Copies the resulting <tool>_<channel>.html into the main repo's
|
|
||||||
# website/releases/.
|
|
||||||
# 5. Removes the worktree.
|
|
||||||
#
|
|
||||||
# The on-page label of the freshened build will be
|
|
||||||
# `<channel> · <today> · <stable-tag-sha>` — the SHA encodes which
|
|
||||||
# stable was used as the source, so anyone debugging can `git checkout`
|
|
||||||
# that exact commit.
|
|
||||||
#
|
|
||||||
# Note: the build pipeline used is the one AT THE TAG, not the latest
|
|
||||||
# main. That is intentional — pure reproducibility. If you have made
|
|
||||||
# build-system improvements since stable was cut and want the freshen
|
|
||||||
# to use them, cut a new stable that includes those changes first.
|
|
||||||
# =============================================================================
|
|
||||||
set -eu
|
|
||||||
|
|
||||||
TOOL="${1:-}"
|
|
||||||
CHANNEL="${2:-}"
|
|
||||||
|
|
||||||
case "$TOOL" in
|
|
||||||
archive | transmittal | classifier | browse | landing | form | tables) ;;
|
|
||||||
*)
|
|
||||||
echo "usage: $0 <tool> <channel>" >&2
|
|
||||||
echo " tool: archive | transmittal | classifier | browse | landing | form | tables" >&2
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
case "$CHANNEL" in
|
|
||||||
alpha | beta) ;;
|
|
||||||
*)
|
|
||||||
echo "usage: $0 <tool> <channel>" >&2
|
|
||||||
echo " channel: alpha | beta (stable is what you are freshening FROM)" >&2
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
REPO=$(cd "$(dirname "$0")" && pwd)
|
|
||||||
|
|
||||||
# Find the latest stable tag for the tool.
|
|
||||||
LATEST_TAG=$(git -C "$REPO" tag --list "${TOOL}-v*" --sort=-v:refname | head -1)
|
|
||||||
if [ -z "$LATEST_TAG" ]; then
|
|
||||||
echo "error: no stable tag found for ${TOOL} (looking for ${TOOL}-v*)" >&2
|
|
||||||
echo " cut a stable release first: sh ${TOOL}/build.sh --release [version]" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Temporary detached worktree at the stable tag. Cleaned up on exit.
|
|
||||||
WT=$(mktemp -d)
|
|
||||||
cleanup() {
|
|
||||||
git -C "$REPO" worktree remove --force "$WT" >/dev/null 2>&1 || true
|
|
||||||
rm -rf "$WT"
|
|
||||||
}
|
|
||||||
trap cleanup EXIT INT TERM
|
|
||||||
|
|
||||||
echo "Freshening ${TOOL} ${CHANNEL} from ${LATEST_TAG}"
|
|
||||||
git -C "$REPO" worktree add --quiet --detach "$WT" "$LATEST_TAG"
|
|
||||||
|
|
||||||
# Build in the worktree. The tool's build.sh resolves its release dir
|
|
||||||
# from $ZDDC_DEPLOY_RELEASES_DIR (default $REPO/dist/release-output);
|
|
||||||
# pass through whatever the parent process has set so freshen-channel
|
|
||||||
# honors the same target as the regular build.
|
|
||||||
DEPLOY_DIR="${ZDDC_DEPLOY_RELEASES_DIR:-$REPO/dist/release-output}"
|
|
||||||
mkdir -p "$DEPLOY_DIR"
|
|
||||||
ZDDC_DEPLOY_RELEASES_DIR="$DEPLOY_DIR" \
|
|
||||||
sh "$WT/${TOOL}/build.sh" --release "$CHANNEL"
|
|
||||||
|
|
||||||
DST="$DEPLOY_DIR/${TOOL}_${CHANNEL}.html"
|
|
||||||
if [ ! -f "$DST" ]; then
|
|
||||||
echo "error: build did not produce $DST" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Wrote $DST"
|
|
||||||
echo "Done. ${CHANNEL} channel for ${TOOL} now reflects ${LATEST_TAG}."
|
|
||||||
echo "Run ./deploy --releases to push it to the live site."
|
|
||||||
|
|
@ -17,42 +17,39 @@
|
||||||
# inlined JS as containing a closing </script>.
|
# inlined JS as containing a closing </script>.
|
||||||
# The JS engine treats \/ as a regular slash,
|
# The JS engine treats \/ as a regular slash,
|
||||||
# so runtime behaviour is unchanged.
|
# so runtime behaviour is unchanged.
|
||||||
# compute_build_label <tool> [--release [<channel-or-version>]]
|
# compute_build_label <tool> [--release [<beta-or-version>]]
|
||||||
# — sets globals: build_label, build_version,
|
# — sets globals: build_label, build_version,
|
||||||
# is_release, is_red, channel.
|
# is_release, is_red, channel.
|
||||||
# See "Channels and release args" below.
|
# See "Release args" below.
|
||||||
# promote_release <tool> — for stable / alpha / beta, copy the dist
|
# promote_release <tool> — for stable cuts, copy the dist HTML into
|
||||||
# HTML into the release-output bundle
|
# the release-output bundle (default
|
||||||
# (default $root_dir/../dist/release-output;
|
# $root_dir/../dist/release-output;
|
||||||
# override $ZDDC_DEPLOY_RELEASES_DIR). Stable
|
# override $ZDDC_DEPLOY_RELEASES_DIR).
|
||||||
# cuts write the immutable per-version file +
|
# Writes the immutable per-version file
|
||||||
# refresh five symlinks (_v<X.Y>, _v<X>,
|
# <tool>_v<X.Y.Z>.html plus the canonical
|
||||||
# _stable, _beta, _alpha) and tag
|
# symlink <tool>.html pointing at it.
|
||||||
# <tool>-v<X.Y.Z>. Alpha/beta cuts
|
# Tagging is centralized in the top-level
|
||||||
# overwrite the channel mirror in place
|
# ./build (after the embedded commit).
|
||||||
# and cascade alpha → beta. No git tags
|
# Beta cuts produce NO public artifact —
|
||||||
# for alpha/beta cuts. The bundle is a
|
# they are an internal SHA snapshot for
|
||||||
# complete intended-live snapshot — the
|
# the BMC dev chart pipeline; the
|
||||||
# top-level ./build seeds it from
|
# embedded/* regeneration + chore commit
|
||||||
# /srv/zddc/releases/ before per-tool
|
# in the top-level ./build is the actual
|
||||||
# promote runs, then ./deploy --releases
|
# artifact (chart appVersion pins to that
|
||||||
# rsyncs it back. See ARCHITECTURE.md
|
# SHA, Dockerfile fetches it from git).
|
||||||
# "Channels" for the full table.
|
|
||||||
#
|
#
|
||||||
# Channels and release args:
|
# Release args:
|
||||||
# <none> dev build, tool/dist/ only, label
|
# <none> dev build, tool/dist/ only, label
|
||||||
# "v<next-stable>-alpha · <ts> · <sha>[-dirty]" (red).
|
# "v<next-stable>-dev · <ts> · <sha>[-dirty]" (red).
|
||||||
# No release-output side-effect. To produce a deployable
|
# No release-output side-effect.
|
||||||
# bundle, re-run with `--release alpha`.
|
# --release stable cut, auto-bump patch from latest tag (or 0.0.1).
|
||||||
# --release stable, auto-bump patch from latest tag (or 0.0.1).
|
# Writes <tool>_v<X.Y.Z>.html + <tool>.html symlink;
|
||||||
# Writes per-version file + symlinks; tags vX.Y.Z.
|
# tagged later by ./build.
|
||||||
# --release X.Y.Z stable, explicit version.
|
# --release X.Y.Z stable cut, explicit version.
|
||||||
# --release alpha alpha channel cut at HEAD;
|
# --release beta internal SHA snapshot for the BMC dev chart. Build
|
||||||
# label "v<next-stable>-alpha · <date> · <sha>" (red).
|
# label is "v<next-stable>-beta · <date> · <sha>";
|
||||||
# Overwrites <tool>_alpha.html. No tag.
|
# no public artifact, no tag. The top-level ./build
|
||||||
# --release beta beta channel; label "v<next-stable>-beta · <date> · <sha>".
|
# regenerates zddc/internal/apps/embedded/ + commits.
|
||||||
# Overwrites <tool>_beta.html. Cascades <tool>_alpha.html
|
|
||||||
# → <tool>_beta.html (symlink). No tag.
|
|
||||||
# --release <other> error.
|
# --release <other> error.
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
|
|
@ -117,7 +114,7 @@ escape_js_close_tags() {
|
||||||
_validate_semver() {
|
_validate_semver() {
|
||||||
_v="$1"
|
_v="$1"
|
||||||
_bad() {
|
_bad() {
|
||||||
echo "error: invalid release argument: '$_v' (expected: alpha, beta, or X.Y.Z stable version)" >&2
|
echo "error: invalid release argument: '$_v' (expected: beta, or X.Y.Z stable version)" >&2
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
_v1="${_v%%.*}"
|
_v1="${_v%%.*}"
|
||||||
|
|
@ -172,28 +169,25 @@ _source_commit_short_sha() {
|
||||||
}
|
}
|
||||||
|
|
||||||
# Compute build label and channel. Reads positional args:
|
# Compute build label and channel. Reads positional args:
|
||||||
# compute_build_label <tool_name> [--release [<channel-or-version>]]
|
# compute_build_label <tool_name> [--release [<beta-or-version>]]
|
||||||
# Sets global variables:
|
# Sets global variables:
|
||||||
# build_label — text rendered into the page's {{BUILD_LABEL}} slot
|
# build_label — text rendered into the page's {{BUILD_LABEL}} slot
|
||||||
# build_version — bare semver string (stable releases only)
|
# build_version — bare semver string (stable releases only)
|
||||||
# is_release — "1" for any --release invocation, else "0"
|
# is_release — "1" for any --release invocation, else "0"
|
||||||
# is_red — "1" if the label should render red+bold (dev/alpha/beta), else "0"
|
# is_red — "1" if the label should render red+bold (dev/beta), else "0"
|
||||||
# channel — "stable" / "alpha" / "beta" / "" (dev)
|
# channel — "stable" / "beta" / "dev"
|
||||||
#
|
#
|
||||||
# Versioning: pre-release semver. The next-stable target is computed from
|
# Versioning: pre-release semver. The next-stable target is computed from
|
||||||
# the latest clean tool-vX.Y.Z tag (patch-bump). Plain builds and
|
# the latest clean tool-vX.Y.Z tag (patch-bump). Plain dev builds and
|
||||||
# `--release alpha`/`--release beta` carry the next-stable target as a
|
# `--release beta` carry the next-stable target as a pre-release suffix
|
||||||
# pre-release suffix in the on-page label so users can see which stable
|
# in the on-page label so users can see which stable the snapshot is
|
||||||
# the alpha/beta is working toward. Stable releases write a clean
|
# working toward. Stable releases write a clean vX.Y.Z label and tag.
|
||||||
# vX.Y.Z label and tag.
|
|
||||||
#
|
#
|
||||||
# HTML tools do NOT tag alpha/beta cuts (consistent with current
|
# HTML tools do NOT tag beta cuts — beta produces no public artifact
|
||||||
# behavior — alpha and beta artifacts are mutable files, not immutable
|
# (the chart pins by SHA via appVersion). Plain dev builds and beta
|
||||||
# per-build snapshots). Plain dev builds and `--release alpha|beta`
|
# cuts share the same on-page label format (full UTC timestamp + short
|
||||||
# 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
|
||||||
# source SHA — so testers see one rendering shape regardless of how the
|
# the working tree has uncommitted changes; release cuts don't.
|
||||||
# build was produced. A plain dev build may carry a "-dirty" SHA suffix
|
|
||||||
# when the working tree has uncommitted changes; release cuts don't.
|
|
||||||
compute_build_label() {
|
compute_build_label() {
|
||||||
_tool="$1"
|
_tool="$1"
|
||||||
_flag="${2:-}"
|
_flag="${2:-}"
|
||||||
|
|
@ -208,17 +202,17 @@ compute_build_label() {
|
||||||
_next_stable=$(_next_stable_for_tool "$_tool")
|
_next_stable=$(_next_stable_for_tool "$_tool")
|
||||||
|
|
||||||
if [ "$_flag" != "--release" ]; then
|
if [ "$_flag" != "--release" ]; then
|
||||||
# Plain builds are dev builds — labeled as the alpha channel because
|
# Plain builds are dev iteration — tool/dist/ only, no release
|
||||||
# that's what the next formal cut would produce, but no Codeberg upload
|
# output. The label includes the next-stable target so a developer
|
||||||
# happens until `--release alpha` is invoked. Full timestamp (granular
|
# opening the local dist file can see which version-in-progress
|
||||||
# than date) and -dirty marker distinguish iterative dev builds from
|
# they're looking at. Full timestamp + dirty marker distinguish
|
||||||
# formal `--release alpha` cuts (which stamp date-only).
|
# iterative dev builds from formal cuts.
|
||||||
_sha=$(git -C "$root_dir" rev-parse --short=7 HEAD 2>/dev/null || echo "unknown")
|
_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
|
if ! git -C "$root_dir" diff --quiet HEAD 2>/dev/null; then
|
||||||
_sha="${_sha}-dirty"
|
_sha="${_sha}-dirty"
|
||||||
fi
|
fi
|
||||||
channel="alpha"
|
channel="dev"
|
||||||
build_label="v${_next_stable}-alpha · ${build_timestamp} · ${_sha}"
|
build_label="v${_next_stable}-dev · ${build_timestamp} · ${_sha}"
|
||||||
_emit_build_label_sidecar "$_tool"
|
_emit_build_label_sidecar "$_tool"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
@ -226,14 +220,16 @@ compute_build_label() {
|
||||||
is_release=1
|
is_release=1
|
||||||
|
|
||||||
case "$_arg" in
|
case "$_arg" in
|
||||||
alpha | beta)
|
beta)
|
||||||
channel="$_arg"
|
channel="beta"
|
||||||
# Full UTC timestamp + short source SHA — same format as
|
# Internal SHA snapshot for the BMC dev chart. The chart's
|
||||||
# plain dev builds. _source_commit_short_sha walks past
|
# appVersion gets set to "<next>-beta-<sha>" and the
|
||||||
# any `chore(embedded): cut …` auto-commit at HEAD so a
|
# Dockerfile parses the suffix to fetch this SHA from git.
|
||||||
# re-cut on unchanged source produces the same SHA.
|
# _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)
|
_sha=$(_source_commit_short_sha)
|
||||||
build_label="v${_next_stable}-${channel} · ${build_timestamp} · ${_sha}"
|
build_label="v${_next_stable}-beta · ${build_timestamp} · ${_sha}"
|
||||||
_emit_build_label_sidecar "$_tool"
|
_emit_build_label_sidecar "$_tool"
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
|
|
@ -266,14 +262,14 @@ _emit_build_label_sidecar() {
|
||||||
|
|
||||||
# Tools that participate in the lockstep release. Source of truth — used
|
# Tools that participate in the lockstep release. Source of truth — used
|
||||||
# by helpers that enumerate "all release artifacts" (matrix render,
|
# by helpers that enumerate "all release artifacts" (matrix render,
|
||||||
# coordinated next-stable, channel-link verifier).
|
# coordinated next-stable).
|
||||||
ZDDC_RELEASE_TOOLS="archive transmittal classifier landing form tables browse zddc-server"
|
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
|
# 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
|
# 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
|
# on-page label reads against its own history (e.g. a beta cut for a
|
||||||
# alpha cut for a tool that's been quiet still labels itself targeting that
|
# tool that's been quiet still labels itself targeting that tool's next
|
||||||
# tool's next stable, even when the lockstep convention is in force).
|
# stable, even when the lockstep convention is in force).
|
||||||
_next_stable_for_tool() {
|
_next_stable_for_tool() {
|
||||||
_t="$1"
|
_t="$1"
|
||||||
_latest=$(git -C "$root_dir" tag --list "${_t}-v*" 2>/dev/null \
|
_latest=$(git -C "$root_dir" tag --list "${_t}-v*" 2>/dev/null \
|
||||||
|
|
@ -315,30 +311,22 @@ _coordinated_next_stable() {
|
||||||
}
|
}
|
||||||
|
|
||||||
# Promote a built dist file to the release-output bundle. Reads from caller
|
# Promote a built dist file to the release-output bundle. Reads from caller
|
||||||
# scope: $channel ("stable" / "alpha" / "beta"), $build_version (stable only),
|
# scope: $channel ("stable" / "beta"), $build_version (stable only),
|
||||||
# $output_html, $root_dir. Bundle path resolves from $ZDDC_DEPLOY_RELEASES_DIR
|
# $output_html, $root_dir. Bundle path resolves from $ZDDC_DEPLOY_RELEASES_DIR
|
||||||
# (default $root_dir/../dist/release-output).
|
# (default $root_dir/../dist/release-output).
|
||||||
#
|
#
|
||||||
# Stable cuts:
|
# Stable cuts:
|
||||||
# 1. Skip if source unchanged since latest stable tag.
|
# 1. Copy dist HTML → <bundle>/<tool>_v<X.Y.Z>.html (immutable).
|
||||||
# 2. Copy dist HTML → <bundle>/<tool>_v<X.Y.Z>.html (immutable).
|
# 2. Refresh canonical symlink: <bundle>/<tool>.html → the new versioned file.
|
||||||
# 3. Refresh symlinks: _v<X.Y>, _v<X>, _stable, _beta, _alpha all → the
|
# 3. Tag the commit <tool>-v<X.Y.Z> (centralized in the top-level ./build).
|
||||||
# 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:
|
# Beta cuts:
|
||||||
# 1. Overwrite <bundle>/<tool>_<channel>.html with dist HTML
|
# No public artifact. The chart's Dockerfile fetches the source at the
|
||||||
# (replaces a symlink with real bytes if one was there).
|
# SHA pinned in chart appVersion and compiles its own binary; the
|
||||||
# 2. For beta: cascade <tool>_alpha.html → <tool>_beta.html (symlink),
|
# embedded/* regeneration + chore commit in the top-level ./build is
|
||||||
# since alpha defaults to beta when no active alpha.
|
# the actual snapshot.
|
||||||
# 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.
|
# 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() {
|
promote_release() {
|
||||||
_tool="$1"
|
_tool="$1"
|
||||||
# The top-level `./build` exports $ZDDC_DEPLOY_RELEASES_DIR pointing
|
# The top-level `./build` exports $ZDDC_DEPLOY_RELEASES_DIR pointing
|
||||||
|
|
@ -363,18 +351,16 @@ promote_release() {
|
||||||
# every tool, even when a tool's source hasn't changed since
|
# every tool, even when a tool's source hasn't changed since
|
||||||
# its last tag. The bytes are identical (build is deterministic
|
# its last tag. The bytes are identical (build is deterministic
|
||||||
# at the same source), so the overwrite is a no-op on disk;
|
# 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)
|
# but the canonical symlink <tool>.html advances to the new
|
||||||
# gets advanced to the new version, which is the actual goal.
|
# 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"
|
_promote_stable "$_tool" "$build_version" "$_releases_dir"
|
||||||
;;
|
;;
|
||||||
alpha | beta)
|
beta)
|
||||||
_promote_channel "$_tool" "$channel" "$_releases_dir"
|
# 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
|
echo "promote_release: unknown channel '$channel'" >&2
|
||||||
|
|
@ -383,35 +369,31 @@ promote_release() {
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
# Stable cut: per-version file + 5 symlinks. Tagging is centralized in
|
# Stable cut: per-version immutable file + canonical symlink. Tagging is
|
||||||
# the top-level ./build (it commits embedded artifacts FIRST, then tags
|
# centralized in the top-level ./build (it commits embedded artifacts
|
||||||
# at the new commit — see "Release commit + tag" block at the bottom of
|
# FIRST, then tags at the new commit — see "Release commit + tag" block
|
||||||
# the script). _promote_stable historically created tags itself, but
|
# at the bottom of the script). _promote_stable historically created
|
||||||
# that placed them on the source-side commit before embedded files were
|
# tags itself, but that placed them on the source-side commit before
|
||||||
# folded in, leaving prod binaries with alpha-dirty bytes baked in.
|
# embedded files were folded in, leaving prod binaries with stale bytes
|
||||||
|
# baked in.
|
||||||
_promote_stable() {
|
_promote_stable() {
|
||||||
_t="$1"
|
_t="$1"
|
||||||
_ver="$2"
|
_ver="$2"
|
||||||
_rdir="$3"
|
_rdir="$3"
|
||||||
|
|
||||||
_major="${_ver%%.*}"
|
|
||||||
_rest="${_ver#*.}"
|
|
||||||
_minor="${_rest%%.*}"
|
|
||||||
_versioned="${_t}_v${_ver}.html"
|
_versioned="${_t}_v${_ver}.html"
|
||||||
|
_canonical="${_t}.html"
|
||||||
|
|
||||||
cp "$output_html" "$_rdir/$_versioned"
|
cp "$output_html" "$_rdir/$_versioned"
|
||||||
echo "Wrote $_rdir/$_versioned"
|
echo "Wrote $_rdir/$_versioned"
|
||||||
|
|
||||||
# Refresh the 5 symlinks. Cascade: stable cut → beta + alpha both
|
ln -sfn "$_versioned" "$_rdir/$_canonical"
|
||||||
# reset to stable (no active dev on either downstream channel).
|
echo " $_canonical → $_versioned"
|
||||||
for _sym in "${_t}_v${_major}.${_minor}.html" \
|
|
||||||
"${_t}_v${_major}.html" \
|
# Companion .sig symlink so `curl <canonical>.sig` resolves. The
|
||||||
"${_t}_stable.html" \
|
# actual .sig file is written by sign_release_artifacts; this
|
||||||
"${_t}_beta.html" \
|
# symlink points there.
|
||||||
"${_t}_alpha.html"; do
|
ln -sfn "${_versioned}.sig" "$_rdir/${_canonical}.sig"
|
||||||
ln -sfn "$_versioned" "$_rdir/$_sym"
|
|
||||||
echo " $_sym → $_versioned"
|
|
||||||
done
|
|
||||||
|
|
||||||
# Pre-flight check only: if the tag already exists pointing at a
|
# Pre-flight check only: if the tag already exists pointing at a
|
||||||
# commit that is NOT an ancestor of HEAD, the operator needs to
|
# commit that is NOT an ancestor of HEAD, the operator needs to
|
||||||
|
|
@ -431,28 +413,6 @@ _promote_stable() {
|
||||||
echo "Released ${_t} v${_ver} (stable; tagging deferred to top-level build)"
|
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
|
# Platforms zddc-server is cross-compiled for. The first three are
|
||||||
# extension-less (Linux/macOS); Windows gets .exe. The build always emits
|
# 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.
|
# all four; the matrix cell's stub page links each by its <platform> tag.
|
||||||
|
|
@ -470,32 +430,36 @@ _zddc_server_platform_label() {
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
# Resolve a zddc-server binary's filename for one (version, platform).
|
# Resolve a zddc-server binary's filename for one (slug, platform).
|
||||||
# Returns the bare name (no path); ".exe" suffix on windows.
|
# Returns the bare name (no path); ".exe" suffix on windows. Empty slug
|
||||||
|
# means the canonical "current stable" symlink (zddc-server_<plat>);
|
||||||
|
# non-empty slug is a per-version asset (zddc-server_v<X.Y.Z>_<plat>).
|
||||||
_zddc_server_binary_name() {
|
_zddc_server_binary_name() {
|
||||||
_ver_or_chan="$1"
|
_slug="$1"
|
||||||
_plat="$2"
|
_plat="$2"
|
||||||
_suffix=""
|
_suffix=""
|
||||||
case "$_plat" in *windows*) _suffix=".exe" ;; esac
|
case "$_plat" in *windows*) _suffix=".exe" ;; esac
|
||||||
if echo "$_ver_or_chan" | grep -qE '^v[0-9]'; then
|
if [ -z "$_slug" ]; then
|
||||||
# Per-version asset, e.g. zddc-server_v0.0.8_linux-amd64
|
printf 'zddc-server_%s%s' "$_plat" "$_suffix"
|
||||||
printf 'zddc-server_%s_%s%s' "$_ver_or_chan" "$_plat" "$_suffix"
|
|
||||||
else
|
else
|
||||||
# Channel mirror, e.g. zddc-server_stable_linux-amd64
|
printf 'zddc-server_%s_%s%s' "$_slug" "$_plat" "$_suffix"
|
||||||
printf 'zddc-server_%s_%s%s' "$_ver_or_chan" "$_plat" "$_suffix"
|
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# Write the small HTML index page that becomes the matrix cell's link for
|
# Write the small HTML index page that becomes the entry point for a
|
||||||
# a zddc-server release. Lists each platform binary with a download link.
|
# zddc-server release. Lists each platform binary with a download link.
|
||||||
# $1 — release directory (absolute)
|
# $1 — release directory (absolute)
|
||||||
# $2 — slug (e.g. v0.0.8, v0.0, stable, beta, alpha)
|
# $2 — slug ("" for canonical "current stable", or "v0.0.8" per-version)
|
||||||
# $3 — display label (e.g. "v0.0.8", "stable channel")
|
# $3 — display label (e.g. "current stable", "v0.0.8")
|
||||||
write_zddc_server_stub() {
|
write_zddc_server_stub() {
|
||||||
_rdir="$1"
|
_rdir="$1"
|
||||||
_slug="$2"
|
_slug="$2"
|
||||||
_label="$3"
|
_label="$3"
|
||||||
_out="$_rdir/zddc-server_${_slug}.html"
|
if [ -z "$_slug" ]; then
|
||||||
|
_out="$_rdir/zddc-server.html"
|
||||||
|
else
|
||||||
|
_out="$_rdir/zddc-server_${_slug}.html"
|
||||||
|
fi
|
||||||
|
|
||||||
{
|
{
|
||||||
cat <<HEAD
|
cat <<HEAD
|
||||||
|
|
@ -541,68 +505,40 @@ TAIL
|
||||||
}
|
}
|
||||||
|
|
||||||
# Refresh every zddc-server stub page based on what's currently in the
|
# Refresh every zddc-server stub page based on what's currently in the
|
||||||
# release-output bundle. Driven by the existing per-version binary files +
|
# release-output bundle: one per-version stub per zddc-server_v*_*
|
||||||
# symlinks that the release flow already maintains; just emits the HTML
|
# binary set, plus a canonical zddc-server.html if the latest-stable
|
||||||
# wrappers for them. Safe to run on every cut (idempotent).
|
# symlinks are in place. Indexed off linux-amd64 since all four
|
||||||
|
# platforms ship in lockstep.
|
||||||
#
|
#
|
||||||
# $1 — releases dir (absolute)
|
# $1 — releases dir (absolute)
|
||||||
write_zddc_server_stubs_all() {
|
write_zddc_server_stubs_all() {
|
||||||
_rdir="$1"
|
_rdir="$1"
|
||||||
|
|
||||||
# Every per-version stable binary that exists. We index off
|
# Per-version stubs (immutable).
|
||||||
# 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
|
for _bin in "$_rdir"/zddc-server_v*_linux-amd64; do
|
||||||
[ -e "$_bin" ] || continue
|
[ -e "$_bin" ] || continue
|
||||||
_name=$(basename "$_bin")
|
_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/')
|
_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
|
case "$_slug" in
|
||||||
v*.*.*) write_zddc_server_stub "$_rdir" "$_slug" "$_slug" ;;
|
v*.*.*) write_zddc_server_stub "$_rdir" "$_slug" "$_slug" ;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
# Partial-version + channel stubs follow the symlink chain. If the
|
# Canonical stub (follows the latest-stable symlink). Probes the
|
||||||
# symlink resolves to a real binary, write the stub; otherwise skip.
|
# linux-amd64 canonical name; if it exists, the platform symlinks
|
||||||
for _slug in stable beta alpha; do
|
# are in place and we can write the entry page.
|
||||||
_probe="$_rdir/zddc-server_${_slug}_linux-amd64"
|
if [ -e "$_rdir/zddc-server_linux-amd64" ]; then
|
||||||
if [ -e "$_probe" ]; then
|
write_zddc_server_stub "$_rdir" "" "current stable"
|
||||||
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
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# Promote a freshly-cross-compiled set of zddc-server binaries to the
|
# 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.
|
# 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" | "alpha" | "beta")
|
# $1 — channel ("stable" | "beta")
|
||||||
# $2 — version (X.Y.Z; required for stable; ignored for alpha/beta but
|
# $2 — version (X.Y.Z; required for stable; ignored for beta)
|
||||||
# passed through so labels can include the next-stable target)
|
|
||||||
# $3 — releases dir (absolute)
|
# $3 — releases dir (absolute)
|
||||||
# $4 — dist dir holding cross-compiled binaries (absolute)
|
# $4 — dist dir holding cross-compiled binaries (absolute)
|
||||||
promote_zddc_server() {
|
promote_zddc_server() {
|
||||||
|
|
@ -628,27 +564,21 @@ promote_zddc_server() {
|
||||||
echo "promote_zddc_server: stable cut requires version" >&2
|
echo "promote_zddc_server: stable cut requires version" >&2
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
_major="${_ver%%.*}"
|
|
||||||
_rest="${_ver#*.}"
|
|
||||||
_minor="${_rest%%.*}"
|
|
||||||
|
|
||||||
# Per-version: copy each binary to its immutable name + refresh
|
# Per-version immutable + canonical per-platform symlink.
|
||||||
# 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
|
for _plat in $ZDDC_SERVER_PLATFORMS; do
|
||||||
_suffix=""
|
_suffix=""
|
||||||
case "$_plat" in *windows*) _suffix=".exe" ;; esac
|
case "$_plat" in *windows*) _suffix=".exe" ;; esac
|
||||||
_src="$_dist/zddc-server-${_plat}${_suffix}"
|
_src="$_dist/zddc-server-${_plat}${_suffix}"
|
||||||
_versioned="zddc-server_v${_ver}_${_plat}${_suffix}"
|
_versioned="zddc-server_v${_ver}_${_plat}${_suffix}"
|
||||||
|
_canonical="zddc-server_${_plat}${_suffix}"
|
||||||
cp "$_src" "$_rdir/$_versioned"
|
cp "$_src" "$_rdir/$_versioned"
|
||||||
echo "Wrote $_rdir/$_versioned"
|
echo "Wrote $_rdir/$_versioned"
|
||||||
for _sym in "zddc-server_v${_major}.${_minor}_${_plat}${_suffix}" \
|
ln -sfn "$_versioned" "$_rdir/$_canonical"
|
||||||
"zddc-server_v${_major}_${_plat}${_suffix}" \
|
echo " $_canonical → $_versioned"
|
||||||
"zddc-server_stable_${_plat}${_suffix}" \
|
# Companion .sig symlink — see _promote_stable for the
|
||||||
"zddc-server_beta_${_plat}${_suffix}" \
|
# same pattern.
|
||||||
"zddc-server_alpha_${_plat}${_suffix}"; do
|
ln -sfn "${_versioned}.sig" "$_rdir/${_canonical}.sig"
|
||||||
ln -sfn "$_versioned" "$_rdir/$_sym"
|
|
||||||
done
|
|
||||||
done
|
done
|
||||||
|
|
||||||
# Pre-flight tag check only — actual tagging happens in the
|
# Pre-flight tag check only — actual tagging happens in the
|
||||||
|
|
@ -667,22 +597,11 @@ promote_zddc_server() {
|
||||||
fi
|
fi
|
||||||
echo "Released zddc-server v${_ver} (stable; tagging deferred to top-level build)"
|
echo "Released zddc-server v${_ver} (stable; tagging deferred to top-level build)"
|
||||||
;;
|
;;
|
||||||
alpha | beta)
|
beta)
|
||||||
# Mutable channel mirror per platform; cascade alpha → beta on
|
# Internal SHA snapshot — the chart's Dockerfile fetches the
|
||||||
# a beta cut.
|
# source at that SHA and compiles its own binary. No public
|
||||||
for _plat in $ZDDC_SERVER_PLATFORMS; do
|
# binary is published.
|
||||||
_suffix=""
|
echo " zddc-server: beta is internal (no public artifact)"
|
||||||
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
|
echo "promote_zddc_server: unknown channel '$_ch'" >&2
|
||||||
|
|
@ -690,70 +609,6 @@ promote_zddc_server() {
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
# Refresh every stub page (covers the new release plus any pre-existing).
|
# Refresh stub pages (per-version + canonical).
|
||||||
write_zddc_server_stubs_all "$_rdir"
|
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"
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,8 @@
|
||||||
//
|
//
|
||||||
// Spec forms (each is a string value in `.zddc apps:`):
|
// Spec forms (each is a string value in `.zddc apps:`):
|
||||||
//
|
//
|
||||||
// :stable / :beta / :alpha / :v0.0.4 / :v0.0 / :v0 — channel-only
|
// :stable / :v0.0.4 — channel-only
|
||||||
// stable / beta / alpha / v0.0.4 / v0.0 / v0 — channel-only (no leading colon)
|
// stable / v0.0.4 / 0.0.4 — channel-only (no leading colon)
|
||||||
// https://host/path — URL-prefix only (combines with cascade channel)
|
// https://host/path — URL-prefix only (combines with cascade channel)
|
||||||
// https://host/path:stable — URL-prefix + channel (composes)
|
// https://host/path:stable — URL-prefix + channel (composes)
|
||||||
// https://host/path/file.html — terminal full URL (used as-is)
|
// https://host/path/file.html — terminal full URL (used as-is)
|
||||||
|
|
@ -84,7 +84,7 @@ type SpecComponents struct {
|
||||||
// Composable forms — either or both may be set, both may be empty
|
// Composable forms — either or both may be set, both may be empty
|
||||||
// (caller should treat empty-everything as a no-op).
|
// (caller should treat empty-everything as a no-op).
|
||||||
URLPrefix string // "https://host/path" (no trailing /)
|
URLPrefix string // "https://host/path" (no trailing /)
|
||||||
Channel string // "stable" / "beta" / "alpha" / "v0.0.4" / "v0.0" / "v0"
|
Channel string // "stable" (latest), "v0.0.4" (exact version pin)
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsTerminal reports whether this spec terminates composition.
|
// IsTerminal reports whether this spec terminates composition.
|
||||||
|
|
@ -141,10 +141,10 @@ func ParseSpec(spec, zddcDir, root string) (SpecComponents, error) {
|
||||||
// parseURLSpec splits a URL spec into prefix vs full-URL based on the
|
// parseURLSpec splits a URL spec into prefix vs full-URL based on the
|
||||||
// last `:` after the last `/`. Examples:
|
// last `:` after the last `/`. Examples:
|
||||||
//
|
//
|
||||||
// https://host:8080/path:stable → URLPrefix=https://host:8080/path, Channel=stable
|
// https://host:8080/path:stable → URLPrefix=https://host:8080/path, Channel=stable
|
||||||
// https://host:8080/path → URLPrefix=https://host:8080/path
|
// https://host:8080/path → URLPrefix=https://host:8080/path
|
||||||
// https://host/path/file.html → FullURL=https://host/path/file.html (terminal)
|
// https://host/path/file.html → FullURL=https://host/path/file.html (terminal)
|
||||||
// https://host/path/file.html:beta → error (terminal URL with extra suffix)
|
// https://host/path/file.html:stable → error (terminal URL with extra suffix)
|
||||||
func parseURLSpec(spec string) (SpecComponents, error) {
|
func parseURLSpec(spec string) (SpecComponents, error) {
|
||||||
// Locate the channel separator: last `:` that comes after the last `/`.
|
// Locate the channel separator: last `:` that comes after the last `/`.
|
||||||
lastSlash := strings.LastIndex(spec, "/")
|
lastSlash := strings.LastIndex(spec, "/")
|
||||||
|
|
@ -191,10 +191,12 @@ func parseURLSpec(spec string) (SpecComponents, error) {
|
||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// isValidChannelOrVersion reports whether s is `stable`/`beta`/`alpha` or a
|
// isValidChannelOrVersion reports whether s is `stable` (the canonical
|
||||||
// version like `v0.0.4`/`0.0.4`/`v0.0`/`v0`.
|
// "current stable" alias) or an exact-version pin like `v0.0.4` / `0.0.4`.
|
||||||
|
// Partial pins (`v0.0`, `v0`) and the legacy `beta`/`alpha` channels
|
||||||
|
// are no longer accepted — the upstream publishes only stable + exact.
|
||||||
func isValidChannelOrVersion(s string) bool {
|
func isValidChannelOrVersion(s string) bool {
|
||||||
if s == "stable" || s == "beta" || s == "alpha" {
|
if s == "stable" {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
rest := strings.TrimPrefix(s, "v")
|
rest := strings.TrimPrefix(s, "v")
|
||||||
|
|
@ -202,7 +204,7 @@ func isValidChannelOrVersion(s string) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
parts := strings.Split(rest, ".")
|
parts := strings.Split(rest, ".")
|
||||||
if len(parts) > 3 {
|
if len(parts) != 3 {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
for _, p := range parts {
|
for _, p := range parts {
|
||||||
|
|
@ -221,7 +223,7 @@ func isValidChannelOrVersion(s string) bool {
|
||||||
// normalizeChannel ensures versions carry the `v` prefix (so the resulting
|
// normalizeChannel ensures versions carry the `v` prefix (so the resulting
|
||||||
// filename is `<app>_v<X.Y.Z>.html` per upstream convention).
|
// filename is `<app>_v<X.Y.Z>.html` per upstream convention).
|
||||||
func normalizeChannel(s string) string {
|
func normalizeChannel(s string) string {
|
||||||
if s == "stable" || s == "beta" || s == "alpha" {
|
if s == "stable" {
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
if !strings.HasPrefix(s, "v") {
|
if !strings.HasPrefix(s, "v") {
|
||||||
|
|
@ -359,9 +361,18 @@ func (s *appsState) finalize() (Source, bool, error) {
|
||||||
if channel == "" {
|
if channel == "" {
|
||||||
channel = DefaultChannel
|
channel = DefaultChannel
|
||||||
}
|
}
|
||||||
|
// channel == "stable" → canonical URL <prefix>/<app>.html (a
|
||||||
|
// symlink that always follows the latest stable cut).
|
||||||
|
// channel == "v<X.Y.Z>" → immutable per-version URL.
|
||||||
|
var name string
|
||||||
|
if channel == "stable" {
|
||||||
|
name = s.app + ".html"
|
||||||
|
} else {
|
||||||
|
name = s.app + "_" + channel + ".html"
|
||||||
|
}
|
||||||
return Source{
|
return Source{
|
||||||
App: s.app,
|
App: s.app,
|
||||||
URL: urlPrefix + "/" + s.app + "_" + channel + ".html",
|
URL: urlPrefix + "/" + name,
|
||||||
}, true, nil
|
}, true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,15 +11,13 @@ import (
|
||||||
// ── ParseSpec ────────────────────────────────────────────────────────────
|
// ── ParseSpec ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
func TestParseSpec_Channels(t *testing.T) {
|
func TestParseSpec_Channels(t *testing.T) {
|
||||||
|
// "stable" is the only channel alias (latest stable). beta and alpha
|
||||||
|
// channels no longer exist as public concepts.
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
spec, wantChan string
|
spec, wantChan string
|
||||||
}{
|
}{
|
||||||
{"stable", "stable"},
|
{"stable", "stable"},
|
||||||
{"beta", "beta"},
|
|
||||||
{"alpha", "alpha"},
|
|
||||||
{":stable", "stable"},
|
{":stable", "stable"},
|
||||||
{":beta", "beta"},
|
|
||||||
{":alpha", "alpha"},
|
|
||||||
}
|
}
|
||||||
for _, tc := range cases {
|
for _, tc := range cases {
|
||||||
t.Run(tc.spec, func(t *testing.T) {
|
t.Run(tc.spec, func(t *testing.T) {
|
||||||
|
|
@ -38,18 +36,17 @@ func TestParseSpec_Channels(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseSpec_Versions(t *testing.T) {
|
func TestParseSpec_Versions(t *testing.T) {
|
||||||
|
// Exact-version pins only. Partial pins (v0.0, v0) no longer exist
|
||||||
|
// — the upstream publishes <tool>.html (current stable) and
|
||||||
|
// <tool>_v<X.Y.Z>.html (exact-version immutable). Bare "0.0.4"
|
||||||
|
// (no v prefix) is normalized to "v0.0.4".
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
spec, wantChan string
|
spec, wantChan string
|
||||||
}{
|
}{
|
||||||
{"v0.0.4", "v0.0.4"},
|
{"v0.0.4", "v0.0.4"},
|
||||||
{"0.0.4", "v0.0.4"},
|
{"0.0.4", "v0.0.4"},
|
||||||
{"v0.0", "v0.0"},
|
|
||||||
{"0.0", "v0.0"},
|
|
||||||
{"v0", "v0"},
|
|
||||||
{"0", "v0"},
|
|
||||||
{":v0.0.4", "v0.0.4"},
|
{":v0.0.4", "v0.0.4"},
|
||||||
{":0.0.4", "v0.0.4"},
|
{":0.0.4", "v0.0.4"},
|
||||||
{":v0", "v0"},
|
|
||||||
}
|
}
|
||||||
for _, tc := range cases {
|
for _, tc := range cases {
|
||||||
t.Run(tc.spec, func(t *testing.T) {
|
t.Run(tc.spec, func(t *testing.T) {
|
||||||
|
|
@ -64,6 +61,19 @@ func TestParseSpec_Versions(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParseSpec_RejectsLegacyChannelsAndPartialPins(t *testing.T) {
|
||||||
|
// alpha/beta channels and partial-version pins are no longer valid.
|
||||||
|
rejected := []string{"alpha", "beta", ":alpha", ":beta", "v0.0", "v0", "0.0", "0", ":v0.0"}
|
||||||
|
for _, spec := range rejected {
|
||||||
|
t.Run(spec, func(t *testing.T) {
|
||||||
|
_, err := ParseSpec(spec, "/root", "/root")
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("expected error for %q, got none", spec)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestParseSpec_URLPrefix(t *testing.T) {
|
func TestParseSpec_URLPrefix(t *testing.T) {
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
spec, wantPrefix, wantChan string
|
spec, wantPrefix, wantChan string
|
||||||
|
|
@ -71,14 +81,13 @@ func TestParseSpec_URLPrefix(t *testing.T) {
|
||||||
{"https://my-mirror.example/releases", "https://my-mirror.example/releases", ""},
|
{"https://my-mirror.example/releases", "https://my-mirror.example/releases", ""},
|
||||||
{"https://my-mirror.example/releases/", "https://my-mirror.example/releases", ""}, // trailing slash stripped
|
{"https://my-mirror.example/releases/", "https://my-mirror.example/releases", ""}, // trailing slash stripped
|
||||||
{"https://my-mirror.example/releases:stable", "https://my-mirror.example/releases", "stable"},
|
{"https://my-mirror.example/releases:stable", "https://my-mirror.example/releases", "stable"},
|
||||||
{"https://my-mirror.example/releases:beta", "https://my-mirror.example/releases", "beta"},
|
|
||||||
{"https://my-mirror.example/releases:v0.0.4", "https://my-mirror.example/releases", "v0.0.4"},
|
{"https://my-mirror.example/releases:v0.0.4", "https://my-mirror.example/releases", "v0.0.4"},
|
||||||
// Port colon must NOT be confused with channel separator.
|
// Port colon must NOT be confused with channel separator.
|
||||||
{"https://my-mirror.example:8080/releases", "https://my-mirror.example:8080/releases", ""},
|
{"https://my-mirror.example:8080/releases", "https://my-mirror.example:8080/releases", ""},
|
||||||
{"https://my-mirror.example:8080/releases:stable", "https://my-mirror.example:8080/releases", "stable"},
|
{"https://my-mirror.example:8080/releases:stable", "https://my-mirror.example:8080/releases", "stable"},
|
||||||
// Colon embedded in path before final slash — treated as part of path.
|
// Colon embedded in path before final slash — treated as part of path.
|
||||||
{"https://host/some:thing/releases", "https://host/some:thing/releases", ""},
|
{"https://host/some:thing/releases", "https://host/some:thing/releases", ""},
|
||||||
{"https://host/some:thing/releases:beta", "https://host/some:thing/releases", "beta"},
|
{"https://host/some:thing/releases:v0.0.4", "https://host/some:thing/releases", "v0.0.4"},
|
||||||
}
|
}
|
||||||
for _, tc := range cases {
|
for _, tc := range cases {
|
||||||
t.Run(tc.spec, func(t *testing.T) {
|
t.Run(tc.spec, func(t *testing.T) {
|
||||||
|
|
@ -192,13 +201,15 @@ func TestResolve_NoEntries(t *testing.T) {
|
||||||
func TestResolve_PerAppChannelOnly(t *testing.T) {
|
func TestResolve_PerAppChannelOnly(t *testing.T) {
|
||||||
root := t.TempDir()
|
root := t.TempDir()
|
||||||
chain := zddc.PolicyChain{Levels: []zddc.ZddcFile{{
|
chain := zddc.PolicyChain{Levels: []zddc.ZddcFile{{
|
||||||
Apps: map[string]string{"archive": "beta"},
|
Apps: map[string]string{"archive": "stable"},
|
||||||
}}}
|
}}}
|
||||||
src, has, err := Resolve(chain, "archive", root, root)
|
src, has, err := Resolve(chain, "archive", root, root)
|
||||||
if err != nil || !has {
|
if err != nil || !has {
|
||||||
t.Fatalf("has=%v err=%v", has, err)
|
t.Fatalf("has=%v err=%v", has, err)
|
||||||
}
|
}
|
||||||
want := DefaultUpstreamReleases + "/archive_beta.html"
|
// stable channel → canonical URL (no _stable_ suffix); the upstream
|
||||||
|
// publishes a symlink at this URL pointing at the latest version.
|
||||||
|
want := DefaultUpstreamReleases + "/archive.html"
|
||||||
if src.URL != want {
|
if src.URL != want {
|
||||||
t.Errorf("got URL=%q, want %q", src.URL, want)
|
t.Errorf("got URL=%q, want %q", src.URL, want)
|
||||||
}
|
}
|
||||||
|
|
@ -223,40 +234,40 @@ func TestResolve_DefaultProvidesURLAndChannel(t *testing.T) {
|
||||||
root := t.TempDir()
|
root := t.TempDir()
|
||||||
chain := zddc.PolicyChain{Levels: []zddc.ZddcFile{{
|
chain := zddc.PolicyChain{Levels: []zddc.ZddcFile{{
|
||||||
Apps: map[string]string{
|
Apps: map[string]string{
|
||||||
"default": "https://mirror.example/releases:beta",
|
"default": "https://mirror.example/releases:v0.0.4",
|
||||||
},
|
},
|
||||||
}}}
|
}}}
|
||||||
src, has, err := Resolve(chain, "archive", root, root)
|
src, has, err := Resolve(chain, "archive", root, root)
|
||||||
if err != nil || !has {
|
if err != nil || !has {
|
||||||
t.Fatalf("has=%v err=%v", has, err)
|
t.Fatalf("has=%v err=%v", has, err)
|
||||||
}
|
}
|
||||||
if src.URL != "https://mirror.example/releases/archive_beta.html" {
|
if src.URL != "https://mirror.example/releases/archive_v0.0.4.html" {
|
||||||
t.Errorf("got URL=%q", src.URL)
|
t.Errorf("got URL=%q", src.URL)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestResolve_DefaultPlusPerAppChannelOverride(t *testing.T) {
|
func TestResolve_DefaultPlusPerAppChannelOverride(t *testing.T) {
|
||||||
// User's example: default=https://zddc.varasys.io/releases:stable,
|
// default=https://zddc.varasys.io/releases:stable, classifier=:v0.0.4
|
||||||
// classifier=:beta → mirror URL with classifier_beta.html.
|
// → classifier pinned to v0.0.4 on the same mirror.
|
||||||
root := t.TempDir()
|
root := t.TempDir()
|
||||||
chain := zddc.PolicyChain{Levels: []zddc.ZddcFile{{
|
chain := zddc.PolicyChain{Levels: []zddc.ZddcFile{{
|
||||||
Apps: map[string]string{
|
Apps: map[string]string{
|
||||||
"default": "https://zddc.varasys.io/releases:stable",
|
"default": "https://zddc.varasys.io/releases:stable",
|
||||||
"classifier": ":beta",
|
"classifier": ":v0.0.4",
|
||||||
},
|
},
|
||||||
}}}
|
}}}
|
||||||
src, _, err := Resolve(chain, "classifier", root, root)
|
src, _, err := Resolve(chain, "classifier", root, root)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if src.URL != "https://zddc.varasys.io/releases/classifier_beta.html" {
|
if src.URL != "https://zddc.varasys.io/releases/classifier_v0.0.4.html" {
|
||||||
t.Errorf("got URL=%q", src.URL)
|
t.Errorf("got URL=%q", src.URL)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestResolve_DefaultPlusPerAppURLPrefixOverride(t *testing.T) {
|
func TestResolve_DefaultPlusPerAppURLPrefixOverride(t *testing.T) {
|
||||||
// User's example: default=...:stable, archive=https://my.local.stuff/releases
|
// default=...:stable, archive=https://my.local.stuff/releases
|
||||||
// → custom URL + default channel (stable).
|
// → custom URL + default channel (stable, canonical filename).
|
||||||
root := t.TempDir()
|
root := t.TempDir()
|
||||||
chain := zddc.PolicyChain{Levels: []zddc.ZddcFile{{
|
chain := zddc.PolicyChain{Levels: []zddc.ZddcFile{{
|
||||||
Apps: map[string]string{
|
Apps: map[string]string{
|
||||||
|
|
@ -268,7 +279,7 @@ func TestResolve_DefaultPlusPerAppURLPrefixOverride(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if src.URL != "https://my.local.stuff/releases/archive_stable.html" {
|
if src.URL != "https://my.local.stuff/releases/archive.html" {
|
||||||
t.Errorf("got URL=%q", src.URL)
|
t.Errorf("got URL=%q", src.URL)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -278,13 +289,13 @@ func TestResolve_DeeperLevelOverridesParentChannel(t *testing.T) {
|
||||||
requestDir := filepath.Join(root, "Project-A")
|
requestDir := filepath.Join(root, "Project-A")
|
||||||
chain := zddc.PolicyChain{Levels: []zddc.ZddcFile{
|
chain := zddc.PolicyChain{Levels: []zddc.ZddcFile{
|
||||||
{Apps: map[string]string{"default": ":stable"}},
|
{Apps: map[string]string{"default": ":stable"}},
|
||||||
{Apps: map[string]string{"default": ":beta"}},
|
{Apps: map[string]string{"default": ":v0.0.4"}},
|
||||||
}}
|
}}
|
||||||
src, _, err := Resolve(chain, "archive", root, requestDir)
|
src, _, err := Resolve(chain, "archive", root, requestDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
want := DefaultUpstreamReleases + "/archive_beta.html"
|
want := DefaultUpstreamReleases + "/archive_v0.0.4.html"
|
||||||
if src.URL != want {
|
if src.URL != want {
|
||||||
t.Errorf("got URL=%q, want %q", src.URL, want)
|
t.Errorf("got URL=%q, want %q", src.URL, want)
|
||||||
}
|
}
|
||||||
|
|
@ -301,8 +312,9 @@ func TestResolve_DeeperLevelOverridesParentURL(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
// b.example URL prefix wins; channel inherited (stable).
|
// b.example URL prefix wins; channel inherited (stable → canonical
|
||||||
want := "https://b.example/releases/archive_stable.html"
|
// filename, no _stable_ suffix).
|
||||||
|
want := "https://b.example/releases/archive.html"
|
||||||
if src.URL != want {
|
if src.URL != want {
|
||||||
t.Errorf("got URL=%q, want %q", src.URL, want)
|
t.Errorf("got URL=%q, want %q", src.URL, want)
|
||||||
}
|
}
|
||||||
|
|
@ -329,13 +341,13 @@ func TestResolve_DeeperNonTerminalOverridesParentTerminal(t *testing.T) {
|
||||||
requestDir := filepath.Join(root, "Project-A")
|
requestDir := filepath.Join(root, "Project-A")
|
||||||
chain := zddc.PolicyChain{Levels: []zddc.ZddcFile{
|
chain := zddc.PolicyChain{Levels: []zddc.ZddcFile{
|
||||||
{Apps: map[string]string{"archive": "https://a.example/archive.html"}}, // terminal
|
{Apps: map[string]string{"archive": "https://a.example/archive.html"}}, // terminal
|
||||||
{Apps: map[string]string{"archive": "alpha"}}, // non-terminal
|
{Apps: map[string]string{"archive": "v0.0.4"}}, // non-terminal
|
||||||
}}
|
}}
|
||||||
src, _, err := Resolve(chain, "archive", root, requestDir)
|
src, _, err := Resolve(chain, "archive", root, requestDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
want := DefaultUpstreamReleases + "/archive_alpha.html"
|
want := DefaultUpstreamReleases + "/archive_v0.0.4.html"
|
||||||
if src.URL != want {
|
if src.URL != want {
|
||||||
t.Errorf("got URL=%q, want %q", src.URL, want)
|
t.Errorf("got URL=%q, want %q", src.URL, want)
|
||||||
}
|
}
|
||||||
|
|
@ -382,7 +394,7 @@ func TestResolve_PerAppOverridesDefaultAtSameLevel(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if src2.URL != "https://a.example/releases/classifier_stable.html" {
|
if src2.URL != "https://a.example/releases/classifier.html" {
|
||||||
t.Errorf("got classifier URL=%q (want a.example default)", src2.URL)
|
t.Errorf("got classifier URL=%q (want a.example default)", src2.URL)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -417,9 +429,9 @@ func TestPreviewLine(t *testing.T) {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
t.Run("default channel → URL", func(t *testing.T) {
|
t.Run("default channel → URL", func(t *testing.T) {
|
||||||
chain := zddc.PolicyChain{Levels: []zddc.ZddcFile{{Apps: map[string]string{"default": ":beta"}}}}
|
chain := zddc.PolicyChain{Levels: []zddc.ZddcFile{{Apps: map[string]string{"default": ":v0.0.4"}}}}
|
||||||
got := PreviewLine(chain, "archive", root, root)
|
got := PreviewLine(chain, "archive", root, root)
|
||||||
if !strings.Contains(got, "archive_beta.html") {
|
if !strings.Contains(got, "archive_v0.0.4.html") {
|
||||||
t.Errorf("got %q", got)
|
t.Errorf("got %q", got)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -221,15 +221,15 @@ func TestServer_FetchFailFallsBackToEmbedded(t *testing.T) {
|
||||||
func TestServer_VParam_CacheHitServesFromCache(t *testing.T) {
|
func TestServer_VParam_CacheHitServesFromCache(t *testing.T) {
|
||||||
srv, _, root := newTestServer(t, []byte("ignored"))
|
srv, _, root := newTestServer(t, []byte("ignored"))
|
||||||
// Pre-populate the cache with a known URL.
|
// Pre-populate the cache with a known URL.
|
||||||
cachedURL := "https://zddc.varasys.io/releases/archive_beta.html"
|
cachedURL := "https://zddc.varasys.io/releases/archive_v0.0.4.html"
|
||||||
cachedBody := []byte("CACHED beta archive")
|
cachedBody := []byte("CACHED v0.0.4 archive")
|
||||||
if err := srv.Cache.Write(cachedURL, cachedBody); err != nil {
|
if err := srv.Cache.Write(cachedURL, cachedBody); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
chain := zddc.PolicyChain{Levels: []zddc.ZddcFile{{}}}
|
chain := zddc.PolicyChain{Levels: []zddc.ZddcFile{{}}}
|
||||||
rec := httptest.NewRecorder()
|
rec := httptest.NewRecorder()
|
||||||
srv.Serve(rec, httptest.NewRequest(http.MethodGet, "/archive.html?v=beta", nil), "archive", chain, root)
|
srv.Serve(rec, httptest.NewRequest(http.MethodGet, "/archive.html?v=v0.0.4", nil), "archive", chain, root)
|
||||||
if rec.Code != http.StatusOK {
|
if rec.Code != http.StatusOK {
|
||||||
t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
|
t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
|
||||||
}
|
}
|
||||||
|
|
@ -245,7 +245,7 @@ func TestServer_VParam_CacheMissReturns404(t *testing.T) {
|
||||||
srv, _, root := newTestServer(t, []byte("ignored"))
|
srv, _, root := newTestServer(t, []byte("ignored"))
|
||||||
chain := zddc.PolicyChain{Levels: []zddc.ZddcFile{{}}}
|
chain := zddc.PolicyChain{Levels: []zddc.ZddcFile{{}}}
|
||||||
rec := httptest.NewRecorder()
|
rec := httptest.NewRecorder()
|
||||||
srv.Serve(rec, httptest.NewRequest(http.MethodGet, "/archive.html?v=beta", nil), "archive", chain, root)
|
srv.Serve(rec, httptest.NewRequest(http.MethodGet, "/archive.html?v=v0.0.4", nil), "archive", chain, root)
|
||||||
if rec.Code != http.StatusNotFound {
|
if rec.Code != http.StatusNotFound {
|
||||||
t.Fatalf("status=%d (want 404)", rec.Code)
|
t.Fatalf("status=%d (want 404)", rec.Code)
|
||||||
}
|
}
|
||||||
|
|
@ -277,19 +277,19 @@ func TestServer_VParam_BadSpecReturns400(t *testing.T) {
|
||||||
func TestServer_VParam_CombinesWithCascadeURLPrefix(t *testing.T) {
|
func TestServer_VParam_CombinesWithCascadeURLPrefix(t *testing.T) {
|
||||||
// Cascade has a default URL prefix; ?v=:beta should resolve against it.
|
// Cascade has a default URL prefix; ?v=:beta should resolve against it.
|
||||||
srv, _, root := newTestServer(t, []byte("ignored"))
|
srv, _, root := newTestServer(t, []byte("ignored"))
|
||||||
cachedURL := "https://my-mirror.example/releases/archive_beta.html"
|
cachedURL := "https://my-mirror.example/releases/archive_v0.0.4.html"
|
||||||
if err := srv.Cache.Write(cachedURL, []byte("MIRROR beta")); err != nil {
|
if err := srv.Cache.Write(cachedURL, []byte("MIRROR v0.0.4")); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
chain := zddc.PolicyChain{Levels: []zddc.ZddcFile{{
|
chain := zddc.PolicyChain{Levels: []zddc.ZddcFile{{
|
||||||
Apps: map[string]string{"default": "https://my-mirror.example/releases:stable"},
|
Apps: map[string]string{"default": "https://my-mirror.example/releases:stable"},
|
||||||
}}}
|
}}}
|
||||||
rec := httptest.NewRecorder()
|
rec := httptest.NewRecorder()
|
||||||
srv.Serve(rec, httptest.NewRequest(http.MethodGet, "/archive.html?v=:beta", nil), "archive", chain, root)
|
srv.Serve(rec, httptest.NewRequest(http.MethodGet, "/archive.html?v=:v0.0.4", nil), "archive", chain, root)
|
||||||
if rec.Code != http.StatusOK {
|
if rec.Code != http.StatusOK {
|
||||||
t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
|
t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
|
||||||
}
|
}
|
||||||
if rec.Body.String() != "MIRROR beta" {
|
if rec.Body.String() != "MIRROR v0.0.4" {
|
||||||
t.Errorf("body=%q", rec.Body.String())
|
t.Errorf("body=%q", rec.Body.String())
|
||||||
}
|
}
|
||||||
if got := rec.Header().Get("X-ZDDC-Source"); got != "cache:"+cachedURL {
|
if got := rec.Header().Get("X-ZDDC-Source"); got != "cache:"+cachedURL {
|
||||||
|
|
@ -299,9 +299,9 @@ func TestServer_VParam_CombinesWithCascadeURLPrefix(t *testing.T) {
|
||||||
|
|
||||||
func TestServer_VParam_OverridesPathTerminalFromCascade(t *testing.T) {
|
func TestServer_VParam_OverridesPathTerminalFromCascade(t *testing.T) {
|
||||||
// Operator's cascade specifies a path source. User passes ?v=stable.
|
// Operator's cascade specifies a path source. User passes ?v=stable.
|
||||||
// ?v= overrides → resolves to canonical/archive_stable.html, then cache check.
|
// ?v= overrides → resolves to canonical/archive.html, then cache check.
|
||||||
srv, _, root := newTestServer(t, []byte("ignored"))
|
srv, _, root := newTestServer(t, []byte("ignored"))
|
||||||
cachedURL := "https://zddc.varasys.io/releases/archive_stable.html"
|
cachedURL := "https://zddc.varasys.io/releases/archive.html"
|
||||||
if err := srv.Cache.Write(cachedURL, []byte("CACHED stable")); err != nil {
|
if err := srv.Cache.Write(cachedURL, []byte("CACHED stable")); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import (
|
||||||
|
|
||||||
// embeddedVersionsRaw is the manifest written by the top-level build.sh
|
// embeddedVersionsRaw is the manifest written by the top-level build.sh
|
||||||
// at compile time. Format is one `<app>=<build label>` line per app —
|
// at compile time. Format is one `<app>=<build label>` line per app —
|
||||||
// e.g. `archive=v0.0.5-alpha · 2026-05-01 14:00:00 · abc1234`. An empty
|
// e.g. `archive=v0.0.5-beta · 2026-05-01 14:00:00 · abc1234`. An empty
|
||||||
// or missing value indicates the embedded slot was not populated (a fresh
|
// or missing value indicates the embedded slot was not populated (a fresh
|
||||||
// clone where build.sh hasn't run yet).
|
// clone where build.sh hasn't run yet).
|
||||||
//
|
//
|
||||||
|
|
|
||||||
|
|
@ -1515,7 +1515,7 @@ body.is-elevated::after {
|
||||||
</svg>
|
</svg>
|
||||||
<div class="header-title-group">
|
<div class="header-title-group">
|
||||||
<span class="app-header__title" id="table-title">ZDDC Table</span>
|
<span class="app-header__title" id="table-title">ZDDC Table</span>
|
||||||
<span class="build-timestamp">v0.0.17</span>
|
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.18-dev · 2026-05-20 14:16:52 · 784ed21-dirty</span></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue