refactor: HTML tools live in website/releases/ as static files + symlink hierarchy

Rolls back the HTML-tool side of the Codeberg-as-canonical refactor
(commits 2dc9ad2, b28c4ae, bdac8dc) in favor of a simpler model:
per-version HTML files committed under website/releases/ as immutable
real files; partial-version pins (<tool>_v<X.Y>.html, <tool>_v<X>.html)
and channel mirrors (<tool>_<channel>.html) are checked-in symlinks.
Docker-tag pattern: :1.2.3 is pinned, :1.2 floats, :1 floats further,
:stable floats furthest.

URL scheme — every URL resolves to actual HTML via the symlink chain;
no JS indirection, no manifest.json, no Caddy regex-rewrite:

  /releases/<tool>_v<X.Y.Z>.html    exact version (real file)
  /releases/<tool>_v<X.Y>.html      latest patch within X.Y.* (symlink)
  /releases/<tool>_v<X>.html        latest within X.*.* (symlink)
  /releases/<tool>_stable.html      current stable (symlink)
  /releases/<tool>_beta.html        current beta (symlink to stable when no
                                    active beta; real file when beta is in flight)
  /releases/<tool>_alpha.html       current alpha (similar — symlink to beta
                                    or stable when no active alpha)

Cascade rule (in shared/build-lib.sh promote_release):
  --release [version] (stable cut) → write per-version file; refresh 5
                                     symlinks (_v<X.Y>, _v<X>, _stable,
                                     _beta, _alpha) → new versioned file;
                                     tag <tool>-v<X.Y.Z>.
  --release beta                   → overwrite <tool>_beta.html with real
                                     bytes; cascade _alpha.html → _beta.html
                                     (symlink). No tag — channel URLs are
                                     stable URLs by design; counters defeat
                                     that.
  --release alpha                  → overwrite <tool>_alpha.html with real
                                     bytes. No tag, no other side-effects.
  Plain `sh tool/build.sh`         → dist/ only. No website/releases/
                                     side-effect, no commit.

Code changes:

- .gitignore — drop website/releases/*.html and website/releases/zddc-server-*
  exclusions; HTML tool files are tracked again. Replace the comment with
  the new model description.
- shared/build-lib.sh — drop next_prerelease (no -alpha.N / -beta.N counter
  tags). Drop the Codeberg-upload path for HTML tools (no longer sourcing
  publish-codeberg-release.sh from build-lib). promote_release rewritten
  with two helpers: _promote_stable (per-version file + 5 symlinks + tag)
  and _promote_channel (overwrite mirror + cascade alpha→beta on beta cut).
- zddc/release.sh — drop alpha/beta channel path entirely; binaries publish
  only on stable cuts. zddc-server's beta/alpha builds-from-source via the
  helm charts (next phase) — no binary distribution needed for those channels.
- bootstrap/level2.html.tmpl — drop manifest.json fetch; resolve ?v= to a
  static URL via the symlink chain. New suffixFor() handles channel names,
  exact versions, and partial-version pins (?v=0.0, ?v=0). Same logic in
  level1.html.tmpl already works because the local-staging files (e.g.
  ../<tool>_v0.0.html) exist via the same symlink scheme.
- build.sh build_releases_index — revert to filesystem scan of
  website/releases/ instead of Codeberg API call. Drop manifest.json
  generation. Per-tool sections list channel chips + per-version pin links;
  zddc-server section links to Codeberg release pages directly.
- tests/build-label.spec.js — fix the channel-label regex to match the
  pre-release-semver format introduced in commit 9459139 ("v0.0.3-alpha · ...").
  Pre-existing test failure that wasn't caught at the time.

Storage:

- 30 new committed files under website/releases/ — 10 real (per-version) +
  20 symlinks (5 tools × 4 partial/channel variants, plus alpha as a real
  file by default).
- Initial state: stable v0.0.2 across all 5 tools; alpha/beta/v0.0/v0
  symlinks all point at <tool>_v0.0.2.html.
- manifest.json deleted (no longer needed).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
ZDDC 2026-04-30 09:40:16 -05:00
parent bdac8dc4fb
commit 408a1a0571
58 changed files with 65802 additions and 864 deletions

19
.gitignore vendored
View file

@ -20,15 +20,16 @@ test-results/
# New tool dist files must be force-added: git add -f tool/dist/tool.html # New tool dist files must be force-added: git add -f tool/dist/tool.html
dist/ dist/
# Release artifacts — published to Codeberg release assets, not committed. # Release artifacts under website/releases/ ARE committed — per-version HTML
# website/releases/index.html and website/releases/manifest.json are # tool files (<tool>_v<X.Y.Z>.html) accumulate as immutable real files; partial
# regenerated by build.sh and DO get committed (they're tiny indices). # version pins (<tool>_v<X.Y>.html, <tool>_v<X>.html) and channels
# Everything else under website/releases/ — versioned HTML tool files, # (<tool>_<channel>.html) are checked-in symlinks. The build script
# zddc-server binaries — lives on Codeberg; the website's Caddy reverse- # (shared/build-lib.sh promote_release) maintains the symlink chain on each
# proxies download URLs to Codeberg release-asset URLs. # release. Caddy serves these as plain static files; no manifest, no proxy.
website/releases/*.html #
!website/releases/index.html # zddc-server binaries are NOT committed — they're per-platform multi-MB
website/releases/zddc-server-* # binaries that ship as Codeberg release assets, attached to clean
# zddc-server-vX.Y.Z tags by zddc/release.sh.
# IDE and project files # IDE and project files
.opencode/ .opencode/

View file

@ -14,47 +14,33 @@
// document.write()s it in place. The default upstream is the // document.write()s it in place. The default upstream is the
// {{CHANNEL}} channel; the URL parameter ?v= overrides it: // {{CHANNEL}} channel; the URL parameter ?v= overrides it:
// //
// ?v=alpha|beta|stable switches to that channel // ?v=stable | beta | alpha switch to that channel
// ?v=0.0.4 (or v0.0.4) pins to that exact stable version // ?v=0.0.4 (or v0.0.4) pin to that exact stable version
// (none) uses the {{CHANNEL}} default // ?v=0.0 (or v0.0) pin to latest patch within 0.0.x (symlink)
// ?v=0 (or v0) pin to latest within 0.x (symlink)
// (none) use the {{CHANNEL}} default
// //
// Resolution path: // Resolution is purely static — every URL maps to a real file or a
// 1. Channel mode (no ?v= or ?v={alpha,beta,stable}): fetch // checked-in symlink under <upstream>/releases/. No manifest lookup,
// manifest.json from the same origin, look up // no JS indirection, no client-side version arithmetic.
// "<tool>-<channel>" → tag, then fetch the asset.
// 2. Version pin: build the URL directly (skip manifest).
//
// The asset URL is /releases/<tag>/<tool>_v<version>.html. Caddy on
// zddc.varasys.io reverse-proxies that to the Codeberg release-asset
// URL — neither this stub nor the user's browser hits codeberg.org
// directly.
(async function () { (async function () {
const params = new URLSearchParams(location.search); const params = new URLSearchParams(location.search);
const v = params.get('v'); const v = params.get('v');
const tool = '{{TOOL}}'; const tool = '{{TOOL}}';
const defaultChannel = '{{CHANNEL}}'; const defaultChannel = '{{CHANNEL}}';
const channels = ['alpha', 'beta', 'stable']; const channels = ['stable', 'beta', 'alpha'];
const base = 'https://zddc.varasys.io/releases/'; const base = 'https://zddc.varasys.io/releases/';
function isChannel(s) { return channels.indexOf(s) >= 0; } function suffixFor(value) {
if (!value) return '_' + defaultChannel;
try { if (channels.indexOf(value) >= 0) return '_' + value;
let assetUrl; // Strip optional leading 'v', accept "0.0.4" / "0.0" / "0".
if (v && !isChannel(v)) { const ver = value.startsWith('v') ? value.slice(1) : value;
// Explicit version pin (e.g. ?v=0.0.4 or ?v=v0.0.4). return '_v' + ver;
const ver = v.replace(/^v/, '');
const tag = tool + '-v' + ver;
assetUrl = base + tag + '/' + tool + '_v' + ver + '.html';
} else {
// Channel mode: resolve via manifest.
const channel = (v && isChannel(v)) ? v : defaultChannel;
const manifest = await (await fetch(base + 'manifest.json', { cache: 'no-cache', credentials: 'omit' })).json();
const tag = manifest[tool + '-' + channel];
if (!tag) throw new Error('manifest has no entry for ' + tool + '-' + channel);
const ver = tag.replace(tool + '-v', '');
assetUrl = base + tag + '/' + tool + '_v' + ver + '.html';
} }
try {
const assetUrl = base + tool + suffixFor(v) + '.html';
const resp = await fetch(assetUrl, { cache: 'no-cache', credentials: 'omit' }); const resp = await fetch(assetUrl, { cache: 'no-cache', credentials: 'omit' });
if (!resp.ok) throw new Error(resp.status + ' ' + resp.statusText); if (!resp.ok) throw new Error(resp.status + ' ' + resp.statusText);
const html = await resp.text(); const html = await resp.text();

172
build.sh
View file

@ -124,71 +124,20 @@ EOF
done done
} }
# Regenerate website/releases/index.html and manifest.json from the # Regenerate website/releases/index.html from a filesystem scan of
# Codeberg release list. Single API call at build time, no runtime # website/releases/. Lists per-version files (real .html files, immutable
# dependency on Codeberg from the page (it's static HTML when served). # archives) plus channel mirrors and partial-version pins (symlinks).
# #
# Page links use /releases/<tag>/<asset> URLs which the website's Caddy # All URLs in the page resolve directly under <upstream>/releases/<file>
# reverse-proxies to codeberg.org/.../releases/download/<tag>/<asset>. # — no Codeberg API call, no manifest, no proxy magic. Page is static
# Operators see one origin (zddc.varasys.io); Codeberg is the storage # and current as of the last `sh build.sh` run.
# backend.
# #
# Skips silently if curl/jq aren't on PATH (offline dev shouldn't blow # zddc-server's section links to its Codeberg release pages directly
# up). Skips with a warning if the API call fails (network down, rate- # (different distribution model — per-platform binaries).
# limited, etc.) — the existing index.html stays as-is.
build_releases_index() { build_releases_index() {
_out="$RELEASES_DIR/index.html" _out="$RELEASES_DIR/index.html"
_manifest="$RELEASES_DIR/manifest.json"
mkdir -p "$RELEASES_DIR" mkdir -p "$RELEASES_DIR"
if ! command -v curl >/dev/null 2>&1 || ! command -v jq >/dev/null 2>&1; then
echo " (curl or jq missing — skipping releases index regeneration)"
return 0
fi
_api_resp=$(curl -fsSL --max-time 30 \
'https://codeberg.org/api/v1/repos/VARASYS/ZDDC/releases?limit=100' \
2>/dev/null) || _api_resp=""
if [ -z "$_api_resp" ]; then
echo " (Codeberg API unreachable — leaving releases/index.html as-is)"
return 0
fi
# Build manifest.json: for each tool/channel, find the latest matching
# release and emit "<tool>-<channel>": "<tag>". Channel resolution:
# alpha = latest tag matching <tool>-vX.Y.Z-alpha.N
# beta = latest tag matching <tool>-vX.Y.Z-beta.N
# stable = latest tag matching <tool>-vX.Y.Z (no suffix)
# "Latest" via sort -V on the version part.
_tools="archive transmittal classifier mdedit landing zddc-server"
{
printf '{\n'
_first=1
for _tool in $_tools; do
for _ch in stable beta alpha; do
if [ "$_ch" = "stable" ]; then
_re="^${_tool}-v[0-9]+\\.[0-9]+\\.[0-9]+\$"
else
_re="^${_tool}-v[0-9]+\\.[0-9]+\\.[0-9]+-${_ch}\\.[0-9]+\$"
fi
_tag=$(printf '%s' "$_api_resp" \
| jq -r --arg re "$_re" '
[.[] | select(.tag_name | test($re)) | .tag_name]
| sort
| last // empty
')
if [ -n "$_tag" ]; then
[ "$_first" = "1" ] || printf ',\n'
printf ' "%s-%s": "%s"' "$_tool" "$_ch" "$_tag"
_first=0
fi
done
done
printf '\n}\n'
} > "$_manifest"
echo "Wrote $_manifest"
{ {
cat <<'HEAD' cat <<'HEAD'
<!DOCTYPE html> <!DOCTYPE html>
@ -252,109 +201,66 @@ build_releases_index() {
<main class="container" style="margin-bottom: var(--spacing-2xl);"> <main class="container" style="margin-bottom: var(--spacing-2xl);">
HEAD HEAD
# Render one section per tool. The HTML tool releases publish a # Render one section per HTML tool, scanning website/releases/ for
# single asset per tag (the inlined HTML); zddc-server publishes # <tool>_v*.html real files (per-version archives) and resolving the
# per-platform binaries. The renderer handles both. # current channel symlinks.
for _tool_entry in 'archive|Archive' \ for _tool_entry in 'archive|Archive' \
'transmittal|Transmittal' \ 'transmittal|Transmittal' \
'classifier|Classifier' \ 'classifier|Classifier' \
'mdedit|Markdown Editor' \ 'mdedit|Markdown Editor' \
'landing|Landing (project picker)' \ 'landing|Landing (project picker)'; do
'zddc-server|zddc-server (Go file server)'; do
_tool="${_tool_entry%%|*}" _tool="${_tool_entry%%|*}"
_title="${_tool_entry#*|}" _title="${_tool_entry#*|}"
_re_stable="^${_tool}-v[0-9]+\\.[0-9]+\\.[0-9]+\$"
_re_alpha="^${_tool}-v[0-9]+\\.[0-9]+\\.[0-9]+-alpha\\.[0-9]+\$"
_re_beta="^${_tool}-v[0-9]+\\.[0-9]+\\.[0-9]+-beta\\.[0-9]+\$"
_latest_stable=$(printf '%s' "$_api_resp" | jq -r --arg re "$_re_stable" \ # All per-version real files for this tool, semver-descending.
'[.[] | select(.tag_name | test($re)) | .tag_name] | sort | last // empty') # Use find to filter out symlinks; grep + sort -Vr for ordering.
_latest_beta=$(printf '%s' "$_api_resp" | jq -r --arg re "$_re_beta" \ _versioned=$(find "$RELEASES_DIR" -maxdepth 1 -type f -name "${_tool}_v*.html" 2>/dev/null \
'[.[] | select(.tag_name | test($re)) | .tag_name] | sort | last // empty') | sed "s|^${RELEASES_DIR}/||" \
_latest_alpha=$(printf '%s' "$_api_resp" | jq -r --arg re "$_re_alpha" \ | sort -Vr)
'[.[] | select(.tag_name | test($re)) | .tag_name] | sort | last // empty')
_all_stables=$(printf '%s' "$_api_resp" | jq -r --arg re "$_re_stable" \
'[.[] | select(.tag_name | test($re)) | .tag_name] | sort | reverse | .[]')
# Skip the section entirely if no releases exist for this tool. if [ -z "$_versioned" ]; then
if [ -z "$_latest_stable$_latest_beta$_latest_alpha" ]; then
continue continue
fi fi
printf ' <section class="rel-tool">\n' printf ' <section class="rel-tool">\n'
printf ' <h2>%s</h2>\n' "$_title" printf ' <h2>%s</h2>\n' "$_title"
# Channel chips. Each link uses /releases/<tag>/<asset> # Channel chips — only render if the symlink exists.
# which Caddy proxies to the Codeberg release-asset URL.
# Asset name is <tool>_v<version>.html for HTML tools; for
# zddc-server we link to the Codeberg release page since
# the asset is per-platform (operator picks one).
printf ' <div class="rel-channels">\n' printf ' <div class="rel-channels">\n'
for _row in "stable|$_latest_stable" "beta|$_latest_beta" "alpha|$_latest_alpha"; do for _ch in stable beta alpha; do
_ch="${_row%%|*}" _link="${_tool}_${_ch}.html"
_tag="${_row#*|}" if [ -e "$RELEASES_DIR/$_link" ]; then
[ -n "$_tag" ] || continue printf ' <a class="%s" href="%s">%s</a>\n' "$_ch" "$_link" "$_ch"
if [ "$_tool" = "zddc-server" ]; then
printf ' <a class="%s" href="https://codeberg.org/VARASYS/ZDDC/releases/tag/%s">%s</a>\n' \
"$_ch" "$_tag" "$_ch"
else
_ver="${_tag#${_tool}-v}"
_asset="${_tool}_v${_ver}.html"
printf ' <a class="%s" href="/releases/%s/%s">%s</a>\n' \
"$_ch" "$_tag" "$_asset" "$_ch"
fi fi
done done
printf ' </div>\n' printf ' </div>\n'
# zddc-server: per-platform binary table, one row per channel. # Pin-to-version row: every per-version real file, descending.
if [ "$_tool" = "zddc-server" ]; then
_platforms="linux-amd64 darwin-amd64 darwin-arm64 windows-amd64"
printf ' <h3 style="font-size:1rem;margin:0.75rem 0 0.4rem;">Standalone binaries</h3>\n'
printf ' <table class="rel-bin-table"><thead><tr><th>Channel</th>'
for _p in $_platforms; do printf '<th>%s</th>' "$_p"; done
printf '</tr></thead><tbody>\n'
for _row in "stable|$_latest_stable" "beta|$_latest_beta" "alpha|$_latest_alpha"; do
_ch="${_row%%|*}"
_tag="${_row#*|}"
printf ' <tr><td class="ch-%s">%s</td>' "$_ch" "$_ch"
if [ -z "$_tag" ]; then
for _p in $_platforms; do printf '<td class="empty">—</td>'; done
else
for _p in $_platforms; do
_ext=""; case "$_p" in windows-*) _ext=".exe" ;; esac
_asset="zddc-server-${_p}${_ext}"
printf '<td><a href="/releases/%s/%s">download</a></td>' "$_tag" "$_asset"
done
fi
printf '</tr>\n'
done
printf ' </tbody></table>\n'
fi
# Pin-to-version row. All stables (semver-descending). HTML
# tools link to the asset directly; zddc-server links to the
# Codeberg release page (per-platform asset choice).
if [ -n "$_all_stables" ]; then
printf ' <div class="rel-versions"><strong>Pin to version:</strong>\n' printf ' <div class="rel-versions"><strong>Pin to version:</strong>\n'
printf '%s\n' "$_all_stables" | while read -r _t; do printf '%s\n' "$_versioned" | while read -r _f; do
[ -n "$_t" ] || continue [ -n "$_f" ] || continue
_v="${_t#${_tool}-v}" # Display as v<X.Y.Z> stripped of <tool>_v prefix and .html suffix.
if [ "$_tool" = "zddc-server" ]; then _v="${_f#${_tool}_v}"
printf ' <a href="https://codeberg.org/VARASYS/ZDDC/releases/tag/%s">v%s</a>\n' "$_t" "$_v" _v="${_v%.html}"
else printf ' <a href="%s">v%s</a>\n' "$_f" "$_v"
printf ' <a href="/releases/%s/%s_v%s.html">v%s</a>\n' "$_t" "$_tool" "$_v" "$_v"
fi
done done
printf ' </div>\n' printf ' </div>\n'
fi
printf ' </section>\n' printf ' </section>\n'
done done
# zddc-server section — links to Codeberg release pages directly,
# since binaries don't live under website/releases/.
printf ' <section class="rel-tool">\n'
printf ' <h2>zddc-server (Go file server)</h2>\n'
printf ' <p>Binaries are published as Codeberg release assets. Pick a platform from the release page; or build from source via the helm charts under <code>helm/</code>.</p>\n'
printf ' <p><a href="https://codeberg.org/VARASYS/ZDDC/releases">Browse zddc-server releases on Codeberg →</a></p>\n'
printf ' </section>\n'
cat <<'TAIL' cat <<'TAIL'
<section style="margin-top: var(--spacing-2xl); color: var(--color-text-muted); font-size: 0.9rem;"> <section style="margin-top: var(--spacing-2xl); color: var(--color-text-muted); font-size: 0.9rem;">
<p>Append <code>?v=alpha</code>, <code>?v=beta</code>, <code>?v=stable</code>, or <code>?v=0.0.1</code> to any deployment URL to switch versions for a single request — see <a href="../">the home page</a>.</p> <p>Append <code>?v=stable</code>, <code>?v=beta</code>, <code>?v=alpha</code>, <code>?v=0.0</code> (latest 0.0.x), or <code>?v=0.0.1</code> (exact) to any deployment URL to switch versions for a single request — see <a href="../">the home page</a>.</p>
</section> </section>
</main> </main>

View file

@ -21,14 +21,17 @@
# — 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 "Channels and release args" below.
# promote_release <tool> — for stable / alpha / beta, tag the # promote_release <tool> — for stable / alpha / beta, copy the dist
# commit and upload the dist HTML as a # HTML into website/releases/. Stable cuts
# Codeberg release asset. No local mirror # write the immutable per-version file +
# under website/releases/ — the website # refresh five symlinks (_v<X.Y>, _v<X>,
# reverse-proxies download URLs to Codeberg # _stable, _beta, _alpha) and tag
# release-asset URLs. Stable cuts skip when # <tool>-v<X.Y.Z>. Alpha/beta cuts
# the tool's source is unchanged since the # overwrite the channel mirror in place
# latest stable tag. # and cascade alpha → beta. No git tags
# for alpha/beta cuts; no Codeberg upload
# for HTML tools. See ARCHITECTURE.md
# "Channels" for the full table.
# #
# Channels and release args: # Channels and release args:
# <none> dev build, dist/ only, label # <none> dev build, dist/ only, label
@ -36,13 +39,14 @@
# No website/releases/ side-effect. To publish, re-run # No website/releases/ side-effect. To publish, re-run
# with `--release alpha`. # with `--release alpha`.
# --release stable, auto-bump patch from latest tag (or 0.0.1). # --release stable, auto-bump patch from latest tag (or 0.0.1).
# Label "vX.Y.Z" (black). Tags + uploads. # Writes per-version file + symlinks; tags vX.Y.Z.
# --release X.Y.Z stable, explicit version. Tags + uploads. # --release X.Y.Z stable, explicit version.
# --release alpha alpha channel cut at HEAD; # --release alpha alpha channel cut at HEAD;
# label "v<next-stable>-alpha · <date> · <sha>" (red). # label "v<next-stable>-alpha · <date> · <sha>" (red).
# Tags <tool>-vX.Y.Z-alpha.N + uploads. # Overwrites <tool>_alpha.html. No tag.
# --release beta beta channel; label "v<next-stable>-beta · <date> · <sha>". # --release beta beta channel; label "v<next-stable>-beta · <date> · <sha>".
# Tags + uploads. # Overwrites <tool>_beta.html. Cascades <tool>_alpha.html
# → <tool>_beta.html (symlink). No tag.
# --release <other> error. # --release <other> error.
# ============================================================================= # =============================================================================
@ -52,10 +56,9 @@ if [ -z "${root_dir:-}" ]; then
exit 1 exit 1
fi fi
# Pull in the Codeberg release-publish helper so promote_release can call # NOTE: shared/publish-codeberg-release.sh is no longer sourced here.
# its publish_codeberg_release function. Sourced unconditionally — the # HTML tools publish to website/releases/ as committed static files; only
# helper has no side effects when sourced (only defines functions). # zddc-server/release.sh uploads to Codeberg (it sources the helper directly).
. "$root_dir/../shared/publish-codeberg-release.sh"
# Fail hard on any missing source file # Fail hard on any missing source file
ensure_exists() { ensure_exists() {
@ -101,58 +104,6 @@ escape_js_close_tags() {
sed 's#</\([sS][cC][rR][iI][pP][tT]\)#<\\/\1#g' "$1" > "$2" sed 's#</\([sS][cC][rR][iI][pP][tT]\)#<\\/\1#g' "$1" > "$2"
} }
# Echo the next pre-release version for a given channel + tag prefix.
# next_prerelease <channel> <tag_prefix>
#
# Channel must be alpha or beta. Tag prefix is the leading text on this
# tool's stable git tags — e.g. "zddc-server-v" or "archive-v" — so the
# function can be called from either the server release script or any
# HTML tool's build.sh against the same monorepo tag namespace.
#
# Algorithm:
# 1. Walk tags matching <prefix>X.Y.Z (clean stable, no suffix); pick the
# semver-highest. Default 0.0.0 if no stable tag exists yet.
# 2. Bump the patch component → next_patch.
# 3. Count existing tags of the form <prefix><next_patch>-<channel>.*
# and emit <next_patch>-<channel>.<count+1>.
#
# The patch-bump assumption: every active pre-release window targets the
# next patch of the latest stable. Cutting a real stable resets the
# counter naturally because next_patch advances. Operators wanting a
# minor or major bump cut stable explicitly with a version arg, then the
# subsequent alphas auto-derive against the new stable.
next_prerelease() {
_channel="$1"
_prefix="$2"
case "$_channel" in
alpha | beta) ;;
*) echo "next_prerelease: channel must be alpha or beta (got '$_channel')" >&2; return 1 ;;
esac
if [ -z "$_prefix" ]; then
echo "next_prerelease: tag prefix is required" >&2
return 1
fi
_latest=$(git -C "$root_dir" tag --list "${_prefix}*" 2>/dev/null \
| grep -E "^${_prefix}[0-9]+\.[0-9]+\.[0-9]+\$" \
| sed "s|^${_prefix}||" \
| sort -V \
| tail -1)
[ -n "$_latest" ] || _latest="0.0.0"
_major="${_latest%%.*}"
_rest="${_latest#*.}"
_minor="${_rest%%.*}"
_patch="${_rest#*.}"
_patch=$((_patch + 1))
_next_patch="${_major}.${_minor}.${_patch}"
_count=$(git -C "$root_dir" tag --list "${_prefix}${_next_patch}-${_channel}.*" 2>/dev/null | wc -l | tr -d ' ')
_count=$((_count + 1))
echo "${_next_patch}-${_channel}.${_count}"
}
# Validate that $1 is a strict X.Y.Z numeric version, where each component # Validate that $1 is a strict X.Y.Z numeric version, where each component
# is a non-empty numeric string. Exits with an error if not. # is a non-empty numeric string. Exits with an error if not.
_validate_semver() { _validate_semver() {
@ -264,33 +215,44 @@ _next_stable_for_tool() {
echo "${_major}.${_minor}.$((_patch + 1))" echo "${_major}.${_minor}.$((_patch + 1))"
} }
# Promote a built dist file to a Codeberg release. # Promote a built dist file to website/releases/. Reads from caller scope:
# Reads from caller scope: $channel, $build_version, $output_html, $root_dir. # $channel ("stable" / "alpha" / "beta"), $build_version (stable only),
# $output_html, $root_dir.
# #
# All three channels (alpha, beta, stable) follow the same shape now: # Stable cuts:
# 1. Compute the version (already done by compute_build_label for stable; # 1. Skip if source unchanged since latest stable tag.
# for alpha/beta we compute next_prerelease here). # 2. Copy dist HTML → website/releases/<tool>_v<X.Y.Z>.html (immutable).
# 2. Tag the commit <tool>-v<version> (or <tool>-v<version>-CHANNEL.N). # 3. Refresh symlinks: _v<X.Y>, _v<X>, _stable, _beta, _alpha all → the
# 3. Upload the built dist HTML as a release asset to Codeberg. # 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>.
# #
# Idempotent: the publish helper replaces a same-named asset on re-upload, # Alpha/beta cuts:
# and the tag step is a no-op if the tag already points at HEAD. # 1. Overwrite website/releases/<tool>_<channel>.html with dist HTML
# (replaces a symlink with real bytes if one was there).
# 2. For beta: cascade <tool>_alpha.html → <tool>_beta.html (symlink),
# since alpha defaults to beta when no active alpha.
# 3. No tag — channel URLs are stable URLs by design; counters defeat
# that. The on-page label encodes <date> · <sha> for traceability.
# #
# For stable: the original "skip if no source change since latest stable # Plain dev builds (no --release): never call promote_release.
# tag" guard still applies — pointless re-releases are silently no-op'd.
# For alpha/beta: the auto-incrementing counter already differentiates
# successive cuts, so no skip check.
# #
# Requires $CODEBERG_TOKEN exported. publish_codeberg_release surfaces a # No Codeberg upload — HTML tools live in git. zddc-server's release.sh
# clear error if it isn't. # handles binary uploads to Codeberg directly (different distribution model).
promote_release() { promote_release() {
_tool="$1" _tool="$1"
_releases_dir="$root_dir/../website/releases"
if [ ! -d "$_releases_dir" ]; then
echo "promote_release: $_releases_dir not found" >&2
return 1
fi
case "$channel" in case "$channel" in
stable) stable)
if [ -z "$build_version" ]; then if [ -z "$build_version" ]; then
echo "promote_release: stable channel but no build_version" >&2 echo "promote_release: stable channel but no build_version" >&2
exit 1 return 1
fi fi
_latest=$(git -C "$root_dir" tag --list "${_tool}-v*" 2>/dev/null \ _latest=$(git -C "$root_dir" tag --list "${_tool}-v*" 2>/dev/null \
| grep -E "^${_tool}-v[0-9]+\.[0-9]+\.[0-9]+\$" \ | grep -E "^${_tool}-v[0-9]+\.[0-9]+\.[0-9]+\$" \
@ -299,25 +261,49 @@ promote_release() {
echo "${_tool}: no source changes since $_latest — skipping" echo "${_tool}: no source changes since $_latest — skipping"
return 0 return 0
fi fi
_version="$build_version" _promote_stable "$_tool" "$build_version" "$_releases_dir"
;; ;;
alpha | beta) alpha | beta)
_version=$(next_prerelease "$channel" "${_tool}-v") _promote_channel "$_tool" "$channel" "$_releases_dir"
;; ;;
*) *)
echo "promote_release: unknown channel '$channel'" >&2 echo "promote_release: unknown channel '$channel'" >&2
exit 1 return 1
;; ;;
esac esac
}
_tag="${_tool}-v${_version}" # Stable cut: per-version file + 5 symlinks + git tag.
_promote_stable() {
_t="$1"
_ver="$2"
_rdir="$3"
# Tag the commit (idempotent: skip if already at HEAD). _major="${_ver%%.*}"
_rest="${_ver#*.}"
_minor="${_rest%%.*}"
_versioned="${_t}_v${_ver}.html"
cp "$output_html" "$_rdir/$_versioned"
echo "Wrote $_rdir/$_versioned"
# Refresh the 5 symlinks. Cascade: stable cut → beta + alpha both
# reset to stable (no active dev on either downstream channel).
for _sym in "${_t}_v${_major}.${_minor}.html" \
"${_t}_v${_major}.html" \
"${_t}_stable.html" \
"${_t}_beta.html" \
"${_t}_alpha.html"; do
ln -sfn "$_versioned" "$_rdir/$_sym"
echo " $_sym$_versioned"
done
_tag="${_t}-v${_ver}"
if git -C "$root_dir" rev-parse -q --verify "refs/tags/$_tag" >/dev/null; then if git -C "$root_dir" rev-parse -q --verify "refs/tags/$_tag" >/dev/null; then
_existing=$(git -C "$root_dir" rev-list -n 1 "$_tag") _existing=$(git -C "$root_dir" rev-list -n 1 "$_tag")
_head=$(git -C "$root_dir" rev-parse HEAD) _head=$(git -C "$root_dir" rev-parse HEAD)
if [ "$_existing" != "$_head" ]; then if [ "$_existing" != "$_head" ]; then
echo "promote_release: tag $_tag already exists at $_existing, but HEAD is $_head" >&2 echo "promote_release: tag $_tag exists at $_existing but HEAD is $_head" >&2
return 1 return 1
fi fi
echo "(tag $_tag already at HEAD)" echo "(tag $_tag already at HEAD)"
@ -326,21 +312,28 @@ promote_release() {
echo "tagged $_tag" echo "tagged $_tag"
fi fi
# Upload to Codeberg. The asset name embeds the version so consumers echo "Released ${_t} v${_ver} (stable)"
# can pin to a specific build (e.g. <tool>_v0.0.3-alpha.1.html).
_asset="${_tool}_v${_version}.html"
_staged="$root_dir/$_tool/dist/$_asset"
cp "$output_html" "$_staged"
if ! command -v publish_codeberg_release >/dev/null 2>&1; then
# build-lib.sh is sourced before publish-codeberg-release.sh in the
# canonical wrapper scripts; if the helper isn't loaded yet, bail
# with a clear pointer.
echo "promote_release: publish_codeberg_release not available; source shared/publish-codeberg-release.sh first" >&2
return 1
fi
publish_codeberg_release "VARASYS/ZDDC" "$_tag" "$_staged"
rm -f "$_staged"
echo "Released $_tag (channel: $channel, version: $_version)"
echo " publish git tag with: git push origin $_tag" echo " publish git tag with: git push origin $_tag"
} }
# 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}"
}

View file

@ -44,11 +44,12 @@ for (const tool of tools) {
const match = html.match(/class="build-timestamp">(?:<span[^>]*>)?([^<]+?)(?:<\/span>)?</); const match = html.match(/class="build-timestamp">(?:<span[^>]*>)?([^<]+?)(?:<\/span>)?</);
expect(match, 'build-timestamp element must have text content').toBeTruthy(); expect(match, 'build-timestamp element must have text content').toBeTruthy();
const label = match[1]; const label = match[1];
// Plain dev builds and --release alpha|beta: // Plain dev builds and --release alpha|beta carry the next-stable
// "alpha · 2026-04-29 00:50:17 · 714faf6-dirty" (plain, with timestamp + dirty marker) // target as a pre-release suffix:
// "alpha · 2026-04-29 · 714faf6" (--release alpha, date only) // "v0.0.3-alpha · 2026-04-29 00:50:17 · 714faf6-dirty" (plain dev)
// "beta · 2026-04-29 · 714faf6" (--release beta) // "v0.0.3-alpha · 2026-04-29 · 714faf6" (--release alpha)
const isChannel = /^(alpha|beta) · 20\d\d-\d\d-\d\d( \d\d:\d\d:\d\d)? · [0-9a-f]+(-dirty)?$/.test(label); // "v0.0.3-beta · 2026-04-29 · 714faf6" (--release beta)
const isChannel = /^v\d+\.\d+\.\d+-(alpha|beta) · 20\d\d-\d\d-\d\d( \d\d:\d\d:\d\d)? · [0-9a-f]+(-dirty)?$/.test(label);
const isVersion = /^v\d+\.\d+\.\d+$/.test(label); const isVersion = /^v\d+\.\d+\.\d+$/.test(label);
expect(isChannel || isVersion, expect(isChannel || isVersion,
`Expected channel or version label, got: "${label}"` `Expected channel or version label, got: "${label}"`

View file

@ -14,47 +14,33 @@
// document.write()s it in place. The default upstream is the // document.write()s it in place. The default upstream is the
// alpha channel; the URL parameter ?v= overrides it: // alpha channel; the URL parameter ?v= overrides it:
// //
// ?v=alpha|beta|stable switches to that channel // ?v=stable | beta | alpha switch to that channel
// ?v=0.0.4 (or v0.0.4) pins to that exact stable version // ?v=0.0.4 (or v0.0.4) pin to that exact stable version
// (none) uses the alpha default // ?v=0.0 (or v0.0) pin to latest patch within 0.0.x (symlink)
// ?v=0 (or v0) pin to latest within 0.x (symlink)
// (none) use the alpha default
// //
// Resolution path: // Resolution is purely static — every URL maps to a real file or a
// 1. Channel mode (no ?v= or ?v={alpha,beta,stable}): fetch // checked-in symlink under <upstream>/releases/. No manifest lookup,
// manifest.json from the same origin, look up // no JS indirection, no client-side version arithmetic.
// "<tool>-<channel>" → tag, then fetch the asset.
// 2. Version pin: build the URL directly (skip manifest).
//
// The asset URL is /releases/<tag>/<tool>_v<version>.html. Caddy on
// zddc.varasys.io reverse-proxies that to the Codeberg release-asset
// URL — neither this stub nor the user's browser hits codeberg.org
// directly.
(async function () { (async function () {
const params = new URLSearchParams(location.search); const params = new URLSearchParams(location.search);
const v = params.get('v'); const v = params.get('v');
const tool = 'archive'; const tool = 'archive';
const defaultChannel = 'alpha'; const defaultChannel = 'alpha';
const channels = ['alpha', 'beta', 'stable']; const channels = ['stable', 'beta', 'alpha'];
const base = 'https://zddc.varasys.io/releases/'; const base = 'https://zddc.varasys.io/releases/';
function isChannel(s) { return channels.indexOf(s) >= 0; } function suffixFor(value) {
if (!value) return '_' + defaultChannel;
try { if (channels.indexOf(value) >= 0) return '_' + value;
let assetUrl; // Strip optional leading 'v', accept "0.0.4" / "0.0" / "0".
if (v && !isChannel(v)) { const ver = value.startsWith('v') ? value.slice(1) : value;
// Explicit version pin (e.g. ?v=0.0.4 or ?v=v0.0.4). return '_v' + ver;
const ver = v.replace(/^v/, '');
const tag = tool + '-v' + ver;
assetUrl = base + tag + '/' + tool + '_v' + ver + '.html';
} else {
// Channel mode: resolve via manifest.
const channel = (v && isChannel(v)) ? v : defaultChannel;
const manifest = await (await fetch(base + 'manifest.json', { cache: 'no-cache', credentials: 'omit' })).json();
const tag = manifest[tool + '-' + channel];
if (!tag) throw new Error('manifest has no entry for ' + tool + '-' + channel);
const ver = tag.replace(tool + '-v', '');
assetUrl = base + tag + '/' + tool + '_v' + ver + '.html';
} }
try {
const assetUrl = base + tool + suffixFor(v) + '.html';
const resp = await fetch(assetUrl, { cache: 'no-cache', credentials: 'omit' }); const resp = await fetch(assetUrl, { cache: 'no-cache', credentials: 'omit' });
if (!resp.ok) throw new Error(resp.status + ' ' + resp.statusText); if (!resp.ok) throw new Error(resp.status + ' ' + resp.statusText);
const html = await resp.text(); const html = await resp.text();

View file

@ -14,47 +14,33 @@
// document.write()s it in place. The default upstream is the // document.write()s it in place. The default upstream is the
// alpha channel; the URL parameter ?v= overrides it: // alpha channel; the URL parameter ?v= overrides it:
// //
// ?v=alpha|beta|stable switches to that channel // ?v=stable | beta | alpha switch to that channel
// ?v=0.0.4 (or v0.0.4) pins to that exact stable version // ?v=0.0.4 (or v0.0.4) pin to that exact stable version
// (none) uses the alpha default // ?v=0.0 (or v0.0) pin to latest patch within 0.0.x (symlink)
// ?v=0 (or v0) pin to latest within 0.x (symlink)
// (none) use the alpha default
// //
// Resolution path: // Resolution is purely static — every URL maps to a real file or a
// 1. Channel mode (no ?v= or ?v={alpha,beta,stable}): fetch // checked-in symlink under <upstream>/releases/. No manifest lookup,
// manifest.json from the same origin, look up // no JS indirection, no client-side version arithmetic.
// "<tool>-<channel>" → tag, then fetch the asset.
// 2. Version pin: build the URL directly (skip manifest).
//
// The asset URL is /releases/<tag>/<tool>_v<version>.html. Caddy on
// zddc.varasys.io reverse-proxies that to the Codeberg release-asset
// URL — neither this stub nor the user's browser hits codeberg.org
// directly.
(async function () { (async function () {
const params = new URLSearchParams(location.search); const params = new URLSearchParams(location.search);
const v = params.get('v'); const v = params.get('v');
const tool = 'classifier'; const tool = 'classifier';
const defaultChannel = 'alpha'; const defaultChannel = 'alpha';
const channels = ['alpha', 'beta', 'stable']; const channels = ['stable', 'beta', 'alpha'];
const base = 'https://zddc.varasys.io/releases/'; const base = 'https://zddc.varasys.io/releases/';
function isChannel(s) { return channels.indexOf(s) >= 0; } function suffixFor(value) {
if (!value) return '_' + defaultChannel;
try { if (channels.indexOf(value) >= 0) return '_' + value;
let assetUrl; // Strip optional leading 'v', accept "0.0.4" / "0.0" / "0".
if (v && !isChannel(v)) { const ver = value.startsWith('v') ? value.slice(1) : value;
// Explicit version pin (e.g. ?v=0.0.4 or ?v=v0.0.4). return '_v' + ver;
const ver = v.replace(/^v/, '');
const tag = tool + '-v' + ver;
assetUrl = base + tag + '/' + tool + '_v' + ver + '.html';
} else {
// Channel mode: resolve via manifest.
const channel = (v && isChannel(v)) ? v : defaultChannel;
const manifest = await (await fetch(base + 'manifest.json', { cache: 'no-cache', credentials: 'omit' })).json();
const tag = manifest[tool + '-' + channel];
if (!tag) throw new Error('manifest has no entry for ' + tool + '-' + channel);
const ver = tag.replace(tool + '-v', '');
assetUrl = base + tag + '/' + tool + '_v' + ver + '.html';
} }
try {
const assetUrl = base + tool + suffixFor(v) + '.html';
const resp = await fetch(assetUrl, { cache: 'no-cache', credentials: 'omit' }); const resp = await fetch(assetUrl, { cache: 'no-cache', credentials: 'omit' });
if (!resp.ok) throw new Error(resp.status + ' ' + resp.statusText); if (!resp.ok) throw new Error(resp.status + ' ' + resp.statusText);
const html = await resp.text(); const html = await resp.text();

View file

@ -14,47 +14,33 @@
// document.write()s it in place. The default upstream is the // document.write()s it in place. The default upstream is the
// alpha channel; the URL parameter ?v= overrides it: // alpha channel; the URL parameter ?v= overrides it:
// //
// ?v=alpha|beta|stable switches to that channel // ?v=stable | beta | alpha switch to that channel
// ?v=0.0.4 (or v0.0.4) pins to that exact stable version // ?v=0.0.4 (or v0.0.4) pin to that exact stable version
// (none) uses the alpha default // ?v=0.0 (or v0.0) pin to latest patch within 0.0.x (symlink)
// ?v=0 (or v0) pin to latest within 0.x (symlink)
// (none) use the alpha default
// //
// Resolution path: // Resolution is purely static — every URL maps to a real file or a
// 1. Channel mode (no ?v= or ?v={alpha,beta,stable}): fetch // checked-in symlink under <upstream>/releases/. No manifest lookup,
// manifest.json from the same origin, look up // no JS indirection, no client-side version arithmetic.
// "<tool>-<channel>" → tag, then fetch the asset.
// 2. Version pin: build the URL directly (skip manifest).
//
// The asset URL is /releases/<tag>/<tool>_v<version>.html. Caddy on
// zddc.varasys.io reverse-proxies that to the Codeberg release-asset
// URL — neither this stub nor the user's browser hits codeberg.org
// directly.
(async function () { (async function () {
const params = new URLSearchParams(location.search); const params = new URLSearchParams(location.search);
const v = params.get('v'); const v = params.get('v');
const tool = 'landing'; const tool = 'landing';
const defaultChannel = 'alpha'; const defaultChannel = 'alpha';
const channels = ['alpha', 'beta', 'stable']; const channels = ['stable', 'beta', 'alpha'];
const base = 'https://zddc.varasys.io/releases/'; const base = 'https://zddc.varasys.io/releases/';
function isChannel(s) { return channels.indexOf(s) >= 0; } function suffixFor(value) {
if (!value) return '_' + defaultChannel;
try { if (channels.indexOf(value) >= 0) return '_' + value;
let assetUrl; // Strip optional leading 'v', accept "0.0.4" / "0.0" / "0".
if (v && !isChannel(v)) { const ver = value.startsWith('v') ? value.slice(1) : value;
// Explicit version pin (e.g. ?v=0.0.4 or ?v=v0.0.4). return '_v' + ver;
const ver = v.replace(/^v/, '');
const tag = tool + '-v' + ver;
assetUrl = base + tag + '/' + tool + '_v' + ver + '.html';
} else {
// Channel mode: resolve via manifest.
const channel = (v && isChannel(v)) ? v : defaultChannel;
const manifest = await (await fetch(base + 'manifest.json', { cache: 'no-cache', credentials: 'omit' })).json();
const tag = manifest[tool + '-' + channel];
if (!tag) throw new Error('manifest has no entry for ' + tool + '-' + channel);
const ver = tag.replace(tool + '-v', '');
assetUrl = base + tag + '/' + tool + '_v' + ver + '.html';
} }
try {
const assetUrl = base + tool + suffixFor(v) + '.html';
const resp = await fetch(assetUrl, { cache: 'no-cache', credentials: 'omit' }); const resp = await fetch(assetUrl, { cache: 'no-cache', credentials: 'omit' });
if (!resp.ok) throw new Error(resp.status + ' ' + resp.statusText); if (!resp.ok) throw new Error(resp.status + ' ' + resp.statusText);
const html = await resp.text(); const html = await resp.text();

View file

@ -14,47 +14,33 @@
// document.write()s it in place. The default upstream is the // document.write()s it in place. The default upstream is the
// alpha channel; the URL parameter ?v= overrides it: // alpha channel; the URL parameter ?v= overrides it:
// //
// ?v=alpha|beta|stable switches to that channel // ?v=stable | beta | alpha switch to that channel
// ?v=0.0.4 (or v0.0.4) pins to that exact stable version // ?v=0.0.4 (or v0.0.4) pin to that exact stable version
// (none) uses the alpha default // ?v=0.0 (or v0.0) pin to latest patch within 0.0.x (symlink)
// ?v=0 (or v0) pin to latest within 0.x (symlink)
// (none) use the alpha default
// //
// Resolution path: // Resolution is purely static — every URL maps to a real file or a
// 1. Channel mode (no ?v= or ?v={alpha,beta,stable}): fetch // checked-in symlink under <upstream>/releases/. No manifest lookup,
// manifest.json from the same origin, look up // no JS indirection, no client-side version arithmetic.
// "<tool>-<channel>" → tag, then fetch the asset.
// 2. Version pin: build the URL directly (skip manifest).
//
// The asset URL is /releases/<tag>/<tool>_v<version>.html. Caddy on
// zddc.varasys.io reverse-proxies that to the Codeberg release-asset
// URL — neither this stub nor the user's browser hits codeberg.org
// directly.
(async function () { (async function () {
const params = new URLSearchParams(location.search); const params = new URLSearchParams(location.search);
const v = params.get('v'); const v = params.get('v');
const tool = 'mdedit'; const tool = 'mdedit';
const defaultChannel = 'alpha'; const defaultChannel = 'alpha';
const channels = ['alpha', 'beta', 'stable']; const channels = ['stable', 'beta', 'alpha'];
const base = 'https://zddc.varasys.io/releases/'; const base = 'https://zddc.varasys.io/releases/';
function isChannel(s) { return channels.indexOf(s) >= 0; } function suffixFor(value) {
if (!value) return '_' + defaultChannel;
try { if (channels.indexOf(value) >= 0) return '_' + value;
let assetUrl; // Strip optional leading 'v', accept "0.0.4" / "0.0" / "0".
if (v && !isChannel(v)) { const ver = value.startsWith('v') ? value.slice(1) : value;
// Explicit version pin (e.g. ?v=0.0.4 or ?v=v0.0.4). return '_v' + ver;
const ver = v.replace(/^v/, '');
const tag = tool + '-v' + ver;
assetUrl = base + tag + '/' + tool + '_v' + ver + '.html';
} else {
// Channel mode: resolve via manifest.
const channel = (v && isChannel(v)) ? v : defaultChannel;
const manifest = await (await fetch(base + 'manifest.json', { cache: 'no-cache', credentials: 'omit' })).json();
const tag = manifest[tool + '-' + channel];
if (!tag) throw new Error('manifest has no entry for ' + tool + '-' + channel);
const ver = tag.replace(tool + '-v', '');
assetUrl = base + tag + '/' + tool + '_v' + ver + '.html';
} }
try {
const assetUrl = base + tool + suffixFor(v) + '.html';
const resp = await fetch(assetUrl, { cache: 'no-cache', credentials: 'omit' }); const resp = await fetch(assetUrl, { cache: 'no-cache', credentials: 'omit' });
if (!resp.ok) throw new Error(resp.status + ' ' + resp.statusText); if (!resp.ok) throw new Error(resp.status + ' ' + resp.statusText);
const html = await resp.text(); const html = await resp.text();

View file

@ -14,47 +14,33 @@
// document.write()s it in place. The default upstream is the // document.write()s it in place. The default upstream is the
// alpha channel; the URL parameter ?v= overrides it: // alpha channel; the URL parameter ?v= overrides it:
// //
// ?v=alpha|beta|stable switches to that channel // ?v=stable | beta | alpha switch to that channel
// ?v=0.0.4 (or v0.0.4) pins to that exact stable version // ?v=0.0.4 (or v0.0.4) pin to that exact stable version
// (none) uses the alpha default // ?v=0.0 (or v0.0) pin to latest patch within 0.0.x (symlink)
// ?v=0 (or v0) pin to latest within 0.x (symlink)
// (none) use the alpha default
// //
// Resolution path: // Resolution is purely static — every URL maps to a real file or a
// 1. Channel mode (no ?v= or ?v={alpha,beta,stable}): fetch // checked-in symlink under <upstream>/releases/. No manifest lookup,
// manifest.json from the same origin, look up // no JS indirection, no client-side version arithmetic.
// "<tool>-<channel>" → tag, then fetch the asset.
// 2. Version pin: build the URL directly (skip manifest).
//
// The asset URL is /releases/<tag>/<tool>_v<version>.html. Caddy on
// zddc.varasys.io reverse-proxies that to the Codeberg release-asset
// URL — neither this stub nor the user's browser hits codeberg.org
// directly.
(async function () { (async function () {
const params = new URLSearchParams(location.search); const params = new URLSearchParams(location.search);
const v = params.get('v'); const v = params.get('v');
const tool = 'transmittal'; const tool = 'transmittal';
const defaultChannel = 'alpha'; const defaultChannel = 'alpha';
const channels = ['alpha', 'beta', 'stable']; const channels = ['stable', 'beta', 'alpha'];
const base = 'https://zddc.varasys.io/releases/'; const base = 'https://zddc.varasys.io/releases/';
function isChannel(s) { return channels.indexOf(s) >= 0; } function suffixFor(value) {
if (!value) return '_' + defaultChannel;
try { if (channels.indexOf(value) >= 0) return '_' + value;
let assetUrl; // Strip optional leading 'v', accept "0.0.4" / "0.0" / "0".
if (v && !isChannel(v)) { const ver = value.startsWith('v') ? value.slice(1) : value;
// Explicit version pin (e.g. ?v=0.0.4 or ?v=v0.0.4). return '_v' + ver;
const ver = v.replace(/^v/, '');
const tag = tool + '-v' + ver;
assetUrl = base + tag + '/' + tool + '_v' + ver + '.html';
} else {
// Channel mode: resolve via manifest.
const channel = (v && isChannel(v)) ? v : defaultChannel;
const manifest = await (await fetch(base + 'manifest.json', { cache: 'no-cache', credentials: 'omit' })).json();
const tag = manifest[tool + '-' + channel];
if (!tag) throw new Error('manifest has no entry for ' + tool + '-' + channel);
const ver = tag.replace(tool + '-v', '');
assetUrl = base + tag + '/' + tool + '_v' + ver + '.html';
} }
try {
const assetUrl = base + tool + suffixFor(v) + '.html';
const resp = await fetch(assetUrl, { cache: 'no-cache', credentials: 'omit' }); const resp = await fetch(assetUrl, { cache: 'no-cache', credentials: 'omit' });
if (!resp.ok) throw new Error(resp.status + ' ' + resp.statusText); if (!resp.ok) throw new Error(resp.status + ' ' + resp.statusText);
const html = await resp.text(); const html = await resp.text();

View file

@ -14,47 +14,33 @@
// document.write()s it in place. The default upstream is the // document.write()s it in place. The default upstream is the
// beta channel; the URL parameter ?v= overrides it: // beta channel; the URL parameter ?v= overrides it:
// //
// ?v=alpha|beta|stable switches to that channel // ?v=stable | beta | alpha switch to that channel
// ?v=0.0.4 (or v0.0.4) pins to that exact stable version // ?v=0.0.4 (or v0.0.4) pin to that exact stable version
// (none) uses the beta default // ?v=0.0 (or v0.0) pin to latest patch within 0.0.x (symlink)
// ?v=0 (or v0) pin to latest within 0.x (symlink)
// (none) use the beta default
// //
// Resolution path: // Resolution is purely static — every URL maps to a real file or a
// 1. Channel mode (no ?v= or ?v={alpha,beta,stable}): fetch // checked-in symlink under <upstream>/releases/. No manifest lookup,
// manifest.json from the same origin, look up // no JS indirection, no client-side version arithmetic.
// "<tool>-<channel>" → tag, then fetch the asset.
// 2. Version pin: build the URL directly (skip manifest).
//
// The asset URL is /releases/<tag>/<tool>_v<version>.html. Caddy on
// zddc.varasys.io reverse-proxies that to the Codeberg release-asset
// URL — neither this stub nor the user's browser hits codeberg.org
// directly.
(async function () { (async function () {
const params = new URLSearchParams(location.search); const params = new URLSearchParams(location.search);
const v = params.get('v'); const v = params.get('v');
const tool = 'archive'; const tool = 'archive';
const defaultChannel = 'beta'; const defaultChannel = 'beta';
const channels = ['alpha', 'beta', 'stable']; const channels = ['stable', 'beta', 'alpha'];
const base = 'https://zddc.varasys.io/releases/'; const base = 'https://zddc.varasys.io/releases/';
function isChannel(s) { return channels.indexOf(s) >= 0; } function suffixFor(value) {
if (!value) return '_' + defaultChannel;
try { if (channels.indexOf(value) >= 0) return '_' + value;
let assetUrl; // Strip optional leading 'v', accept "0.0.4" / "0.0" / "0".
if (v && !isChannel(v)) { const ver = value.startsWith('v') ? value.slice(1) : value;
// Explicit version pin (e.g. ?v=0.0.4 or ?v=v0.0.4). return '_v' + ver;
const ver = v.replace(/^v/, '');
const tag = tool + '-v' + ver;
assetUrl = base + tag + '/' + tool + '_v' + ver + '.html';
} else {
// Channel mode: resolve via manifest.
const channel = (v && isChannel(v)) ? v : defaultChannel;
const manifest = await (await fetch(base + 'manifest.json', { cache: 'no-cache', credentials: 'omit' })).json();
const tag = manifest[tool + '-' + channel];
if (!tag) throw new Error('manifest has no entry for ' + tool + '-' + channel);
const ver = tag.replace(tool + '-v', '');
assetUrl = base + tag + '/' + tool + '_v' + ver + '.html';
} }
try {
const assetUrl = base + tool + suffixFor(v) + '.html';
const resp = await fetch(assetUrl, { cache: 'no-cache', credentials: 'omit' }); const resp = await fetch(assetUrl, { cache: 'no-cache', credentials: 'omit' });
if (!resp.ok) throw new Error(resp.status + ' ' + resp.statusText); if (!resp.ok) throw new Error(resp.status + ' ' + resp.statusText);
const html = await resp.text(); const html = await resp.text();

View file

@ -14,47 +14,33 @@
// document.write()s it in place. The default upstream is the // document.write()s it in place. The default upstream is the
// beta channel; the URL parameter ?v= overrides it: // beta channel; the URL parameter ?v= overrides it:
// //
// ?v=alpha|beta|stable switches to that channel // ?v=stable | beta | alpha switch to that channel
// ?v=0.0.4 (or v0.0.4) pins to that exact stable version // ?v=0.0.4 (or v0.0.4) pin to that exact stable version
// (none) uses the beta default // ?v=0.0 (or v0.0) pin to latest patch within 0.0.x (symlink)
// ?v=0 (or v0) pin to latest within 0.x (symlink)
// (none) use the beta default
// //
// Resolution path: // Resolution is purely static — every URL maps to a real file or a
// 1. Channel mode (no ?v= or ?v={alpha,beta,stable}): fetch // checked-in symlink under <upstream>/releases/. No manifest lookup,
// manifest.json from the same origin, look up // no JS indirection, no client-side version arithmetic.
// "<tool>-<channel>" → tag, then fetch the asset.
// 2. Version pin: build the URL directly (skip manifest).
//
// The asset URL is /releases/<tag>/<tool>_v<version>.html. Caddy on
// zddc.varasys.io reverse-proxies that to the Codeberg release-asset
// URL — neither this stub nor the user's browser hits codeberg.org
// directly.
(async function () { (async function () {
const params = new URLSearchParams(location.search); const params = new URLSearchParams(location.search);
const v = params.get('v'); const v = params.get('v');
const tool = 'classifier'; const tool = 'classifier';
const defaultChannel = 'beta'; const defaultChannel = 'beta';
const channels = ['alpha', 'beta', 'stable']; const channels = ['stable', 'beta', 'alpha'];
const base = 'https://zddc.varasys.io/releases/'; const base = 'https://zddc.varasys.io/releases/';
function isChannel(s) { return channels.indexOf(s) >= 0; } function suffixFor(value) {
if (!value) return '_' + defaultChannel;
try { if (channels.indexOf(value) >= 0) return '_' + value;
let assetUrl; // Strip optional leading 'v', accept "0.0.4" / "0.0" / "0".
if (v && !isChannel(v)) { const ver = value.startsWith('v') ? value.slice(1) : value;
// Explicit version pin (e.g. ?v=0.0.4 or ?v=v0.0.4). return '_v' + ver;
const ver = v.replace(/^v/, '');
const tag = tool + '-v' + ver;
assetUrl = base + tag + '/' + tool + '_v' + ver + '.html';
} else {
// Channel mode: resolve via manifest.
const channel = (v && isChannel(v)) ? v : defaultChannel;
const manifest = await (await fetch(base + 'manifest.json', { cache: 'no-cache', credentials: 'omit' })).json();
const tag = manifest[tool + '-' + channel];
if (!tag) throw new Error('manifest has no entry for ' + tool + '-' + channel);
const ver = tag.replace(tool + '-v', '');
assetUrl = base + tag + '/' + tool + '_v' + ver + '.html';
} }
try {
const assetUrl = base + tool + suffixFor(v) + '.html';
const resp = await fetch(assetUrl, { cache: 'no-cache', credentials: 'omit' }); const resp = await fetch(assetUrl, { cache: 'no-cache', credentials: 'omit' });
if (!resp.ok) throw new Error(resp.status + ' ' + resp.statusText); if (!resp.ok) throw new Error(resp.status + ' ' + resp.statusText);
const html = await resp.text(); const html = await resp.text();

View file

@ -14,47 +14,33 @@
// document.write()s it in place. The default upstream is the // document.write()s it in place. The default upstream is the
// beta channel; the URL parameter ?v= overrides it: // beta channel; the URL parameter ?v= overrides it:
// //
// ?v=alpha|beta|stable switches to that channel // ?v=stable | beta | alpha switch to that channel
// ?v=0.0.4 (or v0.0.4) pins to that exact stable version // ?v=0.0.4 (or v0.0.4) pin to that exact stable version
// (none) uses the beta default // ?v=0.0 (or v0.0) pin to latest patch within 0.0.x (symlink)
// ?v=0 (or v0) pin to latest within 0.x (symlink)
// (none) use the beta default
// //
// Resolution path: // Resolution is purely static — every URL maps to a real file or a
// 1. Channel mode (no ?v= or ?v={alpha,beta,stable}): fetch // checked-in symlink under <upstream>/releases/. No manifest lookup,
// manifest.json from the same origin, look up // no JS indirection, no client-side version arithmetic.
// "<tool>-<channel>" → tag, then fetch the asset.
// 2. Version pin: build the URL directly (skip manifest).
//
// The asset URL is /releases/<tag>/<tool>_v<version>.html. Caddy on
// zddc.varasys.io reverse-proxies that to the Codeberg release-asset
// URL — neither this stub nor the user's browser hits codeberg.org
// directly.
(async function () { (async function () {
const params = new URLSearchParams(location.search); const params = new URLSearchParams(location.search);
const v = params.get('v'); const v = params.get('v');
const tool = 'landing'; const tool = 'landing';
const defaultChannel = 'beta'; const defaultChannel = 'beta';
const channels = ['alpha', 'beta', 'stable']; const channels = ['stable', 'beta', 'alpha'];
const base = 'https://zddc.varasys.io/releases/'; const base = 'https://zddc.varasys.io/releases/';
function isChannel(s) { return channels.indexOf(s) >= 0; } function suffixFor(value) {
if (!value) return '_' + defaultChannel;
try { if (channels.indexOf(value) >= 0) return '_' + value;
let assetUrl; // Strip optional leading 'v', accept "0.0.4" / "0.0" / "0".
if (v && !isChannel(v)) { const ver = value.startsWith('v') ? value.slice(1) : value;
// Explicit version pin (e.g. ?v=0.0.4 or ?v=v0.0.4). return '_v' + ver;
const ver = v.replace(/^v/, '');
const tag = tool + '-v' + ver;
assetUrl = base + tag + '/' + tool + '_v' + ver + '.html';
} else {
// Channel mode: resolve via manifest.
const channel = (v && isChannel(v)) ? v : defaultChannel;
const manifest = await (await fetch(base + 'manifest.json', { cache: 'no-cache', credentials: 'omit' })).json();
const tag = manifest[tool + '-' + channel];
if (!tag) throw new Error('manifest has no entry for ' + tool + '-' + channel);
const ver = tag.replace(tool + '-v', '');
assetUrl = base + tag + '/' + tool + '_v' + ver + '.html';
} }
try {
const assetUrl = base + tool + suffixFor(v) + '.html';
const resp = await fetch(assetUrl, { cache: 'no-cache', credentials: 'omit' }); const resp = await fetch(assetUrl, { cache: 'no-cache', credentials: 'omit' });
if (!resp.ok) throw new Error(resp.status + ' ' + resp.statusText); if (!resp.ok) throw new Error(resp.status + ' ' + resp.statusText);
const html = await resp.text(); const html = await resp.text();

View file

@ -14,47 +14,33 @@
// document.write()s it in place. The default upstream is the // document.write()s it in place. The default upstream is the
// beta channel; the URL parameter ?v= overrides it: // beta channel; the URL parameter ?v= overrides it:
// //
// ?v=alpha|beta|stable switches to that channel // ?v=stable | beta | alpha switch to that channel
// ?v=0.0.4 (or v0.0.4) pins to that exact stable version // ?v=0.0.4 (or v0.0.4) pin to that exact stable version
// (none) uses the beta default // ?v=0.0 (or v0.0) pin to latest patch within 0.0.x (symlink)
// ?v=0 (or v0) pin to latest within 0.x (symlink)
// (none) use the beta default
// //
// Resolution path: // Resolution is purely static — every URL maps to a real file or a
// 1. Channel mode (no ?v= or ?v={alpha,beta,stable}): fetch // checked-in symlink under <upstream>/releases/. No manifest lookup,
// manifest.json from the same origin, look up // no JS indirection, no client-side version arithmetic.
// "<tool>-<channel>" → tag, then fetch the asset.
// 2. Version pin: build the URL directly (skip manifest).
//
// The asset URL is /releases/<tag>/<tool>_v<version>.html. Caddy on
// zddc.varasys.io reverse-proxies that to the Codeberg release-asset
// URL — neither this stub nor the user's browser hits codeberg.org
// directly.
(async function () { (async function () {
const params = new URLSearchParams(location.search); const params = new URLSearchParams(location.search);
const v = params.get('v'); const v = params.get('v');
const tool = 'mdedit'; const tool = 'mdedit';
const defaultChannel = 'beta'; const defaultChannel = 'beta';
const channels = ['alpha', 'beta', 'stable']; const channels = ['stable', 'beta', 'alpha'];
const base = 'https://zddc.varasys.io/releases/'; const base = 'https://zddc.varasys.io/releases/';
function isChannel(s) { return channels.indexOf(s) >= 0; } function suffixFor(value) {
if (!value) return '_' + defaultChannel;
try { if (channels.indexOf(value) >= 0) return '_' + value;
let assetUrl; // Strip optional leading 'v', accept "0.0.4" / "0.0" / "0".
if (v && !isChannel(v)) { const ver = value.startsWith('v') ? value.slice(1) : value;
// Explicit version pin (e.g. ?v=0.0.4 or ?v=v0.0.4). return '_v' + ver;
const ver = v.replace(/^v/, '');
const tag = tool + '-v' + ver;
assetUrl = base + tag + '/' + tool + '_v' + ver + '.html';
} else {
// Channel mode: resolve via manifest.
const channel = (v && isChannel(v)) ? v : defaultChannel;
const manifest = await (await fetch(base + 'manifest.json', { cache: 'no-cache', credentials: 'omit' })).json();
const tag = manifest[tool + '-' + channel];
if (!tag) throw new Error('manifest has no entry for ' + tool + '-' + channel);
const ver = tag.replace(tool + '-v', '');
assetUrl = base + tag + '/' + tool + '_v' + ver + '.html';
} }
try {
const assetUrl = base + tool + suffixFor(v) + '.html';
const resp = await fetch(assetUrl, { cache: 'no-cache', credentials: 'omit' }); const resp = await fetch(assetUrl, { cache: 'no-cache', credentials: 'omit' });
if (!resp.ok) throw new Error(resp.status + ' ' + resp.statusText); if (!resp.ok) throw new Error(resp.status + ' ' + resp.statusText);
const html = await resp.text(); const html = await resp.text();

View file

@ -14,47 +14,33 @@
// document.write()s it in place. The default upstream is the // document.write()s it in place. The default upstream is the
// beta channel; the URL parameter ?v= overrides it: // beta channel; the URL parameter ?v= overrides it:
// //
// ?v=alpha|beta|stable switches to that channel // ?v=stable | beta | alpha switch to that channel
// ?v=0.0.4 (or v0.0.4) pins to that exact stable version // ?v=0.0.4 (or v0.0.4) pin to that exact stable version
// (none) uses the beta default // ?v=0.0 (or v0.0) pin to latest patch within 0.0.x (symlink)
// ?v=0 (or v0) pin to latest within 0.x (symlink)
// (none) use the beta default
// //
// Resolution path: // Resolution is purely static — every URL maps to a real file or a
// 1. Channel mode (no ?v= or ?v={alpha,beta,stable}): fetch // checked-in symlink under <upstream>/releases/. No manifest lookup,
// manifest.json from the same origin, look up // no JS indirection, no client-side version arithmetic.
// "<tool>-<channel>" → tag, then fetch the asset.
// 2. Version pin: build the URL directly (skip manifest).
//
// The asset URL is /releases/<tag>/<tool>_v<version>.html. Caddy on
// zddc.varasys.io reverse-proxies that to the Codeberg release-asset
// URL — neither this stub nor the user's browser hits codeberg.org
// directly.
(async function () { (async function () {
const params = new URLSearchParams(location.search); const params = new URLSearchParams(location.search);
const v = params.get('v'); const v = params.get('v');
const tool = 'transmittal'; const tool = 'transmittal';
const defaultChannel = 'beta'; const defaultChannel = 'beta';
const channels = ['alpha', 'beta', 'stable']; const channels = ['stable', 'beta', 'alpha'];
const base = 'https://zddc.varasys.io/releases/'; const base = 'https://zddc.varasys.io/releases/';
function isChannel(s) { return channels.indexOf(s) >= 0; } function suffixFor(value) {
if (!value) return '_' + defaultChannel;
try { if (channels.indexOf(value) >= 0) return '_' + value;
let assetUrl; // Strip optional leading 'v', accept "0.0.4" / "0.0" / "0".
if (v && !isChannel(v)) { const ver = value.startsWith('v') ? value.slice(1) : value;
// Explicit version pin (e.g. ?v=0.0.4 or ?v=v0.0.4). return '_v' + ver;
const ver = v.replace(/^v/, '');
const tag = tool + '-v' + ver;
assetUrl = base + tag + '/' + tool + '_v' + ver + '.html';
} else {
// Channel mode: resolve via manifest.
const channel = (v && isChannel(v)) ? v : defaultChannel;
const manifest = await (await fetch(base + 'manifest.json', { cache: 'no-cache', credentials: 'omit' })).json();
const tag = manifest[tool + '-' + channel];
if (!tag) throw new Error('manifest has no entry for ' + tool + '-' + channel);
const ver = tag.replace(tool + '-v', '');
assetUrl = base + tag + '/' + tool + '_v' + ver + '.html';
} }
try {
const assetUrl = base + tool + suffixFor(v) + '.html';
const resp = await fetch(assetUrl, { cache: 'no-cache', credentials: 'omit' }); const resp = await fetch(assetUrl, { cache: 'no-cache', credentials: 'omit' });
if (!resp.ok) throw new Error(resp.status + ' ' + resp.statusText); if (!resp.ok) throw new Error(resp.status + ' ' + resp.statusText);
const html = await resp.text(); const html = await resp.text();

View file

@ -14,47 +14,33 @@
// document.write()s it in place. The default upstream is the // document.write()s it in place. The default upstream is the
// stable channel; the URL parameter ?v= overrides it: // stable channel; the URL parameter ?v= overrides it:
// //
// ?v=alpha|beta|stable switches to that channel // ?v=stable | beta | alpha switch to that channel
// ?v=0.0.4 (or v0.0.4) pins to that exact stable version // ?v=0.0.4 (or v0.0.4) pin to that exact stable version
// (none) uses the stable default // ?v=0.0 (or v0.0) pin to latest patch within 0.0.x (symlink)
// ?v=0 (or v0) pin to latest within 0.x (symlink)
// (none) use the stable default
// //
// Resolution path: // Resolution is purely static — every URL maps to a real file or a
// 1. Channel mode (no ?v= or ?v={alpha,beta,stable}): fetch // checked-in symlink under <upstream>/releases/. No manifest lookup,
// manifest.json from the same origin, look up // no JS indirection, no client-side version arithmetic.
// "<tool>-<channel>" → tag, then fetch the asset.
// 2. Version pin: build the URL directly (skip manifest).
//
// The asset URL is /releases/<tag>/<tool>_v<version>.html. Caddy on
// zddc.varasys.io reverse-proxies that to the Codeberg release-asset
// URL — neither this stub nor the user's browser hits codeberg.org
// directly.
(async function () { (async function () {
const params = new URLSearchParams(location.search); const params = new URLSearchParams(location.search);
const v = params.get('v'); const v = params.get('v');
const tool = 'archive'; const tool = 'archive';
const defaultChannel = 'stable'; const defaultChannel = 'stable';
const channels = ['alpha', 'beta', 'stable']; const channels = ['stable', 'beta', 'alpha'];
const base = 'https://zddc.varasys.io/releases/'; const base = 'https://zddc.varasys.io/releases/';
function isChannel(s) { return channels.indexOf(s) >= 0; } function suffixFor(value) {
if (!value) return '_' + defaultChannel;
try { if (channels.indexOf(value) >= 0) return '_' + value;
let assetUrl; // Strip optional leading 'v', accept "0.0.4" / "0.0" / "0".
if (v && !isChannel(v)) { const ver = value.startsWith('v') ? value.slice(1) : value;
// Explicit version pin (e.g. ?v=0.0.4 or ?v=v0.0.4). return '_v' + ver;
const ver = v.replace(/^v/, '');
const tag = tool + '-v' + ver;
assetUrl = base + tag + '/' + tool + '_v' + ver + '.html';
} else {
// Channel mode: resolve via manifest.
const channel = (v && isChannel(v)) ? v : defaultChannel;
const manifest = await (await fetch(base + 'manifest.json', { cache: 'no-cache', credentials: 'omit' })).json();
const tag = manifest[tool + '-' + channel];
if (!tag) throw new Error('manifest has no entry for ' + tool + '-' + channel);
const ver = tag.replace(tool + '-v', '');
assetUrl = base + tag + '/' + tool + '_v' + ver + '.html';
} }
try {
const assetUrl = base + tool + suffixFor(v) + '.html';
const resp = await fetch(assetUrl, { cache: 'no-cache', credentials: 'omit' }); const resp = await fetch(assetUrl, { cache: 'no-cache', credentials: 'omit' });
if (!resp.ok) throw new Error(resp.status + ' ' + resp.statusText); if (!resp.ok) throw new Error(resp.status + ' ' + resp.statusText);
const html = await resp.text(); const html = await resp.text();

View file

@ -14,47 +14,33 @@
// document.write()s it in place. The default upstream is the // document.write()s it in place. The default upstream is the
// stable channel; the URL parameter ?v= overrides it: // stable channel; the URL parameter ?v= overrides it:
// //
// ?v=alpha|beta|stable switches to that channel // ?v=stable | beta | alpha switch to that channel
// ?v=0.0.4 (or v0.0.4) pins to that exact stable version // ?v=0.0.4 (or v0.0.4) pin to that exact stable version
// (none) uses the stable default // ?v=0.0 (or v0.0) pin to latest patch within 0.0.x (symlink)
// ?v=0 (or v0) pin to latest within 0.x (symlink)
// (none) use the stable default
// //
// Resolution path: // Resolution is purely static — every URL maps to a real file or a
// 1. Channel mode (no ?v= or ?v={alpha,beta,stable}): fetch // checked-in symlink under <upstream>/releases/. No manifest lookup,
// manifest.json from the same origin, look up // no JS indirection, no client-side version arithmetic.
// "<tool>-<channel>" → tag, then fetch the asset.
// 2. Version pin: build the URL directly (skip manifest).
//
// The asset URL is /releases/<tag>/<tool>_v<version>.html. Caddy on
// zddc.varasys.io reverse-proxies that to the Codeberg release-asset
// URL — neither this stub nor the user's browser hits codeberg.org
// directly.
(async function () { (async function () {
const params = new URLSearchParams(location.search); const params = new URLSearchParams(location.search);
const v = params.get('v'); const v = params.get('v');
const tool = 'classifier'; const tool = 'classifier';
const defaultChannel = 'stable'; const defaultChannel = 'stable';
const channels = ['alpha', 'beta', 'stable']; const channels = ['stable', 'beta', 'alpha'];
const base = 'https://zddc.varasys.io/releases/'; const base = 'https://zddc.varasys.io/releases/';
function isChannel(s) { return channels.indexOf(s) >= 0; } function suffixFor(value) {
if (!value) return '_' + defaultChannel;
try { if (channels.indexOf(value) >= 0) return '_' + value;
let assetUrl; // Strip optional leading 'v', accept "0.0.4" / "0.0" / "0".
if (v && !isChannel(v)) { const ver = value.startsWith('v') ? value.slice(1) : value;
// Explicit version pin (e.g. ?v=0.0.4 or ?v=v0.0.4). return '_v' + ver;
const ver = v.replace(/^v/, '');
const tag = tool + '-v' + ver;
assetUrl = base + tag + '/' + tool + '_v' + ver + '.html';
} else {
// Channel mode: resolve via manifest.
const channel = (v && isChannel(v)) ? v : defaultChannel;
const manifest = await (await fetch(base + 'manifest.json', { cache: 'no-cache', credentials: 'omit' })).json();
const tag = manifest[tool + '-' + channel];
if (!tag) throw new Error('manifest has no entry for ' + tool + '-' + channel);
const ver = tag.replace(tool + '-v', '');
assetUrl = base + tag + '/' + tool + '_v' + ver + '.html';
} }
try {
const assetUrl = base + tool + suffixFor(v) + '.html';
const resp = await fetch(assetUrl, { cache: 'no-cache', credentials: 'omit' }); const resp = await fetch(assetUrl, { cache: 'no-cache', credentials: 'omit' });
if (!resp.ok) throw new Error(resp.status + ' ' + resp.statusText); if (!resp.ok) throw new Error(resp.status + ' ' + resp.statusText);
const html = await resp.text(); const html = await resp.text();

View file

@ -14,47 +14,33 @@
// document.write()s it in place. The default upstream is the // document.write()s it in place. The default upstream is the
// stable channel; the URL parameter ?v= overrides it: // stable channel; the URL parameter ?v= overrides it:
// //
// ?v=alpha|beta|stable switches to that channel // ?v=stable | beta | alpha switch to that channel
// ?v=0.0.4 (or v0.0.4) pins to that exact stable version // ?v=0.0.4 (or v0.0.4) pin to that exact stable version
// (none) uses the stable default // ?v=0.0 (or v0.0) pin to latest patch within 0.0.x (symlink)
// ?v=0 (or v0) pin to latest within 0.x (symlink)
// (none) use the stable default
// //
// Resolution path: // Resolution is purely static — every URL maps to a real file or a
// 1. Channel mode (no ?v= or ?v={alpha,beta,stable}): fetch // checked-in symlink under <upstream>/releases/. No manifest lookup,
// manifest.json from the same origin, look up // no JS indirection, no client-side version arithmetic.
// "<tool>-<channel>" → tag, then fetch the asset.
// 2. Version pin: build the URL directly (skip manifest).
//
// The asset URL is /releases/<tag>/<tool>_v<version>.html. Caddy on
// zddc.varasys.io reverse-proxies that to the Codeberg release-asset
// URL — neither this stub nor the user's browser hits codeberg.org
// directly.
(async function () { (async function () {
const params = new URLSearchParams(location.search); const params = new URLSearchParams(location.search);
const v = params.get('v'); const v = params.get('v');
const tool = 'landing'; const tool = 'landing';
const defaultChannel = 'stable'; const defaultChannel = 'stable';
const channels = ['alpha', 'beta', 'stable']; const channels = ['stable', 'beta', 'alpha'];
const base = 'https://zddc.varasys.io/releases/'; const base = 'https://zddc.varasys.io/releases/';
function isChannel(s) { return channels.indexOf(s) >= 0; } function suffixFor(value) {
if (!value) return '_' + defaultChannel;
try { if (channels.indexOf(value) >= 0) return '_' + value;
let assetUrl; // Strip optional leading 'v', accept "0.0.4" / "0.0" / "0".
if (v && !isChannel(v)) { const ver = value.startsWith('v') ? value.slice(1) : value;
// Explicit version pin (e.g. ?v=0.0.4 or ?v=v0.0.4). return '_v' + ver;
const ver = v.replace(/^v/, '');
const tag = tool + '-v' + ver;
assetUrl = base + tag + '/' + tool + '_v' + ver + '.html';
} else {
// Channel mode: resolve via manifest.
const channel = (v && isChannel(v)) ? v : defaultChannel;
const manifest = await (await fetch(base + 'manifest.json', { cache: 'no-cache', credentials: 'omit' })).json();
const tag = manifest[tool + '-' + channel];
if (!tag) throw new Error('manifest has no entry for ' + tool + '-' + channel);
const ver = tag.replace(tool + '-v', '');
assetUrl = base + tag + '/' + tool + '_v' + ver + '.html';
} }
try {
const assetUrl = base + tool + suffixFor(v) + '.html';
const resp = await fetch(assetUrl, { cache: 'no-cache', credentials: 'omit' }); const resp = await fetch(assetUrl, { cache: 'no-cache', credentials: 'omit' });
if (!resp.ok) throw new Error(resp.status + ' ' + resp.statusText); if (!resp.ok) throw new Error(resp.status + ' ' + resp.statusText);
const html = await resp.text(); const html = await resp.text();

View file

@ -14,47 +14,33 @@
// document.write()s it in place. The default upstream is the // document.write()s it in place. The default upstream is the
// stable channel; the URL parameter ?v= overrides it: // stable channel; the URL parameter ?v= overrides it:
// //
// ?v=alpha|beta|stable switches to that channel // ?v=stable | beta | alpha switch to that channel
// ?v=0.0.4 (or v0.0.4) pins to that exact stable version // ?v=0.0.4 (or v0.0.4) pin to that exact stable version
// (none) uses the stable default // ?v=0.0 (or v0.0) pin to latest patch within 0.0.x (symlink)
// ?v=0 (or v0) pin to latest within 0.x (symlink)
// (none) use the stable default
// //
// Resolution path: // Resolution is purely static — every URL maps to a real file or a
// 1. Channel mode (no ?v= or ?v={alpha,beta,stable}): fetch // checked-in symlink under <upstream>/releases/. No manifest lookup,
// manifest.json from the same origin, look up // no JS indirection, no client-side version arithmetic.
// "<tool>-<channel>" → tag, then fetch the asset.
// 2. Version pin: build the URL directly (skip manifest).
//
// The asset URL is /releases/<tag>/<tool>_v<version>.html. Caddy on
// zddc.varasys.io reverse-proxies that to the Codeberg release-asset
// URL — neither this stub nor the user's browser hits codeberg.org
// directly.
(async function () { (async function () {
const params = new URLSearchParams(location.search); const params = new URLSearchParams(location.search);
const v = params.get('v'); const v = params.get('v');
const tool = 'mdedit'; const tool = 'mdedit';
const defaultChannel = 'stable'; const defaultChannel = 'stable';
const channels = ['alpha', 'beta', 'stable']; const channels = ['stable', 'beta', 'alpha'];
const base = 'https://zddc.varasys.io/releases/'; const base = 'https://zddc.varasys.io/releases/';
function isChannel(s) { return channels.indexOf(s) >= 0; } function suffixFor(value) {
if (!value) return '_' + defaultChannel;
try { if (channels.indexOf(value) >= 0) return '_' + value;
let assetUrl; // Strip optional leading 'v', accept "0.0.4" / "0.0" / "0".
if (v && !isChannel(v)) { const ver = value.startsWith('v') ? value.slice(1) : value;
// Explicit version pin (e.g. ?v=0.0.4 or ?v=v0.0.4). return '_v' + ver;
const ver = v.replace(/^v/, '');
const tag = tool + '-v' + ver;
assetUrl = base + tag + '/' + tool + '_v' + ver + '.html';
} else {
// Channel mode: resolve via manifest.
const channel = (v && isChannel(v)) ? v : defaultChannel;
const manifest = await (await fetch(base + 'manifest.json', { cache: 'no-cache', credentials: 'omit' })).json();
const tag = manifest[tool + '-' + channel];
if (!tag) throw new Error('manifest has no entry for ' + tool + '-' + channel);
const ver = tag.replace(tool + '-v', '');
assetUrl = base + tag + '/' + tool + '_v' + ver + '.html';
} }
try {
const assetUrl = base + tool + suffixFor(v) + '.html';
const resp = await fetch(assetUrl, { cache: 'no-cache', credentials: 'omit' }); const resp = await fetch(assetUrl, { cache: 'no-cache', credentials: 'omit' });
if (!resp.ok) throw new Error(resp.status + ' ' + resp.statusText); if (!resp.ok) throw new Error(resp.status + ' ' + resp.statusText);
const html = await resp.text(); const html = await resp.text();

View file

@ -14,47 +14,33 @@
// document.write()s it in place. The default upstream is the // document.write()s it in place. The default upstream is the
// stable channel; the URL parameter ?v= overrides it: // stable channel; the URL parameter ?v= overrides it:
// //
// ?v=alpha|beta|stable switches to that channel // ?v=stable | beta | alpha switch to that channel
// ?v=0.0.4 (or v0.0.4) pins to that exact stable version // ?v=0.0.4 (or v0.0.4) pin to that exact stable version
// (none) uses the stable default // ?v=0.0 (or v0.0) pin to latest patch within 0.0.x (symlink)
// ?v=0 (or v0) pin to latest within 0.x (symlink)
// (none) use the stable default
// //
// Resolution path: // Resolution is purely static — every URL maps to a real file or a
// 1. Channel mode (no ?v= or ?v={alpha,beta,stable}): fetch // checked-in symlink under <upstream>/releases/. No manifest lookup,
// manifest.json from the same origin, look up // no JS indirection, no client-side version arithmetic.
// "<tool>-<channel>" → tag, then fetch the asset.
// 2. Version pin: build the URL directly (skip manifest).
//
// The asset URL is /releases/<tag>/<tool>_v<version>.html. Caddy on
// zddc.varasys.io reverse-proxies that to the Codeberg release-asset
// URL — neither this stub nor the user's browser hits codeberg.org
// directly.
(async function () { (async function () {
const params = new URLSearchParams(location.search); const params = new URLSearchParams(location.search);
const v = params.get('v'); const v = params.get('v');
const tool = 'transmittal'; const tool = 'transmittal';
const defaultChannel = 'stable'; const defaultChannel = 'stable';
const channels = ['alpha', 'beta', 'stable']; const channels = ['stable', 'beta', 'alpha'];
const base = 'https://zddc.varasys.io/releases/'; const base = 'https://zddc.varasys.io/releases/';
function isChannel(s) { return channels.indexOf(s) >= 0; } function suffixFor(value) {
if (!value) return '_' + defaultChannel;
try { if (channels.indexOf(value) >= 0) return '_' + value;
let assetUrl; // Strip optional leading 'v', accept "0.0.4" / "0.0" / "0".
if (v && !isChannel(v)) { const ver = value.startsWith('v') ? value.slice(1) : value;
// Explicit version pin (e.g. ?v=0.0.4 or ?v=v0.0.4). return '_v' + ver;
const ver = v.replace(/^v/, '');
const tag = tool + '-v' + ver;
assetUrl = base + tag + '/' + tool + '_v' + ver + '.html';
} else {
// Channel mode: resolve via manifest.
const channel = (v && isChannel(v)) ? v : defaultChannel;
const manifest = await (await fetch(base + 'manifest.json', { cache: 'no-cache', credentials: 'omit' })).json();
const tag = manifest[tool + '-' + channel];
if (!tag) throw new Error('manifest has no entry for ' + tool + '-' + channel);
const ver = tag.replace(tool + '-v', '');
assetUrl = base + tag + '/' + tool + '_v' + ver + '.html';
} }
try {
const assetUrl = base + tool + suffixFor(v) + '.html';
const resp = await fetch(assetUrl, { cache: 'no-cache', credentials: 'omit' }); const resp = await fetch(assetUrl, { cache: 'no-cache', credentials: 'omit' });
if (!resp.ok) throw new Error(resp.status + ' ' + resp.statusText); if (!resp.ok) throw new Error(resp.status + ' ' + resp.statusText);
const html = await resp.text(); const html = await resp.text();

View file

@ -0,0 +1 @@
archive_v0.0.2.html

View file

@ -0,0 +1 @@
archive_v0.0.2.html

View file

@ -0,0 +1 @@
archive_v0.0.2.html

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1 @@
archive_v0.0.2.html

View file

@ -0,0 +1 @@
archive_v0.0.2.html

View file

@ -0,0 +1 @@
classifier_v0.0.2.html

View file

@ -0,0 +1 @@
classifier_v0.0.2.html

View file

@ -0,0 +1 @@
classifier_v0.0.2.html

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1 @@
classifier_v0.0.2.html

View file

@ -0,0 +1 @@
classifier_v0.0.2.html

View file

@ -61,68 +61,71 @@
<section class="rel-tool"> <section class="rel-tool">
<h2>Archive</h2> <h2>Archive</h2>
<div class="rel-channels"> <div class="rel-channels">
<a class="stable" href="/releases/archive-v0.0.2/archive_v0.0.2.html">stable</a> <a class="stable" href="archive_stable.html">stable</a>
<a class="beta" href="archive_beta.html">beta</a>
<a class="alpha" href="archive_alpha.html">alpha</a>
</div> </div>
<div class="rel-versions"><strong>Pin to version:</strong> <div class="rel-versions"><strong>Pin to version:</strong>
<a href="/releases/archive-v0.0.2/archive_v0.0.2.html">v0.0.2</a> <a href="archive_v0.0.2.html">v0.0.2</a>
<a href="/releases/archive-v0.0.1/archive_v0.0.1.html">v0.0.1</a> <a href="archive_v0.0.1.html">v0.0.1</a>
</div> </div>
</section> </section>
<section class="rel-tool"> <section class="rel-tool">
<h2>Transmittal</h2> <h2>Transmittal</h2>
<div class="rel-channels"> <div class="rel-channels">
<a class="stable" href="/releases/transmittal-v0.0.2/transmittal_v0.0.2.html">stable</a> <a class="stable" href="transmittal_stable.html">stable</a>
<a class="beta" href="transmittal_beta.html">beta</a>
<a class="alpha" href="transmittal_alpha.html">alpha</a>
</div> </div>
<div class="rel-versions"><strong>Pin to version:</strong> <div class="rel-versions"><strong>Pin to version:</strong>
<a href="/releases/transmittal-v0.0.2/transmittal_v0.0.2.html">v0.0.2</a> <a href="transmittal_v0.0.2.html">v0.0.2</a>
<a href="/releases/transmittal-v0.0.1/transmittal_v0.0.1.html">v0.0.1</a> <a href="transmittal_v0.0.1.html">v0.0.1</a>
</div> </div>
</section> </section>
<section class="rel-tool"> <section class="rel-tool">
<h2>Classifier</h2> <h2>Classifier</h2>
<div class="rel-channels"> <div class="rel-channels">
<a class="stable" href="/releases/classifier-v0.0.2/classifier_v0.0.2.html">stable</a> <a class="stable" href="classifier_stable.html">stable</a>
<a class="beta" href="classifier_beta.html">beta</a>
<a class="alpha" href="classifier_alpha.html">alpha</a>
</div> </div>
<div class="rel-versions"><strong>Pin to version:</strong> <div class="rel-versions"><strong>Pin to version:</strong>
<a href="/releases/classifier-v0.0.2/classifier_v0.0.2.html">v0.0.2</a> <a href="classifier_v0.0.2.html">v0.0.2</a>
<a href="/releases/classifier-v0.0.1/classifier_v0.0.1.html">v0.0.1</a> <a href="classifier_v0.0.1.html">v0.0.1</a>
</div> </div>
</section> </section>
<section class="rel-tool"> <section class="rel-tool">
<h2>Markdown Editor</h2> <h2>Markdown Editor</h2>
<div class="rel-channels"> <div class="rel-channels">
<a class="stable" href="/releases/mdedit-v0.0.2/mdedit_v0.0.2.html">stable</a> <a class="stable" href="mdedit_stable.html">stable</a>
<a class="beta" href="mdedit_beta.html">beta</a>
<a class="alpha" href="mdedit_alpha.html">alpha</a>
</div> </div>
<div class="rel-versions"><strong>Pin to version:</strong> <div class="rel-versions"><strong>Pin to version:</strong>
<a href="/releases/mdedit-v0.0.2/mdedit_v0.0.2.html">v0.0.2</a> <a href="mdedit_v0.0.2.html">v0.0.2</a>
<a href="/releases/mdedit-v0.0.1/mdedit_v0.0.1.html">v0.0.1</a> <a href="mdedit_v0.0.1.html">v0.0.1</a>
</div> </div>
</section> </section>
<section class="rel-tool"> <section class="rel-tool">
<h2>Landing (project picker)</h2> <h2>Landing (project picker)</h2>
<div class="rel-channels"> <div class="rel-channels">
<a class="stable" href="/releases/landing-v0.0.2/landing_v0.0.2.html">stable</a> <a class="stable" href="landing_stable.html">stable</a>
<a class="beta" href="landing_beta.html">beta</a>
<a class="alpha" href="landing_alpha.html">alpha</a>
</div> </div>
<div class="rel-versions"><strong>Pin to version:</strong> <div class="rel-versions"><strong>Pin to version:</strong>
<a href="/releases/landing-v0.0.2/landing_v0.0.2.html">v0.0.2</a> <a href="landing_v0.0.2.html">v0.0.2</a>
<a href="/releases/landing-v0.0.1/landing_v0.0.1.html">v0.0.1</a> <a href="landing_v0.0.1.html">v0.0.1</a>
</div> </div>
</section> </section>
<section class="rel-tool"> <section class="rel-tool">
<h2>zddc-server (Go file server)</h2> <h2>zddc-server (Go file server)</h2>
<div class="rel-channels"> <p>Binaries are published as Codeberg release assets. Pick a platform from the release page; or build from source via the helm charts under <code>helm/</code>.</p>
<a class="alpha" href="https://codeberg.org/VARASYS/ZDDC/releases/tag/zddc-server-v0.0.8-alpha.2">alpha</a> <p><a href="https://codeberg.org/VARASYS/ZDDC/releases">Browse zddc-server releases on Codeberg →</a></p>
</div>
<h3 style="font-size:1rem;margin:0.75rem 0 0.4rem;">Standalone binaries</h3>
<table class="rel-bin-table"><thead><tr><th>Channel</th><th>linux-amd64</th><th>darwin-amd64</th><th>darwin-arm64</th><th>windows-amd64</th></tr></thead><tbody>
<tr><td class="ch-stable">stable</td><td class="empty"></td><td class="empty"></td><td class="empty"></td><td class="empty"></td></tr>
<tr><td class="ch-beta">beta</td><td class="empty"></td><td class="empty"></td><td class="empty"></td><td class="empty"></td></tr>
<tr><td class="ch-alpha">alpha</td><td><a href="/releases/zddc-server-v0.0.8-alpha.2/zddc-server-linux-amd64">download</a></td><td><a href="/releases/zddc-server-v0.0.8-alpha.2/zddc-server-darwin-amd64">download</a></td><td><a href="/releases/zddc-server-v0.0.8-alpha.2/zddc-server-darwin-arm64">download</a></td><td><a href="/releases/zddc-server-v0.0.8-alpha.2/zddc-server-windows-amd64.exe">download</a></td></tr>
</tbody></table>
</section> </section>
<section style="margin-top: var(--spacing-2xl); color: var(--color-text-muted); font-size: 0.9rem;"> <section style="margin-top: var(--spacing-2xl); color: var(--color-text-muted); font-size: 0.9rem;">
<p>Append <code>?v=alpha</code>, <code>?v=beta</code>, <code>?v=stable</code>, or <code>?v=0.0.1</code> to any deployment URL to switch versions for a single request — see <a href="../">the home page</a>.</p> <p>Append <code>?v=stable</code>, <code>?v=beta</code>, <code>?v=alpha</code>, <code>?v=0.0</code> (latest 0.0.x), or <code>?v=0.0.1</code> (exact) to any deployment URL to switch versions for a single request — see <a href="../">the home page</a>.</p>
</section> </section>
</main> </main>

View file

@ -0,0 +1 @@
landing_v0.0.2.html

View file

@ -0,0 +1 @@
landing_v0.0.2.html

View file

@ -0,0 +1 @@
landing_v0.0.2.html

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1 @@
landing_v0.0.2.html

View file

@ -0,0 +1 @@
landing_v0.0.2.html

View file

@ -1,8 +0,0 @@
{
"archive-stable": "archive-v0.0.2",
"transmittal-stable": "transmittal-v0.0.2",
"classifier-stable": "classifier-v0.0.2",
"mdedit-stable": "mdedit-v0.0.2",
"landing-stable": "landing-v0.0.2",
"zddc-server-alpha": "zddc-server-v0.0.8-alpha.2"
}

View file

@ -0,0 +1 @@
mdedit_v0.0.2.html

View file

@ -0,0 +1 @@
mdedit_v0.0.2.html

View file

@ -0,0 +1 @@
mdedit_v0.0.2.html

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
mdedit_v0.0.2.html

View file

@ -0,0 +1 @@
mdedit_v0.0.2.html

View file

@ -0,0 +1 @@
transmittal_v0.0.2.html

View file

@ -0,0 +1 @@
transmittal_v0.0.2.html

View file

@ -0,0 +1 @@
transmittal_v0.0.2.html

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1 @@
transmittal_v0.0.2.html

View file

@ -0,0 +1 @@
transmittal_v0.0.2.html

View file

@ -1,19 +1,17 @@
#!/bin/sh #!/bin/sh
# release.sh — cut a zddc-server release: tag, compile binaries, # release.sh — cut a zddc-server stable release: tag, cross-compile
# publish them as assets to a Codeberg release. # binaries, publish them as assets to a Codeberg release.
# #
# Usage: # Usage:
# sh zddc/release.sh # alpha cut (default), version auto-derived # sh zddc/release.sh # patch++ from latest stable tag
# sh zddc/release.sh alpha # same # sh zddc/release.sh 0.1.0 # explicit version (X.Y.Z)
# sh zddc/release.sh beta # beta cut, version auto-derived
# sh zddc/release.sh stable # stable cut, patch++ from latest stable
# sh zddc/release.sh stable 0.1.0 # stable cut, explicit version
# #
# What this is NOT: there's no container image build, no registry push. # Why stable-only: zddc-server publishes binaries only on stable cuts.
# Those went away when the upstream codeberg.org/varasys/zddc-server # Beta/alpha channels of zddc-server have no binary distribution — the
# image lost its only consumers (tnd-zddc-chart's two Dockerfiles now # helm/zddc-server-{prod,dev} charts in this repo build from source at
# compile from source directly, fetching this Codeberg release tarball # deploy time, so any commit on main is buildable. There's no
# / binary). See AGENTS.md "Releasing" for the full flow. # cascade/symlink layer for binaries; if you need a specific build,
# pin the chart's commit ref.
# #
# Prerequisites: # Prerequisites:
# - Go 1.24+ on PATH. # - Go 1.24+ on PATH.
@ -21,13 +19,12 @@
# - curl, jq, git. # - curl, jq, git.
# #
# What it does: # What it does:
# 1. Determine version per the pre-release semver scheme: # 1. Derive version: explicit arg, or patch-bumped from latest clean
# alpha/beta → next_prerelease (vX.Y.Z-CHANNEL.N from latest stable) # zddc-server-vX.Y.Z tag.
# stable → operator-supplied or patch-bumped from latest stable
# 2. Tag the current commit zddc-server-v<version>. # 2. Tag the current commit zddc-server-v<version>.
# 3. Cross-compile binaries (linux/darwin/windows × amd64/arm64) # 3. Cross-compile binaries (linux/darwin/windows × amd64/arm64) into
# into zddc/dist/zddc-server-<os>-<arch>[.exe]. Native Go preferred. # zddc/dist/zddc-server-<os>-<arch>[.exe]. Native Go.
# 4. Upload each binary as an asset to the new Codeberg release. # 4. Upload each binary as a release asset on Codeberg.
# 5. Print the operator's next steps (push the tag). # 5. Print the operator's next steps (push the tag).
# #
# The script does NOT push the tag itself — that's a deliberate `git push` # The script does NOT push the tag itself — that's a deliberate `git push`
@ -37,31 +34,19 @@ set -eu
usage() { usage() {
cat >&2 <<'EOF' cat >&2 <<'EOF'
usage: release.sh [alpha|beta|stable] [<version>] usage: release.sh [<version>]
alpha (default) cut alpha. Auto-derive version from the latest clean No args patch-bump from the latest clean zddc-server-vX.Y.Z tag.
zddc-server-vX.Y.Z tag plus next-patch + -alpha.N. <version> explicit X.Y.Z (e.g. 0.1.0).
beta cut beta. Auto-derive version (vX.Y.Z + -beta.N).
stable cut stable. Without <version>, patch-bump from the
latest clean stable tag. With <version>, use it
verbatim (must be a clean X.Y.Z).
EOF EOF
exit 1 exit 1
} }
CHANNEL="${1:-alpha}" case "${1:-}" in
case "$CHANNEL" in
alpha | beta | stable) ;;
-h | --help) usage ;; -h | --help) usage ;;
*) echo "error: unknown channel '$CHANNEL'" >&2; usage ;;
esac esac
EXPLICIT_VERSION="${2:-}" EXPLICIT_VERSION="${1:-}"
if [ -n "$EXPLICIT_VERSION" ] && [ "$CHANNEL" != "stable" ]; then
echo "error: an explicit <version> is only valid with the 'stable' channel" >&2
echo " alpha and beta versions are auto-derived." >&2
exit 1
fi
if [ -z "${CODEBERG_TOKEN:-}" ]; then if [ -z "${CODEBERG_TOKEN:-}" ]; then
echo "error: CODEBERG_TOKEN must be exported in the environment" >&2 echo "error: CODEBERG_TOKEN must be exported in the environment" >&2
@ -74,22 +59,17 @@ SCRIPT_DIR=$(cd "$(dirname "$0")/.." && pwd)
TAG_PREFIX="zddc-server-v" TAG_PREFIX="zddc-server-v"
REPO="VARASYS/ZDDC" REPO="VARASYS/ZDDC"
# Source build-lib.sh for next_prerelease + _validate_semver. It # Source build-lib.sh for _validate_semver. It requires root_dir set;
# requires root_dir set; pointing at the repo root works. # pointing at the repo root works.
root_dir="$SCRIPT_DIR" root_dir="$SCRIPT_DIR"
. "$SCRIPT_DIR/shared/build-lib.sh" . "$SCRIPT_DIR/shared/build-lib.sh"
. "$SCRIPT_DIR/shared/publish-codeberg-release.sh" . "$SCRIPT_DIR/shared/publish-codeberg-release.sh"
# --- Determine version ----------------------------------------------------- # --- Determine version -----------------------------------------------------
case "$CHANNEL" in if [ -n "$EXPLICIT_VERSION" ]; then
alpha | beta)
VERSION=$(next_prerelease "$CHANNEL" "$TAG_PREFIX")
;;
stable)
if [ -n "$EXPLICIT_VERSION" ]; then
_validate_semver "$EXPLICIT_VERSION" _validate_semver "$EXPLICIT_VERSION"
VERSION="$EXPLICIT_VERSION" VERSION="$EXPLICIT_VERSION"
else else
_latest=$(git -C "$SCRIPT_DIR" tag --list "${TAG_PREFIX}*" 2>/dev/null \ _latest=$(git -C "$SCRIPT_DIR" tag --list "${TAG_PREFIX}*" 2>/dev/null \
| grep -E "^${TAG_PREFIX}[0-9]+\.[0-9]+\.[0-9]+\$" \ | grep -E "^${TAG_PREFIX}[0-9]+\.[0-9]+\.[0-9]+\$" \
| sed "s|^${TAG_PREFIX}||" \ | sed "s|^${TAG_PREFIX}||" \
@ -101,14 +81,11 @@ case "$CHANNEL" in
_minor="${_rest%%.*}" _minor="${_rest%%.*}"
_patch="${_rest#*.}" _patch="${_rest#*.}"
VERSION="${_major}.${_minor}.$((_patch + 1))" VERSION="${_major}.${_minor}.$((_patch + 1))"
fi fi
;;
esac
GIT_TAG="${TAG_PREFIX}${VERSION}" GIT_TAG="${TAG_PREFIX}${VERSION}"
echo "=== zddc-server release ===" echo "=== zddc-server stable release ==="
echo "Channel: $CHANNEL"
echo "Version: $VERSION" echo "Version: $VERSION"
echo "Git tag: $GIT_TAG" echo "Git tag: $GIT_TAG"
echo echo
@ -167,20 +144,3 @@ echo
echo "=== Done ===" echo "=== Done ==="
echo "Release: https://codeberg.org/$REPO/releases/tag/$GIT_TAG" echo "Release: https://codeberg.org/$REPO/releases/tag/$GIT_TAG"
echo "Git tag: $GIT_TAG (publish with: git push origin $GIT_TAG)" echo "Git tag: $GIT_TAG (publish with: git push origin $GIT_TAG)"
echo
case "$CHANNEL" in
stable)
echo "Reminder (channel discipline rule 4): freshen alpha + beta now"
echo "so the floating channels resolve to at-least-current code:"
echo " ./freshen-channel zddc-server alpha"
echo " ./freshen-channel zddc-server beta"
;;
beta)
echo "Beta cut. Soak before promoting to stable."
;;
esac
echo
echo "Don't forget to also regenerate the website releases page:"
echo " sh build.sh"
echo " git add website/releases/index.html website/releases/manifest.json"
echo " git commit && git push"