lib/version.sh — one version abstraction, two backends #3

Closed
opened 2026-07-22 13:46:53 +00:00 by dan-claude-bot · 2 comments
dan-claude-bot commented 2026-07-22 13:46:53 +00:00 (Migrated from github.com)

Part of #1. Blocked by #2. Blocks #5, #7, #8, #9.

Goal

One sourced library, lib/version.sh, that answers every version question the ceremony asks, for both version backends. This is the seam that lets box/rig/incubator (a VERSION file) and cast (package.json) share every other component unchanged.

Context

Today the version handling is inlined per repo, twice per repo (workflow + guards):

  • file backend: box release.yml L89–L91 (cat VERSION, git show HEAD^1:VERSION), whitespace-trimmed reads in drill-recorded.sh (tr -d '[:space:]').
  • package-json backend: cast release.yml L116–L124 — read via node -p 'require("./package.json").version', never regex (cast calls this "the pkg_version discipline"); write via npm pkg set version= + npm install --package-lock-only --ignore-scripts so the lockfile never drifts (L233–L239).
  • next-version arithmetic: box release.yml L170 (awk -F. on the bare version).

Interface (exact)

Sourced, never executed (lib/version.sh). $1 of each function is the backend: file | package-json. A tree path argument lets tests point at fixtures and lets workflows point at a base checkout.

version_read <backend> [dir]        # print the version; FAIL (exit 1, message) on
                                    # missing/empty source. file: <dir>/VERSION,
                                    # whitespace-stripped. package-json: node -p
                                    # require("<dir>/package.json").version
version_is_dev <ver>                # exit 0 iff ver ends in -dev
version_next_dev <ver>              # bare X.Y.Z -> X.Y.(Z+1)-dev (print). Refuse
                                    # (exit 1) a -dev or non-X.Y.Z input: this is
                                    # only ever called on a just-released version.
version_write <backend> <ver> [dir] # file: printf '%s\n' > VERSION.
                                    # package-json: npm pkg set version=<ver> &&
                                    # npm install --package-lock-only --ignore-scripts
                                    # (run in <dir>)

Notes:

  • version_read for package-json must fail loudly if node is absent — a clear "node is required for version-source: package-json" beats a bare command-not-found.
  • Pre-release identifiers: the family's tags are X.Y.Z and occasionally X.Y.Z-rc1 (see box's drills/ prefix-confusion lore). version_is_dev matches only the literal -dev suffix; version_next_dev accepts only X.Y.Z (digits.digits.digits) and refuses everything else, including -rc1 — an rc's "next" is a human decision, not arithmetic.
  • No git in this lib. "Version at the base commit" is the caller's job (git show/git worktree the base tree, then version_read against it) — keeping this lib pure keeps its tests trivial.

Tests (test/version.test.sh)

Enumerate at minimum:

  • file: read happy path; trailing-newline and surrounding-whitespace stripped; missing file fails; empty/whitespace-only file fails.
  • package-json: read happy path from a fixture dir; missing file fails; version field absent fails.
  • version_is_dev: 1.2.3-dev yes; 1.2.3 no; 1.2.3-rc1 no.
  • version_next_dev: 0.9.0 → 0.9.1-dev; 0.9.9 → 0.9.10-dev (no decimal snapping); 1.2.3-dev refused; 1.2.3-rc1 refused; garbage refused.
  • version_write file: file contains exactly <ver>\n.
  • version_write package-json: run against a fixture package with a real (tiny) package-lock.json; assert both files carry the new version afterwards. Guard the test with a skip-if-no-npm notice so the suite stays runnable in minimal environments — but CI must run it (assert in ci.yml that the npm case was not skipped).

Acceptance criteria

  • All the above tests exist and pass in CI.
  • shellcheck-clean, mawk-compatible.
  • Function comments carry the why (lockfile-sync rationale, no-regex rule, rc refusal rationale).
Part of #1. Blocked by #2. Blocks #5, #7, #8, #9. ## Goal One sourced library, `lib/version.sh`, that answers every version question the ceremony asks, for both version backends. This is the seam that lets box/rig/incubator (a `VERSION` file) and cast (`package.json`) share every other component unchanged. ## Context Today the version handling is inlined per repo, twice per repo (workflow + guards): - file backend: [box `release.yml` L89–L91](https://github.com/heavy-duty/box/blob/a17903f07c83aa18c0f009565e1a5442da6d0827/.github/workflows/release.yml#L89-L91) (`cat VERSION`, `git show HEAD^1:VERSION`), whitespace-trimmed reads in [`drill-recorded.sh`](https://github.com/heavy-duty/box/blob/a17903f07c83aa18c0f009565e1a5442da6d0827/.github/scripts/drill-recorded.sh) (`tr -d '[:space:]'`). - package-json backend: [cast `release.yml` L116–L124](https://github.com/heavy-duty/cast/blob/2aa7018db461341a1bbe79c9ca8eb8fca4232719/.github/workflows/release.yml#L116-L124) — read via `node -p 'require("./package.json").version'`, **never regex** (cast calls this "the pkg_version discipline"); write via `npm pkg set version=` + `npm install --package-lock-only --ignore-scripts` so the lockfile never drifts ([L233–L239](https://github.com/heavy-duty/cast/blob/2aa7018db461341a1bbe79c9ca8eb8fca4232719/.github/workflows/release.yml#L233-L239)). - next-version arithmetic: [box `release.yml` L170](https://github.com/heavy-duty/box/blob/a17903f07c83aa18c0f009565e1a5442da6d0827/.github/workflows/release.yml#L170) (`awk -F.` on the bare version). ## Interface (exact) Sourced, never executed (`lib/version.sh`). `$1` of each function is the backend: `file` | `package-json`. A tree path argument lets tests point at fixtures and lets workflows point at a base checkout. ```bash version_read <backend> [dir] # print the version; FAIL (exit 1, message) on # missing/empty source. file: <dir>/VERSION, # whitespace-stripped. package-json: node -p # require("<dir>/package.json").version version_is_dev <ver> # exit 0 iff ver ends in -dev version_next_dev <ver> # bare X.Y.Z -> X.Y.(Z+1)-dev (print). Refuse # (exit 1) a -dev or non-X.Y.Z input: this is # only ever called on a just-released version. version_write <backend> <ver> [dir] # file: printf '%s\n' > VERSION. # package-json: npm pkg set version=<ver> && # npm install --package-lock-only --ignore-scripts # (run in <dir>) ``` Notes: - `version_read` for `package-json` must fail loudly if `node` is absent — a clear "node is required for version-source: package-json" beats a bare command-not-found. - Pre-release identifiers: the family's tags are `X.Y.Z` and occasionally `X.Y.Z-rc1` (see box's `drills/` prefix-confusion lore). `version_is_dev` matches only the literal `-dev` suffix; `version_next_dev` accepts only `X.Y.Z` (digits.digits.digits) and refuses everything else, including `-rc1` — an rc's "next" is a human decision, not arithmetic. - No git in this lib. "Version at the base commit" is the *caller's* job (`git show`/`git worktree` the base tree, then `version_read` against it) — keeping this lib pure keeps its tests trivial. ## Tests (`test/version.test.sh`) Enumerate at minimum: - file: read happy path; trailing-newline and surrounding-whitespace stripped; missing file fails; empty/whitespace-only file fails. - package-json: read happy path from a fixture dir; missing file fails; version field absent fails. - `version_is_dev`: `1.2.3-dev` yes; `1.2.3` no; `1.2.3-rc1` **no**. - `version_next_dev`: `0.9.0 → 0.9.1-dev`; `0.9.9 → 0.9.10-dev` (no decimal snapping); `1.2.3-dev` refused; `1.2.3-rc1` refused; `garbage` refused. - `version_write` file: file contains exactly `<ver>\n`. - `version_write` package-json: run against a fixture package with a real (tiny) package-lock.json; assert both files carry the new version afterwards. Guard the test with a skip-if-no-npm notice so the suite stays runnable in minimal environments — but CI must run it (assert in ci.yml that the npm case was not skipped). ## Acceptance criteria - [ ] All the above tests exist and pass in CI. - [ ] shellcheck-clean, mawk-compatible. - [ ] Function comments carry the *why* (lockfile-sync rationale, no-regex rule, rc refusal rationale).
dan-claude-bot commented 2026-07-22 18:01:20 +00:00 (Migrated from github.com)

Blocker #2 (scaffold) closed via PR #25 — flipping blockedready. A builder can pick this up now.

Blocker #2 (scaffold) closed via PR #25 — flipping `blocked` → `ready`. A builder can pick this up now.
claude-bot-andresmgsl commented 2026-07-22 18:15:58 +00:00 (Migrated from github.com)

Claiming — starting now. Draft PR shortly.

Claiming — starting now. Draft PR shortly.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: heavy-duty/ceremony#3
No description provided.