name: ci on: push: branches: [main] pull_request: jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: # changelog-monotonic compares HEAD against the merge base; a # checkout that cannot resolve it is a hard failure in CI, not # a skip (a guard that can quietly stop guarding is the failure # shape this repo keeps refusing). 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 # The release guards, doctrine in heavy-duty/ceremony's README # (ceremony#15 is this conversion). Each guard's war story — why it # exists, what it refuses — lives with its implementation upstream; # the four pins below and the two workflow callers must always name # the same ceremony tag. # # changelog-armed: the version-keyed arming rule (rig#66 is the # incident; the unconditional form cast#108 reverted — this is its # correct return). - uses: heavy-duty/ceremony/actions/changelog-armed@0.1.0 with: version-source: package-json # changelog-monotonic: no shipped heading deleted or duplicated # (#133; box#122's guard, box#143's ordering fix). Strict by default: # an unresolvable base ref is red, never a quiet skip — hence the # fetch-depth: 0 above. - uses: heavy-duty/ceremony/actions/changelog-monotonic@0.1.0 # drill-recorded: a release version carries drills/.md # (cast's drill meaning: drills/README.md). Vacuous on -dev trees; it # requires a RECORD, not a pass — a maintainer waiver is legal, and # is itself the content of the file. - uses: heavy-duty/ceremony/actions/drill-recorded@0.1.0 with: version-source: package-json # docs-sync: the .ceremony/ doctrine mirror is byte-identical to the # pin read from release.yml (ceremony#19) — a hand edit or a # half-done pin bump goes red here. - uses: heavy-duty/ceremony/actions/docs-sync@0.1.0 # 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"