The cast half of the flow designed in heavy-duty/box#83, aligned with box#90 and rig#40, plus the piece unique to cast: a prebuilt release asset, because cast is the one repo where the source tarball is not the package. - CHANGELOG.md (box's format) with this PR's entry under Unreleased; feature PRs land their entry as part of the PR. - `cast --version` / `-V` answers with package.json's version, read relative to the compiled module so a source checkout and an installed prebuilt tree agree. - release.yml, on EVERY tag push (no shape filter — a mismatched tag must fail the assert loudly, not be pattern-skipped): asserts tag == package.json version FIRST, extracts that version's changelog section (.github/scripts/release-notes.sh, shared with the tests; missing or empty refuses), builds once (npm ci && npm run build && npm prune --omit=dev), stages bin/ dist/ node_modules/ package.json as cast-X.Y.Z/ and attaches cast-X.Y.Z.tgz to `gh release create --verify-tag`. No tests here — ci.yml gated the merge commit, and the suite needs age. - install.sh grows the three channels: default = the latest release's asset (tag resolved off the releases/latest redirect Location — no API, no token; failure dies loudly naming CAST_REF=main, never a silent fallback), CAST_REF=<tag> = pinned (asset first, source fallback), CAST_REF=main = dev build-from-source. npm is required only on the source path, and a prebuilt tree is sanity-checked (dist/, node_modules/) before $DEST is replaced. - test/release.test.ts drives it all offline: --version, the extraction against fixtures (0.7.0 never matches 0.7.0-rc1) and the real changelog, and REAL install.sh runs through all three channels with a stub curl and a poisoned npm — including the loud no-releases refusal with no $DEST side effects. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
31 lines
1.5 KiB
Bash
31 lines
1.5 KiB
Bash
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
# release-notes.sh <version> [<changelog>] — print exactly <version>'s
|
|
# section of the changelog: every line between its '## <version> — <date>'
|
|
# header and the next '## '. This is what release.yml hands to
|
|
# 'gh release create', so the release notes are the curated prose we wrote,
|
|
# not the PR list GitHub would generate (#96; box#83's extraction). Fails
|
|
# loudly when the section is missing or empty — a tag without its changelog
|
|
# section is a release ritual skipped, and an empty release body would paper
|
|
# over it.
|
|
#
|
|
# A file of its own (not inlined in release.yml) so test/release.test.ts
|
|
# drives the same extraction against fixtures and the real CHANGELOG.md.
|
|
|
|
ver="${1:-}"
|
|
changelog="${2:-CHANGELOG.md}"
|
|
[ -n "$ver" ] || { echo "usage: release-notes.sh <version> [<changelog>]" >&2; exit 2; }
|
|
[ -f "$changelog" ] || { echo "release-notes: no such file: $changelog" >&2; exit 1; }
|
|
|
|
# $2 of a section header ('## 0.1.0 — 2026-07-18') is the bare version —
|
|
# compared WHOLE, so 0.1.0 can never match a 0.1.0-rc1 section (or vice
|
|
# versa), and no regex-escaping of dots. sed drops the blank padding under
|
|
# the header; the command substitution eats the trailing blanks.
|
|
notes="$(awk -v ver="$ver" '
|
|
/^## / { grab = ($2 == ver); next }
|
|
grab { print }
|
|
' "$changelog" | sed '/./,$!d')"
|
|
|
|
[ -n "$notes" ] || { echo "release-notes: $changelog has no section for '$ver' — the release PR stamps the Unreleased section with version + date BEFORE the tag (#96)" >&2; exit 1; }
|
|
printf '%s\n' "$notes"
|