Skip to content

tasks: a row says its state once, an abandoned call is said in whole … #2438

tasks: a row says its state once, an abandoned call is said in whole …

tasks: a row says its state once, an abandoned call is said in whole … #2438

Workflow file for this run

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" ]