Repository navigation
tasks: a row says its state once, an abandoned call is said in whole … #2438
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: PR gate | |
| # THE LIGHT GATE, AND IT IS DELIBERATELY LIGHT. Work moves into `dev` many | |
| # times a day, much of it written by agents, and a gate that takes fifteen | |
| # minutes is a gate people learn to route around. So this one asks only the | |
| # questions whose answer is always the same on every machine and whose failure | |
| # always means somebody broke something: does the tree compile, is it | |
| # formatted, does vet find a real bug, do the laws hold, and does the manual | |
| # still know about the feature that just landed. | |
| # | |
| # THE LAWS ARE HERE BECAUSE THEY WERE NOT. Until 2026-09-02 this gate's one | |
| # test step was `go test -run 'Manual'` over two packages — a filter nobody | |
| # remembered — and the endings ratchet in internal/session went red on dev | |
| # through two merged pull requests while every check here was green (#372). | |
| # A structural test decides in under a second and the same on every machine; | |
| # it belongs on the light gate by the light gate's own definition, and now | |
| # `make test-laws` finds every one of them without anybody keeping a list. | |
| # | |
| # The whole suite is not here. The packages a change touched run in full in the | |
| # second job below, on every pull request, and `check` — the one name a person | |
| # and a ruleset look at — is green only when both are; everything else runs on | |
| # the way into `staging`, and nightly against `dev` — see ci-full.yml. | |
| # docs/rules/ci.md says why the line is drawn in that place. | |
| on: | |
| pull_request: | |
| branches: | |
| - dev | |
| # Also on the push, because a merge of two individually-green branches can | |
| # still be red, and the merge is exactly the moment nobody is watching. | |
| push: | |
| branches: | |
| - dev | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| # A pull request's newer push cancels its older run; a push to `dev` never | |
| # cancels another, because the group is the commit. Keyed on the ref, the | |
| # second merge inside twenty-five minutes cancelled the first merge's | |
| # `touched packages` run, and a cancelled run leaves no red — on exactly the | |
| # push the trigger above exists for. | |
| concurrency: | |
| group: pr-gate-${{ github.event.pull_request.number || github.sha }} | |
| cancel-in-progress: true | |
| jobs: | |
| # THE LIGHT HALF: the questions that answer in a few minutes. It is not the | |
| # required name — `check`, at the bottom, is — so that the light answer can | |
| # arrive early while the touched packages are still running beside it. | |
| light: | |
| name: light gate | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| # The changelog check compares against the base of the pull request, | |
| # which a shallow clone does not contain. | |
| fetch-depth: 0 | |
| - uses: actions/setup-go@v5 | |
| with: | |
| go-version-file: go.mod | |
| cache: true | |
| # Several sessions work this tree at once and a half-finished file breaks | |
| # the build for everyone. This is the cheapest way to find that out, and | |
| # the one failure that blocks every other kind of work. | |
| - name: Build | |
| run: go build ./... | |
| - name: Vet | |
| run: go vet ./... | |
| # A file gofmt would rewrite is a file the next editor's save rewrites, | |
| # and that diff lands in somebody else's pull request. Two files had | |
| # drifted by the time anybody looked (#372). | |
| - name: gofmt | |
| run: make fmt-check | |
| # THE PACKED CORPORA ARE GENERATED FROM THE FOLDERS. The manuals are | |
| # ignored build products because committing one shared binary made every | |
| # manual-bearing branch conflict; exercise that exact shipped source mode. | |
| # The remaining generated corpora are tracked, so a diff still means real | |
| # drift between their folders and archives. | |
| - name: The packed corpora build from their folders | |
| run: | | |
| set -euo pipefail | |
| go generate ./internal/manual | |
| go test -tags=aforge_packed_manual ./internal/manual | |
| if ! git diff --exit-code; then | |
| echo | |
| echo 'A tracked packed corpus disagrees with its source folder.' | |
| echo 'Run `make embed` and commit the regenerated archive.' | |
| exit 1 | |
| fi | |
| # THE CHANGELOG CARRIES WHAT A DIFF CANNOT — which of the things somebody | |
| # believes about this repository stopped being true. It is checked here | |
| # because it is worth nothing written later: the only moment the author | |
| # knows they contradicted a page is the moment they went and edited it. | |
| # | |
| # Two seconds, and the cost of complying is one file with three fields. | |
| # `kind: internal` with just a title is a legitimate entry; the | |
| # `no-changelog` label is the way out for a typo in a comment. | |
| # docs/rules/changelog.md is the rule. | |
| - name: The change is written down | |
| if: github.event_name == 'pull_request' && !contains(github.event.pull_request.labels.*.name, 'no-changelog') | |
| env: | |
| BASE: ${{ github.event.pull_request.base.sha }} | |
| PRNUM: ${{ github.event.pull_request.number }} | |
| run: | | |
| set -euo pipefail | |
| go run ./cmd/aforge-changes check | |
| if git diff --name-only --diff-filter=A "$BASE" HEAD \ | |
| -- 'docs/changes/unreleased/*.md' | grep -q .; then | |
| echo "This pull request adds a change entry." | |
| exit 0 | |
| fi | |
| cat <<'MSG' | |
| This pull request adds no entry in docs/changes/unreleased/. | |
| The changelog here is written for whoever reads the repository next, | |
| and increasingly that is a model with a fortnight-old memory of it. Its | |
| job is to say what somebody now believes WRONGLY — the branch that | |
| stopped existing, the default that moved, the refusal that became a | |
| capability. No tool can derive that from a diff, which is why it is | |
| asked for here. | |
| MSG | |
| # The number is spelled out so the command pastes unedited — a message | |
| # that can be pasted unedited is a message that gets pasted. | |
| echo " make changelog-new PR=${PRNUM} KIND=changed SLUG=<a-short-slug>" | |
| cat <<'MSG' | |
| Kinds: added changed renamed fixed removed internal. A one-line | |
| `kind: internal` entry is perfectly legitimate and takes ten seconds. | |
| If this really is a typo in a comment, add the `no-changelog` label. | |
| docs/rules/changelog.md says what belongs in the fields. | |
| MSG | |
| exit 1 | |
| # THE MANUAL LAW, ENFORCED HERE BECAUSE IT IS THE LAW AGENTS BREAK MOST. | |
| # A feature that ships without its page is a feature the running chat will | |
| # deny having. These are the three gates CLAUDE.md names — every slash | |
| # command and alias, every tool on the belt, every probe question still | |
| # reaching the page that answers it — and they cost six seconds. | |
| - name: The manual knows about the features | |
| run: | | |
| set -euo pipefail | |
| go test ./internal/manual/ | |
| go test -run 'Manual' ./internal/tui3/ ./internal/session/ | |
| # THE LAWS HOLD. Every test that reads the tree itself — the endings | |
| # ratchet, the guard, the taxonomy, the known-red ratchet, the words the | |
| # e2e suite waits for — found by what it does (scripts/laws.sh) and run as | |
| # one target, about twenty seconds after the link. This is the step that | |
| # would have stopped #279 at the door. | |
| # | |
| # TMPDIR IS SET ON THIS STEP AND NOT ON THE JOB. A test that binds a unix | |
| # socket under t.TempDir() overflows sun_path on the runner's default | |
| # path, so the tests get a short one; but set on the job it reaches | |
| # setup-go's own `go env`, which runs before any step has created the | |
| # directory, and the go command fails making its work dir. Two runs of | |
| # this very pull request died that way. | |
| - name: The laws hold | |
| env: | |
| TMPDIR: /tmp/aforge-ci | |
| run: | | |
| mkdir -p "$TMPDIR" | |
| make test-laws | |
| # THE PACKAGES THIS CHANGE TOUCHED, IN FULL. The laws catch a shape; this | |
| # catches a behaviour, in the one place a change can have broken it. It is a | |
| # job of its own so that the light gate's answer arrives in its few minutes | |
| # while this one takes as long as the slowest touched package — internal/tui3 | |
| # is about eight minutes on this runner — and the two run side by side rather | |
| # than one after the other. | |
| # | |
| # It reads the same ledger and the same timeout as `make test`, through | |
| # `make test`, so a red here is a red on a clean laptop too. AND IT BLOCKS. | |
| # It was neutral for a day and then, for a day, off pull requests altogether | |
| # (#499); in that day #523 merged red on cmd/aforge with `check` green, the | |
| # way #437, #439 and #483 had before the job existed. A red that reaches | |
| # nobody before the merge is the whole story of this file, so the owner's | |
| # ruling is that this runs on every pull request and `check` needs it. | |
| # THE JOB'S OWN CEILING SITS ABOVE THE GO ONE. `make test` gives each | |
| # package fifteen minutes so a hang panics with the package's name; a job | |
| # killed by GitHub first names nothing. A wide change can touch the three | |
| # heaviest packages at once, so this is three of those with room. | |
| # | |
| # | |
| # A change with no Go file and no module file in it — docs, a bench record, | |
| # a manual page — runs nothing here and is green in a minute. A change to | |
| # go.mod or go.sum touches every package and runs the whole tree. | |
| touched: | |
| name: touched packages | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 60 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - uses: actions/setup-go@v5 | |
| with: | |
| go-version-file: go.mod | |
| cache: true | |
| - name: Test the packages this change touched | |
| env: | |
| # A pull request diffs against its base; a push against what the | |
| # branch was before it. A first push has no before, and diffs one | |
| # commit. | |
| BASE: ${{ github.event.pull_request.base.sha || github.event.before }} | |
| TMPDIR: /tmp/aforge-ci | |
| run: | | |
| set -euo pipefail | |
| mkdir -p "$TMPDIR" | |
| case "${BASE:-}" in ''|0000000000000000000000000000000000000000) BASE="$(git rev-parse HEAD~1)";; esac | |
| changed="$(git diff --name-only "$BASE" HEAD -- '*.go' go.mod go.sum)" | |
| if printf '%s\n' "$changed" | grep -qxE 'go\.(mod|sum)'; then | |
| # A module change reaches every package; the tree is the touched set. | |
| pkgs="./..." | |
| else | |
| # `|| true`: a change with no Go file makes this grep exit 1, and | |
| # under pipefail that would turn a docs pull request red. | |
| dirs="$(printf '%s\n' "$changed" | { grep '\.go$' || true; } | xargs -r -n1 dirname | sort -u)" | |
| pkgs="" | |
| for dir in $dirs; do | |
| # A directory the change emptied has no package left to test. | |
| if ls "$dir"/*.go >/dev/null 2>&1; then pkgs="$pkgs ./$dir"; fi | |
| done | |
| fi | |
| if [ -z "$pkgs" ]; then | |
| echo 'No Go file and no module file changed; nothing to run.' | |
| exit 0 | |
| fi | |
| echo "touched:$pkgs" | |
| # -p 1 for the reason ci-full.yml gives: this runner has been shut | |
| # down under the link load of the heavy binaries twice, and a change | |
| # touching tui3, session and cmd/aforge links all three at once. | |
| # -count=1 so a cached pass never stands in for a run. | |
| make test TEST_FLAGS='-count=1 -p 1' PKGS="$pkgs" | |
| # ONE NAME FOR THE TWO. The name of this job is the name of the required | |
| # status check in .github/rulesets/dev.json and the name every landing script | |
| # and every person reads. Renaming it silently un-protects the branch, so the | |
| # two move together or not at all. It is green only when the light gate and | |
| # the touched packages both are — the same shape `full tests` and `cross | |
| # build` use in ci-full.yml for a matrix a rule cannot spell. | |
| check: | |
| name: check | |
| needs: [light, touched] | |
| if: always() | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Both halves passed | |
| run: | | |
| set -euo pipefail | |
| echo "light gate: ${{ needs.light.result }}; touched packages: ${{ needs.touched.result }}" | |
| [ "${{ needs.light.result }}" = "success" ] && [ "${{ needs.touched.result }}" = "success" ] |