name: ci on: push: branches: [main] pull_request: jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: # fetch-depth: 0, for the changelog-monotonic step below and only # for it. That check is about a DIFF — which release headings the # merge base had — so it needs the base branch's history present, # and the default depth-1 checkout has none of it. An explicit # `git fetch origin ` would be narrower, but it has to be # right on both event types and on fork PRs, and getting it subtly # wrong degrades to a SKIP (a guard that silently stops guarding — # the exact failure this repo keeps refusing). Full history on a # tree this size costs a second; the STRICT flag below turns any # remaining skip red rather than green. fetch-depth: 0 - uses: actions/setup-node@v4 with: node-version: "22" cache: npm # the secrets tests round-trip a real age identity - run: sudo apt-get update && sudo apt-get install -y age - run: npm ci - run: npm run check - run: npm run build - run: npm test - name: installer is valid bash run: bash -n install.sh bin/cast scripts/*.sh .github/scripts/*.sh # `bash -n` above is a syntax check on a hand-maintained list; it is not # a linter and it does not notice a script it was never told about. # This sweep derives its list from git and asserts the list covers every # tracked *.sh, so a new script cannot go unlinted quietly (#118). - name: shellcheck — every tracked shell script run: bash .github/scripts/shellcheck-all.sh - name: labels state-machine tests run: bash test/labels-reconcile.sh # ...and no SHIPPED release heading was deleted or DUPLICATED (#133; # box#122's guard, box#143's ordering fix). Its own step so that when it # goes red the log names the invariant that broke — and a DIFFERENT # invariant from the arming rule npm test carries: arming is a fact # about this tree, monotonicity is a fact about this tree versus its # merge base. STRICT=1 so a checkout that cannot reach the base ref # fails here instead of skipping quietly forever. # # NOT pull-request-only, and that is the #133 fix at the workflow level. # The two halves have different vacuity: DELETION is vacuous on a push # to main (the merge base IS HEAD), but DUPLICATION is vacuous on no # tree at all, so gating the whole script on `pull_request` left a # duplicate that reached main by any other route unasserted forever. # # The `|| github.ref_name` fallback is load-bearing, not defensive. On a # push event `github.base_ref` is EMPTY, so the argument would collapse # to a bare `origin/`, which does not resolve — and STRICT=1 correctly # promotes that to a hard failure, turning every push to main red. With # the fallback it resolves to the pushed branch, whose merge base with # HEAD is HEAD or its parent: containment passes vacuously, exactly as # the old `if` intended, while uniqueness now runs on every push. - name: no shipped changelog heading was deleted or duplicated env: CHANGELOG_MONOTONIC_STRICT: "1" run: bash .github/scripts/changelog-monotonic.sh "origin/${{ github.base_ref || github.ref_name }}" # ...and a release carries its DRILL RECORD. CONTRIBUTING has always # asked for the real-hardware drill; nothing asserted it, so it was # performed exactly as often as a reviewer remembered to ask — which is # never, across every release in the family, until a bot blocked on it. # Here it is a fact about the tree instead of a fact about somebody's # memory. # # No `if:` guard on the event or the label. The script keys off # package.json itself: a `-dev` tree has no ship claim and passes # trivially, a bare version is a release ceremony tree and must have a # record. Gating this step on the `release` label instead would put the # assert behind a hand-applied label — the guard would be absent from # exactly the PR that mislabels itself, and unasserted PRs are how the # drill went missing in the first place. # # It requires a RECORD, not a PASS: a maintainer waiver is legal, and is # itself the content of drills/.md. Skipping stays possible and # stays visible. - name: a release version has a drill record run: bash .github/scripts/drill-recorded.sh # The installer, proven by RUNNING it — CAST_INSTALL_SOURCE points it at # this checkout, so CI proves the installer under review (the versioned # layout, the current symlink, the PATH chain, the uninstall's absence # assert), not a hand-built imitation of it. Box's CI installs box the # same way. This is the one place the real npm ci + tsc build path runs # end to end; the vitest installer tests cover the layout semantics # offline with a shimmed npm. install: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: "22" cache: npm - name: install via install.sh, from this checkout run: | CAST_NO_MODIFY_PATH=1 CAST_INSTALL_SOURCE="$GITHUB_WORKSPACE" bash install.sh # assert what landed: the layout, the chain, and that it answers readlink -f "$HOME/.local/bin/cast" | grep '/versions/' "$HOME/.local/bin/cast" --version "$HOME/.local/bin/cast" versions - name: converging no-op — a re-run changes nothing and builds nothing run: | CAST_NO_MODIFY_PATH=1 CAST_INSTALL_SOURCE="$GITHUB_WORKSPACE" bash install.sh \ | tee /tmp/rerun.log grep -q 'already installed' /tmp/rerun.log - name: uninstall --all — ends with the absence assert run: | CAST_YES=1 "$HOME/.local/bin/cast" uninstall --all test ! -e "$HOME/.local/share/cast" test ! -e "$HOME/.local/bin/cast" test ! -L "$HOME/.local/bin/cast"