name: ci on: push: branches: [main] pull_request: jobs: check: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: # changelog-monotonic and changelog-assembled compare 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 these checks exist to refuse). fetch-depth: 0 - name: shellcheck # -x follows the `source=SCRIPTDIR/...` directives into commands/lib/. # globstar so a script in a new subdirectory is linted without anyone # remembering to edit this list; bin/* covers the extensionless entrypoints. # dotglob because globs skip dot-prefixed names: without it `**/` never # descends into `.github/`, so `.github/scripts/*.sh` — release-lib.sh # among them — was swept up by nothing (#70). It also makes `**` # descend into `.git/`, which holds no tracked `.sh` on a checkout. # The file list is printed so under-coverage shows up in the log, and # the comm below turns under-coverage into a failure rather than a # thing someone has to notice: every tracked `.sh` must be in the set. run: | shopt -s globstar dotglob files=(bin/* **/*.sh) printf 'shellcheck: %s\n' "${files[@]}" uncovered=$(comm -23 <(git ls-files '*.sh' | sort) <(printf '%s\n' "${files[@]}" | sort)) if [ -n "$uncovered" ]; then printf 'tracked .sh files the glob does not lint:\n%s\n' "$uncovered" >&2 exit 1 fi shellcheck -x "${files[@]}" - name: cli tests run: bash test/cli.sh - name: release tests — rig's own surfaces run: bash test/release.sh # The drill harness's honesty: refusals, the skip/pass/fail classifier, # the idempotence capture-and-diff, the record emitter. Network-free and # root-free — the live four-leg run is a release's drill, not CI's. - name: drill harness tests — the instrument itself run: bash test/drill.sh # The release guards, doctrine in heavy-duty/ceremony's README (#13's # conversion). Each one's war story — why it exists, what it refuses — # lives with its implementation upstream; the six pins below and the # two workflow callers must always name the same ceremony tag. # # These six are ABSOLUTE and the two callers in release.yml/labels.yml # are BARE. That asymmetry is measured, not stylistic (#112) — do not # "tidy" either half into the other: # a step `uses:` resolves a bare name through DEFAULT_ACTIONS_URL, # which this forge points at code.forgejo.org, where ceremony does not # exist — bare 404s and every guard below fails; # a reusable-workflow `uses:` never consults DEFAULT_ACTIONS_URL at # all — it resolves against the runner's own instance, so bare already # works there. And release.yml's caller is the line ceremony's own # docs-sync greps for its pin, anchored to the bare spelling: make it # absolute and the pin goes invisible and docs-sync exits 1. # # changelog-armed: the version-keyed arming rule (rig#66; the # unconditional form rig#44 reverted — this is its correct return). - uses: https://forgejo.heavyduty.builders/heavy-duty/ceremony/actions/changelog-armed@0.3.0 # changelog-monotonic: no shipped heading deleted or duplicated # (#98, box#122). Strict by default: an unresolvable base ref is red, # never a quiet skip — hence the fetch-depth: 0 above. - uses: https://forgejo.heavyduty.builders/heavy-duty/ceremony/actions/changelog-monotonic@0.3.0 # changelog-assembled: a release's stamped section must exactly match # the fragments it consumed. Vacuous on non-release PRs. - uses: https://forgejo.heavyduty.builders/heavy-duty/ceremony/actions/changelog-assembled@0.3.0 # drill-recorded: a release version carries drills/.md # (rig's drill meaning: drills/README.md). Vacuous on -dev trees. - uses: https://forgejo.heavyduty.builders/heavy-duty/ceremony/actions/drill-recorded@0.3.0 # runner-isolated: PR-triggered workflows never execute unreviewed # branch code on a self-hosted runner. - uses: https://forgejo.heavyduty.builders/heavy-duty/ceremony/actions/runner-isolated@0.3.0 # 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: https://forgejo.heavyduty.builders/heavy-duty/ceremony/actions/docs-sync@0.3.0 # The install LIFECYCLE against a tree install.sh itself produced — the four # beats box and cast already run in CI (#106): install from this checkout, # assert what landed, a converging re-run proven by an EMPTY DIFF (never an # exit code), uninstall --all ending in the absence assert (`! -e` AND # `! -L` — only the second sees a dangling symlink). Separate from `check` # for the same reason db-integration is: fast feedback first. The runner's # real $HOME is the point — no throwaway roots here; the suite refuses to # run where a rig is already installed, so it cannot eat a real install. install: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: the install lifecycle — four beats against a real tree run: bash test/install-lifecycle.sh # Kept SEPARATE from `check` on purpose: this job pulls a Postgres image and # stands up throwaway containers, and a slow image pull must never delay the # fast shellcheck + cli.sh feedback above. ubuntu-latest ships Docker running # and passwordless sudo, so test/db-integration.sh EXECUTES here (it only # skips where Docker is absent). It is the automated proof that dump/restore # actually round-trips, not just that the args parse. db-integration: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: db dump/restore round-trip run: bash test/db-integration.sh