ceremony-ci-probe/lib/version.sh
claude-bot-andresmgsl 36fc98bf40 feat: lib/version.sh — one version abstraction, two backends
The seam that lets box/rig/incubator (a VERSION file) and cast
(package.json + lockfile sync) share every other ceremony component
unchanged. Sourced, pure (no git), fail-loud on every unreadable state.

Carries cast's pkg_version discipline (node's parser, never regex) and
its lockfile-only bump incantation; refuses pre-release arithmetic
(-dev/-rc1) per box's prefix-confusion lore. The npm-backed write test
skips locally without npm but CI sets CEREMONY_REQUIRE_NPM so the skip
is a failure there — the case can never quietly stop running.

Closes #3

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 18:19:50 +00:00

122 lines
4.6 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
# lib/version.sh — one version abstraction, two backends (issue #3).
#
# Sourced, never executed: no set -e/-u here — the sourcing script owns its
# own shell options. No git in this lib either: "version at the base commit"
# is the caller's job (git show / git worktree the base tree, then point
# version_read at it) — keeping the lib pure keeps its tests trivial.
#
# Backends ($1 of version_read / version_write):
# file — a VERSION file in the tree (box, rig, incubator)
# package-json — package.json's version field (cast)
# version_read <backend> [dir] — print the version; fail loudly on a
# missing/empty source. A wrong release is worse than a missing one, so
# every unreadable state is exit 1 with a message, never an empty print.
version_read() {
local backend="${1:?version_read: backend required}" dir="${2:-.}"
local path ver
case "$backend" in
file)
path="$dir/VERSION"
if [ ! -f "$path" ]; then
echo "version_read: $path: no such file" >&2
return 1
fi
# Whitespace-stripped, following box's drill-recorded.sh: a trailing
# newline or a stray space in VERSION must never make 0.7.0 look
# unlike 0.7.0 (whole-version matching everywhere).
ver="$(tr -d '[:space:]' <"$path")"
if [ -z "$ver" ]; then
echo "version_read: $path is empty" >&2
return 1
fi
printf '%s\n' "$ver"
;;
package-json)
# A clear message beats a bare command-not-found from deep inside a
# workflow log.
if ! command -v node >/dev/null 2>&1; then
echo "version_read: node is required for version-source: package-json" >&2
return 1
fi
path="$dir/package.json"
if [ ! -f "$path" ]; then
echo "version_read: $path: no such file" >&2
return 1
fi
# Read via node's own parser, never regex — cast's "pkg_version
# discipline" (cast release.yml): grepping JSON for "version" finds
# dependency versions, engine fields, anything. An absent or
# non-string field fails here rather than printing "undefined".
node -e '
const p = require(require("path").resolve(process.argv[1]));
if (typeof p.version !== "string" || p.version === "") {
console.error("version_read: " + process.argv[1] + ": no version field");
process.exit(1);
}
console.log(p.version);
' "$path"
;;
*)
echo "version_read: unknown backend: $backend" >&2
return 1
;;
esac
}
# version_is_dev <ver> — exit 0 iff ver ends in the literal -dev suffix.
# Only -dev: an rc (1.2.3-rc1) is a pre-release, not a dev tree, and
# treating it as one would let the armed guard key on the wrong state.
version_is_dev() {
case "${1:?version_is_dev: version required}" in
*-dev) return 0 ;;
*) return 1 ;;
esac
}
# version_next_dev <ver> — bare X.Y.Z -> X.Y.(Z+1)-dev (print). Refuses
# anything else, including -dev and -rc1: this is only ever called on a
# just-released version to re-arm main, and an rc's "next" is a human
# decision, not arithmetic (box's drills/ prefix-confusion lore is why
# nothing here guesses around pre-release identifiers).
version_next_dev() {
local ver="${1:?version_next_dev: version required}"
if [[ ! "$ver" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "version_next_dev: refusing '$ver' — expected bare X.Y.Z" >&2
return 1
fi
local major minor patch
IFS=. read -r major minor patch <<<"$ver"
# 10#: a zero-padded patch ("09") would otherwise be read as octal.
printf '%s.%s.%s-dev\n' "$major" "$minor" "$((10#$patch + 1))"
}
# version_write <backend> <ver> [dir] — write the version into the tree.
version_write() {
local backend="${1:?version_write: backend required}"
local ver="${2:?version_write: version required}"
local dir="${3:-.}"
case "$backend" in
file)
printf '%s\n' "$ver" >"$dir/VERSION"
;;
package-json)
if ! command -v npm >/dev/null 2>&1; then
echo "version_write: npm is required for version-source: package-json" >&2
return 1
fi
# npm pkg set + a lockfile-only install, exactly cast's incantation
# (cast release.yml L233L239): package-lock.json embeds the version
# twice, and a bump that skips the lockfile leaves every subsequent
# `npm ci` failing on the mismatch. --ignore-scripts because a
# version bump must never run anybody's install hooks.
(cd "$dir" && npm pkg set version="$ver" \
&& npm install --package-lock-only --ignore-scripts)
;;
*)
echo "version_write: unknown backend: $backend" >&2
return 1
;;
esac
}