Skip to content

feat(cargo-anvil): benchmark regression detection via cargo-bench-history - #68

Open
martin-kolinek wants to merge 35 commits into
mainfrom
anvil-benchmarks
Open

feat(cargo-anvil): benchmark regression detection via cargo-bench-history#68
martin-kolinek wants to merge 35 commits into
mainfrom
anvil-benchmarks

Conversation

@martin-kolinek

@martin-kolinek martin-kolinek commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

Summary

Makes automatic benchmark regression detection a first-class cargo-anvil capability by wrapping cargo-bench-history (cbh). One catalog, both backends (GitHub Actions + Azure DevOps): every consuming repo gets it.

This PR contains both the design and the full implementation.

Design

  • docs/design/benchmarks.md — the subsystem design, part of the opinionated baseline (sibling to containers.md).
  • docs/design/README.md, docs/design/checks.md — register the capability in the design index and check catalog.
  • docs/design/github.md, docs/design/ado.md — per-backend history persistence (artifacts, not cache) and failure surfacing (§12 in each).
  • docs/design/updates.md §2.1 — fork-migration rule for wrappers predating the new fields.
  • docs/implementation-plans/0003.md — the phased build-out.

Implementation

  • Pinned tool registration (cargo-bench-history) in versions.just / tools.just.
  • New bench-history check recipe: collect → bless → analyze → gate. analyze never fails on findings, so the recipe gates itself on direction == "regression" && active.
  • New scheduled-benchmarks check group, wired on both backends, feeding publish-failure.
  • History round-trip via build artifacts on both backends, with a fail-closed restore: the store path is created only on restored / cold-start, so a transient restore failure can never publish a truncated history or report a false clean.
  • Bless application, reviewed-ledger style, applied idempotently before analyze.
  • Per-group pre/post step splicing (__PRE_STEPS__ / __POST_STEPS__) so group-specific extras live in that group's own emitted template and the tier templates stay a plain list of groups.
  • benches/catalog.rs — a criterion benchmark over Catalog::anvil() so anvil benches itself.

Key decisions

  • Detection is cbh's, not anvil's. cbh stores each run immutably, orders series by git first-parent topology, partitions by a hardware machine key, and reports level shifts / drift with noise-aware, FDR-controlled statistics. anvil supplies only the wiring.
  • Regression detection is a scheduled concern — it obeys the catalog rule "a check belongs in scheduled iff its outcome can change without a commit." PR stays compile-only (per-PR benching is too costly and noisy to gate merges).
  • Fail the scheduled build on an active regression — not PR comments, since the regression surfaces after merge with no PR to annotate. Reuses native failure machinery. This reconciles with anvil's "advisory, never fail" rule, which governs PR gating: a scheduled build blocks no one's merge.
  • History is CI-native cross-run state — a rolling-window default (portable, zero-config, harmless cold start on eviction), with opt-in durable Azure Blob for long history.
  • Accept intentional changes via a reviewed bless file — red build → PR adding {benchmark, commit, reason} → the scheduled job idempotently applies it before analyze → green.

Validation

  • Detection quality, out-of-band: a backtest over three months of real Oxidizer benchmark history flagged three known regressions at their exact attributed commits, with zero false positives on a flat control, including a deliberately noisy case. The gating expression was then re-validated against that same store.
  • Functional coverage in tests/recipe_contracts.rs: gate scenarios, bless reconciliation, bless-parser rejection, both restore blocks executed against mocks, and process-scoping of the bless listing.
  • Emission coverage: snapshots on both backends, actionlint over the emitted GitHub tree, and a guard asserting every per-group fragment key names a real member of GROUPS.
  • Verified empirically that a benchmark-free workspace stays green: both collect and analyze exit 0 with a well-formed report, so adopting repos without benchmarks do not go permanently red.

Honest caveats (recorded in the design)

Hosted-runner machine-key density (may split series sparsely — to observe); coarse commit-range attribution under sparse benchmarking; ADO's coarser concurrent-regression signal while already red; cbh maturity (contained by a pinned tool version); durable-store portability (the rolling-window + opt-in-blob compromise).

…ion detection

Captures the design for wrapping cargo-bench-history as a cargo-anvil capability:
scheduled collect+analyze with CI-artifact/cache history persistence, fail the
scheduled build on an active regression (reusing native GitHub/ADO failure
notifications) rather than PR comments, and a reviewed bless-via-file workflow to
accept intentional changes. Cross-backend from one catalog; PR stays compile-only.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a3ecc845-0526-499a-a233-6b82909c4517
@codecov-commenter

Codecov Comments Bot (codecov-commenter) commented Aug 3, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.7%. Comparing base (c4ee270) to head (cbd9b9f).

❌ Your project status has failed because the head coverage (97.7%) is below the target coverage (100.0%). You can increase the head coverage or adjust the target coverage.

Additional details and impacted files
@@          Coverage Diff          @@
##            main     #68   +/-   ##
=====================================
  Coverage   97.7%   97.7%           
=====================================
  Files        286     286           
  Lines      62460   62487   +27     
=====================================
+ Hits       61032   61061   +29     
+ Misses      1428    1426    -2     
Flag Coverage Δ
linux 97.6% <100.0%> (?)
linux-arm 97.6% <100.0%> (?)
windows 97.9% <100.0%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Add docs/design/benchmarks.md describing the capability as part of the opinionated
baseline: cbh as the detection engine, regression detection as a scheduled concern,
history as CI-native cross-run state (rolling-window default, opt-in durable blob),
fail-the-scheduled-build surfacing via native GitHub/ADO notifications, and
bless-via-reviewed-file to accept intentional changes. Register it in the design
index and the check catalog, and point implementation plan 0003 at it as the design
companion.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a3ecc845-0526-499a-a233-6b82909c4517
@martin-kolinek martin-kolinek changed the title docs(cargo-anvil): plan 0003 — benchmark regression detection via cargo-bench-history docs(cargo-anvil): design + plan for benchmark regression detection via cargo-bench-history Aug 3, 2026
martin-kolinek and others added 7 commits August 3, 2026 17:42
…nd design docs

Clarify that the cbh history store is CI-native build ARTIFACTS, not the build cache
(which stays scoped to tool/dep acceleration). Add a "Benchmark regression detection"
section to github.md (Actions artifacts: download-latest-from-default-branch ->
collect/analyze -> upload; fail-the-build -> update-in-place tracking issue) and to
ado.md (Pipeline Artifacts via the existing §4.1 job-wrapper `artifacts` contract +
DownloadPipelineArtifact latestFromBranch; fail-the-build -> native failed-build
notifications; one-bit-status limitation noted). Tighten benchmarks.md to name
artifacts precisely.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a3ecc845-0526-499a-a233-6b82909c4517
…enchmark design

Remove contrastive "artifacts not the cache / separate on purpose" framing, the
"validated / to observe" and "cbh is young" status notes, and the "out of scope for
the first version" roadmap line. The design docs now state the end state (history
persists as build artifacts; the one-bit status limitation is a property) without
justifying-against-alternatives or recording decisions.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a3ecc845-0526-499a-a233-6b82909c4517
… and catalog

Give benchmark regression detection its own scheduled group (isolating the history
artifact round-trip and fail-on-regression from the other exhaustive work). Update
the checks.md tier/group/check flowchart and its scheduled-tier table (now 5 groups),
the github.md and ado.md scheduled-pipeline diagrams and emitted-artifact file trees,
and name the group + check (bench-history) in benchmarks.md.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a3ecc845-0526-499a-a233-6b82909c4517
…e plan

Strip the surfacing/persistence/bless/caveats prose from implementation plan 0003
(all of it now lives in design/benchmarks.md and the backend §12 sections) and
rewrite it as sequencing only: principles plus six phases that reference the design
rather than restating it. Add a "History horizon" boundary to benchmarks.md §7 so
the design fully owns the caveats.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a3ecc845-0526-499a-a233-6b82909c4517
… blob a non-goal

Artifacts are the only supported history store: state it as such in benchmarks.md §4
and reduce cbh's own durable backends (e.g. Azure Blob) to an explicit non-goal;
reframe the §8 history-horizon caveat accordingly and drop plan Phase 6. Add a §6
"Local behavior" section: the recipe is identical locally but has no shared history,
so local analysis is a friendly no-op; regression detection is a scheduled/shared
concern and the self-contained failure surface (finding + cbh's trend chart) is the
interface, with local reproduction a documented manual escape hatch. Note the chart
in §5 so the surface is self-contained.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a3ecc845-0526-499a-a233-6b82909c4517
List the anvil-scheduled-benchmarks group recipe (anvil-bench-history) in
local.md's groups.just and the anvil-scheduled tier aggregator, and note that the
group runs the same recipe locally but needs the shared CI history for regression
analysis (pointing to benchmarks.md section 6 rather than duplicating it).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a3ecc845-0526-499a-a233-6b82909c4517
Wire cargo-bench-history into the catalog as a new `bench-history` check in
its own `scheduled-benchmarks` group. The check runs the workspace benchmarks,
records them into a history that round-trips through CI-native build
artifacts, applies committed blessings, analyzes the accumulated series, and
exits non-zero on an active regression.

Both backends carry the history and the failure surface: GitHub restores from
the newest scheduled run carrying the leg artifact and files a create-or-update
tracking issue on failure; ADO restores via DownloadPipelineArtifact and
publishes through the job wrapper, relying on native failed-build
notifications plus the build summary. Restore and publish are both indifferent
to the run outcome, so samples taken while the pipeline is red survive.

The ADO job wrapper gains a `fetchDepth` parameter, since the analysis walks
the commit graph and needs a full checkout.

Also fix the actionlint schema test, which silently skipped whenever the tool
was installed because its fixture was not a git repository.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a3ecc845-0526-499a-a233-6b82909c4517
@martin-kolinek martin-kolinek added the agency-rocket Touched by a rocket skill label Aug 4, 2026
martin-kolinek and others added 4 commits August 4, 2026 19:32
Only .anvil.lock conflicted; it is generated, so it was resolved by
re-running cargo anvil over the merged tree.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a3ecc845-0526-499a-a233-6b82909c4517
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a3ecc845-0526-499a-a233-6b82909c4517
Adopts two invariants #62 introduced, applying them to the new benchmark
artifacts: the "GENERATED BY cargo-anvil" header on owned files, and
[script("pwsh", "-NoProfile")] on emitted recipes.

The checks.md line stating that benchmark execution is deliberately outside
the catalog is superseded by the scheduled-benchmarks group and now describes
why the `bench` check stays compile-only.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a3ecc845-0526-499a-a233-6b82909c4517
…uled publisher

PR #65 adds a generic publish-failure job that upserts one incident issue for any
failing scheduled group. The benchmark-specific issue step added here would have
filed a second notification for the same event, so it is removed: the group now
just fails and leaves its findings on the build summary, and reporting happens the
same way it does for every other scheduled failure.

The job keeps `actions: read` for the history-artifact restore; `issues: write` is
no longer needed on either the impl or the root workflow.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a3ecc845-0526-499a-a233-6b82909c4517
martin-kolinek and others added 2 commits August 7, 2026 12:44
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a3ecc845-0526-499a-a233-6b82909c4517
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a3ecc845-0526-499a-a233-6b82909c4517
Comment thread crates/cargo-anvil/templates/ado/scheduled-stages.yml Outdated
Comment thread crates/cargo-anvil/templates/github/scheduled-impl-workflow.yml Outdated
displayName: Restore ${{ parameters.artifact }}
# The first run has no artifact to restore; a missing artifact is a
# cold start, not a failure.
continueOnError: true

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖: The ADO restore can cold-start the store while a usable history exists, and cannot tell that apart from a genuine first run. Two mechanisms compound:

  1. continueOnError: true tolerates every failure — authorization, service errors, malformed or partial downloads — not just the missing-artifact case the comment above describes.
  2. buildVersionToDownload: latestFromBranch (:30) selects the latest eligible run and downloads artifactName from that one run; it does not walk back to the newest run that actually carries the artifact. A cancelled or never-publishing latest run therefore yields nothing, even though an older run has the history. That contradicts this template's own comment at :32-35 ("the newest run that carries the artifact"), the rolling chain promised in benchmarks.md:76-86, and the GitHub side, which does loop.

Either way the benchmark leg then analyzes an empty store, reports no regression, and the wrapper's artifacts contract publishes that empty store as the newest snapshot — silently truncating the chain and producing a false clean. Please resolve a run that actually has the per-leg artifact (as GitHub does), and tolerate only a positively identified missing artifact; other restore failures should fail the job and prevent publication of a replacement snapshot.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Both mechanisms fixed; the template no longer uses DownloadPipelineArtifact@2 at all.

You are right that latestFromBranch resolves a single build and does not walk, which contradicted the template's own comment and the GitHub side. The step now walks the build list itself (queryOrder=finishTimeDescending, newest first, outcome-agnostic) and, for each build, queries the artifacts endpoint. A 404 means "this build has no such artifact" and the walk continues; any other status fails the job rather than being read as absence.

continueOnError: true is gone, replaced by that explicit classification. Publishing moved out of the wrapper's artifacts contract into a bench-history-publish.yml step conditioned on ne(variables['ANVIL_BENCH_RESTORE'], ''), so a failed restore cannot truncate the chain. Both backends now behave the same way, which is what the design claimed all along.


AI response generated by rocket

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖: Non-blocking — dropping DownloadPipelineArtifact@2 for an explicit walk addresses both mechanisms, and the 404-is-absence / anything-else-is-failure split is the right shape. The follow-on concern is coverage: this replaced a built-in task with a hand-written REST/download/unzip state machine, and the test still only asserts that status-code and variable strings appear in the template.

Please run the PowerShell body with mocked REST/download commands and cover: a 404 walking back to a valid older artifact; successful extraction setting restored; total absence setting cold-start; and non-404 query / download / archive-extraction failures exiting non-zero without setting ANVIL_BENCH_RESTORE. That last one is the branch the guarded publish depends on — it is what keeps a transient failure from overwriting a good chain with a truncated store, so it is worth pinning behaviourally rather than by string match.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Added, with the same shape as the GitHub side.

ado_restore_separates_absence_from_failure extracts the pwsh: | body, substitutes the template parameters, and runs it with Invoke-RestMethod / Invoke-WebRequest / Expand-Archive shadowed by stand-ins (function definitions take precedence over cmdlets). Covered:

  • a 404 on the newest build walking back to a valid older artifact;
  • successful extraction setting restored;
  • total absence setting cold-start;
  • query (non-404), download, and archive-extraction failures each exiting non-zero without setting ANVIL_BENCH_RESTORE.

That last case is asserted for all three fault points, since it is what the guarded publish depends on.

One thing worth recording: writing the mock exposed that PowerShell's -like treats ? as a single-character wildcard, so my first discriminator (*_apis/build/builds?*) also matched the artifact URI and the mock returned the build list for both. The test failed until I discriminated on /artifacts instead. The production template does not use -like on these URIs, so this was a fault in the test only — but it is the kind of thing that would have made a string-matching test pass while proving nothing.


AI response generated by rocket

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Copilot speaking]

Explain or remove the silent missing-download-URL fallback

The restore logic carefully documents that only confirmed artifact absence may be treated as a cold start and that operational failures must fail closed. Immediately after that classification, however, if (-not $found.resource.downloadUrl) { continue } silently treats a successful artifact response with no download URL as if the build had no artifact. No comment or referenced service contract explains why this response shape is benign rather than malformed or operationally incomplete.

Reproducible reasoning: For each candidate build, the artifacts request either throws, returns a response without a download URL, or returns a downloadable artifact. The catch block explains the first case in detail: a not-found response means absence and every other failure stops the job. The second case bypasses that rule without explanation and continues the walk. If every candidate has that shape, control reaches Complete-Restore 'cold-start', even though the API did not positively report that the artifacts were absent. Azure's artifact response model describes resource.downloadUrl as the link used to download the resource, but the template does not identify a documented benign case where an artifact exists without it. This makes the intended invariant impossible to reconstruct from the code.

Consequence: A maintainer may preserve a fail-open branch that can turn incomplete service responses into a new history chain, or remove a deliberately supported edge case because its rationale is not recorded. Either choice risks silently losing benchmark continuity.

Recommended action: Make the classification explicit. If a returned artifact is expected to have a download URL, report an operational error and fail the restore when it is missing. If Azure DevOps has a documented benign response shape that omits the URL, add a comment and reference explaining that contract and why continuing the search cannot misclassify a restore failure as a cold start.

References:

Impacted locations:

  • crates/cargo-anvil/templates/ado/steps/bench-history-restore.yml:107-119

Comment thread crates/cargo-anvil/templates/justfiles/anvil/checks/bench-history.just Outdated
}

#[test]
fn bench_history_gates_on_active_regressions_and_applies_blessings() {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖: The riskiest code in this PR has no execution coverage; the covered parts are the ones that could not plausibly be wrong. This test — and its siblings in ado.rs:396-424 / github.rs:274-297 — assert that selected command strings occur in the emitted template. That verifies registration and spelling, not behaviour. Nothing in the changeset ever runs the recipe: not the mini-TOML parser, not the blessing reconciliation, not the findings.json regression gate, not the store transition, not exit-code propagation.

Please add a functional scenario with the existing fake-cargo/fake-git harness (e.g. in tests/recipe_contracts.rs) covering the Phase 5 acceptance case: clean analysis → an active regression failing the recipe → applying a committed blessing → the next run green without re-appending. Inactive findings, improvements, and command-failure propagation are worth the same treatment.

This is also what 0003.md:69-70 promises ("A fixture repo exercises red → bless → green") and the plan ships in this PR, so as written it reads as satisfied when it isn't. Either land the fixture, or amend the plan to state what the intended verification actually is.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Agreed — this was the real gap, and the string assertions were the least valuable part of the changeset.

Added to tests/recipe_contracts.rs, using the existing fake-cargo/fake-git harness (extended to stand in for cargo bench-history and for the commit resolution the reconciliation performs):

  • bench_history_gates_on_active_regressions_only — an active regression fails and names benchmark and attributed commit; an inactive finding and an improvement both stay green; an empty history is a clean no-op.
  • bench_history_reports_without_gating_outside_ci — reports the finding but exits 0 locally.
  • bench_history_propagates_tool_failure — a tool that fails to run is not "no regressions".
  • bench_history_bless_reconciles_on_exact_prefix_identity — the boundary from your other comment.
  • bench_history_bless_rejects_malformed_entries# inside a value survives; unquoted values, missing keys and unexpected tables are rejected.

On 0003.md: rather than claim a fixture that does not exist, Phase 5 now describes the reconciliation that shipped. The red → bless → green transition is covered at the recipe level above; a full store-transition fixture would need a real cbh, which the harness deliberately avoids.


AI response generated by rocket

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Copilot speaking]

Align the promised red-to-bless-to-green lifecycle coverage with the tests

Phase 5 still states that a fixture exercises the complete red → bless → green workflow, while the changed tests cover regression gating and blessing reconciliation independently. The public anvil-bench-history fixtures consume supplied analysis reports without carrying blessing state, and the reconciliation fixtures invoke _anvil-bench-history-bless directly. No scenario starts with an active regression, applies a committed blessing through the public recipe, verifies that the following analysis becomes green, and confirms that a later run does not re-append the blessing. The earlier follow-up said the plan had been narrowed, but the reviewed source still contains the lifecycle-fixture claim.

Reproducible reasoning: bench_history_gates_on_active_regressions_only and the related public-recipe tests feed fixed findings through FAKE_CBH_FINDINGS; their fake blessing command cannot change the report returned by a later analysis. The blessing tests instead call the private helper and inspect whether a cargo bench-history bless command was recorded, so they bypass the public recipe's ordering, store/file argument propagation, machine-key handling, and subsequent analyze/gate path. The embedded-recipe catalog test only checks for command fragments. These independent contracts can therefore remain green if the public recipe disconnects blessing application, moves it after analysis, passes incorrect identity or store arguments, or fails to make the next analysis green. That is weaker than the fixture lifecycle claimed in Phase 5 and in the prior thread's resolution.

Consequence: Reviewers and maintainers can treat the acceptance lifecycle as covered even though an integration defect could leave an intentionally accepted regression permanently red or cause its blessing to be appended repeatedly while all focused tests pass.

Recommended action: Make the verification artifacts describe one honest contract. Either add a stateful, hermetic fake-cargo fixture that drives anvil-bench-history through active regression failure, committed blessing application before analysis, a clean subsequent analysis, and another green run without re-appending; or revise Phase 5 and the pull-request validation claim to state that gating and reconciliation are tested separately. Narrowing the claim is the lower-cost option; add the lifecycle fixture if end-to-end store-transition coverage is an acceptance requirement.

References:

  • Earlier lifecycle coverage discussion and claimed resolution
  • crates/cargo-anvil/docs/implementation-plans/0003.md, Phase 5
  • crates/cargo-anvil/tests/recipe_contracts.rs, benchmark-history contract tests
  • crates/cargo-anvil/src/anvil/artifacts/justfile.rs, embedded recipe catalog test
  • justfiles/anvil/checks/bench-history.just, public recipe ordering
  • crates/cargo-anvil/docs/design/benchmarks.md

Impacted locations:

  • crates/cargo-anvil/docs/implementation-plans/0003.md:71-76
  • crates/cargo-anvil/tests/recipe_contracts.rs:1539-1888
  • crates/cargo-anvil/src/anvil/artifacts/justfile.rs:638-662
  • justfiles/anvil/checks/bench-history.just:96-108

Comment thread .github/actions/anvil-scheduled-benchmarks/action.yml.anvil-proposed Outdated
$entries = New-Object System.Collections.Generic.List[object]
$current = $null
foreach ($rawLine in (Get-Content -LiteralPath $blessingsFile)) {
$line = ($rawLine -split '#', 2)[0].Trim()

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖: Non-blocking — the accepted subset here is narrower than the name .toml promises, and one narrowing is a silent data-loss bug. ($rawLine -split '#', 2)[0] splits every line at the first # unconditionally, so a reason containing # — an issue or PR reference, which is exactly what one writes in a blessing rationale — is silently truncated rather than rejected. The regex also accepts escapes without decoding them, and the subset (double-quoted single-line values only, no single quotes, no inline tables) is stated only in this template's comment, not in the header of the emitted .config/bench-blessings.toml where its author will look.

Options, in rough order of preference: use a real parser; or name the file honestly for a format PowerShell parses natively (ConvertFrom-Json); or push it upstream — bless --from-file <toml> would remove the parser entirely and is worth asking cbh for at 0.0.9. If the hand-rolled parser stays, at minimum reject rather than truncate on # inside a quoted value, and document the subset in the emitted file's header.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Fixed the data-loss half; kept the hand-rolled parser.

# is now only treated as a comment at line start, so reason = "accepted in #1234" survives intact — a test asserts it. The value pattern is "([^"\\]*)", so a value containing an escape is now rejected rather than accepted-and-not-decoded, which was the other quiet inaccuracy.

The subset is documented in the recipe header, and the error message on a malformed line states it explicitly rather than saying only "cannot parse".

I did not switch format or push it upstream. JSON would fix parsing at the cost of comments, which an audit file wants; a real TOML parser means another pinned tool for three string fields; bless --from-file is the right long-term answer but is a cbh change. Reasonable to revisit — flagging that I chose the narrow fix deliberately.

One thing I did not do: document the subset in the emitted .config/bench-blessings.toml header. anvil does not emit that file — it is created by the first person to bless. If you want anvil to emit a commented stub, that is a small addition worth doing.


AI response generated by rocket

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Copilot speaking]

Publish a durable blessing-file schema and example

The supported fix-or-accept workflow depends on a user-authored .config/bench-blessings.toml, but public design documentation does not define the exact syntax. The parser accepts only [[blessing]] entries with required benchmark, commit, and reason fields, double-quoted single-line values without escapes, comments only on otherwise empty lines, and non-option-shaped benchmark and commit values. benchmark is interpreted as a prefix. The copyable recipe example also uses an abbreviated commit ID even though the file is a long-lived audit record.

Reproducible reasoning: Ordinary valid TOML forms such as single-quoted strings, escaped values, and inline comments are rejected by the deliberately narrow parser. Users must also know that one benchmark value may match a family. The recipe claims the subset is documented in an emitted file header, but cargo-anvil does not emit that file. Finally, git rev-parse --verify accepts an abbreviated object name only while the prefix remains unique; repository growth can make an unchanged committed entry ambiguous. These syntax and identifier rules are part of the user-authored configuration contract, not private parser details.

Consequence: An adopter can commit valid-looking TOML that fails only in the next scheduled run, misunderstand a family-wide blessing, or have a formerly valid abbreviated commit become ambiguous later.

Recommended action: Document the fixed path and supported grammar in benchmarks.md or a linked configuration page. Include a canonical [[blessing]] example, define the required fields and prefix semantics, list the supported quoting/comment restrictions and option-shaped-value rule, and recommend a full commit object ID for durable entries. Link the recipe header to that contract and remove its inaccurate emitted-header claim. Emitting a commented starter file is an alternative, but adds another generated artifact that adopters must own.

References:

Impacted locations:

  • crates/cargo-anvil/docs/design/benchmarks.md:156-169
  • crates/cargo-anvil/templates/justfiles/anvil/checks/bench-history.just:154-185
  • crates/cargo-anvil/templates/justfiles/anvil/checks/bench-history.just:278-283
  • justfiles/anvil/checks/bench-history.just:154-185
  • justfiles/anvil/checks/bench-history.just:278-283
  • crates/cargo-anvil/tests/recipe_contracts.rs:1841-1886
  • crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap:619-625

Comment thread crates/cargo-anvil/templates/justfiles/anvil/checks/bench-history.just Outdated
# red from a regression — precisely the window that matters.
# The in-progress run of this very workflow has not uploaded
# yet, so it simply fails the download and the loop moves on.
for run_id in $(gh run list --workflow anvil-scheduled.yml \

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖: Non-blocking — two fragilities in this lookup, both cheap to remove. (1) The workflow is identified by the literal filename anvil-scheduled.yml, but the root scheduled workflow is an owned, renameable file; a rename yields a permanently empty history whose only symptom is the log line at :170 and a green job. gh run list --workflow accepts a name, and in a called reusable workflow github.workflow_ref / $GITHUB_WORKFLOW resolve to the caller — deriving the identity removes the hazard entirely. (2) --limit 10 means eleven consecutive scheduled runs without the artifact (a paused schedule, a run of infra failures, the 90-day retention lapsing) silently resets the series. Since the whole point is a long series, consider making a total restore miss visible — a line in $GITHUB_STEP_SUMMARY rather than only in the step log — so "history quietly restarted" is distinguishable from "no regressions".

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Both addressed.

(1) The workflow is identified by ${{ github.workflow }} — the caller's runtime name, since github.workflow resolves to the caller in a called reusable workflow — instead of the literal anvil-scheduled.yml. A rename no longer resets the series. A test asserts the literal filename is absent.

(2) --limit raised 10 → 30, and a total restore miss now writes a "Benchmark history: cold start" section to $GITHUB_STEP_SUMMARY naming the artifact, the window and the branch. "History quietly restarted" is now visibly distinct from "no regressions", which was the substantive half of your point.


AI response generated by rocket

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Copilot speaking]

Align and justify the GitHub benchmark-history horizon

The restore action lists only the most recent 30 matching workflow runs before declaring a cold start, while the design promises the most recent default-branch run that carries the artifact and describes artifact retention as the GitHub history horizon. The upload retains artifacts for 90 days, but neither the design nor source comments explain how the run-count search limit and time-based retention were selected or are intended to relate.

Reproducible reasoning: gh run list --limit "$WINDOW" makes the run count an independent horizon. Manual dispatches, cancelled runs, or runs whose benchmark job never publishes can consume that window while an older artifact remains unexpired. Retention can instead be limiting when runs are infrequent. The fixed values therefore jointly determine recoverability, API work, and storage duration, but the documented contract attributes the horizon only to retention and provides no tolerated-gap or cost policy from which a maintainer can reproduce equivalent settings.

Consequence: A retained and usable history can be ignored after enough intervening runs, causing an avoidable visible cold start, while future changes to either value can alter API or storage cost without a documented policy.

Recommended action: Make the implementation and history contract agree, and record the selection policy. Either search every run that can still hold an unexpired artifact, or document the bounded run search as a separate horizon. In the bounded design, explain the tolerated missed-run or paused-schedule interval, acceptable API cost, and how the search depth and retention period should change together or remain independent. Reference that policy beside both source-template settings and regenerate the action and snapshot.

References:

  • crates/cargo-anvil/docs/design/benchmarks.md, History as cross-run state
  • crates/cargo-anvil/docs/design/github.md, Benchmark regression detection
  • Existing restore-window discussion

Impacted locations:

  • crates/cargo-anvil/templates/github/run-group-action.yml:81
  • crates/cargo-anvil/templates/github/run-group-action.yml:123-124
  • crates/cargo-anvil/templates/github/run-group-action.yml:218-222
  • .github/actions/anvil-run-group/action.yml:81
  • .github/actions/anvil-run-group/action.yml:218-222
  • crates/cargo-anvil/docs/design/benchmarks.md:80-110
  • crates/cargo-anvil/docs/design/github.md:1112-1134
  • crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap:481
  • crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap:621

}
Write-Host ''
Write-Host "Findings: $findingsMd"
Write-Host "Fix the regression, or accept it by adding an entry to $blessingsFile."

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖: Non-blocking — the gate being identical locally and in CI means just anvil-scheduled / anvil-full now fails on laptop measurement noise. That parity is deliberate (benchmarks.md:116-124) and tiers.just:33-40 puts the group on the scheduled tier, which tiers.just documents as the pre-release check. A developer who has run it a few times across commits on a throttling laptop gets an "active regression" no shared history ever saw — and this message tells them to fix it by committing a blessing, polluting the reviewed, audited blessings file with laptop noise.

Parity is worth keeping for what the recipe does; I'd separate it from what the recipe gates on — e.g. always report locally but only exit non-zero under CI, or require an explicit opt-in for the local gate. Mechanism is yours; the outcome to avoid is "the pre-release recipe fails on measurement noise and the suggested remedy is a commit".

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Agreed, and changed — the "suggested remedy is a commit" framing is what made this convincing.

The gate is now CI-only: the recipe still runs the benches and writes findings identically everywhere, but exits non-zero only under CI (GitHub), TF_BUILD (ADO), or an explicit ANVIL_BENCH_GATE=1. A local run prints the findings, then says the numbers come from this machine and points at the opt-in.

benchmarks.md §6 now states the split and why: parity in what the recipe does, CI-only in what it gates on. bench_history_reports_without_gating_outside_ci pins it.


AI response generated by rocket

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Copilot speaking]

Align every surface with one local benchmark-history contract

The recipe uses a machine-local history store and reports active regressions without failing unless CI markers or ANVIL_BENCH_GATE enable the gate. Several new or newly affected surfaces describe a different contract: the implementation plan says the check gates identically before cloud wiring, the canonical check catalog says it restores prior scheduled history and exits non-zero, the crate overview says checks behave identically locally and in cloud workflows, and local.md says regression analysis needs shared CI history. The local diagnostic then says it does not gate on “the shared trend” even though only local history was analyzed.

Reproducible reasoning: Without ANVIL_BENCH_WIRED_STORE, the recipe selects target/anvil/bench-history in the checkout, collects into that directory, and analyzes it. Test-Flag gates only when ANVIL_BENCH_GATE, CI, or TF_BUILD is enabled; otherwise the recipe prints the findings and exits successfully. Cloud jobs additionally restore and publish the shared artifact. The implementation plan, checks.md, crate-level documentation, local.md, and local message variously conflate the local store, shared CI history, analysis, and exit status, so a reader cannot derive one consistent local contract.

Consequence: A developer can treat a successful local run as equivalent to the scheduled gate, assume local analysis is unavailable, or commit a blessing for machine-specific noise because the documentation and diagnostic describe different inputs and failure behavior.

Recommended action: Choose one local contract and make the recipe, implementation plan, check catalog, crate overview, local design, and user-facing diagnostic agree. If the current recipe is intended, state that local runs analyze a gitignored machine-local store, always report, and fail only with the explicit gate opt-in; reserve “shared regression signal” for CI. If identical cloud behavior is intended instead, provide the shared history locally and change the gate and tests to match. Avoid saying a local run analyzed a shared trend unless it actually consumed that history.

References:

Impacted locations:

  • crates/cargo-anvil/docs/implementation-plans/0003.md:19-20
  • crates/cargo-anvil/docs/implementation-plans/0003.md:34-41
  • crates/cargo-anvil/docs/design/checks.md:131
  • crates/cargo-anvil/docs/design/checks.md:277
  • crates/cargo-anvil/docs/design/local.md:152-154
  • crates/cargo-anvil/src/lib.rs:92-98
  • crates/cargo-anvil/src/lib.rs:322
  • crates/cargo-anvil/templates/justfiles/anvil/checks/bench-history.just:16-33
  • crates/cargo-anvil/templates/justfiles/anvil/checks/bench-history.just:131-149
  • justfiles/anvil/checks/bench-history.just:145-149
  • crates/cargo-anvil/docs/design/benchmarks.md:132-147
  • crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap:490-497
  • crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap:589-608

Copilot AI review requested due to automatic review settings August 31, 2026 09:14

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 51 out of 53 changed files in this pull request and generated 1 comment.

Comment thread justfiles/anvil/groups/scheduled-benchmarks.just
Copilot AI review requested due to automatic review settings August 31, 2026 15:32

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 51 out of 53 changed files in this pull request and generated 5 comments.

Suppressed comments (2)

justfiles/anvil/groups/scheduled-benchmarks.just:17

  • anvil-scheduled-benchmarks is defined as a plain dependency list, unlike other scheduled groups which route through _anvil-run and define a private _anvil-scheduled-benchmarks body. As written, this group won’t match the catalog’s standard runner/impact wiring (and won’t work with _anvil-run-based invocation patterns used elsewhere).
    crates/cargo-anvil/docs/implementation-plans/0003.md:60
  • AGENTS.md discourages referencing design-doc section numbers (e.g. “§4.1”) from other sources because it couples text to doc structure. This paragraph references “the §4.1 wrapper contract”; prefer describing it by name (e.g. “the job wrapper contract in steps/job.yml”) without a section number.
  via the runs/artifacts API and downloading with `gh run download`;
  `actions/upload-artifact` with retention, guarded on the restore outcome.
- **ADO** — full-history + LFS checkout as an explicit `checkout` step in the
  group's own step list (the §4.1 wrapper contract stays frozen, since adopters
  fork that file); a restore step resolving the build through the build/artifacts
  REST API; a publish step carrying the same guard.

Comment thread justfiles/anvil/groups/scheduled-benchmarks.just.anvil-proposed Outdated
Comment thread justfiles/anvil/checks/bench-history.just.anvil-proposed Outdated
Comment on lines +3 to +7
The design for this capability is [../design/benchmarks.md](../design/benchmarks.md)
— with the catalog placement in [../design/checks.md](../design/checks.md) and the
backend wiring in [../design/github.md §12](../design/github.md) and
[../design/ado.md §12](../design/ado.md). This plan does not restate *what* the
capability is; it sequences *how* and *in what order* it lands.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖: Declining this one. AGENTS.md scopes the rule to source: "Do not cite design sections from rustdoc, code comments, or other source", and closes with "Link between design docs freely." The concern is coupling code to doc structure so the design cannot be reorganised without churning source.

An implementation plan is documentation, not source, and it is a transient record of one piece of work rather than something a reorganisation has to keep compiling. Happy to drop the anchors if you would rather read the rule as covering all prose, but as written it does not.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Copilot speaking]

Give benchmark regression detection a unique section number

The new benchmark chapter is numbered 12, but the immediately preceding advisory-comments chapter is already 12. This makes references to github.md §12 ambiguous; the implementation plan currently uses exactly that reference for the benchmark backend wiring.

Reproducible reasoning: The document has ## 12. Advisory PR comments at line 1035 and ## 12. Benchmark regression detection at line 1091. A reader following the implementation plan's github.md §12 reference cannot determine from the number which chapter is intended, and sequential navigation no longer reflects the document hierarchy.

Consequence: Cross-references and review discussion can direct readers to the advisory-comment design when they intend the benchmark backend contract, making the documented design harder to navigate and maintain.

Recommended action: Renumber the benchmark chapter to the next unique section number and update documentation references that identify it by number.

References:

Impacted locations:

  • crates/cargo-anvil/docs/design/github.md:1035
  • crates/cargo-anvil/docs/design/github.md:1091
  • crates/cargo-anvil/docs/implementation-plans/0003.md:5-6

Comment thread crates/cargo-anvil/docs/design/local.md
Comment thread crates/cargo-anvil/docs/design/benchmarks.md
# Conflicts:
#	.anvil.lock
#	crates/cargo-anvil/README.md
#	crates/cargo-anvil/docs/design/README.md
#	crates/cargo-anvil/src/anvil/artifacts/justfile.rs
#	crates/cargo-anvil/templates/justfiles/anvil/mod.just
#	crates/cargo-anvil/tests/recipe_contracts.rs
#	crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap
#	crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap
#	crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap
#	justfiles/anvil/mod.just
Copilot AI review requested due to automatic review settings August 31, 2026 16:41

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 51 out of 53 changed files in this pull request and generated no new comments.

Suppressed comments (1)

justfiles/anvil/groups/scheduled-benchmarks.just:17

  • This scheduled group recipe doesn't follow the established scheduled-group pattern of routing through _anvil-unscoped (see scheduled-exhaustive.just), which ensures ANVIL_IMPACT=off is applied before dependencies run regardless of the caller environment. As written, this group can behave inconsistently with other scheduled groups and will also stay perpetually out of sync with the generated template (hence the .anvil-proposed sibling).

…arkers as boolean-like

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a3ecc845-0526-499a-a233-6b82909c4517
Copilot AI review requested due to automatic review settings August 31, 2026 17:48

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 49 out of 51 changed files in this pull request and generated 2 comments.

Comment thread justfiles/anvil/checks/bench-history.just
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a3ecc845-0526-499a-a233-6b82909c4517
Copilot AI review requested due to automatic review settings August 31, 2026 18:51

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 49 out of 51 changed files in this pull request and generated 5 comments.

Comment thread crates/cargo-anvil/templates/github/run-group-action.yml
Comment thread .github/actions/anvil-run-group/action.yml
Comment thread crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap
Comment thread crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap
Comment thread crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a3ecc845-0526-499a-a233-6b82909c4517
Copilot AI review requested due to automatic review settings August 31, 2026 18:57

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 49 out of 51 changed files in this pull request and generated no new comments.

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

crates/cargo-anvil/templates/ado/steps/bench-history-restore.yml:117

  • In the ADO restore step, the catch block assumes Invoke-RestMethod always throws an exception with .Response.StatusCode.value__. For network/DNS/TLS/auth failures, the exception may not have a Response, causing a secondary null-reference error and hiding the real operational failure. Handle the no-response case explicitly and still fail closed (but with a useful message).
              # common case while walking back. Anything else is an
              # operational failure and must not be read as absence.
              $status = $_.Exception.Response.StatusCode.value__
              if ($status -eq 404) { continue }
              Write-Error "anvil: querying artifacts of build $($run.id) failed with HTTP $status; refusing to continue with an empty history."

…story

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a3ecc845-0526-499a-a233-6b82909c4517
Copilot AI review requested due to automatic review settings August 31, 2026 20:08

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 49 out of 51 changed files in this pull request and generated no new comments.

Suppressed comments (3)

Previously missed (3) — in code that hasn't changed since the last review.

.github/actions/anvil-run-group/action.yml:146

  • Using | head -n1 inside a set -euo pipefail script can fail the restore unexpectedly if more than one artifact row matches (duplicate artifact names are possible in a workflow run). Prefer making the --jq expression return a single value so the command substitution is robust without a pipeline.
          artifact_id=$(gh api --paginate \
            "repos/$REPO/actions/runs/$run_id/artifacts" \
            --jq ".artifacts[] | select(.name == \"$ARTIFACT\" and .expired == false) | .id" \
            | head -n1)

crates/cargo-anvil/templates/github/run-group-action.yml:146

  • Using | head -n1 under set -euo pipefail can cause an unexpected failure if more than one artifact matches (duplicate artifact names can exist in a run). Make the jq expression select only the first id so the restore logic is robust without relying on a pipeline.
    crates/cargo-anvil/templates/ado/steps/bench-history-restore.yml:118
  • In the ADO restore walk, a non-HTTP failure (or an exception without Response.StatusCode) will format as HTTP with an empty status, which makes the error hard to diagnose. Consider explicitly handling a missing status code and emitting a clearer message for that case.
              $status = $_.Exception.Response.StatusCode.value__
              if ($status -eq 404) { continue }
              Write-Error "anvil: querying artifacts of build $($run.id) failed with HTTP $status; refusing to continue with an empty history."
              exit 1

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Copilot speaking]

Published 62 findings. 14 findings follow up on existing discussion threads.

See diagnostics
Diagnostic Value
Cache Hit

Comment on lines +159 to +163
sched_impl --> sbench_job["scheduled-benchmarks<br/>matrix: linux, windows"]:::job
sbench_job ==> sbench_act[".github/actions/<br/>anvil-scheduled-benchmarks"]:::action
sbench_act ==> sbench_setup[".github/actions/<br/>anvil-setup"]:::action
sbench_act ==> sbench_just["just anvil-scheduled-benchmarks"]:::recipe
sbench_setup ==> sbench_setup_just["just anvil-setup"]:::recipe

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Copilot speaking]

Align GitHub architecture documentation with the emitted workflow

The scheduled-pipeline diagram depicts scheduled-benchmarks as invoking a nonexistent .github/actions/anvil-scheduled-benchmarks action, while the emitted workflow invokes the shared .github/actions/anvil-run-group action. The diagram also omits the benchmark job's dependency on publish-failure. Separately, the shared-action narrative, sample, and input table do not document the benchmark-history inputs or the conditional restore, save, and summary lifecycle implemented by that action.

Reproducible reasoning: The source workflow and generated workflow pass group: scheduled-benchmarks, bench_history, bench_artifact, and bench_machine_key to anvil-run-group; the shared action owns restoration, group execution, artifact upload, and summary publication. The publisher lists scheduled-benchmarks in needs, so a regression reaches the existing failure issue. The design diagram instead introduces a dedicated benchmark action and no publisher edge, while the shared-action contract lists only its older inputs. These surfaces therefore describe different extension points and different failure-notification paths.

Consequence: Maintainers can customize or create the wrong action, omit required inputs, or accidentally disconnect benchmark failures from the scheduled failure publisher because the design and emitted contract teach different architectures.

Recommended action: Reconcile the design and emitted workflow around one architecture. If the shared-action implementation remains the intended model, route the diagram through anvil-run-group, add the benchmark-to-publish-failure edge, and document bench_history, bench_artifact, and bench_machine_key plus their restore/save/summary behavior. If a dedicated action is intended instead, emit and wire that action and update the tests and input contract accordingly. Label any abbreviated sample explicitly while still enumerating the complete contractual input surface.

References:

  • crates/cargo-anvil/docs/design/github.md, scheduled pipeline diagram, Emitted artifacts, Shared group composite action, and Benchmark regression detection
  • crates/cargo-anvil/templates/github/scheduled-impl-workflow.yml
  • crates/cargo-anvil/templates/github/run-group-action.yml
  • crates/cargo-anvil/src/anvil/artifacts/github.rs
  • crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap
  • .anvil.lock

Impacted locations:

  • crates/cargo-anvil/docs/design/github.md:139-163
  • crates/cargo-anvil/docs/design/github.md:497-501
  • crates/cargo-anvil/docs/design/github.md:582-590
  • crates/cargo-anvil/docs/design/github.md:1091-1124
  • .github/workflows/anvil-scheduled-impl.yml:172-190
  • crates/cargo-anvil/templates/github/scheduled-impl-workflow.yml:172-190
  • crates/cargo-anvil/templates/github/run-group-action.yml:37-58
  • crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap:437-624
  • crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap:1410-1417

Comment on lines +159 to +162
contents: read
# Restoring the history walks the Actions runs/artifacts API. An action
# cannot request permissions, so this has to be granted here.
actions: read

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Copilot speaking]

Align the permission summary with benchmark restoration

The reusable workflow says that after resetting ordinary jobs to contents: read, only the publisher's issue permission is restored. The new scheduled-benchmarks job also restores actions: read so the shared action can list and download prior benchmark-history artifacts.

Reproducible reasoning: The file-level comment describes the permission model for the whole reusable workflow. The benchmark job now adds actions: read, while publish-failure adds issues: write; the nearby benchmark comment explains why an action cannot request the former for itself. The high-level statement is therefore incomplete even though the job-level grants are explicit.

Consequence: A security or maintenance review that relies on the summary can overlook an Actions API capability that must be preserved for benchmark restoration.

Recommended action: Make the high-level summary and the workflow permission model agree. If Actions API restoration remains, describe the reset as followed by capability-specific job grants, including actions: read for benchmark restoration and issues: write for failure publication. If the publisher-only summary is the intended contract, redesign restoration so the benchmark job no longer needs that grant.

References:

  • crates/cargo-anvil/docs/design/github.md, Benchmark regression detection

Impacted locations:

  • .github/workflows/anvil-scheduled-impl.yml:42-43
  • .github/workflows/anvil-scheduled-impl.yml:159-162
  • crates/cargo-anvil/templates/github/scheduled-impl-workflow.yml:42-43
  • crates/cargo-anvil/templates/github/scheduled-impl-workflow.yml:159-162

Comment on lines +153 to +156
strategy:
fail-fast: false
matrix:
os: [linux, windows]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Copilot speaking]

Document why scheduled benchmarks use only x86_64 legs

The new scheduled-benchmarks job selects only the Linux and Windows x86_64 matrix entries even though the reusable workflow exposes ARM runner inputs and neighboring scheduled jobs use all four OS/architecture legs. The design documents identify this as an intentional default matching scheduled-exhaustive, but neither the source template nor its generated workflow has an adjacent explanation or reference for omitting the ARM legs.

Reproducible reasoning: The matrix is a non-obvious policy decision rather than a syntactic necessity: ARM runners are available, while cargo-bench-history partitions measurements per machine, so adding architectures creates separate benchmark history chains and additional long-running jobs. Without a nearby design reference, a maintainer cannot distinguish the deliberate cost and coverage policy from an accidentally incomplete matrix or reliably keep the generated workflow and catalog policy aligned.

Consequence: A future maintainer may add or remove ARM benchmark legs without understanding the intended platform coverage, unexpectedly changing CI cost and machine-specific history series or leaving the documented matrices out of sync.

Recommended action: Add a concise comment above the source template's scheduled-benchmarks matrix stating that its default intentionally matches the x86_64-only scheduled-exhaustive coverage, reference the catalog design entry, note where adopters can customize the matrix when broader histories justify the cost, and regenerate the workflow.

References:

  • crates/cargo-anvil/docs/design/README.md, scheduled-benchmarks catalog row and “Cross-OS test matrices”
  • crates/cargo-anvil/docs/design/checks.md, scheduled group table and scheduled-benchmarks catalog row

Impacted locations:

  • .github/workflows/anvil-scheduled-impl.yml:152-156
  • crates/cargo-anvil/templates/github/scheduled-impl-workflow.yml:152-157

Comment on lines +214 to +221
if: always() && inputs.bench_history == 'true' && env.ANVIL_BENCH_RESTORE != ''
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ${{ inputs.bench_artifact }}
path: target/anvil/bench-history
# Comfortably longer than the scheduled cadence, so a paused or
# infrequent schedule does not break the chain.
retention-days: 90

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Copilot speaking]

Align the durable-store claim with implemented support

The pull request description says the CI-artifact rolling window has an opt-in durable Azure Blob option for longer history. The reviewed GitHub and Azure DevOps workflows only restore and republish target/anvil/bench-history through CI-native artifacts, while the benchmark design says durable cargo-bench-history backends such as Azure Blob are outside cargo-anvil's supported scope.

Reproducible reasoning: The GitHub action uploads the local directory as an Actions artifact. Azure DevOps restores and publishes the same directory as a pipeline artifact. No reviewed surface selects an Azure Blob backend or supplies its endpoint, credentials, restore, or publication behavior. That implementation matches the design's explicit exclusion but not the pull request description's substantive capability claim.

Consequence: Reviewers and adopters can plan around a supported long-retention option that the generated workflows cannot configure, then lose history when CI artifact retention expires.

Recommended action: Reconcile the pull request description, benchmark design, and generated workflows without assuming which side is authoritative. Either describe CI-native artifacts as the only supported store in this change, or add and validate the claimed durable backend with explicit configuration, credential handling, documentation, and equivalent behavior on both workflow backends.

References:

  • Pull request 68 description
  • crates/cargo-anvil/docs/design/benchmarks.md, sections 4 and 8
  • crates/cargo-anvil/docs/design/ado.md, Benchmark regression detection

Impacted locations:

  • .github/actions/anvil-run-group/action.yml:214-221
  • crates/cargo-anvil/docs/design/benchmarks.md:88-111
  • crates/cargo-anvil/docs/design/benchmarks.md:188-193
  • crates/cargo-anvil/docs/design/ado.md:940-963
  • crates/cargo-anvil/templates/justfiles/anvil/checks/bench-history.just
  • crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap:617-630
  • crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap:732-909
  • crates/cargo-anvil/templates/ado/scheduled-stages.yml:127-140

Comment on lines +49 to +51
[[bench]]
name = "catalog"
harness = false

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Copilot speaking]

Exclude development-only sources from the published cargo-anvil package

cargo-anvil is publishable and has no package.include or package.exclude policy, so Cargo's default selection includes tracked development files beneath the crate root. This pull request adds the Criterion benchmark, adds roughly 830 lines of benchmark-history fixtures and contract tests, and enlarges generated snapshots. Those additions support repository development but are not needed to compile or run the published tool.

Reproducible reasoning: Declaring [[bench]] and dev-dependencies does not exclude their source files from the crate archive. With no file-selection fields, Cargo packages tracked benches/**, tests/**, and snapshots together with the runtime source and embedded templates. The pull request therefore increases the published archive with development-only material even though the package's runtime contract is unchanged.

Consequence: Future releases distribute and unpack benchmark code, large test fixtures, and generated snapshots for every registry consumer, and the archive keeps growing with repository-only validation content.

Recommended action: Add an explicit package file-selection policy that omits benches/**, tests/**, and other development-only content while retaining src/**, embedded runtime templates, required metadata, licenses, and package documentation. An allowlist gives the smallest archive but requires care whenever a compile-time asset is added; targeted excludes are less exact but lower-maintenance. Verify the chosen policy with cargo package --list and the repository's existing package checks.

References:

Impacted locations:

  • crates/cargo-anvil/benches/catalog.rs:1-43
  • crates/cargo-anvil/Cargo.toml:4-16
  • crates/cargo-anvil/Cargo.toml:42
  • crates/cargo-anvil/Cargo.toml:49-51
  • crates/cargo-anvil/tests/recipe_contracts.rs:1493-2283
  • crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap

actions: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Copilot speaking]

Align the benchmark checkout rationale with LFS behavior

The benchmark job enables Git Large File Storage (LFS) checkout, which replaces repository pointer files with their stored benchmark inputs, but the adjacent source comment explains only the full Git history setting. The backend design and resolved discussion say LFS is needed because benchmark fixtures or binaries may be LFS-tracked.

Reproducible reasoning: actions/checkout does not download LFS objects by default. This job executes the benchmark code and consumes its data, so removing lfs: true can leave pointer text where the benchmark expects the real file. The current block gives maintainers a durable reason for fetch-depth: 0 but no reason for the neighboring correctness-sensitive option, even though the earlier response said that rationale had been added.

Consequence: A maintainer can remove the apparently unexplained LFS download to reduce checkout cost, causing LFS-backed benchmark inputs to fail or measure the wrong content.

Recommended action: Make the option and documented contract agree. If benchmark inputs may be LFS-tracked, state that reason beside lfs: true or reference the backend design. If the capability is not intended to support such inputs, remove the option and revise the design and validation evidence accordingly.

References:

  • crates/cargo-anvil/docs/design/github.md, “Benchmark regression detection”
  • r3825519684

Impacted locations:

  • crates/cargo-anvil/templates/github/scheduled-impl-workflow.yml:163-169

if ($wired) {
$wantPath = [System.IO.Path]::GetFullPath($wired)
$gotPath = [System.IO.Path]::GetFullPath($store)
if ($wantPath -ne $gotPath) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Copilot speaking]

Compare CI benchmark-store paths without assuming filesystem case rules

The store guard normalizes path syntax, then compares case-insensitively on Windows and case-sensitively elsewhere. Operating-system identity does not determine filesystem case behavior: Windows directories can be case-sensitive, and non-Windows volumes can be case-insensitive. The guard therefore can accept two distinct stores or reject two equivalent spellings while claiming to prove that collection writes into the directory CI restores and publishes.

Reproducible reasoning: GetFullPath resolves .. and relative syntax but does not determine whether case-variant names identify the same filesystem entry. On a case-sensitive Windows directory, -eq can accept different paths and detach collection from persistence. On a case-insensitive non-Windows volume, -ceq can reject one path spelled with different case. The CI override is documented as local-only and normally unnecessary, so the simplest fail-closed contract is to require one normalized spelling rather than infer equivalence. If alternate spellings must be accepted, equivalence has to be established from the actual volume or directory.

Consequence: A detached store can repeatedly lose prior samples and report a false clean; the opposite misclassification can fail a valid job solely because the volume differs from the operating-system default.

Recommended action: Prefer the simpler CI contract: when wiring is present, require ordinal equality after syntactic normalization and document that a CI store override must use the wired spelling exactly. This avoids any filesystem case inference. If accepting case-variant equivalent paths is a requirement, probe identity or case behavior at the store location or nearest existing parent and compare according to that result. Extend the contract test so its expected case behavior comes from the fixture filesystem rather than the operating system.

References:

Impacted locations:

  • crates/cargo-anvil/templates/justfiles/anvil/checks/bench-history.just:58-68
  • crates/cargo-anvil/tests/recipe_contracts.rs:1710-1724
  • crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap:1844-1848
  • crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap:2132-2138
  • crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap:520-525
  • justfiles/anvil/checks/bench-history.just:61-68

$entries = New-Object System.Collections.Generic.List[object]
$current = $null
foreach ($rawLine in (Get-Content -LiteralPath $blessingsFile)) {
$line = ($rawLine -split '#', 2)[0].Trim()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Copilot speaking]

Publish a durable blessing-file schema and example

The supported fix-or-accept workflow depends on a user-authored .config/bench-blessings.toml, but public design documentation does not define the exact syntax. The parser accepts only [[blessing]] entries with required benchmark, commit, and reason fields, double-quoted single-line values without escapes, comments only on otherwise empty lines, and non-option-shaped benchmark and commit values. benchmark is interpreted as a prefix. The copyable recipe example also uses an abbreviated commit ID even though the file is a long-lived audit record.

Reproducible reasoning: Ordinary valid TOML forms such as single-quoted strings, escaped values, and inline comments are rejected by the deliberately narrow parser. Users must also know that one benchmark value may match a family. The recipe claims the subset is documented in an emitted file header, but cargo-anvil does not emit that file. Finally, git rev-parse --verify accepts an abbreviated object name only while the prefix remains unique; repository growth can make an unchanged committed entry ambiguous. These syntax and identifier rules are part of the user-authored configuration contract, not private parser details.

Consequence: An adopter can commit valid-looking TOML that fails only in the next scheduled run, misunderstand a family-wide blessing, or have a formerly valid abbreviated commit become ambiguous later.

Recommended action: Document the fixed path and supported grammar in benchmarks.md or a linked configuration page. Include a canonical [[blessing]] example, define the required fields and prefix semantics, list the supported quoting/comment restrictions and option-shaped-value rule, and recommend a full commit object ID for durable entries. Link the recipe header to that contract and remove its inaccurate emitted-header claim. Emitting a commented starter file is an alternative, but adds another generated artifact that adopters must own.

References:

Impacted locations:

  • crates/cargo-anvil/docs/design/benchmarks.md:156-169
  • crates/cargo-anvil/templates/justfiles/anvil/checks/bench-history.just:154-185
  • crates/cargo-anvil/templates/justfiles/anvil/checks/bench-history.just:278-283
  • justfiles/anvil/checks/bench-history.just:154-185
  • justfiles/anvil/checks/bench-history.just:278-283
  • crates/cargo-anvil/tests/recipe_contracts.rs:1841-1886
  • crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap:619-625

# Process-scoped: two jobs sharing a machine (matrix legs on a self-hosted
# agent, or concurrent local runs) would otherwise race on one filename and
# read each other's listing.
$listJson = Join-Path $tmpDir "anvil-bench-blessings-$PID.json"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Copilot speaking]

Make the process-scoping test concurrent and collision-sensitive

The test is presented as deterministic coverage for two blessing reconciliations sharing one temporary directory, and the resolved thread says it runs them concurrently. The implementation calls the blocking run_just helper in a for loop, so the first process exits before the second starts. Its fake list blessings --all responses also use the prefixes field, while production reconciliation reads only the concrete benchmark field from that command's window-shaped output.

Reproducible reasoning: Command::output() waits for each child, so no overlap reaches the listing write/read window. Reverting the $PID suffix to a fixed name still leaves sequential invocations isolated in time. Even with overlap, the current fixtures cannot make cross-consumption observable: $_.benchmark is null for both prefixes-only responses, so each invocation blesses regardless of which file it reads. A collision-sensitive setup needs alpha's own listing to contain concrete benchmark beta and beta's own listing to contain alpha; each should bless when it reads its own file, but skip if it reads the other's matching entry.

Consequence: The process-scoping suffix can regress to one shared filename while the test remains green, allowing overlapping jobs to read another process's blessing state and incorrectly skip or apply an acceptance entry.

Recommended action: Spawn both just children before waiting for either. Add a deterministic barrier in the fake list blessings path so both processes reach the shared write/read interval before either continues; do not rely on sleeps or timeouts. Use window-shaped benchmark fixtures arranged so swapping the files changes each process's decision, then assert each invocation applies only its own requested blessing and leaves one listing file per process.

Impacted locations:

  • crates/cargo-anvil/tests/recipe_contracts.rs:2220-2283

# prefix test for the broader `foo`, so a committed blessing of
# `foo` would be skipped while still leaving the build red -- with
# a log line claiming it was already in effect.
$already = $applied | Where-Object {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Copilot speaking]

Align persisted-store growth with the documented history horizon

The recipe appends immutable benchmark runs and blessing sidecars, and both workflow backends restore the previous store and upload the whole directory again. Artifact retention deletes older artifact snapshots but does not remove old objects copied into the newest snapshot. The design nevertheless calls retention a rolling history window, and the prefix-blessing discussion relies on that window to bound repeated sidecars.

Reproducible reasoning: collect --skip-existing adds a clean run for each new commit; blessing can add sidecars; no reviewed step invokes cargo bench-history prune or otherwise deletes aged objects. GitHub and Azure DevOps then publish the entire current store, carrying every old object into the newest artifact before older snapshots expire. The reconciliation queries deliberately scan back to 1970, consistent with an accumulating store. Consequently, transfer size, storage, and query work grow with the number of scheduled runs rather than with backend artifact retention.

Consequence: A long-lived adopter can spend increasing time and artifact quota downloading, scanning, and re-uploading history until the scheduled job exceeds service size or runtime limits, despite documentation suggesting bounded cost.

Recommended action: Make the persistence lifecycle and documented horizon agree. If a rolling window is intended, define a store-level age or sample policy and prune the current store before publication, using cargo-bench-history's guarded base-history pruning or an equivalent mechanism that also handles blessing sidecars; add a multi-run contract test showing the newest store stops growing past the policy. If unbounded long-lived history is intended, document and provision that growth, remove claims that artifact expiry bounds store contents, and revise the prefix-sidecar trade-off accordingly.

References:

Impacted locations:

  • crates/cargo-anvil/templates/justfiles/anvil/checks/bench-history.just:96-106
  • crates/cargo-anvil/templates/justfiles/anvil/checks/bench-history.just:252-266
  • justfiles/anvil/checks/bench-history.just:96-106
  • justfiles/anvil/checks/bench-history.just:252-266
  • crates/cargo-anvil/templates/github/run-group-action.yml:214-222
  • .github/actions/anvil-run-group/action.yml:214-222
  • crates/cargo-anvil/templates/ado/scheduled-stages.yml:126-140
  • crates/cargo-anvil/docs/design/benchmarks.md:88-110
  • crates/cargo-anvil/docs/design/benchmarks.md:188-193

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agency-rocket Touched by a rocket skill

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants