Removes the codeberg.org/varasys/zddc-server registry image, which had no remaining consumer outside this shop. The two chart Dockerfiles (tnd-zddc-chart) now compile zddc-server from source at build time, fetching the right tag from a Codeberg release. release-image.sh, zddc/Containerfile, and zddc/podman-compose.yaml are gone. Build artifacts (HTML tools + zddc-server binaries) move from website/releases/ in this repo to Codeberg release assets attached to git tags. The website at zddc.varasys.io serves them by reverse- proxying /releases/<tag>/<asset> to the corresponding Codeberg URL, so consumers (zddc-use, level-2 bootstrap stubs, the chart Dockerfiles) only ever talk to zddc.varasys.io. Releases page becomes server-rendered static HTML regenerated on each build via a single Codeberg API call. A small website/releases/manifest.json maps <tool>-<channel> → tag for runtime channel resolution by zddc-use and the level-2 stubs. Files added: - shared/publish-codeberg-release.sh — POSIX-sh helper that creates a Codeberg release for a tag (sets prerelease flag from tag suffix) and uploads/replaces release assets idempotently. Sourced by build-lib.sh and zddc/release.sh. - zddc/release.sh — replaces release-image.sh. Tags + cross-compiles binaries via native Go (no podman needed; install Go) + uploads to Codeberg release assets. No image build, no registry push. Files modified: - shared/build-lib.sh — promote_release tags + uploads via the helper for stable AND alpha/beta now (alpha/beta were untagged before). update_alpha removed; per-tool build.sh files no longer mirror to website/releases/<tool>_alpha.html on plain dev builds. - build.sh — prefers native go build over the old podman-based cross-compile (which is gone with Containerfile). build_releases_index queries the Codeberg API once and writes static HTML + manifest.json, with graceful fallback when the API is unreachable. - bootstrap/level2.html.tmpl — fetches manifest.json to resolve channel → tag, then fetches the asset from /releases/<tag>/<asset> (Caddy proxy). Replaces the old /releases/<tool>_<channel>.html flat URL pattern. Operators with curl'd level-2 stubs need to re-issue them — this is a breaking change. - AGENTS.md, CLAUDE.md — rewritten to describe the new flow. - .gitignore — releases/ artifacts now expected to be on Codeberg, not committed locally. NOT in this commit (deferred until $CODEBERG_TOKEN is provisioned): - Backfilling existing tags as Codeberg releases. - Cleanup commit: git rm-ing the existing artifacts in website/releases/. Until backfill happens, those files are how operators with old bootstrap stubs still get content. Once Codeberg has the assets, drop them. - The Caddy reverse-proxy config on zddc.varasys.io. Operator-side changes (not in this repo): - tnd-zddc-chart Dockerfile.prod and Dockerfile (dev) need updating to compile from source rather than `FROM codeberg.org/...:stable`. Done in a separate commit on that repo. - Caddyfile rule for the /releases/<tag>/<asset> reverse-proxy. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
344 lines
14 KiB
Bash
Executable file
344 lines
14 KiB
Bash
Executable file
#!/bin/sh
|
|
# =============================================================================
|
|
# ZDDC shared build helpers — sourced by each tool's build.sh
|
|
#
|
|
# Usage in a tool build.sh:
|
|
# root_dir=$(cd "$(dirname "$0")" && pwd)
|
|
# . "$root_dir/../shared/build-lib.sh"
|
|
#
|
|
# Provides:
|
|
# ensure_exists <path> — abort with error if file missing
|
|
# concat_files <file ...> — cat each relative path under $root_dir
|
|
# build_timestamp — ISO UTC timestamp string, set at source time;
|
|
# used as build_label for dev builds
|
|
# escape_js_close_tags <in> <out>
|
|
# — copy <in> to <out> with all '</' rewritten as
|
|
# '<\/' so the HTML parser cannot misread the
|
|
# inlined JS as containing a closing </script>.
|
|
# The JS engine treats \/ as a regular slash,
|
|
# so runtime behaviour is unchanged.
|
|
# compute_build_label <tool> [--release [<channel-or-version>]]
|
|
# — sets globals: build_label, build_version,
|
|
# is_release, is_red, channel.
|
|
# See "Channels and release args" below.
|
|
# promote_release <tool> — for stable / alpha / beta, tag the
|
|
# commit and upload the dist HTML as a
|
|
# Codeberg release asset. No local mirror
|
|
# under website/releases/ — the website
|
|
# reverse-proxies download URLs to Codeberg
|
|
# release-asset URLs. Stable cuts skip when
|
|
# the tool's source is unchanged since the
|
|
# latest stable tag.
|
|
#
|
|
# Channels and release args:
|
|
# <none> dev build, dist/ only, label
|
|
# "v<next-stable>-alpha · <ts> · <sha>[-dirty]" (red).
|
|
# No website/releases/ side-effect. To publish, re-run
|
|
# with `--release alpha`.
|
|
# --release stable, auto-bump patch from latest tag (or 0.0.1).
|
|
# Label "vX.Y.Z" (black). Tags + uploads.
|
|
# --release X.Y.Z stable, explicit version. Tags + uploads.
|
|
# --release alpha alpha channel cut at HEAD;
|
|
# label "v<next-stable>-alpha · <date> · <sha>" (red).
|
|
# Tags <tool>-vX.Y.Z-alpha.N + uploads.
|
|
# --release beta beta channel; label "v<next-stable>-beta · <date> · <sha>".
|
|
# Tags + uploads.
|
|
# --release <other> error.
|
|
# =============================================================================
|
|
|
|
# Abort if root_dir is not set by the caller
|
|
if [ -z "${root_dir:-}" ]; then
|
|
echo "build-lib.sh: root_dir must be set before sourcing this file" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Pull in the Codeberg release-publish helper so promote_release can call
|
|
# its publish_codeberg_release function. Sourced unconditionally — the
|
|
# helper has no side effects when sourced (only defines functions).
|
|
. "$root_dir/../shared/publish-codeberg-release.sh"
|
|
|
|
# Fail hard on any missing source file
|
|
ensure_exists() {
|
|
_path="$1"
|
|
if [ ! -f "$_path" ]; then
|
|
echo "error: missing file: $_path" >&2
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
# Concatenate files listed as positional args, each relative to root_dir
|
|
concat_files() {
|
|
for _rel do
|
|
ensure_exists "$root_dir/$_rel"
|
|
cat "$root_dir/$_rel"
|
|
printf '\n'
|
|
done
|
|
}
|
|
|
|
# ISO UTC build timestamp — set once when this file is sourced
|
|
build_timestamp=$(date -u +"%Y-%m-%d %H:%M:%S")
|
|
|
|
# Read shared/favicon.svg, base64-encode it, and assemble a data: URI suitable
|
|
# for inlining into a <link rel="icon" type="image/svg+xml" href="..."> tag.
|
|
# Set once at source time so every tool's build.sh can pick it up via the
|
|
# $favicon_data_uri global. Editing shared/favicon.svg auto-propagates to all
|
|
# tools on the next build.
|
|
_favicon_path="$root_dir/../shared/favicon.svg"
|
|
if [ -f "$_favicon_path" ]; then
|
|
favicon_data_uri="data:image/svg+xml;base64,$(base64 -w 0 "$_favicon_path")"
|
|
else
|
|
favicon_data_uri=""
|
|
fi
|
|
|
|
# Rewrite '</script' (case-insensitive) in JS as '<\/script' so the HTML parser
|
|
# cannot mistake string contents for a closing </script> tag. Per the HTML5 spec
|
|
# only </script terminates a <script> block — other tags like </div> are safe
|
|
# inside a script's text content. Narrowly targeting </script avoids corrupting
|
|
# regex literals like /</g whose trailing letter is a flag, not a tag name.
|
|
# The JS engine treats '\/' the same as '/' inside a string, so behaviour is
|
|
# unchanged. See ARCHITECTURE.md "HTML Embedding Safety".
|
|
escape_js_close_tags() {
|
|
sed 's#</\([sS][cC][rR][iI][pP][tT]\)#<\\/\1#g' "$1" > "$2"
|
|
}
|
|
|
|
# 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
|
|
# is a non-empty numeric string. Exits with an error if not.
|
|
_validate_semver() {
|
|
_v="$1"
|
|
_bad() {
|
|
echo "error: invalid release argument: '$_v' (expected: alpha, beta, or X.Y.Z stable version)" >&2
|
|
exit 1
|
|
}
|
|
_v1="${_v%%.*}"
|
|
_rest="${_v#*.}"
|
|
[ "$_rest" = "$_v" ] && _bad
|
|
_v2="${_rest%%.*}"
|
|
_v3="${_rest#*.}"
|
|
{ [ "$_v3" = "$_rest" ] || [ "$_v3" != "${_v3%.*}" ]; } && _bad
|
|
case "$_v1" in '' | *[!0-9]*) _bad ;; esac
|
|
case "$_v2" in '' | *[!0-9]*) _bad ;; esac
|
|
case "$_v3" in '' | *[!0-9]*) _bad ;; esac
|
|
}
|
|
|
|
# Compute build label and channel. Reads positional args:
|
|
# compute_build_label <tool_name> [--release [<channel-or-version>]]
|
|
# Sets global variables:
|
|
# build_label — text rendered into the page's {{BUILD_LABEL}} slot
|
|
# build_version — bare semver string (stable releases only)
|
|
# is_release — "1" for any --release invocation, else "0"
|
|
# is_red — "1" if the label should render red+bold (dev/alpha/beta), else "0"
|
|
# channel — "stable" / "alpha" / "beta" / "" (dev)
|
|
#
|
|
# Versioning: pre-release semver. The next-stable target is computed from
|
|
# the latest clean tool-vX.Y.Z tag (patch-bump). Plain builds and
|
|
# `--release alpha`/`--release beta` carry the next-stable target as a
|
|
# pre-release suffix in the on-page label so users can see which stable
|
|
# the alpha/beta is working toward. Stable releases write a clean
|
|
# vX.Y.Z label and tag.
|
|
#
|
|
# HTML tools do NOT tag alpha/beta cuts (consistent with current
|
|
# behavior — alpha and beta artifacts are mutable files, not immutable
|
|
# per-build snapshots). The label distinguishes plain dev builds from
|
|
# explicit channel cuts via the timestamp granularity (full ts + dirty
|
|
# marker for plain builds vs. date-only for `--release alpha|beta`).
|
|
compute_build_label() {
|
|
_tool="$1"
|
|
_flag="${2:-}"
|
|
_arg="${3:-}"
|
|
|
|
is_release=0
|
|
is_red=1
|
|
channel=""
|
|
build_version=""
|
|
|
|
# Compute the next-stable target once for label inclusion.
|
|
_next_stable=$(_next_stable_for_tool "$_tool")
|
|
|
|
if [ "$_flag" != "--release" ]; then
|
|
# Plain builds mirror to website/releases/<tool>_alpha.html, so they ARE
|
|
# alpha builds. Full timestamp (granular than date) and -dirty marker
|
|
# distinguish iterative dev builds from formal `--release alpha` cuts.
|
|
_sha=$(git -C "$root_dir" rev-parse --short=7 HEAD 2>/dev/null || echo "unknown")
|
|
if ! git -C "$root_dir" diff --quiet HEAD 2>/dev/null; then
|
|
_sha="${_sha}-dirty"
|
|
fi
|
|
channel="alpha"
|
|
build_label="v${_next_stable}-alpha · ${build_timestamp} · ${_sha}"
|
|
return 0
|
|
fi
|
|
|
|
is_release=1
|
|
|
|
case "$_arg" in
|
|
alpha | beta)
|
|
channel="$_arg"
|
|
_date=$(date -u +"%Y-%m-%d")
|
|
_sha=$(git -C "$root_dir" rev-parse --short=7 HEAD 2>/dev/null || echo "unknown")
|
|
build_label="v${_next_stable}-${channel} · ${_date} · ${_sha}"
|
|
return 0
|
|
;;
|
|
'')
|
|
# Stable cut, auto-bump patch.
|
|
build_version="$_next_stable"
|
|
;;
|
|
*)
|
|
_validate_semver "$_arg"
|
|
build_version="$_arg"
|
|
;;
|
|
esac
|
|
|
|
channel="stable"
|
|
is_red=0
|
|
build_label="v${build_version}"
|
|
}
|
|
|
|
# Compute the next-stable target version for a tool — i.e., the patch-bump
|
|
# of the latest clean <tool>-vX.Y.Z tag. Used by compute_build_label to
|
|
# embed the target version in alpha/beta labels.
|
|
_next_stable_for_tool() {
|
|
_t="$1"
|
|
_latest=$(git -C "$root_dir" tag --list "${_t}-v*" 2>/dev/null \
|
|
| grep -E "^${_t}-v[0-9]+\.[0-9]+\.[0-9]+\$" \
|
|
| sed "s|^${_t}-v||" \
|
|
| sort -V \
|
|
| tail -1)
|
|
[ -n "$_latest" ] || _latest="0.0.0"
|
|
_major="${_latest%%.*}"
|
|
_rest="${_latest#*.}"
|
|
_minor="${_rest%%.*}"
|
|
_patch="${_rest#*.}"
|
|
echo "${_major}.${_minor}.$((_patch + 1))"
|
|
}
|
|
|
|
# Promote a built dist file to a Codeberg release.
|
|
# Reads from caller scope: $channel, $build_version, $output_html, $root_dir.
|
|
#
|
|
# All three channels (alpha, beta, stable) follow the same shape now:
|
|
# 1. Compute the version (already done by compute_build_label for stable;
|
|
# for alpha/beta we compute next_prerelease here).
|
|
# 2. Tag the commit <tool>-v<version> (or <tool>-v<version>-CHANNEL.N).
|
|
# 3. Upload the built dist HTML as a release asset to Codeberg.
|
|
#
|
|
# Idempotent: the publish helper replaces a same-named asset on re-upload,
|
|
# and the tag step is a no-op if the tag already points at HEAD.
|
|
#
|
|
# For stable: the original "skip if no source change since latest stable
|
|
# 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
|
|
# clear error if it isn't.
|
|
promote_release() {
|
|
_tool="$1"
|
|
|
|
case "$channel" in
|
|
stable)
|
|
if [ -z "$build_version" ]; then
|
|
echo "promote_release: stable channel but no build_version" >&2
|
|
exit 1
|
|
fi
|
|
_latest=$(git -C "$root_dir" tag --list "${_tool}-v*" 2>/dev/null \
|
|
| grep -E "^${_tool}-v[0-9]+\.[0-9]+\.[0-9]+\$" \
|
|
| sort -V | tail -1)
|
|
if [ -n "$_latest" ] && git -C "$root_dir" diff --quiet "$_latest" HEAD -- . ../shared 2>/dev/null; then
|
|
echo "${_tool}: no source changes since $_latest — skipping"
|
|
return 0
|
|
fi
|
|
_version="$build_version"
|
|
;;
|
|
alpha | beta)
|
|
_version=$(next_prerelease "$channel" "${_tool}-v")
|
|
;;
|
|
*)
|
|
echo "promote_release: unknown channel '$channel'" >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
|
|
_tag="${_tool}-v${_version}"
|
|
|
|
# Tag the commit (idempotent: skip if already at HEAD).
|
|
if git -C "$root_dir" rev-parse -q --verify "refs/tags/$_tag" >/dev/null; then
|
|
_existing=$(git -C "$root_dir" rev-list -n 1 "$_tag")
|
|
_head=$(git -C "$root_dir" rev-parse HEAD)
|
|
if [ "$_existing" != "$_head" ]; then
|
|
echo "promote_release: tag $_tag already exists at $_existing, but HEAD is $_head" >&2
|
|
return 1
|
|
fi
|
|
echo "(tag $_tag already at HEAD)"
|
|
else
|
|
git -C "$root_dir" tag "$_tag"
|
|
echo "tagged $_tag"
|
|
fi
|
|
|
|
# Upload to Codeberg. The asset name embeds the version so consumers
|
|
# 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"
|
|
}
|