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>
74 lines
2.9 KiB
YAML
74 lines
2.9 KiB
YAML
name: release
|
|
|
|
# A release is a PR, then a tag (cast#96, the flow shared with box#83):
|
|
# the `release: X.Y.Z` PR bumps package.json and stamps the CHANGELOG's
|
|
# Unreleased section; merging and pushing the bare `X.Y.Z` tag lands here.
|
|
# This workflow is where cast differs from its siblings: box/rig are pure
|
|
# bash, so the source tarball IS the package — cast compiles, so the build
|
|
# happens ONCE, here, and the release carries a prebuilt `cast-X.Y.Z.tgz`
|
|
# the installer can drop in without npm ci or tsc on the operator's machine.
|
|
|
|
on:
|
|
push:
|
|
# Bare X.Y.Z tags (the family scheme — box's 0.6.0 set the precedent,
|
|
# no `v` prefix). The glob is loose; the assert step below is the gate.
|
|
tags: ["[0-9]*.*.*"]
|
|
|
|
permissions:
|
|
contents: write
|
|
|
|
jobs:
|
|
release:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
- uses: actions/setup-node@v4
|
|
with:
|
|
node-version: "22"
|
|
cache: npm
|
|
|
|
- name: assert tag == package.json version
|
|
# Fail loudly, create nothing: a tag that contradicts package.json
|
|
# would mint a release whose `cast --version` disagrees with its
|
|
# own name. The mismatch is a ritual error — retag, don't patch.
|
|
run: |
|
|
version="$(node -p 'require("./package.json").version')"
|
|
if [ "$GITHUB_REF_NAME" != "$version" ]; then
|
|
echo "tag '$GITHUB_REF_NAME' != package.json version '$version' — refusing to release" >&2
|
|
exit 1
|
|
fi
|
|
|
|
- name: extract the release notes from CHANGELOG.md
|
|
# The release body is the curated section we wrote, never the
|
|
# auto-generated PR list. Missing/empty section fails the release —
|
|
# before the tag has minted anything.
|
|
run: bash scripts/changelog-section.sh "$GITHUB_REF_NAME" CHANGELOG.md > /tmp/release-notes.md
|
|
|
|
- name: build the package, once
|
|
run: |
|
|
npm ci
|
|
npm run check
|
|
npm run build
|
|
npm test
|
|
|
|
- name: assemble cast-${{ github.ref_name }}.tgz
|
|
# The runnable tree and nothing else: bin/, dist/, production
|
|
# node_modules/, package.json. Pruned AFTER the tests so what ships
|
|
# is the tree that passed. Top-level dir named like a GitHub
|
|
# archive's, so the installer handles both shapes identically.
|
|
run: |
|
|
npm prune --omit=dev
|
|
stage="$(mktemp -d)/cast-$GITHUB_REF_NAME"
|
|
mkdir -p "$stage"
|
|
cp -R bin dist node_modules package.json "$stage/"
|
|
tar -C "$(dirname "$stage")" -czf "cast-$GITHUB_REF_NAME.tgz" "cast-$GITHUB_REF_NAME"
|
|
tar -tzf "cast-$GITHUB_REF_NAME.tgz" | head -5
|
|
|
|
- name: create the GitHub release
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
run: |
|
|
gh release create "$GITHUB_REF_NAME" "cast-$GITHUB_REF_NAME.tgz" \
|
|
--verify-tag \
|
|
--title "cast $GITHUB_REF_NAME" \
|
|
--notes-file /tmp/release-notes.md
|