#!/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 — abort with error if file missing # concat_files — 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 # — copy to with all '. # The JS engine treats \/ as a regular slash, # so runtime behaviour is unchanged. # compute_build_label [--release []] # — sets globals: build_label, build_version, # is_release, is_red, channel. # See "Channels and release args" below. # promote_release — 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: # dev build, dist/ only, label # "v-alpha · · [-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-alpha · · " (red). # Tags -vX.Y.Z-alpha.N + uploads. # --release beta beta channel; label "v-beta · · ". # Tags + uploads. # --release 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 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 ' tag. Per the HTML5 spec # only block — other tags like are safe # inside a script's text content. Narrowly targeting "$2" } # Echo the next pre-release version for a given channel + tag prefix. # next_prerelease # # 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 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 -.* # and emit -.. # # 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 [--release []] # Sets global variables: # build_label — text rendered into the page's {{BUILD_LABEL}} slot # build_version — bare semver string (stable releases only) # is_release — "1" for any --release invocation, else "0" # is_red — "1" if the label should render red+bold (dev/alpha/beta), else "0" # channel — "stable" / "alpha" / "beta" / "" (dev) # # Versioning: pre-release semver. The next-stable target is computed from # the latest clean tool-vX.Y.Z tag (patch-bump). Plain builds and # `--release alpha`/`--release beta` carry the next-stable target as a # pre-release suffix in the on-page label so users can see which stable # the alpha/beta is working toward. Stable releases write a clean # vX.Y.Z label and tag. # # HTML tools do NOT tag alpha/beta cuts (consistent with current # behavior — alpha and beta artifacts are mutable files, not immutable # per-build snapshots). The label distinguishes plain dev builds from # explicit channel cuts via the timestamp granularity (full ts + dirty # marker for plain builds vs. date-only for `--release alpha|beta`). compute_build_label() { _tool="$1" _flag="${2:-}" _arg="${3:-}" is_release=0 is_red=1 channel="" build_version="" # Compute the next-stable target once for label inclusion. _next_stable=$(_next_stable_for_tool "$_tool") if [ "$_flag" != "--release" ]; then # Plain builds are dev builds — labeled as the alpha channel because # that's what the next formal cut would produce, but no Codeberg upload # happens until `--release alpha` is invoked. Full timestamp (granular # than date) and -dirty marker distinguish iterative dev builds from # formal `--release alpha` cuts (which stamp date-only). _sha=$(git -C "$root_dir" rev-parse --short=7 HEAD 2>/dev/null || echo "unknown") if ! git -C "$root_dir" diff --quiet HEAD 2>/dev/null; then _sha="${_sha}-dirty" fi channel="alpha" build_label="v${_next_stable}-alpha · ${build_timestamp} · ${_sha}" 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 -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 -v (or -v-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. _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" }