#!/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 — write to website/releases/ in the layout # driven by $channel and $build_version. For # stable, also update the _stable.html # symlink and create the git tag. # update_alpha — copy the just-built dist file to # website/releases/_alpha.html so the # site's alpha hyperlinks always serve the # latest dev build. Called automatically by # each tool's build.sh on plain (non-release) # builds. Plain copy (not symlink) so it # survives deployments whose web server only # mounts website/. `--release alpha` writes # the same file with the formal # "alpha · · " label; the next # plain build clobbers it. # # Channels and release args: # dev build, dist/ + alpha mirror, label # "v-alpha · · [-dirty]" (red). # Plain builds ARE alpha builds — every dev build cascades # to the alpha channel via update_alpha. The version embeds # the next-stable target (patch-bump of the latest clean # -vX.Y.Z tag). # --release stable, auto-bump patch from latest tag (or 0.0.1). # Label "vX.Y.Z" (black). # --release X.Y.Z stable, explicit version. # --release alpha alpha channel cut at HEAD; # label "v-alpha · · " (red). # --release beta beta channel; label "v-beta · · ". # Opt-in soak channel before stable promotion. # --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 # 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 mirror to website/releases/_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 -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 the appropriate slot under website/releases/. # Reads from caller scope: $channel, $build_version, $output_html, $root_dir. # # Stable releases write website/releases/_v.html, refresh the # website/releases/_stable.html symlink, and tag -v in # git. Skips silently when the source has not changed since the latest tag. # # Alpha and beta channel releases overwrite website/releases/_.html # in place with no tag (the embedded label carries date + commit SHA, so the # source is recoverable from git directly). promote_release() { _tool="$1" _releases_dir="$root_dir/../website/releases" mkdir -p "$_releases_dir" if [ "$channel" = "alpha" ] || [ "$channel" = "beta" ]; then _dest="${_releases_dir}/${_tool}_${channel}.html" cp "$output_html" "$_dest" echo "Released $channel to $_dest" return 0 fi if [ "$channel" != "stable" ] || [ -z "$build_version" ]; then echo "promote_release: refusing to promote — channel=$channel build_version=$build_version" >&2 exit 1 fi _latest=$(git -C "$root_dir" tag --list "${_tool}-v*" --sort=-v:refname 2>/dev/null | head -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 _versioned="${_releases_dir}/${_tool}_v${build_version}.html" cp "$output_html" "$_versioned" echo "Released $_versioned" # Symlink target is relative to its own directory so the link survives # path moves and works regardless of where the website is mounted. (cd "$_releases_dir" && ln -sfn "${_tool}_v${build_version}.html" "${_tool}_stable.html") echo "Updated ${_tool}_stable.html -> ${_tool}_v${build_version}.html" git -C "$root_dir" tag "${_tool}-v${build_version}" echo "Tagged ${_tool}-v${build_version} — run: git push --tags" } # Mirror the just-built dist file to website/releases/_alpha.html so # the website's alpha hyperlinks always serve whatever dist currently holds. # Plain copy (not symlink): symlinks pointing outside website/ break under # deployments whose web server only mounts website/ (notably the canonical # Caddy setup at /etc/containers/systemd/caddy.container, which mounts # /home/user/src/zddc/website read-only and cannot follow ../ paths to # landing/dist or archive/dist on the host filesystem). # # Trade-off: every dev build that touches a tool's source dirties the # corresponding _alpha.html file in git. Commit those alongside the source # change (or `git checkout` them before pushing) since the alpha channel is # explicitly mutable. # # `--release alpha` overwrites the same file with a "alpha · date · sha" # labeled build; the next plain build clobbers it again. That's the alpha- # is-mutable contract. # # Reads $output_html and $root_dir from caller scope. update_alpha() { _tool="$1" _releases_dir="$root_dir/../website/releases" _dest="${_releases_dir}/${_tool}_alpha.html" mkdir -p "$_releases_dir" # rm first: if the dest is currently a symlink to dist (legacy from the # earlier symlink approach), `cp` would follow the symlink and try to # write to the same file it's reading from. Removing first replaces the # symlink with a plain regular file copy. rm -f "$_dest" cp "$output_html" "$_dest" echo "Mirrored to $_dest" }