cast/scripts/changelog-section.sh
dan-claude-bot d992f1833d feat: versioned installs — tagged releases with a prebuilt dist asset (#96)
cast gets the family's release flow (box#83's shape), plus the piece
unique to cast: because cast compiles, the source tarball is not the
package — so release.yml builds ONCE in CI and attaches cast-X.Y.Z.tgz,
and the installer's default channel extracts that asset instead of
running npm ci + tsc on the operator's machine.

- cast --version: package.json is the single source of truth (no VERSION
  file); prints the install root too, rig-style.
- CHANGELOG.md with Unreleased; release notes are the curated section
  (scripts/changelog-section.sh), never the auto-generated PR list.
- release.yml on a bare X.Y.Z tag: assert tag == package.json version,
  check + build + test, prune, tar the runnable tree, gh release create.
- install.sh channels: unset → latest release asset (resolved via the
  releases/latest redirect — no API, no token); CAST_REF=X.Y.Z → that
  tag's asset; CAST_REF=<branch> → build-from-source, the old path.
- Tests drive the REAL install.sh offline via curl/npm PATH shims (all
  three channels, plus the broken-asset and no-release refusals), and
  the real changelog-section.sh against fixture changelogs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 20:51:15 +00:00

43 lines
1.6 KiB
Bash

#!/usr/bin/env bash
set -euo pipefail
# changelog-section.sh <version> [changelog-file]
#
# Print the body of CHANGELOG.md's `## <version>` section — everything
# between that heading and the next `## ` heading (or EOF), with the
# leading/trailing blank lines trimmed. release.yml uses this as the
# GitHub release body, so the notes are the curated prose we actually
# wrote, never an auto-generated PR list (cast#96 / box#83).
#
# Fails loudly when the section is missing or empty: a release with no
# written history is a release that skipped the changelog discipline,
# and the tag push is exactly the moment to catch that — before a
# release object exists.
version="${1:-}"
file="${2:-CHANGELOG.md}"
[ -n "$version" ] || { echo "usage: changelog-section.sh <version> [changelog-file]" >&2; exit 2; }
[ -f "$file" ] || { echo "changelog-section: no such file: $file" >&2; exit 1; }
# The heading is `## <version>` optionally followed by more (a date stamp:
# `## 0.1.0 — 2026-07-18`). Match on the version as the second word so the
# stamp's format never becomes load-bearing here.
section="$(awk -v ver="$version" '
/^## / { if (found) exit; if ($2 == ver) { found = 1; next } }
found { print }
END { exit found ? 0 : 3 }
' "$file")" || {
echo "changelog-section: no \"## $version\" section in $file" >&2
exit 1
}
# Trim leading blank lines; command substitution already ate the trailing ones.
section="$(printf '%s\n' "$section" | sed '/./,$!d')"
[ -n "$section" ] || {
echo "changelog-section: the \"## $version\" section in $file is empty" >&2
exit 1
}
printf '%s\n' "$section"