👷 ci: spend a pull request run on its critical path - #2252
Merged
Conversation
gaborbernat
added a commit
that referenced
this pull request
Sep 9, 2026
Read the Docs rejects a configuration with neither `build.tools` nor `build.commands`, and dropping the Rust toolchain in #2252 left it with neither: build 34467122 stopped after `cat .readthedocs.yaml` with "At least one of the following configuration options is required". Node is what the build already uses for pagefind and the cheapest tool the image ships, so it stands in for the compiler the build no longer needs.
Merging this PR will not alter performance
Comparing Footnotes
|
A green pull request run takes about 20 minutes, and the audit in #2251 shows where they go. `coverage` sets the critical path at 17.4 m. `frontend` takes 10.1 m because its target directory sits outside the cached `target/`, so it compiles 1,241 units from nothing on every run. The Cargo cache holds 10,239 MB against a 10 GB limit, 3.3 GB of it duplicates written from pull request refs or keyed on the runner image's own toolchain, and the evictions that follow are what left `contracts` cold. Every merge to main cancelled the run before it, so main wrote no caches and got no verdict. The CodSpeed serving leg rebuilt for 7.9 m behind a cache the parsing leg had written first. Read the Docs compiled the whole `peryx` binary on a two-job builder for one `openapi` call, 440 to 490 s, and hit its 15-minute limit on #2239. Caches now come from main alone, including the conformance and mise ones, and the rust-cache key hashes only the pinned toolchain so an image update stops rotating every entry. Push runs group by commit so a merge burst keeps its verdict and its cache writes. The frontend build roots under `target/` where rust-cache picks it up. The serving leg is the one that writes the benchmarks cache, since it builds the superset. The site serves a committed `site/static/openapi.json` that `just docs` checks against the binary, and Read the Docs drops Rust. A `changes` filter stands the Rust jobs down for a pull request that touches only the site or Markdown, and skips benchmarks for one that touches only tests; a justfile contract keeps the list of site files a crate embeds current so that filter cannot hide a real change. Closes #2251
Read the Docs rejects a configuration with neither `build.tools` nor `build.commands`, and dropping the Rust toolchain in #2252 left it with neither: build 34467122 stopped after `cat .readthedocs.yaml` with "At least one of the following configuration options is required". Node is what the build already uses for pagefind and the cheapest tool the image ships, so it stands in for the compiler the build no longer needs.
gaborbernat
force-pushed
the
ci/fast-ci
branch
from
September 9, 2026 09:51
b2001eb to
3a8fc32
Compare
gaborbernat
added a commit
that referenced
this pull request
Sep 9, 2026
The site serves a committed copy of the document since #2252, and `just docs` fails when it differs from what the binary produces. Renaming the trash record's `name` and `reference` to the `resource` and `artifact` the handler actually reads, and giving the schema endpoint the example the document-wide check requires, both changed the generated output. Rebuilt through `just openapi`. Those two edits are the whole diff.
gaborbernat
added a commit
that referenced
this pull request
Sep 9, 2026
* 🧪 test(backup): pin the flags a backup opens with The open flags carry the guarantees: a member is created fresh rather than reused, is never reached through a symlink, and no descriptor the backup holds survives into a child process. Almost none of that was held to anything. Existing tests cover five of the ways the set can be broken, all through `CREATE` and `EXCL`, because dropping either makes a backup fail outright. The rest were free. `EXCL` already refuses an existing path, symlink or not, so the tests that plant a symlink pass whether or not `NOFOLLOW` is set, and nothing looked at `CLOEXEC` at all. `open_dir` was unheld in every respect: it would open a regular file as the backup root, follow a symlink to somewhere the operator never named, and hand the descriptor to any child it spawned. The descriptor assertions compare the whole flag set rather than testing for membership, since a descriptor carrying `CLOEXEC` alongside something else is not the guarantee being claimed. * ♻️ refactor(backup): name the two open flag sets Three call sites spelled out flag lists, two of them the identical four flags. Naming each set says what it is once, and puts the reason beside it rather than leaving a reader to infer it from four constants. Spelling a fixed set as a bit operation also invites the wrong operator. `^` produces the same value as `|` here, and only because every flag is a disjoint bit; a flag that later overlapped an existing one would make the two diverge with nothing to notice it. `union` names the operation the set actually wants. * 🐛 fix(api): document trash with its wire names The trash record example and the inspect query in the OpenAPI document named the resource `name` and the artifact `reference`, while the handler writes `resource` and `artifact` and reads the same names from the query. A client built from the document sent the wrong parameter and could not find any record. The docs site repeated the same names in its schema table. The document now uses the handler's names, and a test pins both the record example keys and the inspect query parameters to that set, so the two cannot drift apart again without the suite saying so. * 🧪 test(peryx): close the app and config survivors A file-scoped mutation run over app/cache.rs, app/config.rs and config/raw.rs left four mutants alive, each because the suite never asked the question the mutated line answers. The fsck and repair fixture driver ignored the indexes it was handed, so a command that resolved no indexes at all produced identical output. It now echoes their names, which makes the resolved index set part of every fsck and repair assertion. The cache listing fixture only filtered by an age of one second, which a placeholder clock of one satisfies; a threshold of decades passes only against the real clock, because the gadget page was fetched at the epoch. The config check only ever printed a plural index count, and the prefetch table was checked on the raw index but never on the resolved one. Each gap has a case now, confirmed by rerunning the same mutants to zero missed. * 🧪 test(api): pin the shape of every documented operation A file-scoped mutation run over the OpenAPI builders showed that an operation could lose its summary, tag, responses or request body, and a shared example record could turn into null, without any test noticing. The existing checks read specific routes for specific facts, so a builder emptied wholesale slipped between them. One document-wide test now requires every operation to carry a tag, a summary and at least one described response, every JSON body to carry an example or a schema, and every request body to carry content. Binary bodies are exempt because an octet stream has no example worth printing. The schema endpoint gained the example this rule asks of it. The shared records that nest inside larger examples, the analytics window, the quota meters and the retention candidate, are pinned by their field sets, since a document-wide rule cannot tell a real nested record from a null. * 🧪 test(peryx): close the config and operator survivors Four mutants from the 2026-08-30 inventory were still alive in config and operator code, each on a boundary the suite only ever drove from one side. A secret file was tested one byte over its limit and never at it, so the inclusive comparison could become exclusive unnoticed. The TLS debug form was never printed with only one half of a client identity, which is the case where "either half counts" differs from "both halves". A restore was tested rolling back and moving forward but never onto a target at exactly the backup's serial, where a rollback warning would be wrong. And no backup round trip carried index settings, so a snapshot that dropped them restored to an equal config. Each has its case now, and each was confirmed by applying the mutation and watching the new case fail. The archive copy buffer is spelled as a literal rather than as an arithmetic expression. No test can observe the buffer's size, so an operator there carries no meaning a mutation could contradict. * 🧪 test(config): drive every operand of the merge predicates The 2026-08-30 inventory listed the `||` chains in index, upstream, OIDC, LDAP and webhook classification as surviving mutation, and reading the tables showed why: each chain was driven through one operand. Routing options were refused only for `fallback`, refresh controls only for `credential_refresh_secs`, direct-bind LDAP only for `bind_dn`, and the issuer check only for user info and case. Turning any `||` into `&&` left the tested operand's verdict unchanged. Each table now carries one case per operand, so any single operand refuses on its own. The webhook guards gained the empty-string cases they exist for: an empty `secret` or `secret_env` is neither a literal nor a variable name, and the guard that says so was never exercised. Every mutant was applied by column and watched fail against the new case before the case was kept. * 🧪 test(peryx): close the model and restore survivors A file-scoped mutation run over config/model.rs, operator/restore.rs and operator/snapshot.rs (164 mutants) left two alive. The local-member lookup in the scheduled-job check could match any node other than this one and no test would notice, because every roster in the suite contained the identity. A roster that names other nodes but not this one now expects the schedule's own refusal, which the mutated lookup turns into a later topology error instead. The metadata and config copy check on restore could accept a member that changed between verification and copy. The blob variant of that check had its own cases; the member variant now has the same two, one per field. * 🧪 test(config): accept a replica page at the primary limit A file-scoped mutation run over config/merge.rs (159 mutants) left one alive: the replica page-size bound was driven from one side only. A page one past the primary limit was refused, but no test asked for exactly the limit, so the inclusive comparison could turn exclusive unnoticed. The runtime-bounds case now runs at the limit as well as below it, with the bound named through the primary's own constant rather than a copied number. * 🧪 test(api): evaluate the document checks unconditionally The coverage gate rejected the document-wide OpenAPI check: it recorded each violation inside an `if`, so on a document that satisfies every rule the six recording lines never ran and the gate counted them uncovered. Each rule now produces a label and a verdict on every operation, and the violations are the labels whose verdict says so. Every line runs whatever the document holds, and the assertion is unchanged: the list of operations with missing pieces must be empty. * 🧪 test(operator): pin what a backup verification counts A file-scoped mutation run over operator/verify.rs (111 mutants) left five alive, all for the same reason: the tests asserted that a report contained the line they cared about and never what else it held. Under a mutated problem counter the expected line still appears, so a `contains` check agrees with a broken count. Each report is now pinned by its problem total, which makes the count part of every assertion: a tampered blob is two problems and a missing one is one, a duplicate index row is three, and a metadata mismatch is two lines and nothing from the scans an opened store would have run. The placement count needed a case rather than an assertion, because no test put a placement row in a single-node store, where the mode check makes the backup and its verification agree on counting none. * 📝 docs(api): regenerate the committed OpenAPI document The site serves a committed copy of the document since #2252, and `just docs` fails when it differs from what the binary produces. Renaming the trash record's `name` and `reference` to the `resource` and `artifact` the handler actually reads, and giving the schema endpoint the example the document-wide check requires, both changed the generated output. Rebuilt through `just openapi`. Those two edits are the whole diff.
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A green pull request run takes about 20 minutes, and the audit in #2251 says where they go.
coveragesets the critical path at 17.4 m.frontendtakes 10.1 m becausecoverage-frontendbuilds under.tox/coverage-target/frontend, outside thetarget/that rust-cache saves, so it compiles 1,241 units from nothing on every run. The Cargo cache holds 10,239 MB against GitHub's 10 GB limit, and 3.3 GB of that is duplicates: entries the conformance and mise steps wrote from pull request refs, and a second copy of every key after the runner image's stable toolchain moved from 1.98.0 to 1.98.1, since rust-cache hashes each installed toolchain. The evictions that follow are what leftcontractswithout an entry (#2244, fixed in #2245). Every merge tomaincancelled the run before it, so a merge burst wrote no caches and reached no verdict. The CodSpeed serving leg spent 7.9 m rebuilding behind an entry the parsing leg had written first. Read the Docs compiled the wholeperyxbinary on a two-job builder for oneopenapicall, 440 to 490 s per build, and hit its 15-minute limit on #2239. 📏The setup action now writes its tool and Cargo caches from
mainonly, the conformance workflow gets the samecache-save-if, and the action uninstalls the toolchains the runner image ships before rust-cache computes its key, so the key hashes only the pinned one and an image update stops rotating every entry. Push runs group by commit and only pull request runs cancel each other. Thefrontendjob rootsCARGO_TARGET_DIRattarget/, which putstarget/frontendwhere rust-cache already looks. The serving benchmark leg is the one allowed to write the shared CodSpeed entry, because it builds the superset the other two legs need. The site serves a committedsite/static/openapi.json,just docsregenerates the document and fails when the two differ,just openapirefreshes it, and.readthedocs.yamldrops the Rust tool and the cargo step; two runs ofperyx openapion the same tree are byte-identical, so the check is stable. 🧭A
changesjob withdorny/paths-filterstandscontracts,coverage,frontendandplatformdown for a pull request that touches onlysite/**, Markdown,.readthedocs.yaml,renovate.jsonorLICENSE. Crates compile three site files in throughinclude_str!(site/content/ecosystems/oci/reference/policy.md,site/static/icon.svg,site/static/mark.svg), so anembeddedfilter names them andjust _embedded-docs-contract, now part oflint-automation, fails whenever a crate embeds a site file the filter does not list. CodSpeed skips its benchmarks for a pull request that changes only tests or docs. Both gates acceptskippedfor those jobs and nothing else, and pushes and dispatches run everything as before.Expected after this lands:
frontendabout 4.5 m,PyPI serving benchmarksabout 4 m, the Read the Docs build about 90 s, and a docs-only pull request done in about 4 m.coverageis untouched, so a pull request run stays coverage-bound at about 18 m; splitting it is the next item in #2251. A fully cold run is now bounded bycontractsat 5.3 m pluscoverage. Thechangesjob adds about 15 s before the Rust jobs start. PR #2162 also editsci-gate, so whichever lands second rebases the gate condition.Closes #2251