PR CI acceleration, SPEC-009, and the repairs of #756 and #757 #432
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: ci | |
| # THE CI OF ONE COMMIT, IN STAGES. The design and its measurements are in | |
| # .agents/docs/2026-10-02-pr-ci-acceleration-and-the-toolchain-specification-design.md | |
| # (Part II); the rules it cites are R1-R7 there. | |
| # | |
| # changes what the commit changed decides what runs (R2). A change of | |
| # documentation that nothing reads runs `docs` and nothing else. | |
| # docs the checks that need no binary, on every change. | |
| # build-* this commit's mcpp, built ONCE per host (R1) by build.yml and | |
| # uploaded as `mcpp-built-<host>`. | |
| # the rest the per-area workflows, called as reusable workflows. Each waits | |
| # for its own host's build only, takes the binary with | |
| # .github/actions/use-built-mcpp, and keeps the job names it had as | |
| # a workflow of its own. | |
| # e2e-coverage every e2e test ran somewhere or says why not (R5). | |
| # | |
| # Measured before this shape (2026-10-01, seven commits): 35 to 47 jobs started | |
| # at once against the organisation's 20 slots, 30 to 37 of them built mcpp | |
| # from source, and a commit waited 36 to 50 minutes, 9 to 13 of them in the | |
| # queue. | |
| on: | |
| push: | |
| branches: [ main ] | |
| pull_request: | |
| branches: [ main ] | |
| workflow_dispatch: | |
| # A superseded pull-request run is cancelled. Every push to main runs to the | |
| # end, in a group of its own commit: its build jobs are the only writers of the | |
| # caches (R3), and a cancelled run saves nothing. A group shared by all pushes | |
| # to main would still lose runs, because GitHub keeps one pending run per group | |
| # and cancels the older pending one when a third arrives. | |
| concurrency: | |
| group: ci-${{ github.event_name == 'push' && github.sha || github.ref }} | |
| cancel-in-progress: ${{ github.event_name == 'pull_request' }} | |
| permissions: | |
| contents: read | |
| # `changes` lists the files of the pull request. | |
| pull-requests: read | |
| jobs: | |
| changes: | |
| name: what the change starts | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 5 | |
| outputs: | |
| code: ${{ steps.classify.outputs.code }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Classify the changed paths | |
| id: classify | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| EVENT: ${{ github.event_name }} | |
| REPO: ${{ github.repository }} | |
| PR: ${{ github.event.pull_request.number }} | |
| BEFORE: ${{ github.event.before }} | |
| SHA: ${{ github.sha }} | |
| run: | | |
| set -euo pipefail | |
| full() { echo "code=true" >> "$GITHUB_OUTPUT"; echo "code=true ($1)"; } | |
| # Anything this step cannot list runs the whole CI: an API error, a | |
| # force-push whose previous commit is gone, an event without a diff. | |
| case "$EVENT" in | |
| pull_request) | |
| gh api --paginate "repos/$REPO/pulls/$PR/files" \ | |
| --jq '.[] | .filename, (.previous_filename // empty)' > changed.txt \ | |
| || { full "the files of the pull request could not be listed"; exit 0; } ;; | |
| push) | |
| if [ -z "$BEFORE" ] || [ "$BEFORE" = 0000000000000000000000000000000000000000 ]; then | |
| full "a push with no previous commit"; exit 0 | |
| fi | |
| gh api "repos/$REPO/compare/$BEFORE...$SHA" > compare.json \ | |
| || { full "the push could not be compared with its previous commit"; exit 0; } | |
| # The compare API lists at most 300 files; a longer list is | |
| # treated as a change of everything. | |
| if [ "$(jq '.files | length' compare.json)" -ge 300 ]; then | |
| full "300 or more changed files"; exit 0 | |
| fi | |
| jq -r '.files[] | .filename, (.previous_filename // empty)' compare.json > changed.txt ;; | |
| *) | |
| full "event $EVENT"; exit 0 ;; | |
| esac | |
| python3 .github/tools/classify_changes.py --github-output "$GITHUB_OUTPUT" < changed.txt | |
| docs: | |
| name: documentation and repository checks | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 15 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Check version / xlings pin consistency | |
| run: bash .github/tools/check_version_pins.sh | |
| - name: Check modules/ wiring | |
| run: bash .github/tools/check_modules_wiring.sh | |
| - name: Check src/build/prepare* file lengths | |
| run: bash .github/tools/check_file_lengths.sh | |
| - name: Check no walk-derived path is narrowed directly | |
| run: bash .github/tools/check_narrow_conversions.sh | |
| - name: Where the CI assertions live | |
| run: bash tools/lint-ci-assertions.sh | |
| - name: Steps assert what their names say | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| python3 tests/scripts/test_check_workflow_assertions.py | |
| python3 .github/tools/check_workflow_assertions.py --check-open | |
| - name: The release canary runner runs each command under the named bash | |
| run: python3 tests/scripts/test_release_canaries.py | |
| - name: The protocol table of SPEC-007 names the engine's protocol | |
| run: python3 tests/scripts/test_protocol_table.py | |
| - name: The target matrix names every refusal | |
| run: bash .github/tools/check_matrix_reasons.sh | |
| - name: Check documentation style and bilingual parity | |
| run: bash .github/tools/check_docs_style.sh | |
| - name: Check documentation structure | |
| run: bash .github/tools/check_docs_structure.sh | |
| - name: Documented target tiers agree with the table | |
| run: python3 .github/tools/check_target_tiers.py | |
| - name: Reason tokens agree with the engine, and with the mirror | |
| run: bash .github/tools/check_reason_tokens.sh | |
| - name: The tools of the CI stages pass their fixtures | |
| run: | | |
| python3 tests/scripts/test_classify_changes.py | |
| python3 tests/scripts/test_check_e2e_coverage.py | |
| python3 tests/scripts/test_check_default_toolchain_docs.py | |
| build-linux: | |
| needs: changes | |
| if: needs.changes.outputs.code == 'true' | |
| uses: ./.github/workflows/build.yml | |
| with: | |
| host: linux-x86_64 | |
| runs-on: ubuntu-24.04 | |
| # What the Linux consumers install beyond the build's own toolchain: the | |
| # e2e shards (musl, llvm, mingw-cross), the toolchain legs (musl, llvm). | |
| prewarm: gcc 16.1.0-musl; llvm 22.1.8; mingw-cross 16.1.0 | |
| build-linux-arm: | |
| needs: changes | |
| if: needs.changes.outputs.code == 'true' | |
| uses: ./.github/workflows/build.yml | |
| with: | |
| host: linux-aarch64 | |
| runs-on: ubuntu-24.04-arm | |
| build-macos: | |
| needs: changes | |
| if: needs.changes.outputs.code == 'true' | |
| uses: ./.github/workflows/build.yml | |
| with: | |
| host: macos-arm64 | |
| runs-on: macos-15 | |
| build-windows: | |
| needs: changes | |
| if: needs.changes.outputs.code == 'true' | |
| uses: ./.github/workflows/build.yml | |
| with: | |
| host: windows-x86_64 | |
| runs-on: windows-latest | |
| # The MinGW payload every Windows e2e shard installs. | |
| prewarm: mingw 16.1.0 | |
| linux: | |
| needs: build-linux | |
| uses: ./.github/workflows/ci-linux.yml | |
| linux-e2e: | |
| needs: build-linux | |
| uses: ./.github/workflows/ci-linux-e2e.yml | |
| macos: | |
| needs: build-macos | |
| uses: ./.github/workflows/ci-macos.yml | |
| with: | |
| # R7: the known-red legs (#669) run where they can change a decision: on | |
| # main, on dispatch, and on a pull request labelled `macos-27`. The label | |
| # is read when the pull request is pushed to; adding it starts no run, | |
| # because a run on every label of every pull request would cost a whole | |
| # CI each time. | |
| known-red: ${{ github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'macos-27') }} | |
| macos-e2e: | |
| needs: build-macos | |
| uses: ./.github/workflows/ci-macos-e2e.yml | |
| with: | |
| known-red: ${{ github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'macos-27') }} | |
| macos-ios: | |
| needs: build-macos | |
| uses: ./.github/workflows/ci-macos-ios.yml | |
| windows: | |
| needs: build-windows | |
| uses: ./.github/workflows/ci-windows.yml | |
| windows-e2e: | |
| needs: build-windows | |
| uses: ./.github/workflows/ci-windows-e2e.yml | |
| windows-msvc-xlings: | |
| needs: build-windows | |
| uses: ./.github/workflows/ci-windows-msvc-xlings.yml | |
| cross: | |
| needs: [build-linux, build-windows] | |
| uses: ./.github/workflows/cross-build-test.yml | |
| target-matrix: | |
| needs: [build-linux, build-linux-arm, build-macos, build-windows] | |
| uses: ./.github/workflows/ci-target-matrix.yml | |
| # Pull requests and dispatch only, as when it was a workflow of its own. | |
| openkal: | |
| needs: [build-linux, build-macos, build-windows] | |
| if: github.event_name != 'push' | |
| uses: ./.github/workflows/openkal-cross.yml | |
| e2e-coverage: | |
| name: every e2e test runs somewhere | |
| needs: [changes, linux-e2e, windows-e2e, macos-e2e] | |
| if: ${{ always() && needs.changes.outputs.code == 'true' && !cancelled() }} | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 10 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Fetch the shards' reports | |
| uses: actions/download-artifact@v4 | |
| with: | |
| pattern: e2e-report-* | |
| path: reports | |
| merge-multiple: true | |
| - name: Every test ran, is run by a dedicated job, or is excused | |
| run: python3 .github/tools/check_e2e_coverage.py --reports reports --timings-out timings | |
| # The measured durations, merged per host, in the format of | |
| # tests/e2e/timings/; refreshing those tables is copying these files. | |
| - name: Upload the measured durations | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: e2e-timings | |
| path: timings/ | |
| if-no-files-found: ignore | |
| retention-days: 14 |