name: ci on: push: branches: [main] pull_request: jobs: check: 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 # pure-bash tree costs a second; the STRICT flag below turns any # remaining skip red rather than green. 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 # test/labels-reconcile.sh existed here since #87 but ran nowhere: the # label state machine gates every PR on this repo and its fixtures were # green only when someone remembered to run them by hand. Same step, same # place as heavy-duty/box. - name: labels state-machine tests run: bash test/labels-reconcile.sh - name: release-flow tests run: bash test/release.sh # No SHIPPED release heading was deleted or DUPLICATED (#98). Its own step # rather than a line inside test/release.sh: that suite drives the arming # rule against constructed VERSION + CHANGELOG.md trees that are not git # repos, and this assert needs a git history — folding it in would make # those cases skip or lie. It is also a DIFFERENT invariant: 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 #98 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 }}" # The release this tree would ship has a recorded real-hardware drill # (drills/.md). CONTRIBUTING ("Releasing") has always required one and # nothing enforced it, so no release in this family has ever carried one # — the drill was the single ceremony step checked by a reviewer # remembering rather than by a script. # # Deliberately NOT trigger-scoped, and for the opposite reason to the # step above. That one needs a base ref, so its argument has to be right # on both event types; this one reads two files in the checkout and is # VACUOUS BY CONSTRUCTION on a `-dev` VERSION, which every ordinary PR # and every push to main carries. It has something to say on exactly one # tree — the `release: X.Y.Z` ceremony PR — so an `if:` could only add a # way for that one tree to slip past. # # PER-REPO on purpose: rig reads rig's own record, never box's repo. A # cross-repo lookup fails on a token, a network blip or a fork checkout, # and every one of those lands on "could not read" — which degrades to # green on precisely the tree that ships (the UNREADABLE-vs-NONE shape # #90 fixed). - name: a release version has a recorded drill run: bash .github/scripts/drill-recorded.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