Repository navigation
Full check #92
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: Full check | |
| # THE HEAVY GATE SITS ON BOTH PROMOTION POINTERS. Work reaches `dev` fast and | |
| # lightly checked, which is the right trade while people are building; the | |
| # price is paid before a commit reaches `staging` or the release branch `main`. | |
| # | |
| # It also runs nightly against `dev`, because the failure this repository | |
| # actually suffers is not "the pull request was bad" but "the merge of two good | |
| # pull requests was bad", and nobody is watching at the moment of the merge. | |
| # A nightly run names the day something started being broken. | |
| # | |
| # docs/rules/ci.md says which of these are required to merge and what has to be | |
| # true before the rest join them. | |
| on: | |
| pull_request: | |
| branches: | |
| - staging | |
| - main | |
| push: | |
| branches: | |
| - staging | |
| - main | |
| schedule: | |
| # 09:00 UTC, which is the small hours in California and the morning in India | |
| # — a result waiting on both desks rather than a run nobody sees finish. | |
| - cron: "0 9 * * *" | |
| workflow_call: | |
| inputs: | |
| ref: | |
| required: true | |
| type: string | |
| workflow_dispatch: | |
| inputs: | |
| ref: | |
| description: "commit to check; empty = the branch you dispatch on" | |
| required: false | |
| type: string | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: full-check-${{ inputs.ref || github.ref }} | |
| cancel-in-progress: false | |
| jobs: | |
| # THE WHOLE SUITE, MINUS WHAT IS ALREADY OWED. .github/known-red.txt lists the | |
| # tests that fail on a clean tree; they are skipped here so that red in this | |
| # job means this change caused it. The ledger is the debt and shrinking it is | |
| # the work — see the file's own header. | |
| # | |
| # IN THREE SHARDS, BECAUSE THE MACHINE IS SMALL AND THE BINARIES ARE NOT. A | |
| # free-plan private repository gets a two-core, seven-gigabyte runner, and this | |
| # repository's test binaries are heavy — internal/tui3's links most of the | |
| # product. The suite's first run died of exactly that: every package result in | |
| # by seven minutes except the eight link-heaviest, then thirteen silent | |
| # minutes, then the runner itself was shut down under it. Test time was never | |
| # the problem — 483 seconds across 82 packages — the peak was. Three machines | |
| # holding a third of the load each stay comfortably inside their memory, and | |
| # the suite's wall clock becomes compile-plus-a-few-minutes. | |
| # | |
| # The split is round-robin over `go list` order, computed at run time. Not a | |
| # hand-kept list of packages per shard: a list would go stale with the next | |
| # package added, and stale here means silently untested. | |
| # THE JOB'S CEILING SITS ABOVE THE GO ONE. Each package gets `make test`'s | |
| # fifteen minutes so that a hang panics with the package's name; a job | |
| # GitHub kills first names nothing. A shard is about thirty packages run | |
| # one at a time, most in seconds, and the whole of it has come in under | |
| # ten minutes; sixty leaves a hung package its full fifteen and the rest | |
| # of the shard theirs. | |
| full-tests-shard: | |
| name: full tests ${{ matrix.shard }}/3 | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 60 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| shard: [0, 1, 2] | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ inputs.ref }} | |
| - uses: actions/setup-go@v5 | |
| with: | |
| go-version-file: go.mod | |
| cache: true | |
| # A short TMPDIR, because at least one test builds a unix socket under it | |
| # and the default runner path is long enough to overflow the 104-byte | |
| # sun_path limit. Cheaper to give it room than to skip the test. | |
| # | |
| # THE SKIP LIST AND THE TIMEOUT ARE THE MAKEFILE'S, through `make test`, | |
| # so this run and a laptop's `make check` are one proof. The old eight | |
| # minutes here was a guess, and internal/tui3 takes about 485 seconds on | |
| # this runner: the 2026-09-02 nightly cut it off at the finish line and | |
| # reported a test that had been running for two seconds as a hang (#372). | |
| # The Makefile's TEST_TIMEOUT is measured with headroom instead. | |
| - name: Test | |
| env: | |
| TMPDIR: /tmp/codeaf-ci | |
| SHARD: ${{ matrix.shard }} | |
| run: | | |
| set -euo pipefail | |
| mkdir -p "$TMPDIR" | |
| pkgs="$(go list ./... | awk -v s="$SHARD" 'NR % 3 == s' | paste -sd' ' -)" | |
| echo "shard ${SHARD}: $(echo "$pkgs" | wc -w) packages" | |
| # -p 1: ONE PACKAGE AT A TIME WITHIN A SHARD. The second full run died | |
| # the same way as the first — a runner shutdown with slow packages in | |
| # flight and no Go timeout — after sharding had already cut each | |
| # machine's load to a third. Halving the peak again by serialising is | |
| # the experiment that settles whether the killer is memory: if a shard | |
| # still dies at -p 1, it is not, and the package that was running is | |
| # the suspect by name. The shards keep the wall clock acceptable | |
| # while each one runs serial. | |
| make test TEST_FLAGS='-p 1' PKGS="$pkgs" | |
| # ONE NAME FOR THE THREE, for the same reason cross build has one below: a | |
| # branch rule can only require a name it can spell, and a matrix's legs are | |
| # not spellable in advance. | |
| full-tests: | |
| name: full tests | |
| needs: [full-tests-shard] | |
| if: always() | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Every shard passed | |
| run: | | |
| set -euo pipefail | |
| result='${{ needs.full-tests-shard.result }}' | |
| echo "full tests shards: ${result}" | |
| [ "$result" = "success" ] | |
| # EVERY PLATFORM THE PRODUCT SHIPS ON, COMPILED. This is the one gate here | |
| # whose answer is identical on every machine and every run, so it is the one | |
| # that blocks a promotion. A build tag or a syscall that only exists on Linux | |
| # is invisible on the laptop that wrote it and obvious here. | |
| cross-build-matrix: | |
| # One check per target, so a red one names the platform it broke on. | |
| name: cross build (${{ matrix.goos }}/${{ matrix.goarch }}) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - { goos: darwin, goarch: amd64 } | |
| - { goos: darwin, goarch: arm64 } | |
| - { goos: linux, goarch: amd64 } | |
| - { goos: linux, goarch: arm64 } | |
| - { goos: windows, goarch: amd64 } | |
| - { goos: windows, goarch: arm64 } | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ inputs.ref }} | |
| - uses: actions/setup-go@v5 | |
| with: | |
| go-version-file: go.mod | |
| cache: true | |
| - name: Build | |
| env: | |
| CGO_ENABLED: "0" | |
| GOOS: ${{ matrix.goos }} | |
| GOARCH: ${{ matrix.goarch }} | |
| run: go build ./... | |
| # ONE NAME FOR THE SIX. A matrix publishes its checks as `cross build (linux, | |
| # arm64)` and so on, and a branch rule can only require a name it can spell — | |
| # adding a target would silently leave the new one unrequired. This job is the | |
| # spellable name: it is green only when every leg of the matrix was. | |
| cross-build: | |
| name: cross build | |
| needs: [cross-build-matrix] | |
| if: always() | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Every target built | |
| run: | | |
| set -euo pipefail | |
| result='${{ needs.cross-build-matrix.result }}' | |
| echo "cross build matrix: ${result}" | |
| [ "$result" = "success" ] | |
| # TWO MACHINES, ACTUALLY TWO — three containers sharing no path, no home and | |
| # no credential, so a session that only works because both halves happen to be | |
| # one filesystem fails here. The Makefile calls this a nightly or pre-release | |
| # gate rather than an every-change one, and this is that nightly. | |
| remote: | |
| name: remote | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 25 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ inputs.ref }} | |
| - uses: actions/setup-go@v5 | |
| with: | |
| go-version-file: go.mod | |
| cache: true | |
| - name: Two machines | |
| run: make test-remote | |
| # A RED NIGHTLY IS A PAGE, NOT A LOG LINE. Every nightly this repository had | |
| # run before 2026-09-02 was red, and nobody knew, because a scheduled run | |
| # reports to nobody — there is no pull request to decorate and no push to | |
| # notify. So a red night opens one issue, or adds the night's run to the one | |
| # already open, and the issue is what somebody sees in the morning. Green | |
| # nights say nothing; the issue is closed by the person who fixed it. | |
| page: | |
| name: page on a red nightly | |
| needs: [full-tests, cross-build, remote] | |
| # A cancelled job — the runner shutdown this file describes above — is | |
| # not a failure to GitHub, and it is exactly the night to be told about. | |
| if: always() && !inputs.ref && github.event_name == 'schedule' && (contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')) | |
| runs-on: ubuntu-latest | |
| permissions: | |
| issues: write | |
| steps: | |
| - name: Open or update the issue | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| GH_REPO: ${{ github.repository }} | |
| RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} | |
| SHA: ${{ github.sha }} | |
| RESULTS: "full tests: ${{ needs.full-tests.result }}, cross build: ${{ needs.cross-build.result }}, remote: ${{ needs.remote.result }}" | |
| run: | | |
| set -euo pipefail | |
| title='ci: the nightly full check is red on dev' | |
| body="$(printf 'The nightly full check failed on `%s`.\n\n%s\n\nRun: %s\n\nThis issue is opened by ci-full.yml and closed by whoever makes the nightly green. docs/rules/ci.md says what runs there.' "${SHA:0:8}" "$RESULTS" "$RUN_URL")" | |
| number="$(gh issue list --state open --search "\"$title\" in:title" --json number,title --jq "map(select(.title == \"$title\")) | .[0].number // empty")" | |
| if [ -n "$number" ]; then | |
| gh issue comment "$number" --body "$body" | |
| else | |
| gh issue create --title "$title" --label bug --body "$body" | |
| fi | |
| # THE BINARY'S WEIGHT, REPORTED AND NOT ENFORCED — YET. SIZE-BUDGET is set on | |
| # darwin/amd64, the heaviest shipped platform (PERF.md, the size ratchet), and | |
| # this runner is linux/amd64, about a megabyte lighter, so a pass here does not | |
| # prove the cap holds where it is measured. It prints both so the drift is | |
| # visible; making it block means weighing darwin/amd64 here as well. | |
| size: | |
| name: size (informational) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| continue-on-error: true | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ inputs.ref }} | |
| - uses: actions/setup-go@v5 | |
| with: | |
| go-version-file: go.mod | |
| cache: true | |
| - name: Weigh the binary | |
| run: | | |
| set -euo pipefail | |
| make build | |
| actual="$(stat -c%s bin/codeaf)" | |
| budget="$(cat SIZE-BUDGET)" | |
| printf 'bin/codeaf on linux/amd64: %s bytes. SIZE-BUDGET (set on darwin/amd64): %s.\n' \ | |
| "$actual" "$budget" |