Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 49 additions & 12 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,19 @@ env:
OPENBLAS_NUM_THREADS: "1"
MKL_NUM_THREADS: "1"

# GitHub runs an explicit `shell: bash` as `bash -eo pipefail`, and a step
# with no shell as `bash -e`, where a pipe hides the exit status of the command
# that feeds it.
defaults:
run:
shell: bash

# Every job sets timeout-minutes (GitHub's default is 360) so a hung test
# frees its runner from the org's shared 60-runner pool. The engine-free and
# engine-us caps are about 1.5x their slowest successful leg on 2026-10-07..08;
# the engine-uk cap is about twice its slowest successful leg after the
# policyengine-uk 2.162.3 bump (29 minutes, microcosm#1400); shorter jobs get
# at least 5 minutes and at least twice their slowest run.
# frees its runner from the org's shared 60-runner pool. Each cap follows the
# job's slowest successful leg on 2026-11-09, rounded up to a multiple of 5
# minutes: 1.5 times it for engine-free (109 minutes) and engine-us (73), twice
# for engine-uk (45) and integration-uk (8), and for each shorter job at least
# 5 minutes and at least twice its slowest run.

jobs:
select-countries:
Expand Down Expand Up @@ -55,14 +62,14 @@ jobs:
run: uv run python tools/graph_acceptance_burndown.py --verify
- name: Sync workspace
run: uv sync --all-packages --locked
- name: Verify the workflow against the test plan
run: uv run --no-sync python tools/ci_test_plan.py verify-workflow
- name: Lint
run: uv run --no-sync ruff check .

engine-free:
runs-on: ubuntu-latest
# Successful legs took 64-74 min on 2026-10-26, and a Python 3.14 leg on
# microcosm#1404 hit the old 75-minute cap; 120 is about 1.6x the slowest.
timeout-minutes: 120
timeout-minutes: 165
strategy:
fail-fast: false
matrix:
Expand All @@ -86,8 +93,7 @@ jobs:
needs: select-countries
if: needs['select-countries'].outputs.run_us == 'true'
runs-on: ubuntu-latest
# Successful legs took 40-60 min on 2026-10-07..08 (n=23).
timeout-minutes: 90
timeout-minutes: 110
strategy:
fail-fast: false
matrix:
Expand Down Expand Up @@ -118,7 +124,7 @@ jobs:
needs: select-countries
if: needs['select-countries'].outputs.run_uk == 'true'
runs-on: ubuntu-latest
timeout-minutes: 60
timeout-minutes: 90
strategy:
fail-fast: false
matrix:
Expand All @@ -139,7 +145,7 @@ jobs:
permissions:
contents: read
runs-on: ubuntu-latest
timeout-minutes: 15
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
with:
Expand Down Expand Up @@ -185,3 +191,34 @@ jobs:
run: uv sync --all-packages --locked
- name: Run installed-emitter tests
run: bash tools/run_installed_emitter_tests.sh

ci:
# ci gives the main ruleset one check that covers the country jobs. It
# fails unless every other job passed, or skipped because select-countries
# succeeded and reported `false` for its country. A skipped matrix job
# reports under its unexpanded name, so a ruleset cannot require engine-us
# or engine-uk by name. GitHub reports a skipped required check as passing,
# so ci runs with `if: always()`. verify-workflow refuses any other shape
# of this job, and the plan's tests, which engine-free runs, fail on one.
if: always()
needs:
- select-countries
- lint
- engine-free
- engine-us
- engine-uk
- integration-uk
- wheels
- installed-emitter
permissions:
contents: read
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Require every job to pass, or the selection to leave it out
env:
NEEDS: ${{ toJSON(needs) }}
run: python3 tools/ci_test_plan.py require-results
1 change: 1 addition & 0 deletions changelog.d/ci-aggregate-check.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add a `ci` workflow job that gives the `main` ruleset one check covering the country jobs. It fails unless every other job passed, or skipped because `select-countries` succeeded and reported `false` for its country. The lint job runs a new `tools/ci_test_plan.py verify-workflow` command, which parses the workflow and refuses a `ci` job that could skip, omits a job from `needs` or does anything but require the other jobs' results.
1 change: 1 addition & 0 deletions changelog.d/ci-job-timeouts-2026-11.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Raise four job timeouts to at least 1.5 times each job's slowest successful leg on 2026-11-09: engine-free from 120 to 165 minutes (its slowest leg took 109), engine-us from 90 to 110 (73), engine-uk from 60 to 90 (45) and integration-uk from 15 to 20 (8).
1 change: 1 addition & 0 deletions changelog.d/ci-selection-listing-limit.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Select both countries' engine jobs for a pull request whose file listing reaches GitHub's 3,000-file limit, where the listing stops.
56 changes: 52 additions & 4 deletions docs/agent-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,55 @@ or `uv sync --all-packages --all-extras`. To keep both countries set up, point

PR CI (`.github/workflows/test.yml`) has `lint`, `engine-free`, `engine-us`,
`engine-uk`, `integration-uk`, `wheels`, and `installed-emitter` jobs, plus the
`select-countries` orchestration job.
`select-countries` orchestration job and the `ci` job.

`ci` gives the `main` ruleset one check that covers the country jobs. It fails
unless every other job passed, or skipped because `select-countries` succeeded
and reported `false` for its country. A skipped matrix job reports under its
unexpanded name, so a ruleset cannot require `engine-us` or `engine-uk` by
name. The ruleset also names each other check that runs on every pull
request: `lint`, `wheels`, `select-countries`, `installed-emitter` and both
`engine-free` legs. When `engine-free`'s matrix changes, change the ruleset
too, because a leg's check name holds its Python version.

The workflow runs on a pull request only when it targets `main`. Retargeting a
pull request to `main` starts no run, because GitHub reports a base change as
an `edited` event, which the workflow doesn't run on. After a stacked pull
request retargets, push a commit or close and reopen it, then merge once its
checks pass.

The plan names the jobs the selection may leave out: each job whose test
groups all need the same country's engine. The lint job runs
`tools/ci_test_plan.py verify-workflow`, which parses the workflow with PyYAML
and refuses:

- two keys that PyYAML reads as one, a merge key (`<<`), and U+0085, U+2028
or U+2029, which YAML 1.1 reads as line breaks and YAML 1.2 doesn't;
- a job outside the plan, a plan job the workflow lacks, and a job whose `if`
differs from the one the plan gives it;
- a job the selection may leave out that doesn't need `select-countries`, and
a job that needs one the selection may leave out, unless both follow the
same country's selection;
- a `ci` job that lacks `if: always()`, omits a job from `needs`, or does
anything but check out the repository and run `require-results` on its
`needs` context;
- a `select-countries` job that doesn't publish the outputs its `plan` step
writes;
- `continue-on-error` on any job or step, a step `if` other than `always()`,
`success()` or `!cancelled()`, and a job-level `name`;
- a workflow `defaults` other than `run: shell: bash`, a job-level `defaults`
and a step `shell`. GitHub runs an explicit `shell: bash` as
`bash -eo pipefail`, so a pipe cannot hide a failed command.

The plan's tests in `test_ci_test_plan.py` run the same checks and test the
code `ci` runs. The ruleset reads `engine-free` without `ci`, so those tests
stay in that job, and `tools/ci_test_plan.py verify` refuses a plan that moves
them out. After editing the workflow, run
`uv run python tools/ci_test_plan.py verify-workflow`. A pull request's run
takes the workflow and the plan from the pull request's own merge with `main`,
so a pull request that rewrites them can pass itself, and review has to catch
that.

`tools/ci_test_plan.py` is the only authority for test-directory ownership, CI
job assignment, country ownership, US timing-report categories, and
changed-path country selection. Its `TEST_GROUPS` registry defines every valid
Expand Down Expand Up @@ -84,9 +132,9 @@ test in a directory registered by `TEST_GROUPS` and must run through that
category's existing workflow job. Supporting programs and data may live under
`tools/`, but they do not receive separate workflow jobs. The test-plan verifier
allows only registered test jobs and the explicitly declared orchestration,
lint, and wheel-building jobs. Add a test category to `TEST_GROUPS` before its
executor; do not create a separate selection system, test workflow, or
single-purpose behavioral test job.
lint, wheel-building, and aggregate-result jobs. Add a test category to
`TEST_GROUPS` before its executor; do not create a separate selection system,
test workflow, or single-purpose behavioral test job.

The Orrery parser compatibility assertion lives in
`packages/microcosm-graph/tests/engine_free/shared/test_graph_orrery.py`. The
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ def test_integration_job_is_uk_selected_and_read_only() -> None:

assert "needs: select-countries" in integration
assert "if: needs['select-countries'].outputs.run_uk == 'true'" in integration
assert "timeout-minutes: 15" in integration
assert "timeout-minutes: 20" in integration
assert "permissions:\n contents: read" in integration
assert "persist-credentials: false" in integration
assert "environment:" not in integration
Expand Down
Loading
Loading