Repository navigation
CI's tests and tests-no-submodule jobs take about an hour, so every landing and every fix push waits an hour: split the shell suite, shard the rest, and give changes that touch only docs or specs a short path #300
Description
Filed by lane openRepoTools-3 on Brett Heap's word of 2026-10-21T13:4xZ: "yes, file the CI speed issue in v1.2.0".
What it costs
Every landing waits for CI's tests and tests-no-submodule jobs, and they take about an hour.
- Every fix push costs another hour, and so does every re-test when main moves under a waiting pull request.
- The other checks are quick:
packageabout 2 minutes,guard-launch-modeunder 30 s, SonarCloud under a minute.
Recent runs, all on ubuntu-24.04 except the last:
| run | what | tests |
tests-no-submodule |
|---|---|---|---|
| 39593550301 | #296's pull_request run | 61 min | 56 min |
| 39595166877 | #293's queue prefix | 54 min | 57 min |
| 39514276103 | #282 at 15c8e3f |
59 min | 60 min |
| 39544671103 | the nightly at a42946e, on 26.04 |
80 min | 74 min |
On 2026-10-21 two pull requests changed no code, yet each waited the full hour:
- The v1.1.0 notes now say that the fixes for #255 and #280 are part of 1.1.0 and list, by area and from the squash titles of aaa7bc8..a42946e, what else changed on main since v1.0.0 (#228) #293 changed only the release notes;
- T010's assembly exact-pin job runs on ubuntu-24.04 like the code leg's jobs under #296, and the Gate B documents name that runner and the patched tests.yml's job lines as #296 leaves them (part of #186) #299 changed only
specs/004-migrate-to-triad/.
Where the hour goes
The tests job of run 39593550301 reported "2934 passed, 75 skipped in 3663.41s (1:01:03)".
One test is 40% of the job. That test runs tests/test_lane_helpers.sh, and its own clock (the table from #261) says 1469 s, about 24.5 minutes. Its longest sections are:
- Amendment 18 Addendum 1, at 277 s (18%);
- Amendment 14, at 118 s;
- Amendment 17, at 117 s;
- the Amendment 11 hotfix, at 114 s;
- Amendment 11, at 93 s.
The other ~2,930 tests take the remaining ~36 minutes, on one 4-vCPU runner.
tests-no-submodule repeats most of the same work: 1974 passed and 959 skipped at a42946e.
What to do
- Split the shell suite. Its sections are independent enough to have their own clocks, so run them as separate pytest cases or separate jobs, and the 24.5 minutes run in parallel.
- CI's Linux test job takes 46–72 minutes a run, because its Python tests and the shell suite run one after the other #77 tracks the shell suite's duration, and its step 1a (The shell suite times each of its sections and its exit-trap teardown, and prints both in its transcript and in CI's step summary (#77, step 1a) #261) added the per-section clock this needs.
- Shard the rest of the pytest run across parallel jobs, split by file and balanced on recorded durations (for example
pytest --durations=0from a full run, orpytest-split).- Prefer separate jobs to
pytest-xdiston one runner. Several tests are timing-sensitive under load, such as the[zombie]case's 30 s limit (Every register write goes through its lane's own checkout <W>-lanes/<lane>/, where lane-start and lane-end read the lane's row; a LANES_REPO naming W refuses; locks name their holder's container; refresh-workspace fast-forwards W (Amendment 23) (#198) #282 6314548563), and separate runners do not load each other. - This repository is public, so standard runners cost nothing. A matrix of shards costs only runner concurrency.
- Prefer separate jobs to
- A short path for changes that touch only docs or specs.
- When a pull request changes only
docs/,specs/oropenspec/(and nothing undertests/,.github/,src/or a shipped script), thetestsjobs run only-k repo_hygiene, the tests that read those files. - The classifier needs its own test, so that a path can never be wrongly called docs-only.
- The jobs must still report success under their usual names, not skip, for the reasons below.
- When a pull request changes only
- Measure before and after.
- Publish the per-test durations once.
- Re-run the sharded suite several times, and check that it adds no flakes.
- The target is the
testswall time under about 20 minutes.
Constraints
- Keep the check names.
bin/land-ort3.shand the other lanes' landing scripts requiretests,tests-no-submodule,guard-launch-mode,packageand SonarCloud to pass, by name.release.yml's gate readstests.yml's job ids too.- Shards need an aggregate job under the old name, which
needs:every shard and fails if any shard fails. - Otherwise, the scripts and the release gate change in the same pull request.
- Shards need an aggregate job under the old name, which
- Do this after the cutover.
tests.ymlmoves toopensoft/openRepoTools-code, and lane openRepoTools-1's cutover appliesspecs/004-migrate-to-triad/patches/t010-code-leg-tests-workflow.patchto it. So this work lands there, after rehearsal D and the cutover, not before. tests/run.sh's lock is for workstations, where several lanes share one machine. In CI each job is its own runner, so the lock does not limit sharding there.- Merge-queue prefixes (a prefix branch,
workflow_dispatch, then landing on tree equality) keep working. A faster suite only shortens them.
Related
- CI's Linux test job takes 46–72 minutes a run, because its Python tests and the shell suite run one after the other #77: the shell suite's duration.
- The shell suite times each of its sections and its exit-trap teardown, and prints both in its transcript and in CI's step summary (#77, step 1a) #261: the per-section clock.
- CI and the release workflow run on ubuntu-24.04, because ubuntu-latest is now Ubuntu 26.04, where setup-python has no Python 3.9 and the TERM-ignoring kill test fails #296 and Ubuntu 26.04's uutils
timeout -knever sends its KILL to the process group, so a bounded command can wait out a child that survives TERM, and setup-python has no Python 3.9 there; CI runs on ubuntu-24.04 until both are handled #297: the runner pin, and Ubuntu 26.04.
Lane: openRepoTools-3
Activity
brettheap commentedon Oct 22, 2026
CLAIMED — lane openRepoTools-3, session 83619980-06bb-4469-ae73-2a8c5e61981a@Eagle, 2026-10-22T12:29:38Z, for #300
Logged in
lanes/log/openRepoTools-3.mdat9c15492ab1ccd1b3e127b55f4b80f1a7d6d1a180.The three reads (lane-collision-protocol Rule 1):
brettheap commentedon Oct 22, 2026
Lane openRepoTools-3 on #300, 2026-10-22T13:1xZ: a measured plan. CI is slow because the suite runs serially on one runner, not because of queueing or setup. Shards that keep the gate's check names cut a run from about 71 min to about 29 (step 1). Splitting the shell suite into chunks then gets it to about 11 (step 2). This is analysis only. Nothing was pushed, dispatched or re-run, and the build waits for the cutover and Brett's word.
Where the time goes. These are medians, with ranges in brackets, over the 15 all-green
tests.ymlruns onubuntu-24.04. They come from the 40 newest runs, 2026-10-21 14:59Z to 2026-10-22 12:21Z.teststests-no-submoduletestsjobs ran onubuntu-26.04. Every one came from afeat/198-*branch whosetests.ymlpredates CI and the release workflow run on ubuntu-24.04, because ubuntu-latest is now Ubuntu 26.04, where setup-python has no Python 3.9 and the TERM-ignoring kill test fails #296 and still saysubuntu-latest. All 22 failed, taking 86 min [65–103], so the table leaves them out. That is Ubuntu 26.04's uutilstimeout -knever sends its KILL to the process group, so a bounded command can wait out a child that survives TERM, and setup-python has no Python 3.9 there; CI runs on ubuntu-24.04 until both are handled #297's subject.test_supervised_restart_regressions.py, 661 s (16%; probably 700–750, because module times are split out of-qprogress lines);test_lanes_index.py, 267 s (6%);test_status_command.py, 146 s;The options.
testswall timepytest -n 4(xdist)tests-no-submoduleupstream/path, so this saves 21 runner-minutes per run, but narrows that job's claimdocs/,openspec/andspecs/move to the spec legpull_requestrunsWhat step 2 involves.
$E,$LOGD,$WS_Sand$CLONE2. They move to the preamble.test_lane_helpers_suite_timeout.pycallstest_the_lane_helper_suite_passes()with no arguments in six places, so that signature must stay callable.Step 1 as drafted. The patch is below: 4 files, +299/−17.
tests.yml.testsbecomes the matrix jobtests-shard, overshell,py-1,py-2andpy-3, withfail-fast: false.tests-no-submodulebecomestests-no-submodule-shardthe same way.python3 -m pytest tests -q(or-q -rs), withOPENREPOTOOLS_CI_SHARDnaming its shard.testsandtests-no-submoduleeachneeds:its shard job and runs underif: always(). Each is green only when the combined result issuccess. Thealways()is there because a job skipped by a failedneeds:would read as a passing required check.tests/ci_shards.pyholds the plan. Every test lands in exactly one shard: the first shard with a name that matches it, or elsepy-3. A new module is therefore never left out.tests/conftest.py.OPENREPOTOOLS_CI_SHARDinpytest_configure, so a pytest that a test starts never inherits a shard.--durations=25, the data for rebalancing.tests/test_tests_workflow.pychecks that:if:is the gatherers'always();success, and red forfailure,cancelled,skippedand an empty result;ci_shards.SHARDS;release.yml's gate (about lines 549–594) andbin/land-ort3.shreadtests,tests-no-submodule,guard-launch-modeandpackageby exact name. Queue mode also needs the run's conclusion to besuccess, and that requires every shard. A flaky shard re-runs alone, in 14–29 min instead of 70. The shard rows are extra checks. Never make them required, because they move with the plan.Verified locally. All of it ran one pytest process at a time, through
tests/run.sh, with every lane variable unset.actionlintis clean, and the pinnedrepo_shape.load_yamlparses the draft.test_tests_workflow.py(45 tests),test_repo_hygiene.py(237) andtest_test_roots.py(40).tests.ymlpatch. Applied to main'stests.yml(blob46feb68) in either order, the two patches give identical files, and both checks pass on the result.needs.<job>.resultover a matrix, and how a skipped required check reads.The order of work.
tests.ymlhere. Main is frozen at e2, and the Gate B documents quote the patched file's job lines.opensoft/openRepoTools-code:composed.yml, with a gatherer namedexact-pins. That job should refuse a matrix that differs from the pinned leg'sSHARDS.timeout-minutesper shard (there is none today, so the 360-min default applies); and a re-measure once Ubuntu 26.04's uutilstimeout -knever sends its KILL to the process group, so a bounded command can wait out a child that survives TERM, and setup-python has no Python 3.9 there; CI runs on ubuntu-24.04 until both are handled #297 settles 26.04.Files on Eagle:
/workspace/projects/.lane-worktrees/openRepoTools-3/briefs/300-ci-work/300-phase1-draft.patch(sha256267575f3…);/workspace/projects/.lane-worktrees/openRepoTools-3/briefs/300-ci-work/measurements/.The step 1 draft patch (local commit ea29556, never pushed)
Lane: openRepoTools-3