#!/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, copy the dist # HTML into the release-output bundle # (default $root_dir/../dist/release-output; # override $ZDDC_DEPLOY_RELEASES_DIR). Stable # cuts write the immutable per-version file + # refresh five symlinks (_v, _v, # _stable, _beta, _alpha) and tag # -v. Alpha/beta cuts # overwrite the channel mirror in place # and cascade alpha → beta. No git tags # for alpha/beta cuts. The bundle is a # complete intended-live snapshot — the # top-level ./build seeds it from # /srv/zddc/releases/ before per-tool # promote runs, then ./deploy --releases # rsyncs it back. See ARCHITECTURE.md # "Channels" for the full table. # # Channels and release args: # dev build, tool/dist/ only, label # "v-alpha · · [-dirty]" (red). # No release-output side-effect. To produce a deployable # bundle, re-run with `--release alpha`. # --release stable, auto-bump patch from latest tag (or 0.0.1). # Writes per-version file + symlinks; tags vX.Y.Z. # --release X.Y.Z stable, explicit version. # --release alpha alpha channel cut at HEAD; # label "v-alpha · · " (red). # Overwrites _alpha.html. No tag. # --release beta beta channel; label "v-beta · · ". # Overwrites _beta.html. Cascades _alpha.html # → _beta.html (symlink). No tag. # --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 # NOTE: there's no Codeberg release-asset publication path anymore. All # release artifacts (HTML tools + zddc-server binaries) materialize in # dist/release-output/ via the lockstep ./build, then ./deploy rsyncs # them to /srv/zddc/ on the deploy host. The deprecated zddc/release.sh # is now a no-op guard that prints a redirection message. # 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" } # 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}" _emit_build_label_sidecar "$_tool" 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}" _emit_build_label_sidecar "$_tool" 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}" _emit_build_label_sidecar "$_tool" } # Write the resolved build label to a sidecar file the top-level build.sh # reads to assemble zddc/internal/apps/embedded/versions.txt. No-op when # BUILD_LABELS_DIR is not set in the env (tools built standalone). _emit_build_label_sidecar() { if [ -z "${BUILD_LABELS_DIR:-}" ]; then return 0 fi mkdir -p "$BUILD_LABELS_DIR" printf '%s\n' "$build_label" > "$BUILD_LABELS_DIR/$1.label" } # Tools that participate in the lockstep release. Source of truth — used # by helpers that enumerate "all release artifacts" (matrix render, # coordinated next-stable, channel-link verifier). ZDDC_RELEASE_TOOLS="archive transmittal classifier mdedit landing zddc-server" # Compute the next-stable target for a single tool — patch-bump of its own # latest -vX.Y.Z tag. Used by compute_build_label so a tool's # alpha/beta on-page label still reads against its own history (e.g. an # alpha cut for a tool that's been quiet still labels itself targeting that # tool's next stable, even when the lockstep convention is in force). _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))" } # Compute the coordinated next-stable target across every release artifact # (5 HTML tools + zddc-server). Used by the top-level build.sh on # `--release` (no explicit version) to enforce lockstep — every tool cuts # at the same version even if it hasn't changed. Picks max(latest tag # across all tools) + patch bump, so a tool at v0.0.2 jumps straight to # wherever the leader is + 1 the first time the lockstep rule fires. _coordinated_next_stable() { _max="0.0.0" for _t in $ZDDC_RELEASE_TOOLS; do _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" ] || continue # sort -V picks the larger of two semvers _max=$(printf '%s\n%s\n' "$_max" "$_latest" | sort -V | tail -1) done _major="${_max%%.*}" _rest="${_max#*.}" _minor="${_rest%%.*}" _patch="${_rest#*.}" echo "${_major}.${_minor}.$((_patch + 1))" } # Promote a built dist file to the release-output bundle. Reads from caller # scope: $channel ("stable" / "alpha" / "beta"), $build_version (stable only), # $output_html, $root_dir. Bundle path resolves from $ZDDC_DEPLOY_RELEASES_DIR # (default $root_dir/../dist/release-output). # # Stable cuts: # 1. Skip if source unchanged since latest stable tag. # 2. Copy dist HTML → /_v.html (immutable). # 3. Refresh symlinks: _v, _v, _stable, _beta, _alpha all → the # 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 -v. # # Alpha/beta cuts: # 1. Overwrite /_.html with dist HTML # (replaces a symlink with real bytes if one was there). # 2. For beta: cascade _alpha.html → _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 · for traceability. # # Plain dev builds (no --release): never call promote_release. # # No Codeberg upload — HTML tools live in git. zddc-server's release.sh # handles binary uploads to Codeberg directly (different distribution model). promote_release() { _tool="$1" # The top-level `./build` exports $ZDDC_DEPLOY_RELEASES_DIR pointing # at $SCRIPT_DIR/dist/release-output. Single-tool standalone # invocations fall back to the same default — no inheritance from a # parent build run. _releases_dir="${ZDDC_DEPLOY_RELEASES_DIR:-$root_dir/../dist/release-output}" mkdir -p "$_releases_dir" if [ ! -d "$_releases_dir" ]; then echo "promote_release: $_releases_dir not found" >&2 return 1 fi case "$channel" in stable) if [ -z "$build_version" ]; then echo "promote_release: stable channel but no build_version" >&2 return 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 _promote_stable "$_tool" "$build_version" "$_releases_dir" ;; alpha | beta) _promote_channel "$_tool" "$channel" "$_releases_dir" ;; *) echo "promote_release: unknown channel '$channel'" >&2 return 1 ;; esac } # Stable cut: per-version file + 5 symlinks + git tag. _promote_stable() { _t="$1" _ver="$2" _rdir="$3" _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 _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 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 echo "Released ${_t} v${_ver} (stable)" 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}" } # Platforms zddc-server is cross-compiled for. The first three are # extension-less (Linux/macOS); Windows gets .exe. The build always emits # all four; the matrix cell's stub page links each by its tag. ZDDC_SERVER_PLATFORMS="linux-amd64 darwin-amd64 darwin-arm64 windows-amd64" # Display label for the stub-page download list. Keeps the binary-asset # names canonical even if we later add e.g. linux-arm64. _zddc_server_platform_label() { case "$1" in linux-amd64) echo "Linux (x86_64)" ;; darwin-amd64) echo "macOS (Intel)" ;; darwin-arm64) echo "macOS (Apple Silicon)" ;; windows-amd64) echo "Windows (x86_64)" ;; *) echo "$1" ;; esac } # Resolve a zddc-server binary's filename for one (version, platform). # Returns the bare name (no path); ".exe" suffix on windows. _zddc_server_binary_name() { _ver_or_chan="$1" _plat="$2" _suffix="" case "$_plat" in *windows*) _suffix=".exe" ;; esac if echo "$_ver_or_chan" | grep -qE '^v[0-9]'; then # Per-version asset, e.g. zddc-server_v0.0.8_linux-amd64 printf 'zddc-server_%s_%s%s' "$_ver_or_chan" "$_plat" "$_suffix" else # Channel mirror, e.g. zddc-server_stable_linux-amd64 printf 'zddc-server_%s_%s%s' "$_ver_or_chan" "$_plat" "$_suffix" fi } # Write the small HTML index page that becomes the matrix cell's link for # a zddc-server release. Lists each platform binary with a download link. # $1 — release directory (absolute) # $2 — slug (e.g. v0.0.8, v0.0, stable, beta, alpha) # $3 — display label (e.g. "v0.0.8", "stable channel") write_zddc_server_stub() { _rdir="$1" _slug="$2" _label="$3" _out="$_rdir/zddc-server_${_slug}.html" { cat < zddc-server ${_label} — ZDDC

zddc-server — ${_label}

Cross-compiled binaries. Download for your platform, mark executable, and run with ZDDC_ROOT=/path/to/archive ./zddc-server.

HEAD for _plat in $ZDDC_SERVER_PLATFORMS; do _bin=$(_zddc_server_binary_name "$_slug" "$_plat") _plabel=$(_zddc_server_platform_label "$_plat") printf ' \n' "$_plabel" "$_bin" "$_bin" done cat <<'TAIL'
PlatformDownload
%s%s

Need a different platform? Build from source: (cd zddc && go build -o zddc-server ./cmd/zddc-server) from the repo at the matching tag.

TAIL } > "$_out" } # Refresh every zddc-server stub page based on what's currently in the # release-output bundle. Driven by the existing per-version binary files + # symlinks that the release flow already maintains; just emits the HTML # wrappers for them. Safe to run on every cut (idempotent). # # $1 — releases dir (absolute) write_zddc_server_stubs_all() { _rdir="$1" # Every per-version stable binary that exists. We index off # linux-amd64 specifically since all four platforms ship in lockstep # — if the linux build is missing the version is incomplete anyway. for _bin in "$_rdir"/zddc-server_v*_linux-amd64; do [ -e "$_bin" ] || continue _name=$(basename "$_bin") # zddc-server_vX.Y.Z_linux-amd64 → vX.Y.Z _slug=$(echo "$_name" | sed -E 's/^zddc-server_(v[^_]+)_linux-amd64$/\1/') # Skip partial-version pins (vX.Y, vX) — these are written # separately below from symlink resolution. case "$_slug" in v*.*.*) write_zddc_server_stub "$_rdir" "$_slug" "$_slug" ;; esac done # Partial-version + channel stubs follow the symlink chain. If the # symlink resolves to a real binary, write the stub; otherwise skip. for _slug in stable beta alpha; do _probe="$_rdir/zddc-server_${_slug}_linux-amd64" if [ -e "$_probe" ]; then write_zddc_server_stub "$_rdir" "$_slug" "${_slug} channel" fi done # vX.Y and vX partial pins — derive the slug list from the per-version # binaries so we only emit pages we actually have artifacts for. _all_versions=$(find "$_rdir" -maxdepth 1 -name 'zddc-server_v*_linux-amd64' \ | sed -E 's|^.*/zddc-server_(v[0-9]+\.[0-9]+\.[0-9]+)_linux-amd64$|\1|' \ | sort -Vu) if [ -n "$_all_versions" ]; then # vX.Y pins — pick the highest patch within each X.Y, then make # sure the symlink and stub exist. echo "$_all_versions" | sed -E 's|^v([0-9]+\.[0-9]+)\.[0-9]+$|\1|' | sort -Vu | while read -r _xy; do _probe="$_rdir/zddc-server_v${_xy}_linux-amd64" if [ -e "$_probe" ]; then write_zddc_server_stub "$_rdir" "v${_xy}" "v${_xy}" fi done # vX pins. echo "$_all_versions" | sed -E 's|^v([0-9]+)\..*$|\1|' | sort -Vu | while read -r _x; do _probe="$_rdir/zddc-server_v${_x}_linux-amd64" if [ -e "$_probe" ]; then write_zddc_server_stub "$_rdir" "v${_x}" "v${_x}" fi done fi } # Promote a freshly-cross-compiled set of zddc-server binaries to the # release-output bundle. Called by the top-level ./build on a release cut. # # $1 — channel ("stable" | "alpha" | "beta") # $2 — version (X.Y.Z; required for stable; ignored for alpha/beta but # passed through so labels can include the next-stable target) # $3 — releases dir (absolute) # $4 — dist dir holding cross-compiled binaries (absolute) promote_zddc_server() { _ch="$1" _ver="$2" _rdir="$3" _dist="$4" # Verify all four binaries exist before doing anything destructive. for _plat in $ZDDC_SERVER_PLATFORMS; do _suffix="" case "$_plat" in *windows*) _suffix=".exe" ;; esac _src="$_dist/zddc-server-${_plat}${_suffix}" if [ ! -f "$_src" ]; then echo "promote_zddc_server: missing source binary $_src" >&2 return 1 fi done case "$_ch" in stable) if [ -z "$_ver" ]; then echo "promote_zddc_server: stable cut requires version" >&2 return 1 fi _major="${_ver%%.*}" _rest="${_ver#*.}" _minor="${_rest%%.*}" # Per-version: copy each binary to its immutable name + refresh # the partial-version + channel symlinks. Mirrors the HTML-tool # cascade: stable cut → beta + alpha both reset to stable. for _plat in $ZDDC_SERVER_PLATFORMS; do _suffix="" case "$_plat" in *windows*) _suffix=".exe" ;; esac _src="$_dist/zddc-server-${_plat}${_suffix}" _versioned="zddc-server_v${_ver}_${_plat}${_suffix}" cp "$_src" "$_rdir/$_versioned" echo "Wrote $_rdir/$_versioned" for _sym in "zddc-server_v${_major}.${_minor}_${_plat}${_suffix}" \ "zddc-server_v${_major}_${_plat}${_suffix}" \ "zddc-server_stable_${_plat}${_suffix}" \ "zddc-server_beta_${_plat}${_suffix}" \ "zddc-server_alpha_${_plat}${_suffix}"; do ln -sfn "$_versioned" "$_rdir/$_sym" done done # Tag the commit so the binary set is reproducible. _tag="zddc-server-v${_ver}" 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_zddc_server: tag $_tag 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 echo "Released zddc-server v${_ver} (stable)" ;; alpha | beta) # Mutable channel mirror per platform; cascade alpha → beta on # a beta cut. for _plat in $ZDDC_SERVER_PLATFORMS; do _suffix="" case "$_plat" in *windows*) _suffix=".exe" ;; esac _src="$_dist/zddc-server-${_plat}${_suffix}" _file="zddc-server_${_ch}_${_plat}${_suffix}" rm -f "$_rdir/$_file" cp "$_src" "$_rdir/$_file" echo "Wrote $_rdir/$_file" if [ "$_ch" = "beta" ]; then ln -sfn "$_file" "$_rdir/zddc-server_alpha_${_plat}${_suffix}" fi done echo "Released zddc-server ${_ch}" ;; *) echo "promote_zddc_server: unknown channel '$_ch'" >&2 return 1 ;; esac # Refresh every stub page (covers the new release plus any pre-existing). write_zddc_server_stubs_all "$_rdir" } # Verify every channel link for every release tool exists and resolves. # Runs at the end of every build. Fails the build if anything is dangling. # Channel verification covers both HTML tools (one .html per channel) and # zddc-server (one stub HTML + four binaries per channel). # # Bootstrap-friendly: if zddc-server has no per-version artifacts at all # (i.e. no release has been cut yet under the new lockstep model), the # zddc-server entries are skipped with a heads-up rather than failing. The # first stable cut materializes them. verify_channel_links() { _rdir="$1" _missing=0 _verified=0 for _t in archive transmittal classifier mdedit landing; do for _ch in stable beta alpha; do _f="$_rdir/${_t}_${_ch}.html" if [ -e "$_f" ]; then _verified=$((_verified + 1)) else echo " MISSING: ${_t}_${_ch}.html" >&2 _missing=$((_missing + 1)) fi done done # zddc-server's stable cut anchors the channel chain (cascade rule: # stable cut → alpha + beta both reset to stable). Until stable # exists, the verifier runs in bootstrap mode and skips — alpha/beta # cuts in isolation are valid bootstrap state but have no cascade # fallback target yet. _zs_stable_exists=$(find "$_rdir" -maxdepth 1 -name 'zddc-server_stable_linux-amd64' -print -quit 2>/dev/null) if [ -z "$_zs_stable_exists" ]; then echo " (zddc-server stable not yet cut — run 'sh build.sh --release' to anchor the channel chain)" else for _ch in stable beta alpha; do _f="$_rdir/zddc-server_${_ch}.html" if [ -e "$_f" ]; then _verified=$((_verified + 1)) else echo " MISSING: zddc-server_${_ch}.html" >&2 _missing=$((_missing + 1)) fi for _plat in $ZDDC_SERVER_PLATFORMS; do _suffix="" case "$_plat" in *windows*) _suffix=".exe" ;; esac _f="$_rdir/zddc-server_${_ch}_${_plat}${_suffix}" if [ -e "$_f" ]; then _verified=$((_verified + 1)) else echo " MISSING: zddc-server_${_ch}_${_plat}${_suffix}" >&2 _missing=$((_missing + 1)) fi done done fi if [ "$_missing" -gt 0 ]; then echo "channel-link verification: $_missing missing artifact(s)" >&2 return 1 fi echo "channel-link verification: $_verified link(s) ok" }