From 08a6f334a4cfbc3ac6d4bfbb09dad3cbd6636b5a Mon Sep 17 00:00:00 2001 From: "Martin Kolinek (from Dev Box)" Date: Fri, 31 Jul 2026 18:19:00 +0200 Subject: [PATCH 01/20] feat(anvil): publish scheduled failures as issues Generate a failure publisher for scheduled GitHub workflows that creates one incident issue and comments on repeated failures. Document the design and allow repositories to opt out through a reusable-workflow input. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>`nCopilot-Session: 1c90ffee-5127-4069-8bb1-cef1492a400c --- .anvil.lock | 6 +- .github/workflows/anvil-scheduled-impl.yml | 70 ++++++++++++++++++ .github/workflows/anvil-scheduled.yml | 1 + crates/cargo-anvil/README.md | 6 ++ crates/cargo-anvil/docs/design/github.md | 57 ++++++++++++++- .../cargo-anvil/src/anvil/artifacts/github.rs | 7 ++ crates/cargo-anvil/src/lib.rs | 6 ++ .../github/scheduled-impl-workflow.yml | 70 ++++++++++++++++++ .../github/scheduled-root-workflow.yml | 1 + .../snapshots/snapshots__github_backend.snap | 71 +++++++++++++++++++ 10 files changed, 291 insertions(+), 4 deletions(-) diff --git a/.anvil.lock b/.anvil.lock index 0c987e7b..1dc7554f 100644 --- a/.anvil.lock +++ b/.anvil.lock @@ -1,7 +1,7 @@ version = 1 tool = "anvil" tool_version = "0.4.0" -catalog_checksum = "sha256:df5327676750f40b3d34bea369cd683e40628dcfcab79d6c8fa3a3e209c33828" +catalog_checksum = "sha256:8fc40f9789d523f9daa4a2389376dac9e2bbc5e003280262f80702a0309c8e41" [[file]] path = ".anvil/container/Containerfile" @@ -85,11 +85,11 @@ checksum = "sha256:cb7996cc978eb3f6db6572a78eec1341bfbebb592c90f78f69d622eaacf6d [[file]] path = ".github/workflows/anvil-scheduled-impl.yml" -checksum = "sha256:c0404d734d90a3d4cb184fcb40536f61417c735c497212e39c156c40fcd374c8" +checksum = "sha256:781b61f724d79b769d1ec2af4767a4602e5498978f3bfd9aa277c0978351f64d" [[file]] path = ".github/workflows/anvil-scheduled.yml" -checksum = "sha256:91408602dc3ee274b593e234841934c749ff03bba0ee7846ab88247c06f20cae" +checksum = "sha256:d31b9878b377bc9bbfe4c0156e75b279dc1aeb6a421ff5089aeae623fe2a8974" [[file]] path = "justfiles/anvil/checks/aprz.just" diff --git a/.github/workflows/anvil-scheduled-impl.yml b/.github/workflows/anvil-scheduled-impl.yml index 8a622be7..c17e9406 100644 --- a/.github/workflows/anvil-scheduled-impl.yml +++ b/.github/workflows/anvil-scheduled-impl.yml @@ -24,6 +24,10 @@ on: description: Runner label for aarch64 Windows jobs. type: string default: windows-11-arm + publish_failure_issue: + description: Create or update a GitHub issue when scheduled checks fail. + type: boolean + default: true secrets: CODECOV_TOKEN: description: | @@ -122,3 +126,69 @@ jobs: with: lfs: true - uses: ./.github/actions/anvil-scheduled-exhaustive + + publish-failure: + name: Publish scheduled failure + needs: + - scheduled-test + - scheduled-advisories + - scheduled-runtime-analysis + - scheduled-exhaustive + if: ${{ always() && inputs.publish_failure_issue && contains(needs.*.result, 'failure') }} + runs-on: ${{ inputs.linux_runner }} + permissions: + contents: read + issues: write + steps: + - name: Create or update failure issue + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + ANVIL_JOB_RESULTS: ${{ toJSON(needs) }} + with: + script: | + const title = "[Anvil] Scheduled checks failed"; + const runUrl = + `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}` + + `/actions/runs/${context.runId}`; + const results = JSON.parse(process.env.ANVIL_JOB_RESULTS); + const failedJobs = Object.entries(results) + .filter(([, job]) => job.result === "failure") + .map(([job]) => `- \`${job}\``) + .join("\n"); + const body = [ + "The Anvil scheduled workflow failed.", + "", + "Failed jobs:", + failedJobs, + "", + `[View workflow run](${runUrl})`, + ].join("\n"); + + const openIssues = await github.paginate( + github.rest.issues.listForRepo, + { + owner: context.repo.owner, + repo: context.repo.repo, + state: "open", + per_page: 100, + }, + ); + const existing = openIssues.find( + issue => !issue.pull_request && issue.title === title, + ); + + if (existing) { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: existing.number, + body, + }); + } else { + await github.rest.issues.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title, + body, + }); + } diff --git a/.github/workflows/anvil-scheduled.yml b/.github/workflows/anvil-scheduled.yml index 7421a886..0c4e3812 100644 --- a/.github/workflows/anvil-scheduled.yml +++ b/.github/workflows/anvil-scheduled.yml @@ -18,4 +18,5 @@ jobs: uses: ./.github/workflows/anvil-scheduled-impl.yml permissions: contents: read + issues: write secrets: inherit diff --git a/crates/cargo-anvil/README.md b/crates/cargo-anvil/README.md index 3c4887c9..9d3931bd 100644 --- a/crates/cargo-anvil/README.md +++ b/crates/cargo-anvil/README.md @@ -95,6 +95,12 @@ runs perform impact analysis (via [`cargo-delta`][__link0]) and run each check only over the affected packages, whereas a local `just anvil-pr` runs every check over the whole workspace. +The generated GitHub scheduled workflow publishes failures as GitHub +issues. It creates one issue for an active failure and comments on that +issue when later scheduled runs also fail, providing a durable incident +record without creating one issue per run. Repositories can disable this +behavior through the reusable workflow's `publish_failure_issue` input. + ### Containerized local checks Anvil can run any generated recipe in a content-addressed Linux container. diff --git a/crates/cargo-anvil/docs/design/github.md b/crates/cargo-anvil/docs/design/github.md index 0edffe2c..cb607414 100644 --- a/crates/cargo-anvil/docs/design/github.md +++ b/crates/cargo-anvil/docs/design/github.md @@ -128,6 +128,7 @@ flowchart LR sadv_job["scheduled-advisories
matrix: linux, windows,
linux-arm, windows-arm"]:::job srun_job["scheduled-runtime-analysis
matrix: linux, windows,
linux-arm, windows-arm"]:::job sexh_job["scheduled-exhaustive
matrix: linux, windows"]:::job + publish_job["publish-failure
upsert incident issue"]:::job stest_setup[".github/actions/
anvil-setup"]:::action sadv_setup[".github/actions/
anvil-setup"]:::action srun_setup[".github/actions/
anvil-setup"]:::action @@ -137,6 +138,7 @@ flowchart LR srun_act[".github/actions/
anvil-scheduled-runtime-analysis"]:::action sexh_act[".github/actions/
anvil-scheduled-exhaustive"]:::action codecov_act["codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f
v7.0.0"]:::external + github_issues["GitHub Issues"]:::external stest_just["just anvil-scheduled-test"]:::recipe stest_setup_just["just anvil-setup"]:::recipe sadv_just["just anvil-scheduled-advisories"]:::recipe @@ -150,22 +152,32 @@ flowchart LR sched_root -. uses .-> sched_impl sched_impl --> stest_job sched_impl --> sadv_job + sched_impl --> srun_job sched_impl --> sexh_job + stest_job --> publish_job + sadv_job --> publish_job + srun_job --> publish_job + sexh_job --> publish_job stest_job ==> stest_act stest_job ==> codecov_act sadv_job ==> sadv_act + srun_job ==> srun_act sexh_job ==> sexh_act + publish_job ==> github_issues stest_act ==> stest_setup stest_act ==> stest_just sadv_act ==> sadv_setup sadv_act ==> sadv_just + srun_act ==> srun_setup + srun_act ==> srun_just sexh_act ==> sexh_setup sexh_act ==> sexh_just stest_setup ==> stest_setup_just sadv_setup ==> sadv_setup_just + srun_setup ==> srun_setup_just sexh_setup ==> sexh_setup_just classDef trigger fill:#fff4d6,stroke:#b08800,stroke-width:1px; @@ -392,6 +404,7 @@ on: windows_runner: { type: string, default: windows-latest } linux_arm_runner: { type: string, default: ubuntu-24.04-arm } windows_arm_runner: { type: string, default: windows-11-arm } + publish_failure_issue: { type: boolean, default: true } jobs: scheduled-test: strategy: @@ -421,6 +434,15 @@ jobs: os: [linux, windows] runs-on: ${{ matrix.os == 'linux' && inputs.linux_runner || inputs.windows_runner }} steps: [ { uses: actions/checkout }, { uses: ./.github/actions/anvil-scheduled-exhaustive } ] + + publish-failure: + needs: [scheduled-test, scheduled-advisories, scheduled-runtime-analysis, scheduled-exhaustive] + if: ${{ always() && inputs.publish_failure_issue && contains(needs.*.result, 'failure') }} + runs-on: ${{ inputs.linux_runner }} + permissions: { contents: read, issues: write } + steps: + - uses: actions/github-script + # Upsert the stable "[Anvil] Scheduled checks failed" issue. ``` Scheduled composite actions don't receive any `include_*` inputs at all — their inputs @@ -441,6 +463,7 @@ The reusable workflow declares a small input set so the root workflow can pass o | `windows_runner` | string | `windows-latest` | Runner label for x86_64 Windows jobs. | | `linux_arm_runner` | string | `ubuntu-24.04-arm` | Runner label for aarch64 Linux jobs. | | `windows_arm_runner` | string | `windows-11-arm` | Runner label for aarch64 Windows jobs. | +| `publish_failure_issue` | boolean | `true` | Scheduled workflow only: create or update an issue when any scheduled group fails. | The input surface is intentionally narrow: only per-leg *runner labels* are exposed, because swapping in self-hosted runners is the one common need that doesn't require @@ -645,6 +668,9 @@ Recommended root workflow shape: - `permissions: contents: read` at the workflow level. anvil's default ships with this. +- The scheduled reusable-workflow call grants `issues: write` at job scope so its + publisher can create or comment on the failure issue. The PR workflow never receives + this permission. - No `pull-requests: write` (the PR-title check only needs the title from the event payload, which is already in `${{ github.event.pull_request.title }}`). - Scheduled-tier secrets, if any, live on `anvil-scheduled.yml` only — never on `anvil-pr.yml`. @@ -692,7 +718,36 @@ anvil does not gate the PR on coverage. The lcov upload is informational; Codeco own status check is the gating layer when the adopter wants one (configured in Codecov, visible as a separate required check in branch protection). -## 11. Advisory PR comments +## 11. Scheduled failure issues + +The GitHub scheduled reusable workflow publishes a failure as a repository issue by +default. The publisher depends on every scheduled group and uses `always()` so it can +inspect their terminal results even when one or more groups fail. It runs only when at +least one result is `failure`; successful, skipped, and cancelled runs do not create +issues. + +The issue title is the stable `[Anvil] Scheduled checks failed`. The publisher searches +all open repository issues for that exact title: + +- If none exists, it creates one containing the failed group names and a link to the + workflow run. +- If one exists, it adds the new failure details as a comment instead of creating a + duplicate. + +No label is required because repositories can remove or rename their default labels. +The issue remains open until a maintainer resolves the underlying failure and closes it. +If a later run fails after closure, the publisher creates a new incident issue. + +The publisher uses the workflow's short-lived `GITHUB_TOKEN`, with `issues: write` +granted only to the scheduled root call and publishing job. It does not receive repository +contents beyond read access and does not forward logs or environment data into the issue. +This narrow GitHub-native path also lets GitHub's Teams app relay issue notifications +without an external webhook or additional secret. + +Repositories that do not want issue publication set `publish_failure_issue: false` in +the root workflow's `with:` block and can remove `issues: write` from that call. + +## 12. Advisory PR comments Recipes that surface non-blocking findings exit 0 and write a markdown body to `target/anvil/comments/.md` (see [checks.md §6](./checks.md#6-advisory-pr-comments) diff --git a/crates/cargo-anvil/src/anvil/artifacts/github.rs b/crates/cargo-anvil/src/anvil/artifacts/github.rs index 9b030f85..9bdf09de 100644 --- a/crates/cargo-anvil/src/anvil/artifacts/github.rs +++ b/crates/cargo-anvil/src/anvil/artifacts/github.rs @@ -254,6 +254,7 @@ mod tests { "scheduled-advisories:", "scheduled-runtime-analysis:", "scheduled-exhaustive:", + "publish-failure:", ] { assert!( SCHEDULED_IMPL_WORKFLOW.contains(needle), @@ -261,6 +262,11 @@ mod tests { ); } assert!(SCHEDULED_IMPL_WORKFLOW.contains("codecov/codecov-action")); + assert!(SCHEDULED_IMPL_WORKFLOW.contains("publish_failure_issue:")); + assert!(SCHEDULED_IMPL_WORKFLOW.contains("contains(needs.*.result, 'failure')")); + assert!(SCHEDULED_IMPL_WORKFLOW.contains("actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd")); + assert!(SCHEDULED_IMPL_WORKFLOW.contains("github.rest.issues.createComment")); + assert!(SCHEDULED_IMPL_WORKFLOW.contains("github.rest.issues.create")); assert_eq!( SCHEDULED_IMPL_WORKFLOW.matches("free-disk-space: true").count(), 1, @@ -275,6 +281,7 @@ mod tests { assert!(PR_ROOT_WORKFLOW.contains("merge_group:")); assert!(SCHEDULED_ROOT_WORKFLOW.contains("uses: ./.github/workflows/anvil-scheduled-impl.yml")); assert!(SCHEDULED_ROOT_WORKFLOW.contains("schedule:")); + assert!(SCHEDULED_ROOT_WORKFLOW.contains("issues: write")); } #[test] diff --git a/crates/cargo-anvil/src/lib.rs b/crates/cargo-anvil/src/lib.rs index e9275f5d..f73916a7 100644 --- a/crates/cargo-anvil/src/lib.rs +++ b/crates/cargo-anvil/src/lib.rs @@ -96,6 +96,12 @@ //! and run each check only over the affected packages, whereas a local //! `just anvil-pr` runs every check over the whole workspace. //! +//! The generated GitHub scheduled workflow publishes failures as GitHub +//! issues. It creates one issue for an active failure and comments on that +//! issue when later scheduled runs also fail, providing a durable incident +//! record without creating one issue per run. Repositories can disable this +//! behavior through the reusable workflow's `publish_failure_issue` input. +//! //! ## Containerized local checks //! //! Anvil can run any generated recipe in a content-addressed Linux container. diff --git a/crates/cargo-anvil/templates/github/scheduled-impl-workflow.yml b/crates/cargo-anvil/templates/github/scheduled-impl-workflow.yml index 8a622be7..c17e9406 100644 --- a/crates/cargo-anvil/templates/github/scheduled-impl-workflow.yml +++ b/crates/cargo-anvil/templates/github/scheduled-impl-workflow.yml @@ -24,6 +24,10 @@ on: description: Runner label for aarch64 Windows jobs. type: string default: windows-11-arm + publish_failure_issue: + description: Create or update a GitHub issue when scheduled checks fail. + type: boolean + default: true secrets: CODECOV_TOKEN: description: | @@ -122,3 +126,69 @@ jobs: with: lfs: true - uses: ./.github/actions/anvil-scheduled-exhaustive + + publish-failure: + name: Publish scheduled failure + needs: + - scheduled-test + - scheduled-advisories + - scheduled-runtime-analysis + - scheduled-exhaustive + if: ${{ always() && inputs.publish_failure_issue && contains(needs.*.result, 'failure') }} + runs-on: ${{ inputs.linux_runner }} + permissions: + contents: read + issues: write + steps: + - name: Create or update failure issue + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + ANVIL_JOB_RESULTS: ${{ toJSON(needs) }} + with: + script: | + const title = "[Anvil] Scheduled checks failed"; + const runUrl = + `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}` + + `/actions/runs/${context.runId}`; + const results = JSON.parse(process.env.ANVIL_JOB_RESULTS); + const failedJobs = Object.entries(results) + .filter(([, job]) => job.result === "failure") + .map(([job]) => `- \`${job}\``) + .join("\n"); + const body = [ + "The Anvil scheduled workflow failed.", + "", + "Failed jobs:", + failedJobs, + "", + `[View workflow run](${runUrl})`, + ].join("\n"); + + const openIssues = await github.paginate( + github.rest.issues.listForRepo, + { + owner: context.repo.owner, + repo: context.repo.repo, + state: "open", + per_page: 100, + }, + ); + const existing = openIssues.find( + issue => !issue.pull_request && issue.title === title, + ); + + if (existing) { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: existing.number, + body, + }); + } else { + await github.rest.issues.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title, + body, + }); + } diff --git a/crates/cargo-anvil/templates/github/scheduled-root-workflow.yml b/crates/cargo-anvil/templates/github/scheduled-root-workflow.yml index 7421a886..0c4e3812 100644 --- a/crates/cargo-anvil/templates/github/scheduled-root-workflow.yml +++ b/crates/cargo-anvil/templates/github/scheduled-root-workflow.yml @@ -18,4 +18,5 @@ jobs: uses: ./.github/workflows/anvil-scheduled-impl.yml permissions: contents: read + issues: write secrets: inherit diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap index a3438bd3..9fc2dd0b 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap @@ -2304,6 +2304,10 @@ on: description: Runner label for aarch64 Windows jobs. type: string default: windows-11-arm + publish_failure_issue: + description: Create or update a GitHub issue when scheduled checks fail. + type: boolean + default: true secrets: CODECOV_TOKEN: description: | @@ -2403,6 +2407,72 @@ jobs: lfs: true - uses: ./.github/actions/anvil-scheduled-exhaustive + publish-failure: + name: Publish scheduled failure + needs: + - scheduled-test + - scheduled-advisories + - scheduled-runtime-analysis + - scheduled-exhaustive + if: ${{ always() && inputs.publish_failure_issue && contains(needs.*.result, 'failure') }} + runs-on: ${{ inputs.linux_runner }} + permissions: + contents: read + issues: write + steps: + - name: Create or update failure issue + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + ANVIL_JOB_RESULTS: ${{ toJSON(needs) }} + with: + script: | + const title = "[Anvil] Scheduled checks failed"; + const runUrl = + `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}` + + `/actions/runs/${context.runId}`; + const results = JSON.parse(process.env.ANVIL_JOB_RESULTS); + const failedJobs = Object.entries(results) + .filter(([, job]) => job.result === "failure") + .map(([job]) => `- \`${job}\``) + .join("\n"); + const body = [ + "The Anvil scheduled workflow failed.", + "", + "Failed jobs:", + failedJobs, + "", + `[View workflow run](${runUrl})`, + ].join("\n"); + + const openIssues = await github.paginate( + github.rest.issues.listForRepo, + { + owner: context.repo.owner, + repo: context.repo.repo, + state: "open", + per_page: 100, + }, + ); + const existing = openIssues.find( + issue => !issue.pull_request && issue.title === title, + ); + + if (existing) { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: existing.number, + body, + }); + } else { + await github.rest.issues.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title, + body, + }); + } + === .github/workflows/anvil-scheduled.yml === # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. @@ -2424,6 +2494,7 @@ jobs: uses: ./.github/workflows/anvil-scheduled-impl.yml permissions: contents: read + issues: write secrets: inherit === Cargo.toml === From 0f0ab97685f34732b2c575aa400ffc61c3268d1d Mon Sep 17 00:00:00 2001 From: "Martin Kolinek (from Dev Box)" Date: Fri, 31 Jul 2026 18:57:17 +0200 Subject: [PATCH 02/20] fix(anvil): configure issue publishing without workflow edits Use an Actions repository variable for the opt-out so adopters keep the generated root workflow on Anvil's automatic update path. Regenerate documentation and snapshots to satisfy README and spell checks. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1c90ffee-5127-4069-8bb1-cef1492a400c --- .anvil.lock | 4 ++-- .github/workflows/anvil-scheduled-impl.yml | 7 ++----- crates/cargo-anvil/README.md | 5 +++-- crates/cargo-anvil/docs/design/github.md | 12 +++++++----- crates/cargo-anvil/src/anvil/artifacts/github.rs | 2 +- crates/cargo-anvil/src/lib.rs | 3 ++- .../templates/github/scheduled-impl-workflow.yml | 7 ++----- .../tests/snapshots/snapshots__github_backend.snap | 7 ++----- 8 files changed, 21 insertions(+), 26 deletions(-) diff --git a/.anvil.lock b/.anvil.lock index 1dc7554f..df06241a 100644 --- a/.anvil.lock +++ b/.anvil.lock @@ -1,7 +1,7 @@ version = 1 tool = "anvil" tool_version = "0.4.0" -catalog_checksum = "sha256:8fc40f9789d523f9daa4a2389376dac9e2bbc5e003280262f80702a0309c8e41" +catalog_checksum = "sha256:3c19da7729ec213d3ce0c48f54cfcd5bbf1629b6cc87154604b8de6d2c1bb005" [[file]] path = ".anvil/container/Containerfile" @@ -85,7 +85,7 @@ checksum = "sha256:cb7996cc978eb3f6db6572a78eec1341bfbebb592c90f78f69d622eaacf6d [[file]] path = ".github/workflows/anvil-scheduled-impl.yml" -checksum = "sha256:781b61f724d79b769d1ec2af4767a4602e5498978f3bfd9aa277c0978351f64d" +checksum = "sha256:289701715bb4ec90f42cb5f341fe4afcc695ac06eff377b092b8463dd588a708" [[file]] path = ".github/workflows/anvil-scheduled.yml" diff --git a/.github/workflows/anvil-scheduled-impl.yml b/.github/workflows/anvil-scheduled-impl.yml index c17e9406..991899b1 100644 --- a/.github/workflows/anvil-scheduled-impl.yml +++ b/.github/workflows/anvil-scheduled-impl.yml @@ -24,10 +24,6 @@ on: description: Runner label for aarch64 Windows jobs. type: string default: windows-11-arm - publish_failure_issue: - description: Create or update a GitHub issue when scheduled checks fail. - type: boolean - default: true secrets: CODECOV_TOKEN: description: | @@ -134,7 +130,8 @@ jobs: - scheduled-advisories - scheduled-runtime-analysis - scheduled-exhaustive - if: ${{ always() && inputs.publish_failure_issue && contains(needs.*.result, 'failure') }} + if: ${{ always() && vars.ANVIL_PUBLISH_FAILURE_ISSUE != 'false' + && contains(needs.*.result, 'failure') }} runs-on: ${{ inputs.linux_runner }} permissions: contents: read diff --git a/crates/cargo-anvil/README.md b/crates/cargo-anvil/README.md index 9d3931bd..aab7ee3d 100644 --- a/crates/cargo-anvil/README.md +++ b/crates/cargo-anvil/README.md @@ -99,7 +99,8 @@ The generated GitHub scheduled workflow publishes failures as GitHub issues. It creates one issue for an active failure and comments on that issue when later scheduled runs also fail, providing a durable incident record without creating one issue per run. Repositories can disable this -behavior through the reusable workflow's `publish_failure_issue` input. +behavior by setting the `ANVIL_PUBLISH_FAILURE_ISSUE` Actions repository +variable to `false`. ### Containerized local checks @@ -433,7 +434,7 @@ And `docs/verification.md` for the continuous-validation strategy. This crate was developed as part of The Oxidizer Project. Browse this crate's source code. - [__cargo_doc2readme_dependencies_info]: ggGmYW0CYXZlMC43LjJhdIQbFhzZ8rzWNNYbuRaDSGWynFgbH4PMdoT7GNcbVwNPtPjAhvFhYvRhcoQbRQpVpEjw3x0b7FHf_9HBExgbfia0zvhKdz8bZ7R_zqIR8z1hZIGDa2NhcmdvLWFudmlsZTAuNC4wa2NhcmdvX2Fudmls + [__cargo_doc2readme_dependencies_info]: ggGmYW0CYXZlMC43LjJhdIQbFhzZ8rzWNNYbuRaDSGWynFgbH4PMdoT7GNcbVwNPtPjAhvFhYvRhcoQbngRfcjTrm3AbvT9_mReE10AbWwjIrRrJUP8bMn8ZsSasRElhZIGDa2NhcmdvLWFudmlsZTAuNC4wa2NhcmdvX2Fudmls [__link0]: https://crates.io/crates/cargo-delta [__link1]: https://crates.io/crates/cargo-spellcheck [__link2]: https://crates.io/crates/cargo-coverage-gate diff --git a/crates/cargo-anvil/docs/design/github.md b/crates/cargo-anvil/docs/design/github.md index cb607414..6add419e 100644 --- a/crates/cargo-anvil/docs/design/github.md +++ b/crates/cargo-anvil/docs/design/github.md @@ -404,7 +404,6 @@ on: windows_runner: { type: string, default: windows-latest } linux_arm_runner: { type: string, default: ubuntu-24.04-arm } windows_arm_runner: { type: string, default: windows-11-arm } - publish_failure_issue: { type: boolean, default: true } jobs: scheduled-test: strategy: @@ -437,7 +436,8 @@ jobs: publish-failure: needs: [scheduled-test, scheduled-advisories, scheduled-runtime-analysis, scheduled-exhaustive] - if: ${{ always() && inputs.publish_failure_issue && contains(needs.*.result, 'failure') }} + if: ${{ always() && vars.ANVIL_PUBLISH_FAILURE_ISSUE != 'false' + && contains(needs.*.result, 'failure') }} runs-on: ${{ inputs.linux_runner }} permissions: { contents: read, issues: write } steps: @@ -463,7 +463,6 @@ The reusable workflow declares a small input set so the root workflow can pass o | `windows_runner` | string | `windows-latest` | Runner label for x86_64 Windows jobs. | | `linux_arm_runner` | string | `ubuntu-24.04-arm` | Runner label for aarch64 Linux jobs. | | `windows_arm_runner` | string | `windows-11-arm` | Runner label for aarch64 Windows jobs. | -| `publish_failure_issue` | boolean | `true` | Scheduled workflow only: create or update an issue when any scheduled group fails. | The input surface is intentionally narrow: only per-leg *runner labels* are exposed, because swapping in self-hosted runners is the one common need that doesn't require @@ -744,8 +743,11 @@ contents beyond read access and does not forward logs or environment data into t This narrow GitHub-native path also lets GitHub's Teams app relay issue notifications without an external webhook or additional secret. -Repositories that do not want issue publication set `publish_failure_issue: false` in -the root workflow's `with:` block and can remove `issues: write` from that call. +Repositories that do not want issue publication set the +`ANVIL_PUBLISH_FAILURE_ISSUE` Actions repository variable to `false`. This configuration +lives in repository settings instead of an Anvil-owned workflow, so the root workflow +stays on the automatic update path. The scheduled call retains `issues: write`; the +publisher's condition prevents use of that permission when publication is disabled. ## 12. Advisory PR comments diff --git a/crates/cargo-anvil/src/anvil/artifacts/github.rs b/crates/cargo-anvil/src/anvil/artifacts/github.rs index 9bdf09de..6007574f 100644 --- a/crates/cargo-anvil/src/anvil/artifacts/github.rs +++ b/crates/cargo-anvil/src/anvil/artifacts/github.rs @@ -262,7 +262,7 @@ mod tests { ); } assert!(SCHEDULED_IMPL_WORKFLOW.contains("codecov/codecov-action")); - assert!(SCHEDULED_IMPL_WORKFLOW.contains("publish_failure_issue:")); + assert!(SCHEDULED_IMPL_WORKFLOW.contains("vars.ANVIL_PUBLISH_FAILURE_ISSUE != 'false'")); assert!(SCHEDULED_IMPL_WORKFLOW.contains("contains(needs.*.result, 'failure')")); assert!(SCHEDULED_IMPL_WORKFLOW.contains("actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd")); assert!(SCHEDULED_IMPL_WORKFLOW.contains("github.rest.issues.createComment")); diff --git a/crates/cargo-anvil/src/lib.rs b/crates/cargo-anvil/src/lib.rs index f73916a7..8e725fbc 100644 --- a/crates/cargo-anvil/src/lib.rs +++ b/crates/cargo-anvil/src/lib.rs @@ -100,7 +100,8 @@ //! issues. It creates one issue for an active failure and comments on that //! issue when later scheduled runs also fail, providing a durable incident //! record without creating one issue per run. Repositories can disable this -//! behavior through the reusable workflow's `publish_failure_issue` input. +//! behavior by setting the `ANVIL_PUBLISH_FAILURE_ISSUE` Actions repository +//! variable to `false`. //! //! ## Containerized local checks //! diff --git a/crates/cargo-anvil/templates/github/scheduled-impl-workflow.yml b/crates/cargo-anvil/templates/github/scheduled-impl-workflow.yml index c17e9406..991899b1 100644 --- a/crates/cargo-anvil/templates/github/scheduled-impl-workflow.yml +++ b/crates/cargo-anvil/templates/github/scheduled-impl-workflow.yml @@ -24,10 +24,6 @@ on: description: Runner label for aarch64 Windows jobs. type: string default: windows-11-arm - publish_failure_issue: - description: Create or update a GitHub issue when scheduled checks fail. - type: boolean - default: true secrets: CODECOV_TOKEN: description: | @@ -134,7 +130,8 @@ jobs: - scheduled-advisories - scheduled-runtime-analysis - scheduled-exhaustive - if: ${{ always() && inputs.publish_failure_issue && contains(needs.*.result, 'failure') }} + if: ${{ always() && vars.ANVIL_PUBLISH_FAILURE_ISSUE != 'false' + && contains(needs.*.result, 'failure') }} runs-on: ${{ inputs.linux_runner }} permissions: contents: read diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap index 9fc2dd0b..d51a61ba 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap @@ -2304,10 +2304,6 @@ on: description: Runner label for aarch64 Windows jobs. type: string default: windows-11-arm - publish_failure_issue: - description: Create or update a GitHub issue when scheduled checks fail. - type: boolean - default: true secrets: CODECOV_TOKEN: description: | @@ -2414,7 +2410,8 @@ jobs: - scheduled-advisories - scheduled-runtime-analysis - scheduled-exhaustive - if: ${{ always() && inputs.publish_failure_issue && contains(needs.*.result, 'failure') }} + if: ${{ always() && vars.ANVIL_PUBLISH_FAILURE_ISSUE != 'false' + && contains(needs.*.result, 'failure') }} runs-on: ${{ inputs.linux_runner }} permissions: contents: read From c91cc44fb31c5d380aaac316f233e11362e925ab Mon Sep 17 00:00:00 2001 From: "Martin Kolinek (from Dev Box)" Date: Fri, 31 Jul 2026 19:10:17 +0200 Subject: [PATCH 03/20] docs: document scheduled issue publishing opt-out Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1c90ffee-5127-4069-8bb1-cef1492a400c --- crates/cargo-anvil/README.md | 12 +++++++++++- crates/cargo-anvil/src/lib.rs | 10 ++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/crates/cargo-anvil/README.md b/crates/cargo-anvil/README.md index aab7ee3d..6443e105 100644 --- a/crates/cargo-anvil/README.md +++ b/crates/cargo-anvil/README.md @@ -299,6 +299,16 @@ Four escape valves, in increasing severity: region. The next `update` detects the dirt and writes a `.anvil-proposed` sibling instead of overwriting. +#### Scheduled failure issue publication (GitHub) + +The generated GitHub scheduled workflow creates or updates +`[Anvil] Scheduled checks failed` when a scheduled group fails. +To disable this behavior without editing an Anvil-owned workflow, +set the Actions repository variable `ANVIL_PUBLISH_FAILURE_ISSUE` +to `false` under **Settings → Secrets and variables → Actions → +Variables**. Removing the variable or setting any other value +restores the default publication behavior. + ### In-tree tool customization anvil follows a few source-level and `Cargo.toml` conventions so you @@ -434,7 +444,7 @@ And `docs/verification.md` for the continuous-validation strategy. This crate was developed as part of The Oxidizer Project. Browse this crate's source code. - [__cargo_doc2readme_dependencies_info]: ggGmYW0CYXZlMC43LjJhdIQbFhzZ8rzWNNYbuRaDSGWynFgbH4PMdoT7GNcbVwNPtPjAhvFhYvRhcoQbngRfcjTrm3AbvT9_mReE10AbWwjIrRrJUP8bMn8ZsSasRElhZIGDa2NhcmdvLWFudmlsZTAuNC4wa2NhcmdvX2Fudmls + [__cargo_doc2readme_dependencies_info]: ggGmYW0CYXZlMC43LjJhdIQbFhzZ8rzWNNYbuRaDSGWynFgbH4PMdoT7GNcbVwNPtPjAhvFhYvRhcoQbRcIvCeEgxSQbz0a05iQ92jUbOvdahLnAmC8bqB4WbnXRqkVhZIGDa2NhcmdvLWFudmlsZTAuNC4wa2NhcmdvX2Fudmls [__link0]: https://crates.io/crates/cargo-delta [__link1]: https://crates.io/crates/cargo-spellcheck [__link2]: https://crates.io/crates/cargo-coverage-gate diff --git a/crates/cargo-anvil/src/lib.rs b/crates/cargo-anvil/src/lib.rs index 8e725fbc..c410e5af 100644 --- a/crates/cargo-anvil/src/lib.rs +++ b/crates/cargo-anvil/src/lib.rs @@ -300,6 +300,16 @@ //! region. The next `update` detects the dirt and writes a //! `.anvil-proposed` sibling instead of overwriting. //! +//! ### Scheduled failure issue publication (GitHub) +//! +//! The generated GitHub scheduled workflow creates or updates +//! `[Anvil] Scheduled checks failed` when a scheduled group fails. +//! To disable this behavior without editing an Anvil-owned workflow, +//! set the Actions repository variable `ANVIL_PUBLISH_FAILURE_ISSUE` +//! to `false` under **Settings → Secrets and variables → Actions → +//! Variables**. Removing the variable or setting any other value +//! restores the default publication behavior. +//! //! ## In-tree tool customization //! //! anvil follows a few source-level and `Cargo.toml` conventions so you From 3cdd50576476888a3632ea93c77f7c5b2ef2c64c Mon Sep 17 00:00:00 2001 From: "Martin Kolinek (from Dev Box)" Date: Fri, 31 Jul 2026 20:23:31 +0200 Subject: [PATCH 04/20] fix: use scoped search for failure issues Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1c90ffee-5127-4069-8bb1-cef1492a400c --- .anvil.lock | 4 ++-- .github/workflows/anvil-scheduled-impl.yml | 19 ++++++++----------- .../cargo-anvil/src/anvil/artifacts/github.rs | 1 + .../github/scheduled-impl-workflow.yml | 19 ++++++++----------- .../snapshots/snapshots__github_backend.snap | 19 ++++++++----------- 5 files changed, 27 insertions(+), 35 deletions(-) diff --git a/.anvil.lock b/.anvil.lock index df06241a..7b1b1d30 100644 --- a/.anvil.lock +++ b/.anvil.lock @@ -1,7 +1,7 @@ version = 1 tool = "anvil" tool_version = "0.4.0" -catalog_checksum = "sha256:3c19da7729ec213d3ce0c48f54cfcd5bbf1629b6cc87154604b8de6d2c1bb005" +catalog_checksum = "sha256:115e6c9c29a7371a2a1a2ad652d458a55a7421b2717f365d06572ad34e7f1e35" [[file]] path = ".anvil/container/Containerfile" @@ -85,7 +85,7 @@ checksum = "sha256:cb7996cc978eb3f6db6572a78eec1341bfbebb592c90f78f69d622eaacf6d [[file]] path = ".github/workflows/anvil-scheduled-impl.yml" -checksum = "sha256:289701715bb4ec90f42cb5f341fe4afcc695ac06eff377b092b8463dd588a708" +checksum = "sha256:53090e7771191d9ced355337441f9c5c89924420d05fcf4a19aba32ab6d15729" [[file]] path = ".github/workflows/anvil-scheduled.yml" diff --git a/.github/workflows/anvil-scheduled-impl.yml b/.github/workflows/anvil-scheduled-impl.yml index 991899b1..615e6fe5 100644 --- a/.github/workflows/anvil-scheduled-impl.yml +++ b/.github/workflows/anvil-scheduled-impl.yml @@ -161,18 +161,15 @@ jobs: `[View workflow run](${runUrl})`, ].join("\n"); - const openIssues = await github.paginate( - github.rest.issues.listForRepo, - { - owner: context.repo.owner, - repo: context.repo.repo, - state: "open", + const query = + `repo:${context.repo.owner}/${context.repo.repo} ` + + `is:issue is:open in:title "${title}"`; + const { data: search } = + await github.rest.search.issuesAndPullRequests({ + q: query, per_page: 100, - }, - ); - const existing = openIssues.find( - issue => !issue.pull_request && issue.title === title, - ); + }); + const existing = search.items.find(issue => issue.title === title); if (existing) { await github.rest.issues.createComment({ diff --git a/crates/cargo-anvil/src/anvil/artifacts/github.rs b/crates/cargo-anvil/src/anvil/artifacts/github.rs index 6007574f..b3a74e53 100644 --- a/crates/cargo-anvil/src/anvil/artifacts/github.rs +++ b/crates/cargo-anvil/src/anvil/artifacts/github.rs @@ -265,6 +265,7 @@ mod tests { assert!(SCHEDULED_IMPL_WORKFLOW.contains("vars.ANVIL_PUBLISH_FAILURE_ISSUE != 'false'")); assert!(SCHEDULED_IMPL_WORKFLOW.contains("contains(needs.*.result, 'failure')")); assert!(SCHEDULED_IMPL_WORKFLOW.contains("actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd")); + assert!(SCHEDULED_IMPL_WORKFLOW.contains("github.rest.search.issuesAndPullRequests")); assert!(SCHEDULED_IMPL_WORKFLOW.contains("github.rest.issues.createComment")); assert!(SCHEDULED_IMPL_WORKFLOW.contains("github.rest.issues.create")); assert_eq!( diff --git a/crates/cargo-anvil/templates/github/scheduled-impl-workflow.yml b/crates/cargo-anvil/templates/github/scheduled-impl-workflow.yml index 991899b1..615e6fe5 100644 --- a/crates/cargo-anvil/templates/github/scheduled-impl-workflow.yml +++ b/crates/cargo-anvil/templates/github/scheduled-impl-workflow.yml @@ -161,18 +161,15 @@ jobs: `[View workflow run](${runUrl})`, ].join("\n"); - const openIssues = await github.paginate( - github.rest.issues.listForRepo, - { - owner: context.repo.owner, - repo: context.repo.repo, - state: "open", + const query = + `repo:${context.repo.owner}/${context.repo.repo} ` + + `is:issue is:open in:title "${title}"`; + const { data: search } = + await github.rest.search.issuesAndPullRequests({ + q: query, per_page: 100, - }, - ); - const existing = openIssues.find( - issue => !issue.pull_request && issue.title === title, - ); + }); + const existing = search.items.find(issue => issue.title === title); if (existing) { await github.rest.issues.createComment({ diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap index d51a61ba..615af4d9 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap @@ -2441,18 +2441,15 @@ jobs: `[View workflow run](${runUrl})`, ].join("\n"); - const openIssues = await github.paginate( - github.rest.issues.listForRepo, - { - owner: context.repo.owner, - repo: context.repo.repo, - state: "open", + const query = + `repo:${context.repo.owner}/${context.repo.repo} ` + + `is:issue is:open in:title "${title}"`; + const { data: search } = + await github.rest.search.issuesAndPullRequests({ + q: query, per_page: 100, - }, - ); - const existing = openIssues.find( - issue => !issue.pull_request && issue.title === title, - ); + }); + const existing = search.items.find(issue => issue.title === title); if (existing) { await github.rest.issues.createComment({ From 3c5b6d57dad16ebbbf6a3b210856ddceea721f84 Mon Sep 17 00:00:00 2001 From: "Martin Kolinek (from Dev Box)" Date: Mon, 3 Aug 2026 17:13:42 +0200 Subject: [PATCH 05/20] review: harden scheduled failure issue upsert Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1c90ffee-5127-4069-8bb1-cef1492a400c --- .anvil.lock | 4 +- .github/workflows/anvil-scheduled-impl.yml | 9 +- crates/cargo-anvil/docs/design/github.md | 19 +++- .../cargo-anvil/src/anvil/artifacts/github.rs | 105 ++++++++++++++++++ .../github/scheduled-impl-workflow.yml | 9 +- .../snapshots/snapshots__github_backend.snap | 9 +- 6 files changed, 145 insertions(+), 10 deletions(-) diff --git a/.anvil.lock b/.anvil.lock index 7b1b1d30..a22c2887 100644 --- a/.anvil.lock +++ b/.anvil.lock @@ -1,7 +1,7 @@ version = 1 tool = "anvil" tool_version = "0.4.0" -catalog_checksum = "sha256:115e6c9c29a7371a2a1a2ad652d458a55a7421b2717f365d06572ad34e7f1e35" +catalog_checksum = "sha256:9758e00880ab0b93cc26b387c9c1131917eb20491fe2d60cb128afb782f27a8b" [[file]] path = ".anvil/container/Containerfile" @@ -85,7 +85,7 @@ checksum = "sha256:cb7996cc978eb3f6db6572a78eec1341bfbebb592c90f78f69d622eaacf6d [[file]] path = ".github/workflows/anvil-scheduled-impl.yml" -checksum = "sha256:53090e7771191d9ced355337441f9c5c89924420d05fcf4a19aba32ab6d15729" +checksum = "sha256:06cb668f5de38b3a06742a96fd78e575d459af682d418f31119d62b2fa2c3ecf" [[file]] path = ".github/workflows/anvil-scheduled.yml" diff --git a/.github/workflows/anvil-scheduled-impl.yml b/.github/workflows/anvil-scheduled-impl.yml index 615e6fe5..6e0861c5 100644 --- a/.github/workflows/anvil-scheduled-impl.yml +++ b/.github/workflows/anvil-scheduled-impl.yml @@ -144,6 +144,7 @@ jobs: with: script: | const title = "[Anvil] Scheduled checks failed"; + const marker = ""; const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}` + `/actions/runs/${context.runId}`; @@ -153,6 +154,8 @@ jobs: .map(([job]) => `- \`${job}\``) .join("\n"); const body = [ + marker, + "", "The Anvil scheduled workflow failed.", "", "Failed jobs:", @@ -163,13 +166,15 @@ jobs: const query = `repo:${context.repo.owner}/${context.repo.repo} ` + - `is:issue is:open in:title "${title}"`; + `is:issue is:open in:body "anvil scheduled failure"`; const { data: search } = await github.rest.search.issuesAndPullRequests({ q: query, per_page: 100, }); - const existing = search.items.find(issue => issue.title === title); + const existing = search.items.find( + issue => issue.body?.includes(marker), + ); if (existing) { await github.rest.issues.createComment({ diff --git a/crates/cargo-anvil/docs/design/github.md b/crates/cargo-anvil/docs/design/github.md index 6add419e..9fdb2e2e 100644 --- a/crates/cargo-anvil/docs/design/github.md +++ b/crates/cargo-anvil/docs/design/github.md @@ -725,14 +725,22 @@ inspect their terminal results even when one or more groups fail. It runs only w least one result is `failure`; successful, skipped, and cancelled runs do not create issues. -The issue title is the stable `[Anvil] Scheduled checks failed`. The publisher searches -all open repository issues for that exact title: +The issue title is `[Anvil] Scheduled checks failed`, while the stable hidden marker +`` identifies an issue owned by the publisher. The +publisher makes one repository-scoped Search API request for open issues whose bodies +match the marker terms, then verifies the exact marker client-side: - If none exists, it creates one containing the failed group names and a link to the workflow run. - If one exists, it adds the new failure details as a comment instead of creating a duplicate. +This is a best-effort upsert: GitHub's search index is eventually consistent and the +single request considers at most 100 results, so closely overlapping failures can +occasionally create duplicate incident issues. Marker-based identity prevents a +human-authored issue with the same title from being reused and survives a maintainer +renaming an Anvil incident. + No label is required because repositories can remove or rename their default labels. The issue remains open until a maintainer resolves the underlying failure and closes it. If a later run fails after closure, the publisher creates a new incident issue. @@ -743,6 +751,13 @@ contents beyond read access and does not forward logs or environment data into t This narrow GitHub-native path also lets GitHub's Teams app relay issue notifications without an external webhook or additional secret. +The generated root and implementation workflows must be updated together. A repository +that has taken ownership of the root workflow must retain `issues: write` on the reusable +workflow call (or apply the generated `.anvil-proposed` update) when adopting this job. +Repositories with Issues disabled cannot publish failure incidents. Missing permission or +disabled Issues deliberately fails the publishing job rather than silently losing the +notification; the original failing scheduled jobs remain visible alongside that error. + Repositories that do not want issue publication set the `ANVIL_PUBLISH_FAILURE_ISSUE` Actions repository variable to `false`. This configuration lives in repository settings instead of an Anvil-owned workflow, so the root workflow diff --git a/crates/cargo-anvil/src/anvil/artifacts/github.rs b/crates/cargo-anvil/src/anvil/artifacts/github.rs index b3a74e53..bce4b102 100644 --- a/crates/cargo-anvil/src/anvil/artifacts/github.rs +++ b/crates/cargo-anvil/src/anvil/artifacts/github.rs @@ -142,6 +142,8 @@ pub(crate) fn all() -> Vec { #[cfg(test)] #[cfg_attr(coverage_nightly, coverage(off))] mod tests { + use std::{fs, process::Command}; + use super::*; #[test] @@ -275,6 +277,109 @@ mod tests { ); } + #[test] + fn scheduled_failure_script_upserts_marker_owned_issues() { + let script = SCHEDULED_IMPL_WORKFLOW + .split_once(" script: |\n") + .expect("scheduled workflow should contain an inline script") + .1 + .lines() + .map(|line| line.strip_prefix(" ").unwrap_or(line)) + .collect::>() + .join("\n"); + let harness = format!("const workflowScript = {script:?};\n") + + r#" +const assert = require("node:assert/strict"); +const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor; +const run = new AsyncFunction("github", "context", "process", workflowScript); +const marker = ""; +const title = "[Anvil] Scheduled checks failed"; +const context = { + serverUrl: "https://github.com", + repo: { owner: "microsoft", repo: "ox-tools" }, + runId: 42, +}; + +async function scenario(items) { + const calls = { search: [], create: [], comment: [] }; + const github = { + rest: { + search: { + issuesAndPullRequests: async args => { + calls.search.push(args); + return { data: { items } }; + }, + }, + issues: { + create: async args => calls.create.push(args), + createComment: async args => calls.comment.push(args), + }, + }, + }; + const process = { + env: { + ANVIL_JOB_RESULTS: JSON.stringify({ + "scheduled-test": { result: "failure" }, + "scheduled-advisories": { result: "success" }, + "scheduled-runtime-analysis": { result: "cancelled" }, + "scheduled-exhaustive": { result: "failure" }, + }), + }, + }; + await run(github, context, process); + return calls; +} + +(async () => { + const created = await scenario([]); + assert.equal(created.search.length, 1); + assert.match(created.search[0].q, /in:body/); + assert.equal(created.create.length, 1); + assert.equal(created.comment.length, 0); + assert.equal(created.create[0].title, title); + assert.match(created.create[0].body, new RegExp(marker)); + assert.match(created.create[0].body, /- `scheduled-test`/); + assert.match(created.create[0].body, /- `scheduled-exhaustive`/); + assert.doesNotMatch(created.create[0].body, /scheduled-advisories/); + assert.doesNotMatch(created.create[0].body, /scheduled-runtime-analysis/); + assert.match( + created.create[0].body, + /https:\/\/github\.com\/microsoft\/ox-tools\/actions\/runs\/42/, + ); + + const existing = await scenario([ + { number: 17, title: "Maintainer-renamed incident", body: marker }, + ]); + assert.equal(existing.create.length, 0); + assert.equal(existing.comment.length, 1); + assert.equal(existing.comment[0].issue_number, 17); + + const collision = await scenario([ + { number: 23, title, body: "A human-authored issue without the marker." }, + ]); + assert.equal(collision.create.length, 1); + assert.equal(collision.comment.length, 0); +})().catch(error => { + console.error(error); + process.exitCode = 1; +}); +"#; + + let dir = tempfile::tempdir().expect("create temporary test directory"); + let path = dir.path().join("scheduled-failure.test.cjs"); + fs::write(&path, harness).expect("write JavaScript behavior test"); + let output = Command::new("node") + .arg(&path) + .output() + .expect("Node.js is required to test the generated github-script"); + assert!( + output.status.success(), + "generated github-script behavior test failed:\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + } + #[test] fn root_workflows_call_reusable_workflows() { assert!(PR_ROOT_WORKFLOW.contains("uses: ./.github/workflows/anvil-pr-impl.yml")); diff --git a/crates/cargo-anvil/templates/github/scheduled-impl-workflow.yml b/crates/cargo-anvil/templates/github/scheduled-impl-workflow.yml index 615e6fe5..6e0861c5 100644 --- a/crates/cargo-anvil/templates/github/scheduled-impl-workflow.yml +++ b/crates/cargo-anvil/templates/github/scheduled-impl-workflow.yml @@ -144,6 +144,7 @@ jobs: with: script: | const title = "[Anvil] Scheduled checks failed"; + const marker = ""; const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}` + `/actions/runs/${context.runId}`; @@ -153,6 +154,8 @@ jobs: .map(([job]) => `- \`${job}\``) .join("\n"); const body = [ + marker, + "", "The Anvil scheduled workflow failed.", "", "Failed jobs:", @@ -163,13 +166,15 @@ jobs: const query = `repo:${context.repo.owner}/${context.repo.repo} ` + - `is:issue is:open in:title "${title}"`; + `is:issue is:open in:body "anvil scheduled failure"`; const { data: search } = await github.rest.search.issuesAndPullRequests({ q: query, per_page: 100, }); - const existing = search.items.find(issue => issue.title === title); + const existing = search.items.find( + issue => issue.body?.includes(marker), + ); if (existing) { await github.rest.issues.createComment({ diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap index 615af4d9..7607b6d3 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap @@ -2424,6 +2424,7 @@ jobs: with: script: | const title = "[Anvil] Scheduled checks failed"; + const marker = ""; const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}` + `/actions/runs/${context.runId}`; @@ -2433,6 +2434,8 @@ jobs: .map(([job]) => `- \`${job}\``) .join("\n"); const body = [ + marker, + "", "The Anvil scheduled workflow failed.", "", "Failed jobs:", @@ -2443,13 +2446,15 @@ jobs: const query = `repo:${context.repo.owner}/${context.repo.repo} ` + - `is:issue is:open in:title "${title}"`; + `is:issue is:open in:body "anvil scheduled failure"`; const { data: search } = await github.rest.search.issuesAndPullRequests({ q: query, per_page: 100, }); - const existing = search.items.find(issue => issue.title === title); + const existing = search.items.find( + issue => issue.body?.includes(marker), + ); if (existing) { await github.rest.issues.createComment({ From d1e77c979c4eae2a5cc540f58e338cf9e17d0380 Mon Sep 17 00:00:00 2001 From: "Martin Kolinek (from Dev Box)" Date: Mon, 3 Aug 2026 17:20:31 +0200 Subject: [PATCH 06/20] fix: align failure marker with search phrase Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1c90ffee-5127-4069-8bb1-cef1492a400c --- .github/workflows/anvil-scheduled-impl.yml | 2 +- crates/cargo-anvil/docs/design/github.md | 2 +- crates/cargo-anvil/src/anvil/artifacts/github.rs | 2 +- crates/cargo-anvil/templates/github/scheduled-impl-workflow.yml | 2 +- .../cargo-anvil/tests/snapshots/snapshots__github_backend.snap | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/anvil-scheduled-impl.yml b/.github/workflows/anvil-scheduled-impl.yml index 6e0861c5..04310dfc 100644 --- a/.github/workflows/anvil-scheduled-impl.yml +++ b/.github/workflows/anvil-scheduled-impl.yml @@ -144,7 +144,7 @@ jobs: with: script: | const title = "[Anvil] Scheduled checks failed"; - const marker = ""; + const marker = ""; const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}` + `/actions/runs/${context.runId}`; diff --git a/crates/cargo-anvil/docs/design/github.md b/crates/cargo-anvil/docs/design/github.md index 9fdb2e2e..97120e62 100644 --- a/crates/cargo-anvil/docs/design/github.md +++ b/crates/cargo-anvil/docs/design/github.md @@ -726,7 +726,7 @@ least one result is `failure`; successful, skipped, and cancelled runs do not cr issues. The issue title is `[Anvil] Scheduled checks failed`, while the stable hidden marker -`` identifies an issue owned by the publisher. The +`` identifies an issue owned by the publisher. The publisher makes one repository-scoped Search API request for open issues whose bodies match the marker terms, then verifies the exact marker client-side: diff --git a/crates/cargo-anvil/src/anvil/artifacts/github.rs b/crates/cargo-anvil/src/anvil/artifacts/github.rs index bce4b102..01fc5cab 100644 --- a/crates/cargo-anvil/src/anvil/artifacts/github.rs +++ b/crates/cargo-anvil/src/anvil/artifacts/github.rs @@ -292,7 +292,7 @@ mod tests { const assert = require("node:assert/strict"); const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor; const run = new AsyncFunction("github", "context", "process", workflowScript); -const marker = ""; +const marker = ""; const title = "[Anvil] Scheduled checks failed"; const context = { serverUrl: "https://github.com", diff --git a/crates/cargo-anvil/templates/github/scheduled-impl-workflow.yml b/crates/cargo-anvil/templates/github/scheduled-impl-workflow.yml index 6e0861c5..04310dfc 100644 --- a/crates/cargo-anvil/templates/github/scheduled-impl-workflow.yml +++ b/crates/cargo-anvil/templates/github/scheduled-impl-workflow.yml @@ -144,7 +144,7 @@ jobs: with: script: | const title = "[Anvil] Scheduled checks failed"; - const marker = ""; + const marker = ""; const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}` + `/actions/runs/${context.runId}`; diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap index 7607b6d3..827b96b0 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap @@ -2424,7 +2424,7 @@ jobs: with: script: | const title = "[Anvil] Scheduled checks failed"; - const marker = ""; + const marker = ""; const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}` + `/actions/runs/${context.runId}`; From f34b2410940eda86164a6d22eff1fae47da58da5 Mon Sep 17 00:00:00 2001 From: "Martin Kolinek (from Dev Box)" Date: Mon, 3 Aug 2026 17:27:14 +0200 Subject: [PATCH 07/20] fix: match nightly rustfmt import layout Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1c90ffee-5127-4069-8bb1-cef1492a400c --- crates/cargo-anvil/src/anvil/artifacts/github.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/cargo-anvil/src/anvil/artifacts/github.rs b/crates/cargo-anvil/src/anvil/artifacts/github.rs index 01fc5cab..cf00f369 100644 --- a/crates/cargo-anvil/src/anvil/artifacts/github.rs +++ b/crates/cargo-anvil/src/anvil/artifacts/github.rs @@ -142,7 +142,8 @@ pub(crate) fn all() -> Vec { #[cfg(test)] #[cfg_attr(coverage_nightly, coverage(off))] mod tests { - use std::{fs, process::Command}; + use std::fs; + use std::process::Command; use super::*; From 3a64a649ac41f521a39814fc96031fe8b0234abe Mon Sep 17 00:00:00 2001 From: "Martin Kolinek (from Dev Box)" Date: Mon, 3 Aug 2026 17:34:22 +0200 Subject: [PATCH 08/20] fix: skip subprocess test under Miri isolation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1c90ffee-5127-4069-8bb1-cef1492a400c --- crates/cargo-anvil/src/anvil/artifacts/github.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/cargo-anvil/src/anvil/artifacts/github.rs b/crates/cargo-anvil/src/anvil/artifacts/github.rs index cf00f369..ccdacd2f 100644 --- a/crates/cargo-anvil/src/anvil/artifacts/github.rs +++ b/crates/cargo-anvil/src/anvil/artifacts/github.rs @@ -279,6 +279,7 @@ mod tests { } #[test] + #[cfg_attr(miri, ignore = "uses filesystem and subprocesses; miri isolation forbids them")] fn scheduled_failure_script_upserts_marker_owned_issues() { let script = SCHEDULED_IMPL_WORKFLOW .split_once(" script: |\n") From f64bf6c5e0654ed46c663d27c46608e92c99e30b Mon Sep 17 00:00:00 2001 From: "Martin Kolinek (from Dev Box)" Date: Mon, 3 Aug 2026 18:01:06 +0200 Subject: [PATCH 09/20] test: harden scheduled issue script harness Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1c90ffee-5127-4069-8bb1-cef1492a400c --- crates/cargo-anvil/src/anvil/artifacts/github.rs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/crates/cargo-anvil/src/anvil/artifacts/github.rs b/crates/cargo-anvil/src/anvil/artifacts/github.rs index ccdacd2f..5e610507 100644 --- a/crates/cargo-anvil/src/anvil/artifacts/github.rs +++ b/crates/cargo-anvil/src/anvil/artifacts/github.rs @@ -295,12 +295,16 @@ const assert = require("node:assert/strict"); const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor; const run = new AsyncFunction("github", "context", "process", workflowScript); const marker = ""; +const searchableMarker = marker.replace(/^$/g, ""); const title = "[Anvil] Scheduled checks failed"; const context = { serverUrl: "https://github.com", repo: { owner: "microsoft", repo: "ox-tools" }, runId: 42, }; +const expectedQuery = + `repo:${context.repo.owner}/${context.repo.repo} ` + + `is:issue is:open in:body "${searchableMarker}"`; async function scenario(items) { const calls = { search: [], create: [], comment: [] }; @@ -309,7 +313,7 @@ async function scenario(items) { search: { issuesAndPullRequests: async args => { calls.search.push(args); - return { data: { items } }; + return { data: { items: args.q === expectedQuery ? items : [] } }; }, }, issues: { @@ -335,7 +339,7 @@ async function scenario(items) { (async () => { const created = await scenario([]); assert.equal(created.search.length, 1); - assert.match(created.search[0].q, /in:body/); + assert.equal(created.search[0].q, expectedQuery); assert.equal(created.create.length, 1); assert.equal(created.comment.length, 0); assert.equal(created.create[0].title, title); @@ -367,13 +371,17 @@ async function scenario(items) { }); "#; + if Command::new("node").arg("--version").output().is_err() { + return; + } + let dir = tempfile::tempdir().expect("create temporary test directory"); let path = dir.path().join("scheduled-failure.test.cjs"); fs::write(&path, harness).expect("write JavaScript behavior test"); let output = Command::new("node") .arg(&path) .output() - .expect("Node.js is required to test the generated github-script"); + .expect("execute generated github-script behavior test"); assert!( output.status.success(), "generated github-script behavior test failed:\nstdout:\n{}\nstderr:\n{}", From d43587ebb42d88f4b7c5e978d9441080ed1003d0 Mon Sep 17 00:00:00 2001 From: "Martin Kolinek (from Dev Box)" Date: Mon, 3 Aug 2026 18:06:19 +0200 Subject: [PATCH 10/20] fix: keep scheduled issue test within lint limit Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1c90ffee-5127-4069-8bb1-cef1492a400c --- crates/cargo-anvil/src/anvil/artifacts/github.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/cargo-anvil/src/anvil/artifacts/github.rs b/crates/cargo-anvil/src/anvil/artifacts/github.rs index 5e610507..273ad98e 100644 --- a/crates/cargo-anvil/src/anvil/artifacts/github.rs +++ b/crates/cargo-anvil/src/anvil/artifacts/github.rs @@ -302,9 +302,7 @@ const context = { repo: { owner: "microsoft", repo: "ox-tools" }, runId: 42, }; -const expectedQuery = - `repo:${context.repo.owner}/${context.repo.repo} ` + - `is:issue is:open in:body "${searchableMarker}"`; +const expectedQuery = `repo:${context.repo.owner}/${context.repo.repo} is:issue is:open in:body "${searchableMarker}"`; async function scenario(items) { const calls = { search: [], create: [], comment: [] }; From 77456af47489ba15b1baf7401e850908292db05d Mon Sep 17 00:00:00 2001 From: "Martin Kolinek (from Dev Box)" Date: Mon, 3 Aug 2026 20:00:31 +0200 Subject: [PATCH 11/20] security: scope issue write to failure publisher Apply the least-privilege workflow change after confirming the restriction only reduces scheduled check job permissions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1c90ffee-5127-4069-8bb1-cef1492a400c --- .github/workflows/anvil-scheduled-impl.yml | 3 +++ crates/cargo-anvil/docs/design/github.md | 18 +++++++++++------- .../cargo-anvil/src/anvil/artifacts/github.rs | 2 ++ .../github/scheduled-impl-workflow.yml | 3 +++ .../snapshots/snapshots__github_backend.snap | 3 +++ 5 files changed, 22 insertions(+), 7 deletions(-) diff --git a/.github/workflows/anvil-scheduled-impl.yml b/.github/workflows/anvil-scheduled-impl.yml index 04310dfc..556548d1 100644 --- a/.github/workflows/anvil-scheduled-impl.yml +++ b/.github/workflows/anvil-scheduled-impl.yml @@ -31,6 +31,9 @@ on: configured at Codecov; required for private repos. required: false +permissions: + contents: read + # Note on matrices: see pr-impl-workflow.yml for the rationale. OS # matrices are hardcoded; per-leg runner labels are inputs. diff --git a/crates/cargo-anvil/docs/design/github.md b/crates/cargo-anvil/docs/design/github.md index 97120e62..4c440f04 100644 --- a/crates/cargo-anvil/docs/design/github.md +++ b/crates/cargo-anvil/docs/design/github.md @@ -668,8 +668,10 @@ Recommended root workflow shape: - `permissions: contents: read` at the workflow level. anvil's default ships with this. - The scheduled reusable-workflow call grants `issues: write` at job scope so its - publisher can create or comment on the failure issue. The PR workflow never receives - this permission. + publisher can create or comment on the failure issue. The called workflow resets its + default permissions to `contents: read`, then restores `issues: write` only on the + publishing job; scheduled check jobs do not inherit write access. The PR workflow + never receives this permission. - No `pull-requests: write` (the PR-title check only needs the title from the event payload, which is already in `${{ github.event.pull_request.title }}`). - Scheduled-tier secrets, if any, live on `anvil-scheduled.yml` only — never on `anvil-pr.yml`. @@ -745,11 +747,13 @@ No label is required because repositories can remove or rename their default lab The issue remains open until a maintainer resolves the underlying failure and closes it. If a later run fails after closure, the publisher creates a new incident issue. -The publisher uses the workflow's short-lived `GITHUB_TOKEN`, with `issues: write` -granted only to the scheduled root call and publishing job. It does not receive repository -contents beyond read access and does not forward logs or environment data into the issue. -This narrow GitHub-native path also lets GitHub's Teams app relay issue notifications -without an external webhook or additional secret. +The publisher uses the workflow's short-lived `GITHUB_TOKEN`. The scheduled root call +allows `issues: write`, while the reusable workflow defaults to `contents: read` and +grants `issues: write` only to the publishing job. Scheduled check jobs therefore retain +read-only access. The publisher does not receive repository contents beyond read access +and does not forward logs or environment data into the issue. This narrow GitHub-native +path also lets GitHub's Teams app relay issue notifications without an external webhook +or additional secret. The generated root and implementation workflows must be updated together. A repository that has taken ownership of the root workflow must retain `issues: write` on the reusable diff --git a/crates/cargo-anvil/src/anvil/artifacts/github.rs b/crates/cargo-anvil/src/anvil/artifacts/github.rs index 273ad98e..a2b7d7bf 100644 --- a/crates/cargo-anvil/src/anvil/artifacts/github.rs +++ b/crates/cargo-anvil/src/anvil/artifacts/github.rs @@ -271,6 +271,8 @@ mod tests { assert!(SCHEDULED_IMPL_WORKFLOW.contains("github.rest.search.issuesAndPullRequests")); assert!(SCHEDULED_IMPL_WORKFLOW.contains("github.rest.issues.createComment")); assert!(SCHEDULED_IMPL_WORKFLOW.contains("github.rest.issues.create")); + assert!(SCHEDULED_IMPL_WORKFLOW.contains("\npermissions:\n contents: read\n")); + assert_eq!(SCHEDULED_IMPL_WORKFLOW.matches("issues: write").count(), 1); assert_eq!( SCHEDULED_IMPL_WORKFLOW.matches("free-disk-space: true").count(), 1, diff --git a/crates/cargo-anvil/templates/github/scheduled-impl-workflow.yml b/crates/cargo-anvil/templates/github/scheduled-impl-workflow.yml index 04310dfc..556548d1 100644 --- a/crates/cargo-anvil/templates/github/scheduled-impl-workflow.yml +++ b/crates/cargo-anvil/templates/github/scheduled-impl-workflow.yml @@ -31,6 +31,9 @@ on: configured at Codecov; required for private repos. required: false +permissions: + contents: read + # Note on matrices: see pr-impl-workflow.yml for the rationale. OS # matrices are hardcoded; per-leg runner labels are inputs. diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap index 827b96b0..b47d6844 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap @@ -2311,6 +2311,9 @@ on: configured at Codecov; required for private repos. required: false +permissions: + contents: read + # Note on matrices: see pr-impl-workflow.yml for the rationale. OS # matrices are hardcoded; per-leg runner labels are inputs. From 9ba74a74f01ce188558db42d9c4cecd9e4a97582 Mon Sep 17 00:00:00 2001 From: "Martin Kolinek (from Dev Box)" Date: Mon, 3 Aug 2026 20:34:07 +0200 Subject: [PATCH 12/20] security: scope PR comment write permission Default reusable PR workflow jobs to read-only and grant pull-request write access only to pr-fast, where advisory comments are managed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1c90ffee-5127-4069-8bb1-cef1492a400c --- .github/workflows/anvil-pr-impl.yml | 6 ++++++ crates/cargo-anvil/docs/design/github.md | 13 ++++++++----- crates/cargo-anvil/src/anvil/artifacts/github.rs | 2 ++ .../templates/github/pr-impl-workflow.yml | 6 ++++++ .../tests/snapshots/snapshots__github_backend.snap | 6 ++++++ 5 files changed, 28 insertions(+), 5 deletions(-) diff --git a/.github/workflows/anvil-pr-impl.yml b/.github/workflows/anvil-pr-impl.yml index 7b9d5cfe..24f5ea61 100644 --- a/.github/workflows/anvil-pr-impl.yml +++ b/.github/workflows/anvil-pr-impl.yml @@ -31,6 +31,9 @@ on: configured at Codecov; required for private repos. required: false +permissions: + contents: read + # Note on matrices: every multi-OS job below hardcodes its OS axis as # an inline YAML array. Per-leg runner *labels* are inputs (so adopters # can swap in self-hosted runners), but the OS axis itself is part of @@ -79,6 +82,9 @@ jobs: uses: ./.github/actions/anvil-impact pr-fast: + permissions: + contents: read + pull-requests: write # Cross-OS / cross-arch because pr-fast contains compile-sensitive # checks (clippy, doc-build, udeps, semver-check, external-types) # whose results can differ across host for crates that use diff --git a/crates/cargo-anvil/docs/design/github.md b/crates/cargo-anvil/docs/design/github.md index 4c440f04..105b8489 100644 --- a/crates/cargo-anvil/docs/design/github.md +++ b/crates/cargo-anvil/docs/design/github.md @@ -672,8 +672,11 @@ Recommended root workflow shape: default permissions to `contents: read`, then restores `issues: write` only on the publishing job; scheduled check jobs do not inherit write access. The PR workflow never receives this permission. -- No `pull-requests: write` (the PR-title check only needs the title from the event - payload, which is already in `${{ github.event.pull_request.title }}`). +- The PR reusable-workflow call grants `pull-requests: write` for advisory comments. + The called workflow resets its default permissions to `contents: read`, then restores + `pull-requests: write` only on `pr-fast`, where the sticky-comment steps run. Other PR + jobs do not inherit write access. The PR-title check itself reads the title from the + event payload and does not use the write permission. - Scheduled-tier secrets, if any, live on `anvil-scheduled.yml` only — never on `anvil-pr.yml`. - All cargo-tool installs done by the catalog setup recipes use `--locked` (with `cargo install` or `cargo binstall` depending on `installer`). @@ -811,9 +814,9 @@ Conditions explained: `pull-requests: write` to fork-PR workflow runs by default, so the action would 403. Permissions: the reusable workflow's caller (`anvil-pr.yml`) declares -`pull-requests: write` on the `anvil-pr` job that calls `anvil-pr-impl.yml`. The -top-level `permissions:` block stays at `contents: read` so unrelated reads in the same -workflow are still least-privilege. +`pull-requests: write` on the `anvil-pr` job that calls `anvil-pr-impl.yml`. The called +workflow resets its default to `contents: read` and restores `pull-requests: write` only +on `pr-fast`, where the sticky-comment steps run. Other called jobs remain read-only. Adding a new advisory check is a two-step change: the recipe writes `target/anvil/comments/.md` (and removes it on a clean run); the workflow gains diff --git a/crates/cargo-anvil/src/anvil/artifacts/github.rs b/crates/cargo-anvil/src/anvil/artifacts/github.rs index a2b7d7bf..eda47c84 100644 --- a/crates/cargo-anvil/src/anvil/artifacts/github.rs +++ b/crates/cargo-anvil/src/anvil/artifacts/github.rs @@ -243,6 +243,8 @@ mod tests { ); assert!(PR_IMPL_WORKFLOW.contains("matrix.os != 'windows-arm'")); assert!(PR_IMPL_WORKFLOW.contains("flags: ${{ matrix.os }}")); + assert!(PR_IMPL_WORKFLOW.contains("\npermissions:\n contents: read\n")); + assert_eq!(PR_IMPL_WORKFLOW.matches("pull-requests: write").count(), 1); assert_eq!( PR_IMPL_WORKFLOW.matches("free-disk-space: true").count(), 1, diff --git a/crates/cargo-anvil/templates/github/pr-impl-workflow.yml b/crates/cargo-anvil/templates/github/pr-impl-workflow.yml index 7b9d5cfe..24f5ea61 100644 --- a/crates/cargo-anvil/templates/github/pr-impl-workflow.yml +++ b/crates/cargo-anvil/templates/github/pr-impl-workflow.yml @@ -31,6 +31,9 @@ on: configured at Codecov; required for private repos. required: false +permissions: + contents: read + # Note on matrices: every multi-OS job below hardcodes its OS axis as # an inline YAML array. Per-leg runner *labels* are inputs (so adopters # can swap in self-hosted runners), but the OS axis itself is part of @@ -79,6 +82,9 @@ jobs: uses: ./.github/actions/anvil-impact pr-fast: + permissions: + contents: read + pull-requests: write # Cross-OS / cross-arch because pr-fast contains compile-sensitive # checks (clippy, doc-build, udeps, semver-check, external-types) # whose results can differ across host for crates that use diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap index b47d6844..65def1b3 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap @@ -2043,6 +2043,9 @@ on: configured at Codecov; required for private repos. required: false +permissions: + contents: read + # Note on matrices: every multi-OS job below hardcodes its OS axis as # an inline YAML array. Per-leg runner *labels* are inputs (so adopters # can swap in self-hosted runners), but the OS axis itself is part of @@ -2091,6 +2094,9 @@ jobs: uses: ./.github/actions/anvil-impact pr-fast: + permissions: + contents: read + pull-requests: write # Cross-OS / cross-arch because pr-fast contains compile-sensitive # checks (clippy, doc-build, udeps, semver-check, external-types) # whose results can differ across host for crates that use From 95fa86c0ea41a1e29cc32fc7c8ec6dc8cad50f1f Mon Sep 17 00:00:00 2001 From: "Martin Kolinek (from Dev Box)" Date: Wed, 5 Aug 2026 14:41:36 +0200 Subject: [PATCH 13/20] fix: provision spellcheck dependency on ARM runners Prevent cargo-binstall from compiling before Anvil's source prerequisite gate, and provision libclang only for GitHub-hosted Linux ARM64 jobs that install cargo-spellcheck. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1c90ffee-5127-4069-8bb1-cef1492a400c --- .anvil.lock | 4 ++-- .github/actions/anvil-setup/action.yml | 9 +++++++++ crates/cargo-anvil/docs/design/github.md | 8 ++++++++ crates/cargo-anvil/docs/design/local.md | 16 ++++++++++------ .../cargo-anvil/src/anvil/artifacts/github.rs | 5 ++++- .../src/anvil/artifacts/justfile.rs | 4 ++++ .../templates/github/setup-action.yml | 9 +++++++++ .../templates/justfiles/anvil/tools.just | 10 ++++++---- .../snapshots/snapshots__github_backend.snap | 19 +++++++++++++++---- justfiles/anvil/tools.just | 10 ++++++---- 10 files changed, 73 insertions(+), 21 deletions(-) diff --git a/.anvil.lock b/.anvil.lock index a22c2887..40329405 100644 --- a/.anvil.lock +++ b/.anvil.lock @@ -73,7 +73,7 @@ checksum = "sha256:08123d39920c9d73c8d22c36ae20cbe594420078e25b53d0b8eb47e8361cc [[file]] path = ".github/actions/anvil-setup/action.yml" -checksum = "sha256:a153fa2ce4307ef9da91ff215e4f37e5a329d0bbb2c04c2b1f26096887f6459d" +checksum = "sha256:f74aea5b572a989a0d02310160141c648adf663b51191e4f49e3380c27a664e1" [[file]] path = ".github/workflows/anvil-pr-impl.yml" @@ -269,7 +269,7 @@ checksum = "sha256:713c5a2ae28b6b5aa20dd38226278b3b7f71bbc5e6b84a16eaf5244713f27 [[file]] path = "justfiles/anvil/tools.just" -checksum = "sha256:662ad55792b05981ada347a4ff4a8026ad0969a61ae9bd046b8e5a03cc7e820c" +checksum = "sha256:69049ea8b26aed36e25055916bc333f303d85636850cb863096548e339f8426e" [[file]] path = "justfiles/anvil/versions.just" diff --git a/.github/actions/anvil-setup/action.yml b/.github/actions/anvil-setup/action.yml index ec3d9ee1..72ba893b 100644 --- a/.github/actions/anvil-setup/action.yml +++ b/.github/actions/anvil-setup/action.yml @@ -147,6 +147,15 @@ runs: cargo binstall --no-confirm --locked just || cargo install --locked just fi + # cargo-spellcheck has no prebuilt Linux ARM64 artifact at the catalog + # version, so pr-fast must compile it. Provision its system dependency only + # on the managed runner and groups that install cargo-spellcheck; local and + # self-hosted environments retain Anvil's no-auto-install policy. + - name: Install cargo-spellcheck source dependency (Linux ARM64) + if: runner.environment == 'github-hosted' && runner.os == 'Linux' && runner.arch == 'ARM64' && (inputs.group == '' || inputs.group == 'pr-fast') + shell: bash + run: sudo apt-get update && sudo apt-get install -y libclang-dev + - name: Install anvil toolchains + tools shell: bash # When `group` is empty (the default), installs the full catalog diff --git a/crates/cargo-anvil/docs/design/github.md b/crates/cargo-anvil/docs/design/github.md index 105b8489..913295f6 100644 --- a/crates/cargo-anvil/docs/design/github.md +++ b/crates/cargo-anvil/docs/design/github.md @@ -572,6 +572,14 @@ workflows explicitly enable this input only for `pr-test` and `scheduled-test`, mirroring the testing-job integration in [microsoft/oxidizer#583](https://github.com/microsoft/oxidizer/pull/583). Other groups retain the action's disabled default. +GitHub-hosted Linux ARM64 runners do not provide `libclang`, and +`cargo-spellcheck` has no prebuilt ARM64 artifact at the catalog version. For the +full catalog and `pr-fast` group only, the setup action installs `libclang-dev` +before the catalog recipes run. This managed-runner provisioning is intentionally +scoped by environment, OS, architecture, and group; local and self-hosted setup +continues to report the missing source dependency without choosing a system +package manager or elevating privileges. + ## 6. Impact scoping `.github/actions/anvil-impact/action.yml` is a composite action with no branch input. diff --git a/crates/cargo-anvil/docs/design/local.md b/crates/cargo-anvil/docs/design/local.md index 46650639..4baa06c0 100644 --- a/crates/cargo-anvil/docs/design/local.md +++ b/crates/cargo-anvil/docs/design/local.md @@ -278,10 +278,13 @@ The `installer` argument: source builds; works in any cargo environment with no extra runtime dependency. Slow on a cold runner (~30 min for the full catalog) because every tool re-compiles common deps (`clap`, `syn`, `quote`, ...) from scratch independently. -- `binstall` -- `cargo binstall --no-confirm --locked --version '='`. - Downloads a prebuilt binary from each tool's GitHub Releases when available. - Cuts the cold-runner install phase from ~30 min to ~1 min. `cargo-binstall` - itself needs to be on PATH; the GH setup composite arranges this. +- `binstall` -- `cargo binstall --no-confirm --locked --disable-strategies compile + --version '='`. Downloads a prebuilt binary from each tool's GitHub + Releases when available. If no binary is available, Anvil runs the tool's source + prerequisite before invoking `cargo install` itself; disabling binstall's compile + strategy prevents an uncontrolled source build from bypassing that check. The + binary path cuts the cold-runner install phase from ~30 min to ~1 min. + `cargo-binstall` itself needs to be on PATH; the GH setup composite arranges this. The GitHub composite setup action calls `just anvil--setup binstall` (or just `anvil-setup binstall` when no group is scoped). The ADO setup step @@ -341,8 +344,9 @@ scoop / winget) and exits non-zero. **No auto-install** -- admin/sudo decisions package-manager choice stay with the user. The cargo-spellcheck install recipe passes `anvil-tool-cargo-spellcheck-source-deps-check` to `_install-tool` as its source prerequisite. The prerequisite runs for an explicit source-build `install` backend and when `binstall` -cannot provide a binary and falls back to a source build, so missing libclang surfaces as a -clear hint instead of a cryptic clang-sys build error 10 minutes into the install. +cannot provide a binary. Anvil disables binstall's own compile strategy and performs the +source fallback itself, so missing libclang surfaces as a clear hint before compilation +instead of a cryptic clang-sys build error 10 minutes into the install. Each tool with a source-build system dependency owns a tool-specific prerequisite recipe and wires it into `_install-tool`. Catalog changes propagate to adopters via `cargo anvil` like diff --git a/crates/cargo-anvil/src/anvil/artifacts/github.rs b/crates/cargo-anvil/src/anvil/artifacts/github.rs index eda47c84..682c095f 100644 --- a/crates/cargo-anvil/src/anvil/artifacts/github.rs +++ b/crates/cargo-anvil/src/anvil/artifacts/github.rs @@ -168,7 +168,10 @@ mod tests { assert!(SETUP_ACTION.contains("runner.environment == 'github-hosted'")); assert!(SETUP_ACTION.contains("/usr/local/lib/android")); assert!(SETUP_ACTION.contains(r"C:\Program Files (x86)\Android")); - assert!(!SETUP_ACTION.contains("Install libclang")); + assert!(SETUP_ACTION.contains("Install cargo-spellcheck source dependency (Linux ARM64)")); + assert!(SETUP_ACTION.contains("runner.arch == 'ARM64'")); + assert!(SETUP_ACTION.contains("inputs.group == '' || inputs.group == 'pr-fast'")); + assert!(SETUP_ACTION.contains("sudo apt-get install -y libclang-dev")); } #[test] diff --git a/crates/cargo-anvil/src/anvil/artifacts/justfile.rs b/crates/cargo-anvil/src/anvil/artifacts/justfile.rs index f493aabe..f9f10e3b 100644 --- a/crates/cargo-anvil/src/anvil/artifacts/justfile.rs +++ b/crates/cargo-anvil/src/anvil/artifacts/justfile.rs @@ -295,6 +295,10 @@ mod tests { #[test] fn spellcheck_checks_source_prerequisites_before_source_builds() { + assert!( + TOOLS_JUST.contains("--disable-strategies compile"), + "binstall must not compile before Anvil checks source prerequisites" + ); assert!( TOOLS_JUST.contains( "_install-tool \"cargo-spellcheck\" cargo_spellcheck_version installer \"anvil-tool-cargo-spellcheck-source-deps-check\"" diff --git a/crates/cargo-anvil/templates/github/setup-action.yml b/crates/cargo-anvil/templates/github/setup-action.yml index ec3d9ee1..72ba893b 100644 --- a/crates/cargo-anvil/templates/github/setup-action.yml +++ b/crates/cargo-anvil/templates/github/setup-action.yml @@ -147,6 +147,15 @@ runs: cargo binstall --no-confirm --locked just || cargo install --locked just fi + # cargo-spellcheck has no prebuilt Linux ARM64 artifact at the catalog + # version, so pr-fast must compile it. Provision its system dependency only + # on the managed runner and groups that install cargo-spellcheck; local and + # self-hosted environments retain Anvil's no-auto-install policy. + - name: Install cargo-spellcheck source dependency (Linux ARM64) + if: runner.environment == 'github-hosted' && runner.os == 'Linux' && runner.arch == 'ARM64' && (inputs.group == '' || inputs.group == 'pr-fast') + shell: bash + run: sudo apt-get update && sudo apt-get install -y libclang-dev + - name: Install anvil toolchains + tools shell: bash # When `group` is empty (the default), installs the full catalog diff --git a/crates/cargo-anvil/templates/justfiles/anvil/tools.just b/crates/cargo-anvil/templates/justfiles/anvil/tools.just index 7e7df78f..92de15d1 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/tools.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/tools.just @@ -173,9 +173,9 @@ anvil-tool-pwsh-validate-prereqs: # or no-op if it is already installed at or above that version. The # `installer` parameter selects between: # - "install" (cargo install --locked, pure-source). Default. -# - "binstall" (cargo binstall --no-confirm --locked, with cargo install -# fallback if binstall fails). Bootstraps cargo-binstall -# itself if not on PATH. +# - "binstall" (cargo binstall --no-confirm --locked with binstall's compile +# strategy disabled, followed by a controlled cargo install +# fallback). Bootstraps cargo-binstall itself if not on PATH. # `source_prereq`, when set, runs immediately before a source installation, # including a binstall fallback. [script("pwsh", "-NoProfile")] @@ -223,7 +223,9 @@ _install-tool name version installer source_prereq="": exit $LASTEXITCODE } } - cargo binstall --no-confirm --locked $name --version "=$version" + # Keep source builds behind Anvil's prerequisite check instead of + # allowing binstall to compile before that check can run. + cargo binstall --no-confirm --locked --disable-strategies compile $name --version "=$version" if ($LASTEXITCODE -eq 0) { exit 0 } Write-Host ' binstall failed; falling back to cargo install' -ForegroundColor Yellow } diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap index 65def1b3..c3265d44 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap @@ -1960,6 +1960,15 @@ runs: cargo binstall --no-confirm --locked just || cargo install --locked just fi + # cargo-spellcheck has no prebuilt Linux ARM64 artifact at the catalog + # version, so pr-fast must compile it. Provision its system dependency only + # on the managed runner and groups that install cargo-spellcheck; local and + # self-hosted environments retain Anvil's no-auto-install policy. + - name: Install cargo-spellcheck source dependency (Linux ARM64) + if: runner.environment == 'github-hosted' && runner.os == 'Linux' && runner.arch == 'ARM64' && (inputs.group == '' || inputs.group == 'pr-fast') + shell: bash + run: sudo apt-get update && sudo apt-get install -y libclang-dev + - name: Install anvil toolchains + tools shell: bash # When `group` is empty (the default), installs the full catalog @@ -5620,9 +5629,9 @@ anvil-tool-pwsh-validate-prereqs: # or no-op if it is already installed at or above that version. The # `installer` parameter selects between: # - "install" (cargo install --locked, pure-source). Default. -# - "binstall" (cargo binstall --no-confirm --locked, with cargo install -# fallback if binstall fails). Bootstraps cargo-binstall -# itself if not on PATH. +# - "binstall" (cargo binstall --no-confirm --locked with binstall's compile +# strategy disabled, followed by a controlled cargo install +# fallback). Bootstraps cargo-binstall itself if not on PATH. # `source_prereq`, when set, runs immediately before a source installation, # including a binstall fallback. [script("pwsh", "-NoProfile")] @@ -5670,7 +5679,9 @@ _install-tool name version installer source_prereq="": exit $LASTEXITCODE } } - cargo binstall --no-confirm --locked $name --version "=$version" + # Keep source builds behind Anvil's prerequisite check instead of + # allowing binstall to compile before that check can run. + cargo binstall --no-confirm --locked --disable-strategies compile $name --version "=$version" if ($LASTEXITCODE -eq 0) { exit 0 } Write-Host ' binstall failed; falling back to cargo install' -ForegroundColor Yellow } diff --git a/justfiles/anvil/tools.just b/justfiles/anvil/tools.just index 7e7df78f..92de15d1 100644 --- a/justfiles/anvil/tools.just +++ b/justfiles/anvil/tools.just @@ -173,9 +173,9 @@ anvil-tool-pwsh-validate-prereqs: # or no-op if it is already installed at or above that version. The # `installer` parameter selects between: # - "install" (cargo install --locked, pure-source). Default. -# - "binstall" (cargo binstall --no-confirm --locked, with cargo install -# fallback if binstall fails). Bootstraps cargo-binstall -# itself if not on PATH. +# - "binstall" (cargo binstall --no-confirm --locked with binstall's compile +# strategy disabled, followed by a controlled cargo install +# fallback). Bootstraps cargo-binstall itself if not on PATH. # `source_prereq`, when set, runs immediately before a source installation, # including a binstall fallback. [script("pwsh", "-NoProfile")] @@ -223,7 +223,9 @@ _install-tool name version installer source_prereq="": exit $LASTEXITCODE } } - cargo binstall --no-confirm --locked $name --version "=$version" + # Keep source builds behind Anvil's prerequisite check instead of + # allowing binstall to compile before that check can run. + cargo binstall --no-confirm --locked --disable-strategies compile $name --version "=$version" if ($LASTEXITCODE -eq 0) { exit 0 } Write-Host ' binstall failed; falling back to cargo install' -ForegroundColor Yellow } From 1767cf45f8d1a2d7f56e0d29eb1159ffb01150d4 Mon Sep 17 00:00:00 2001 From: "Martin Kolinek (from Dev Box)" Date: Wed, 5 Aug 2026 14:44:59 +0200 Subject: [PATCH 14/20] test: refresh shared installer snapshots Update the local and ADO artifact snapshots for the shared cargo-binstall strategy change. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1c90ffee-5127-4069-8bb1-cef1492a400c --- .../tests/snapshots/snapshots__ado_backend.snap | 10 ++++++---- .../tests/snapshots/snapshots__local_only.snap | 10 ++++++---- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap index b9b7c91d..c6b37a6c 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap @@ -5604,9 +5604,9 @@ anvil-tool-pwsh-validate-prereqs: # or no-op if it is already installed at or above that version. The # `installer` parameter selects between: # - "install" (cargo install --locked, pure-source). Default. -# - "binstall" (cargo binstall --no-confirm --locked, with cargo install -# fallback if binstall fails). Bootstraps cargo-binstall -# itself if not on PATH. +# - "binstall" (cargo binstall --no-confirm --locked with binstall's compile +# strategy disabled, followed by a controlled cargo install +# fallback). Bootstraps cargo-binstall itself if not on PATH. # `source_prereq`, when set, runs immediately before a source installation, # including a binstall fallback. [script("pwsh", "-NoProfile")] @@ -5654,7 +5654,9 @@ _install-tool name version installer source_prereq="": exit $LASTEXITCODE } } - cargo binstall --no-confirm --locked $name --version "=$version" + # Keep source builds behind Anvil's prerequisite check instead of + # allowing binstall to compile before that check can run. + cargo binstall --no-confirm --locked --disable-strategies compile $name --version "=$version" if ($LASTEXITCODE -eq 0) { exit 0 } Write-Host ' binstall failed; falling back to cargo install' -ForegroundColor Yellow } diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap index 72bf2202..89bfb1c0 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap @@ -4344,9 +4344,9 @@ anvil-tool-pwsh-validate-prereqs: # or no-op if it is already installed at or above that version. The # `installer` parameter selects between: # - "install" (cargo install --locked, pure-source). Default. -# - "binstall" (cargo binstall --no-confirm --locked, with cargo install -# fallback if binstall fails). Bootstraps cargo-binstall -# itself if not on PATH. +# - "binstall" (cargo binstall --no-confirm --locked with binstall's compile +# strategy disabled, followed by a controlled cargo install +# fallback). Bootstraps cargo-binstall itself if not on PATH. # `source_prereq`, when set, runs immediately before a source installation, # including a binstall fallback. [script("pwsh", "-NoProfile")] @@ -4394,7 +4394,9 @@ _install-tool name version installer source_prereq="": exit $LASTEXITCODE } } - cargo binstall --no-confirm --locked $name --version "=$version" + # Keep source builds behind Anvil's prerequisite check instead of + # allowing binstall to compile before that check can run. + cargo binstall --no-confirm --locked --disable-strategies compile $name --version "=$version" if ($LASTEXITCODE -eq 0) { exit 0 } Write-Host ' binstall failed; falling back to cargo install' -ForegroundColor Yellow } From 9aa3aab5e624104d608c067c175221fdfe43470b Mon Sep 17 00:00:00 2001 From: "Martin Kolinek (from Dev Box)" Date: Wed, 5 Aug 2026 14:54:41 +0200 Subject: [PATCH 15/20] fix: skip incompatible spellcheck on ARM64 cargo-spellcheck 0.15.1 cannot compile on current Linux ARM64 and crashes on Windows ARM64. Skip its setup, validation, and execution on ARM64 while retaining x64 coverage. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1c90ffee-5127-4069-8bb1-cef1492a400c --- .anvil.lock | 6 ++-- .github/actions/anvil-setup/action.yml | 9 ----- crates/cargo-anvil/docs/design/github.md | 8 ----- crates/cargo-anvil/docs/design/local.md | 6 ++++ .../cargo-anvil/src/anvil/artifacts/github.rs | 6 ++-- .../src/anvil/artifacts/justfile.rs | 12 +++++-- .../templates/github/setup-action.yml | 9 ----- .../justfiles/anvil/checks/spellcheck.just | 4 +++ .../templates/justfiles/anvil/tools.just | 23 ++++++++++-- .../snapshots/snapshots__ado_backend.snap | 27 ++++++++++++-- .../snapshots/snapshots__github_backend.snap | 36 +++++++++++++------ .../snapshots/snapshots__local_only.snap | 27 ++++++++++++-- justfiles/anvil/checks/spellcheck.just | 4 +++ justfiles/anvil/tools.just | 23 ++++++++++-- 14 files changed, 145 insertions(+), 55 deletions(-) diff --git a/.anvil.lock b/.anvil.lock index 40329405..fe2c81f5 100644 --- a/.anvil.lock +++ b/.anvil.lock @@ -73,7 +73,7 @@ checksum = "sha256:08123d39920c9d73c8d22c36ae20cbe594420078e25b53d0b8eb47e8361cc [[file]] path = ".github/actions/anvil-setup/action.yml" -checksum = "sha256:f74aea5b572a989a0d02310160141c648adf663b51191e4f49e3380c27a664e1" +checksum = "sha256:a153fa2ce4307ef9da91ff215e4f37e5a329d0bbb2c04c2b1f26096887f6459d" [[file]] path = ".github/workflows/anvil-pr-impl.yml" @@ -205,7 +205,7 @@ checksum = "sha256:3210a749418e6716d1b689c3da5bb5e3af36700e09da0c2077fc44ade11c9 [[file]] path = "justfiles/anvil/checks/spellcheck.just" -checksum = "sha256:86bb66c426eb332af10941e949204eeb8a4564acb4ec32425fdbb1b34fb04a55" +checksum = "sha256:7fac855bb6d2560c3a465993e86b0ed5e8225838e67ecb589ff6a3bab5881a66" [[file]] path = "justfiles/anvil/checks/udeps.just" @@ -269,7 +269,7 @@ checksum = "sha256:713c5a2ae28b6b5aa20dd38226278b3b7f71bbc5e6b84a16eaf5244713f27 [[file]] path = "justfiles/anvil/tools.just" -checksum = "sha256:69049ea8b26aed36e25055916bc333f303d85636850cb863096548e339f8426e" +checksum = "sha256:334dde29c3d17d4f9f30357fe1d17a594a0ba9895b7b3ca8ac22542754a8bbed" [[file]] path = "justfiles/anvil/versions.just" diff --git a/.github/actions/anvil-setup/action.yml b/.github/actions/anvil-setup/action.yml index 72ba893b..ec3d9ee1 100644 --- a/.github/actions/anvil-setup/action.yml +++ b/.github/actions/anvil-setup/action.yml @@ -147,15 +147,6 @@ runs: cargo binstall --no-confirm --locked just || cargo install --locked just fi - # cargo-spellcheck has no prebuilt Linux ARM64 artifact at the catalog - # version, so pr-fast must compile it. Provision its system dependency only - # on the managed runner and groups that install cargo-spellcheck; local and - # self-hosted environments retain Anvil's no-auto-install policy. - - name: Install cargo-spellcheck source dependency (Linux ARM64) - if: runner.environment == 'github-hosted' && runner.os == 'Linux' && runner.arch == 'ARM64' && (inputs.group == '' || inputs.group == 'pr-fast') - shell: bash - run: sudo apt-get update && sudo apt-get install -y libclang-dev - - name: Install anvil toolchains + tools shell: bash # When `group` is empty (the default), installs the full catalog diff --git a/crates/cargo-anvil/docs/design/github.md b/crates/cargo-anvil/docs/design/github.md index 913295f6..105b8489 100644 --- a/crates/cargo-anvil/docs/design/github.md +++ b/crates/cargo-anvil/docs/design/github.md @@ -572,14 +572,6 @@ workflows explicitly enable this input only for `pr-test` and `scheduled-test`, mirroring the testing-job integration in [microsoft/oxidizer#583](https://github.com/microsoft/oxidizer/pull/583). Other groups retain the action's disabled default. -GitHub-hosted Linux ARM64 runners do not provide `libclang`, and -`cargo-spellcheck` has no prebuilt ARM64 artifact at the catalog version. For the -full catalog and `pr-fast` group only, the setup action installs `libclang-dev` -before the catalog recipes run. This managed-runner provisioning is intentionally -scoped by environment, OS, architecture, and group; local and self-hosted setup -continues to report the missing source dependency without choosing a system -package manager or elevating privileges. - ## 6. Impact scoping `.github/actions/anvil-impact/action.yml` is a composite action with no branch input. diff --git a/crates/cargo-anvil/docs/design/local.md b/crates/cargo-anvil/docs/design/local.md index 4baa06c0..d42ffec0 100644 --- a/crates/cargo-anvil/docs/design/local.md +++ b/crates/cargo-anvil/docs/design/local.md @@ -352,6 +352,12 @@ Each tool with a source-build system dependency owns a tool-specific prerequisit wires it into `_install-tool`. Catalog changes propagate to adopters via `cargo anvil` like any other template edit. +`cargo-spellcheck 0.15.1` is skipped end-to-end on ARM64. Its pinned +`ra_ap_stdx` dependency does not compile with current Rust on Linux ARM64, and +the available Windows ARM64 binary terminates with an access violation. Because +spelling results are architecture-independent, x64 workflow legs retain the +check while ARM64 setup, prerequisite validation, and execution are no-ops. + ### 3.4 Per-check warnings Every check recipe depends on `anvil--validate-prereqs` so even ad-hoc diff --git a/crates/cargo-anvil/src/anvil/artifacts/github.rs b/crates/cargo-anvil/src/anvil/artifacts/github.rs index 682c095f..e1324111 100644 --- a/crates/cargo-anvil/src/anvil/artifacts/github.rs +++ b/crates/cargo-anvil/src/anvil/artifacts/github.rs @@ -168,10 +168,8 @@ mod tests { assert!(SETUP_ACTION.contains("runner.environment == 'github-hosted'")); assert!(SETUP_ACTION.contains("/usr/local/lib/android")); assert!(SETUP_ACTION.contains(r"C:\Program Files (x86)\Android")); - assert!(SETUP_ACTION.contains("Install cargo-spellcheck source dependency (Linux ARM64)")); - assert!(SETUP_ACTION.contains("runner.arch == 'ARM64'")); - assert!(SETUP_ACTION.contains("inputs.group == '' || inputs.group == 'pr-fast'")); - assert!(SETUP_ACTION.contains("sudo apt-get install -y libclang-dev")); + assert!(!SETUP_ACTION.contains("Install libclang")); + assert!(!SETUP_ACTION.contains("apt-get install -y libclang-dev")); } #[test] diff --git a/crates/cargo-anvil/src/anvil/artifacts/justfile.rs b/crates/cargo-anvil/src/anvil/artifacts/justfile.rs index f9f10e3b..a60e427c 100644 --- a/crates/cargo-anvil/src/anvil/artifacts/justfile.rs +++ b/crates/cargo-anvil/src/anvil/artifacts/justfile.rs @@ -300,16 +300,22 @@ mod tests { "binstall must not compile before Anvil checks source prerequisites" ); assert!( - TOOLS_JUST.contains( - "_install-tool \"cargo-spellcheck\" cargo_spellcheck_version installer \"anvil-tool-cargo-spellcheck-source-deps-check\"" - ), + TOOLS_JUST.contains("anvil-tool-cargo-spellcheck-source-deps-check"), "spellcheck installer must run libclang validation before source builds" ); + assert!( + TOOLS_JUST.contains("anvil-tool-cargo-spellcheck-install: ARM64 -- skipping (upstream tool incompatibility)"), + "spellcheck installation must skip unsupported ARM64 hosts" + ); let checks = all_check_bodies(); assert!( !checks.contains("anvil-spellcheck-setup installer=\"install\": anvil-tool-cargo-spellcheck-source-deps-check"), "spellcheck setup must not require libclang before binstall" ); + assert!( + checks.contains("anvil-spellcheck: ARM64 -- skipping (upstream tool incompatibility)"), + "spellcheck execution must skip unsupported ARM64 hosts" + ); } #[test] diff --git a/crates/cargo-anvil/templates/github/setup-action.yml b/crates/cargo-anvil/templates/github/setup-action.yml index 72ba893b..ec3d9ee1 100644 --- a/crates/cargo-anvil/templates/github/setup-action.yml +++ b/crates/cargo-anvil/templates/github/setup-action.yml @@ -147,15 +147,6 @@ runs: cargo binstall --no-confirm --locked just || cargo install --locked just fi - # cargo-spellcheck has no prebuilt Linux ARM64 artifact at the catalog - # version, so pr-fast must compile it. Provision its system dependency only - # on the managed runner and groups that install cargo-spellcheck; local and - # self-hosted environments retain Anvil's no-auto-install policy. - - name: Install cargo-spellcheck source dependency (Linux ARM64) - if: runner.environment == 'github-hosted' && runner.os == 'Linux' && runner.arch == 'ARM64' && (inputs.group == '' || inputs.group == 'pr-fast') - shell: bash - run: sudo apt-get update && sudo apt-get install -y libclang-dev - - name: Install anvil toolchains + tools shell: bash # When `group` is empty (the default), installs the full catalog diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/spellcheck.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/spellcheck.just index 032aada6..f3690f3a 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/spellcheck.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/spellcheck.just @@ -24,6 +24,10 @@ [script("pwsh", "-NoProfile")] anvil-spellcheck: anvil-spellcheck-validate-prereqs $ErrorActionPreference = 'Stop' + if ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture -eq [System.Runtime.InteropServices.Architecture]::Arm64) { + Write-Host 'anvil-spellcheck: ARM64 -- skipping (upstream tool incompatibility)' + exit 0 + } if ($env:ANVIL_INCLUDE_MODIFIED -eq '--skip') { Write-Host 'anvil-spellcheck: no modified packages; skipping' exit 0 diff --git a/crates/cargo-anvil/templates/justfiles/anvil/tools.just b/crates/cargo-anvil/templates/justfiles/anvil/tools.just index 92de15d1..c714b0ec 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/tools.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/tools.just @@ -718,13 +718,32 @@ anvil-tool-cargo-sort-install installer="install": (_install-tool "cargo-sort" c [group("anvil-setup")] anvil-tool-cargo-sort-validate-prereqs: (_check-tool "cargo-sort" cargo_sort_version) +# cargo-spellcheck 0.15.1 is not usable on ARM64: its source dependency +# ra_ap_stdx no longer compiles with current Rust, and its Windows ARM64 binary +# segfaults. Spelling is architecture-independent and runs on the x64 matrix +# legs, so installation, validation, and execution all self-skip on ARM64. + # Install the pinned `cargo-spellcheck` tool. [group("anvil-setup")] -anvil-tool-cargo-spellcheck-install installer="install": (_install-tool "cargo-spellcheck" cargo_spellcheck_version installer "anvil-tool-cargo-spellcheck-source-deps-check") +[script("pwsh", "-NoProfile")] +anvil-tool-cargo-spellcheck-install installer="install": + if ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture -eq [System.Runtime.InteropServices.Architecture]::Arm64) { + Write-Host 'anvil-tool-cargo-spellcheck-install: ARM64 -- skipping (upstream tool incompatibility)' + exit 0 + } + & "{{just_executable()}}" _install-tool cargo-spellcheck {{cargo_spellcheck_version}} {{installer}} anvil-tool-cargo-spellcheck-source-deps-check + exit $LASTEXITCODE # Validate that the pinned `cargo-spellcheck` tool is available. [group("anvil-setup")] -anvil-tool-cargo-spellcheck-validate-prereqs: (_check-tool "cargo-spellcheck" cargo_spellcheck_version) +[script("pwsh", "-NoProfile")] +anvil-tool-cargo-spellcheck-validate-prereqs: + if ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture -eq [System.Runtime.InteropServices.Architecture]::Arm64) { + Write-Host 'anvil-tool-cargo-spellcheck-validate-prereqs: ARM64 -- skipping (upstream tool incompatibility)' + exit 0 + } + & "{{just_executable()}}" _check-tool cargo-spellcheck {{cargo_spellcheck_version}} + exit $LASTEXITCODE # Install the pinned `cargo-udeps` tool. [group("anvil-setup")] diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap index c6b37a6c..661b785a 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap @@ -4458,6 +4458,10 @@ anvil-semver-check-validate-prereqs: anvil-tool-cargo-semver-checks-validate-pre [script("pwsh", "-NoProfile")] anvil-spellcheck: anvil-spellcheck-validate-prereqs $ErrorActionPreference = 'Stop' + if ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture -eq [System.Runtime.InteropServices.Architecture]::Arm64) { + Write-Host 'anvil-spellcheck: ARM64 -- skipping (upstream tool incompatibility)' + exit 0 + } if ($env:ANVIL_INCLUDE_MODIFIED -eq '--skip') { Write-Host 'anvil-spellcheck: no modified packages; skipping' exit 0 @@ -6149,13 +6153,32 @@ anvil-tool-cargo-sort-install installer="install": (_install-tool "cargo-sort" c [group("anvil-setup")] anvil-tool-cargo-sort-validate-prereqs: (_check-tool "cargo-sort" cargo_sort_version) +# cargo-spellcheck 0.15.1 is not usable on ARM64: its source dependency +# ra_ap_stdx no longer compiles with current Rust, and its Windows ARM64 binary +# segfaults. Spelling is architecture-independent and runs on the x64 matrix +# legs, so installation, validation, and execution all self-skip on ARM64. + # Install the pinned `cargo-spellcheck` tool. [group("anvil-setup")] -anvil-tool-cargo-spellcheck-install installer="install": (_install-tool "cargo-spellcheck" cargo_spellcheck_version installer "anvil-tool-cargo-spellcheck-source-deps-check") +[script("pwsh", "-NoProfile")] +anvil-tool-cargo-spellcheck-install installer="install": + if ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture -eq [System.Runtime.InteropServices.Architecture]::Arm64) { + Write-Host 'anvil-tool-cargo-spellcheck-install: ARM64 -- skipping (upstream tool incompatibility)' + exit 0 + } + & "{{just_executable()}}" _install-tool cargo-spellcheck {{cargo_spellcheck_version}} {{installer}} anvil-tool-cargo-spellcheck-source-deps-check + exit $LASTEXITCODE # Validate that the pinned `cargo-spellcheck` tool is available. [group("anvil-setup")] -anvil-tool-cargo-spellcheck-validate-prereqs: (_check-tool "cargo-spellcheck" cargo_spellcheck_version) +[script("pwsh", "-NoProfile")] +anvil-tool-cargo-spellcheck-validate-prereqs: + if ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture -eq [System.Runtime.InteropServices.Architecture]::Arm64) { + Write-Host 'anvil-tool-cargo-spellcheck-validate-prereqs: ARM64 -- skipping (upstream tool incompatibility)' + exit 0 + } + & "{{just_executable()}}" _check-tool cargo-spellcheck {{cargo_spellcheck_version}} + exit $LASTEXITCODE # Install the pinned `cargo-udeps` tool. [group("anvil-setup")] diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap index c3265d44..47b9a8f0 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap @@ -1960,15 +1960,6 @@ runs: cargo binstall --no-confirm --locked just || cargo install --locked just fi - # cargo-spellcheck has no prebuilt Linux ARM64 artifact at the catalog - # version, so pr-fast must compile it. Provision its system dependency only - # on the managed runner and groups that install cargo-spellcheck; local and - # self-hosted environments retain Anvil's no-auto-install policy. - - name: Install cargo-spellcheck source dependency (Linux ARM64) - if: runner.environment == 'github-hosted' && runner.os == 'Linux' && runner.arch == 'ARM64' && (inputs.group == '' || inputs.group == 'pr-fast') - shell: bash - run: sudo apt-get update && sudo apt-get install -y libclang-dev - - name: Install anvil toolchains + tools shell: bash # When `group` is empty (the default), installs the full catalog @@ -4483,6 +4474,10 @@ anvil-semver-check-validate-prereqs: anvil-tool-cargo-semver-checks-validate-pre [script("pwsh", "-NoProfile")] anvil-spellcheck: anvil-spellcheck-validate-prereqs $ErrorActionPreference = 'Stop' + if ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture -eq [System.Runtime.InteropServices.Architecture]::Arm64) { + Write-Host 'anvil-spellcheck: ARM64 -- skipping (upstream tool incompatibility)' + exit 0 + } if ($env:ANVIL_INCLUDE_MODIFIED -eq '--skip') { Write-Host 'anvil-spellcheck: no modified packages; skipping' exit 0 @@ -6174,13 +6169,32 @@ anvil-tool-cargo-sort-install installer="install": (_install-tool "cargo-sort" c [group("anvil-setup")] anvil-tool-cargo-sort-validate-prereqs: (_check-tool "cargo-sort" cargo_sort_version) +# cargo-spellcheck 0.15.1 is not usable on ARM64: its source dependency +# ra_ap_stdx no longer compiles with current Rust, and its Windows ARM64 binary +# segfaults. Spelling is architecture-independent and runs on the x64 matrix +# legs, so installation, validation, and execution all self-skip on ARM64. + # Install the pinned `cargo-spellcheck` tool. [group("anvil-setup")] -anvil-tool-cargo-spellcheck-install installer="install": (_install-tool "cargo-spellcheck" cargo_spellcheck_version installer "anvil-tool-cargo-spellcheck-source-deps-check") +[script("pwsh", "-NoProfile")] +anvil-tool-cargo-spellcheck-install installer="install": + if ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture -eq [System.Runtime.InteropServices.Architecture]::Arm64) { + Write-Host 'anvil-tool-cargo-spellcheck-install: ARM64 -- skipping (upstream tool incompatibility)' + exit 0 + } + & "{{just_executable()}}" _install-tool cargo-spellcheck {{cargo_spellcheck_version}} {{installer}} anvil-tool-cargo-spellcheck-source-deps-check + exit $LASTEXITCODE # Validate that the pinned `cargo-spellcheck` tool is available. [group("anvil-setup")] -anvil-tool-cargo-spellcheck-validate-prereqs: (_check-tool "cargo-spellcheck" cargo_spellcheck_version) +[script("pwsh", "-NoProfile")] +anvil-tool-cargo-spellcheck-validate-prereqs: + if ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture -eq [System.Runtime.InteropServices.Architecture]::Arm64) { + Write-Host 'anvil-tool-cargo-spellcheck-validate-prereqs: ARM64 -- skipping (upstream tool incompatibility)' + exit 0 + } + & "{{just_executable()}}" _check-tool cargo-spellcheck {{cargo_spellcheck_version}} + exit $LASTEXITCODE # Install the pinned `cargo-udeps` tool. [group("anvil-setup")] diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap index 89bfb1c0..359711c4 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap @@ -3198,6 +3198,10 @@ anvil-semver-check-validate-prereqs: anvil-tool-cargo-semver-checks-validate-pre [script("pwsh", "-NoProfile")] anvil-spellcheck: anvil-spellcheck-validate-prereqs $ErrorActionPreference = 'Stop' + if ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture -eq [System.Runtime.InteropServices.Architecture]::Arm64) { + Write-Host 'anvil-spellcheck: ARM64 -- skipping (upstream tool incompatibility)' + exit 0 + } if ($env:ANVIL_INCLUDE_MODIFIED -eq '--skip') { Write-Host 'anvil-spellcheck: no modified packages; skipping' exit 0 @@ -4889,13 +4893,32 @@ anvil-tool-cargo-sort-install installer="install": (_install-tool "cargo-sort" c [group("anvil-setup")] anvil-tool-cargo-sort-validate-prereqs: (_check-tool "cargo-sort" cargo_sort_version) +# cargo-spellcheck 0.15.1 is not usable on ARM64: its source dependency +# ra_ap_stdx no longer compiles with current Rust, and its Windows ARM64 binary +# segfaults. Spelling is architecture-independent and runs on the x64 matrix +# legs, so installation, validation, and execution all self-skip on ARM64. + # Install the pinned `cargo-spellcheck` tool. [group("anvil-setup")] -anvil-tool-cargo-spellcheck-install installer="install": (_install-tool "cargo-spellcheck" cargo_spellcheck_version installer "anvil-tool-cargo-spellcheck-source-deps-check") +[script("pwsh", "-NoProfile")] +anvil-tool-cargo-spellcheck-install installer="install": + if ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture -eq [System.Runtime.InteropServices.Architecture]::Arm64) { + Write-Host 'anvil-tool-cargo-spellcheck-install: ARM64 -- skipping (upstream tool incompatibility)' + exit 0 + } + & "{{just_executable()}}" _install-tool cargo-spellcheck {{cargo_spellcheck_version}} {{installer}} anvil-tool-cargo-spellcheck-source-deps-check + exit $LASTEXITCODE # Validate that the pinned `cargo-spellcheck` tool is available. [group("anvil-setup")] -anvil-tool-cargo-spellcheck-validate-prereqs: (_check-tool "cargo-spellcheck" cargo_spellcheck_version) +[script("pwsh", "-NoProfile")] +anvil-tool-cargo-spellcheck-validate-prereqs: + if ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture -eq [System.Runtime.InteropServices.Architecture]::Arm64) { + Write-Host 'anvil-tool-cargo-spellcheck-validate-prereqs: ARM64 -- skipping (upstream tool incompatibility)' + exit 0 + } + & "{{just_executable()}}" _check-tool cargo-spellcheck {{cargo_spellcheck_version}} + exit $LASTEXITCODE # Install the pinned `cargo-udeps` tool. [group("anvil-setup")] diff --git a/justfiles/anvil/checks/spellcheck.just b/justfiles/anvil/checks/spellcheck.just index 032aada6..f3690f3a 100644 --- a/justfiles/anvil/checks/spellcheck.just +++ b/justfiles/anvil/checks/spellcheck.just @@ -24,6 +24,10 @@ [script("pwsh", "-NoProfile")] anvil-spellcheck: anvil-spellcheck-validate-prereqs $ErrorActionPreference = 'Stop' + if ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture -eq [System.Runtime.InteropServices.Architecture]::Arm64) { + Write-Host 'anvil-spellcheck: ARM64 -- skipping (upstream tool incompatibility)' + exit 0 + } if ($env:ANVIL_INCLUDE_MODIFIED -eq '--skip') { Write-Host 'anvil-spellcheck: no modified packages; skipping' exit 0 diff --git a/justfiles/anvil/tools.just b/justfiles/anvil/tools.just index 92de15d1..c714b0ec 100644 --- a/justfiles/anvil/tools.just +++ b/justfiles/anvil/tools.just @@ -718,13 +718,32 @@ anvil-tool-cargo-sort-install installer="install": (_install-tool "cargo-sort" c [group("anvil-setup")] anvil-tool-cargo-sort-validate-prereqs: (_check-tool "cargo-sort" cargo_sort_version) +# cargo-spellcheck 0.15.1 is not usable on ARM64: its source dependency +# ra_ap_stdx no longer compiles with current Rust, and its Windows ARM64 binary +# segfaults. Spelling is architecture-independent and runs on the x64 matrix +# legs, so installation, validation, and execution all self-skip on ARM64. + # Install the pinned `cargo-spellcheck` tool. [group("anvil-setup")] -anvil-tool-cargo-spellcheck-install installer="install": (_install-tool "cargo-spellcheck" cargo_spellcheck_version installer "anvil-tool-cargo-spellcheck-source-deps-check") +[script("pwsh", "-NoProfile")] +anvil-tool-cargo-spellcheck-install installer="install": + if ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture -eq [System.Runtime.InteropServices.Architecture]::Arm64) { + Write-Host 'anvil-tool-cargo-spellcheck-install: ARM64 -- skipping (upstream tool incompatibility)' + exit 0 + } + & "{{just_executable()}}" _install-tool cargo-spellcheck {{cargo_spellcheck_version}} {{installer}} anvil-tool-cargo-spellcheck-source-deps-check + exit $LASTEXITCODE # Validate that the pinned `cargo-spellcheck` tool is available. [group("anvil-setup")] -anvil-tool-cargo-spellcheck-validate-prereqs: (_check-tool "cargo-spellcheck" cargo_spellcheck_version) +[script("pwsh", "-NoProfile")] +anvil-tool-cargo-spellcheck-validate-prereqs: + if ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture -eq [System.Runtime.InteropServices.Architecture]::Arm64) { + Write-Host 'anvil-tool-cargo-spellcheck-validate-prereqs: ARM64 -- skipping (upstream tool incompatibility)' + exit 0 + } + & "{{just_executable()}}" _check-tool cargo-spellcheck {{cargo_spellcheck_version}} + exit $LASTEXITCODE # Install the pinned `cargo-udeps` tool. [group("anvil-setup")] From 0fd8954dcd62472341ddf981547bff9b08c27dfa Mon Sep 17 00:00:00 2001 From: "Martin Kolinek (from Dev Box)" Date: Wed, 5 Aug 2026 15:45:08 +0200 Subject: [PATCH 16/20] fix: keep recipe fixture within lint limits Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1c90ffee-5127-4069-8bb1-cef1492a400c --- crates/cargo-anvil/tests/recipe_contracts.rs | 73 ++++++++++---------- 1 file changed, 37 insertions(+), 36 deletions(-) diff --git a/crates/cargo-anvil/tests/recipe_contracts.rs b/crates/cargo-anvil/tests/recipe_contracts.rs index a30e4a0b..5ca5a86b 100644 --- a/crates/cargo-anvil/tests/recipe_contracts.rs +++ b/crates/cargo-anvil/tests/recipe_contracts.rs @@ -22,41 +22,7 @@ const LLVM_COV: &str = include_str!("../templates/justfiles/anvil/checks/llvm-co const SEMVER: &str = include_str!("../templates/justfiles/anvil/checks/semver-check.just"); const EXTERNAL_TYPES: &str = include_str!("../templates/justfiles/anvil/checks/external-types.just"); const VERSIONS: &str = include_str!("../templates/justfiles/anvil/versions.just"); - -fn write(path: &Path, contents: &str) { - if let Some(parent) = path.parent() { - std::fs::create_dir_all(parent).unwrap(); - } - std::fs::write(path, contents).unwrap(); -} - -fn tools_available() -> bool { - Command::new("just").arg("--version").output().is_ok() && Command::new("pwsh").arg("--version").output().is_ok() -} - -fn fixture(imports: &[(&str, &str)], dependency_recipes: &[&str]) -> TempDir { - let tmp = TempDir::new().unwrap(); - let mut justfile = String::from("set unstable\n\nrust_nightly := \"nightly-test\"\n\n"); - for (name, contents) in imports { - write(&tmp.path().join(name), contents); - writeln!(justfile, "import '{name}'").unwrap(); - } - justfile.push('\n'); - for recipe in dependency_recipes { - justfile.push_str(recipe); - justfile.push_str(":\n\n"); - } - write(&tmp.path().join("Justfile"), &justfile); - write( - &tmp.path().join("Cargo.toml"), - "[package]\nname = \"fixture\"\nversion = \"0.1.0\"\n", - ); - - let bin = tmp.path().join("fake-bin"); - std::fs::create_dir_all(&bin).unwrap(); - write( - &bin.join("cargo.ps1"), - r#" +const FAKE_CARGO_PS1: &str = r#" $joined = $args -join ' ' if ($env:FAKE_CARGO_LOG) { Add-Content -LiteralPath $env:FAKE_CARGO_LOG -Value $joined @@ -124,8 +90,43 @@ if ($args -contains 'nextest') { exit [int]$env:FAKE_NEXTEST_EXIT } exit 0 -"#, +"#; + +fn write(path: &Path, contents: &str) { + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent).unwrap(); + } + std::fs::write(path, contents).unwrap(); +} + +fn tools_available() -> bool { + Command::new("just").arg("--version").output().is_ok() && Command::new("pwsh").arg("--version").output().is_ok() +} + +fn fixture(imports: &[(&str, &str)], dependency_recipes: &[&str]) -> TempDir { + let tmp = TempDir::new().unwrap(); + let mut justfile = String::from("set unstable\n\n"); + if !imports.iter().any(|(name, _)| *name == "versions.just") { + justfile.push_str("rust_nightly := \"nightly-test\"\n\n"); + } + for (name, contents) in imports { + write(&tmp.path().join(name), contents); + writeln!(justfile, "import '{name}'").unwrap(); + } + justfile.push('\n'); + for recipe in dependency_recipes { + justfile.push_str(recipe); + justfile.push_str(":\n\n"); + } + write(&tmp.path().join("Justfile"), &justfile); + write( + &tmp.path().join("Cargo.toml"), + "[package]\nname = \"fixture\"\nversion = \"0.1.0\"\n", ); + + let bin = tmp.path().join("fake-bin"); + std::fs::create_dir_all(&bin).unwrap(); + write(&bin.join("cargo.ps1"), FAKE_CARGO_PS1); write(&bin.join("git.ps1"), "exit 0\n"); tmp } From 3d22490aab15dab83d0db466a5c88fb2418d7890 Mon Sep 17 00:00:00 2001 From: "Martin Kolinek (from Dev Box)" Date: Wed, 19 Aug 2026 18:01:23 +0200 Subject: [PATCH 17/20] fix: run cargo-spellcheck on ARM64 Upgrade cargo-spellcheck to 0.15.7, remove the 0.15.1 ARM64 skip policy and version tripwire, and regenerate Anvil workflows, justfiles, lock metadata, and snapshots. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1c90ffee-5127-4069-8bb1-cef1492a400c --- .anvil.lock | 10 +++---- crates/cargo-anvil/docs/design/local.md | 6 ----- .../cargo-anvil/src/anvil/artifacts/github.rs | 4 +++ .../src/anvil/artifacts/justfile.rs | 8 ++---- .../justfiles/anvil/checks/spellcheck.just | 4 --- .../templates/justfiles/anvil/tools.just | 23 ++-------------- .../snapshots/snapshots__ado_backend.snap | 27 ++----------------- .../snapshots/snapshots__github_backend.snap | 27 ++----------------- .../snapshots/snapshots__local_only.snap | 27 ++----------------- justfiles/anvil/checks/spellcheck.just | 4 --- justfiles/anvil/tools.just | 23 ++-------------- 11 files changed, 21 insertions(+), 142 deletions(-) diff --git a/.anvil.lock b/.anvil.lock index fe2c81f5..3608af97 100644 --- a/.anvil.lock +++ b/.anvil.lock @@ -1,7 +1,7 @@ version = 1 tool = "anvil" tool_version = "0.4.0" -catalog_checksum = "sha256:9758e00880ab0b93cc26b387c9c1131917eb20491fe2d60cb128afb782f27a8b" +catalog_checksum = "sha256:9f1682fb6c917aa5be66e786f064900ba79a1715c3534a727a163ad2a0aeee34" [[file]] path = ".anvil/container/Containerfile" @@ -77,7 +77,7 @@ checksum = "sha256:a153fa2ce4307ef9da91ff215e4f37e5a329d0bbb2c04c2b1f26096887f64 [[file]] path = ".github/workflows/anvil-pr-impl.yml" -checksum = "sha256:bf8b39c2ccb0cb6a682b68f5055df08d944913f863414202454ea75dafe4a839" +checksum = "sha256:6920b089ab3b36ee754e949147990c037acafdb53ca229e43e401da310a9b4cd" [[file]] path = ".github/workflows/anvil-pr.yml" @@ -85,7 +85,7 @@ checksum = "sha256:cb7996cc978eb3f6db6572a78eec1341bfbebb592c90f78f69d622eaacf6d [[file]] path = ".github/workflows/anvil-scheduled-impl.yml" -checksum = "sha256:06cb668f5de38b3a06742a96fd78e575d459af682d418f31119d62b2fa2c3ecf" +checksum = "sha256:a4bbf3fd9d2b757431856d8ad4cce0ba3dabbb3455e164521bdbd8da2bc84ba8" [[file]] path = ".github/workflows/anvil-scheduled.yml" @@ -205,7 +205,7 @@ checksum = "sha256:3210a749418e6716d1b689c3da5bb5e3af36700e09da0c2077fc44ade11c9 [[file]] path = "justfiles/anvil/checks/spellcheck.just" -checksum = "sha256:7fac855bb6d2560c3a465993e86b0ed5e8225838e67ecb589ff6a3bab5881a66" +checksum = "sha256:86bb66c426eb332af10941e949204eeb8a4564acb4ec32425fdbb1b34fb04a55" [[file]] path = "justfiles/anvil/checks/udeps.just" @@ -269,7 +269,7 @@ checksum = "sha256:713c5a2ae28b6b5aa20dd38226278b3b7f71bbc5e6b84a16eaf5244713f27 [[file]] path = "justfiles/anvil/tools.just" -checksum = "sha256:334dde29c3d17d4f9f30357fe1d17a594a0ba9895b7b3ca8ac22542754a8bbed" +checksum = "sha256:69049ea8b26aed36e25055916bc333f303d85636850cb863096548e339f8426e" [[file]] path = "justfiles/anvil/versions.just" diff --git a/crates/cargo-anvil/docs/design/local.md b/crates/cargo-anvil/docs/design/local.md index d42ffec0..4baa06c0 100644 --- a/crates/cargo-anvil/docs/design/local.md +++ b/crates/cargo-anvil/docs/design/local.md @@ -352,12 +352,6 @@ Each tool with a source-build system dependency owns a tool-specific prerequisit wires it into `_install-tool`. Catalog changes propagate to adopters via `cargo anvil` like any other template edit. -`cargo-spellcheck 0.15.1` is skipped end-to-end on ARM64. Its pinned -`ra_ap_stdx` dependency does not compile with current Rust on Linux ARM64, and -the available Windows ARM64 binary terminates with an access violation. Because -spelling results are architecture-independent, x64 workflow legs retain the -check while ARM64 setup, prerequisite validation, and execution are no-ops. - ### 3.4 Per-check warnings Every check recipe depends on `anvil--validate-prereqs` so even ad-hoc diff --git a/crates/cargo-anvil/src/anvil/artifacts/github.rs b/crates/cargo-anvil/src/anvil/artifacts/github.rs index e1324111..bbdfde6e 100644 --- a/crates/cargo-anvil/src/anvil/artifacts/github.rs +++ b/crates/cargo-anvil/src/anvil/artifacts/github.rs @@ -216,6 +216,10 @@ mod tests { #[test] fn pr_impl_workflow_has_expected_jobs() { assert!(PR_IMPL_WORKFLOW.contains("workflow_call:")); + assert!( + !PR_IMPL_WORKFLOW.contains("ANVIL_SPELLCHECK_SKIP_UNSUPPORTED_ARM64"), + "the PR workflow must run spellcheck on ARM64" + ); for needle in [ "impact-linux:", "impact-windows:", diff --git a/crates/cargo-anvil/src/anvil/artifacts/justfile.rs b/crates/cargo-anvil/src/anvil/artifacts/justfile.rs index a60e427c..0b11669e 100644 --- a/crates/cargo-anvil/src/anvil/artifacts/justfile.rs +++ b/crates/cargo-anvil/src/anvil/artifacts/justfile.rs @@ -303,18 +303,14 @@ mod tests { TOOLS_JUST.contains("anvil-tool-cargo-spellcheck-source-deps-check"), "spellcheck installer must run libclang validation before source builds" ); - assert!( - TOOLS_JUST.contains("anvil-tool-cargo-spellcheck-install: ARM64 -- skipping (upstream tool incompatibility)"), - "spellcheck installation must skip unsupported ARM64 hosts" - ); let checks = all_check_bodies(); assert!( !checks.contains("anvil-spellcheck-setup installer=\"install\": anvil-tool-cargo-spellcheck-source-deps-check"), "spellcheck setup must not require libclang before binstall" ); assert!( - checks.contains("anvil-spellcheck: ARM64 -- skipping (upstream tool incompatibility)"), - "spellcheck execution must skip unsupported ARM64 hosts" + !TOOLS_JUST.contains("ANVIL_SPELLCHECK_SKIP_UNSUPPORTED_ARM64") && !checks.contains("ANVIL_SPELLCHECK_SKIP_UNSUPPORTED_ARM64"), + "spellcheck must run normally on ARM64" ); } diff --git a/crates/cargo-anvil/templates/justfiles/anvil/checks/spellcheck.just b/crates/cargo-anvil/templates/justfiles/anvil/checks/spellcheck.just index f3690f3a..032aada6 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/checks/spellcheck.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/checks/spellcheck.just @@ -24,10 +24,6 @@ [script("pwsh", "-NoProfile")] anvil-spellcheck: anvil-spellcheck-validate-prereqs $ErrorActionPreference = 'Stop' - if ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture -eq [System.Runtime.InteropServices.Architecture]::Arm64) { - Write-Host 'anvil-spellcheck: ARM64 -- skipping (upstream tool incompatibility)' - exit 0 - } if ($env:ANVIL_INCLUDE_MODIFIED -eq '--skip') { Write-Host 'anvil-spellcheck: no modified packages; skipping' exit 0 diff --git a/crates/cargo-anvil/templates/justfiles/anvil/tools.just b/crates/cargo-anvil/templates/justfiles/anvil/tools.just index c714b0ec..92de15d1 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/tools.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/tools.just @@ -718,32 +718,13 @@ anvil-tool-cargo-sort-install installer="install": (_install-tool "cargo-sort" c [group("anvil-setup")] anvil-tool-cargo-sort-validate-prereqs: (_check-tool "cargo-sort" cargo_sort_version) -# cargo-spellcheck 0.15.1 is not usable on ARM64: its source dependency -# ra_ap_stdx no longer compiles with current Rust, and its Windows ARM64 binary -# segfaults. Spelling is architecture-independent and runs on the x64 matrix -# legs, so installation, validation, and execution all self-skip on ARM64. - # Install the pinned `cargo-spellcheck` tool. [group("anvil-setup")] -[script("pwsh", "-NoProfile")] -anvil-tool-cargo-spellcheck-install installer="install": - if ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture -eq [System.Runtime.InteropServices.Architecture]::Arm64) { - Write-Host 'anvil-tool-cargo-spellcheck-install: ARM64 -- skipping (upstream tool incompatibility)' - exit 0 - } - & "{{just_executable()}}" _install-tool cargo-spellcheck {{cargo_spellcheck_version}} {{installer}} anvil-tool-cargo-spellcheck-source-deps-check - exit $LASTEXITCODE +anvil-tool-cargo-spellcheck-install installer="install": (_install-tool "cargo-spellcheck" cargo_spellcheck_version installer "anvil-tool-cargo-spellcheck-source-deps-check") # Validate that the pinned `cargo-spellcheck` tool is available. [group("anvil-setup")] -[script("pwsh", "-NoProfile")] -anvil-tool-cargo-spellcheck-validate-prereqs: - if ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture -eq [System.Runtime.InteropServices.Architecture]::Arm64) { - Write-Host 'anvil-tool-cargo-spellcheck-validate-prereqs: ARM64 -- skipping (upstream tool incompatibility)' - exit 0 - } - & "{{just_executable()}}" _check-tool cargo-spellcheck {{cargo_spellcheck_version}} - exit $LASTEXITCODE +anvil-tool-cargo-spellcheck-validate-prereqs: (_check-tool "cargo-spellcheck" cargo_spellcheck_version) # Install the pinned `cargo-udeps` tool. [group("anvil-setup")] diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap index 661b785a..c6b37a6c 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap @@ -4458,10 +4458,6 @@ anvil-semver-check-validate-prereqs: anvil-tool-cargo-semver-checks-validate-pre [script("pwsh", "-NoProfile")] anvil-spellcheck: anvil-spellcheck-validate-prereqs $ErrorActionPreference = 'Stop' - if ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture -eq [System.Runtime.InteropServices.Architecture]::Arm64) { - Write-Host 'anvil-spellcheck: ARM64 -- skipping (upstream tool incompatibility)' - exit 0 - } if ($env:ANVIL_INCLUDE_MODIFIED -eq '--skip') { Write-Host 'anvil-spellcheck: no modified packages; skipping' exit 0 @@ -6153,32 +6149,13 @@ anvil-tool-cargo-sort-install installer="install": (_install-tool "cargo-sort" c [group("anvil-setup")] anvil-tool-cargo-sort-validate-prereqs: (_check-tool "cargo-sort" cargo_sort_version) -# cargo-spellcheck 0.15.1 is not usable on ARM64: its source dependency -# ra_ap_stdx no longer compiles with current Rust, and its Windows ARM64 binary -# segfaults. Spelling is architecture-independent and runs on the x64 matrix -# legs, so installation, validation, and execution all self-skip on ARM64. - # Install the pinned `cargo-spellcheck` tool. [group("anvil-setup")] -[script("pwsh", "-NoProfile")] -anvil-tool-cargo-spellcheck-install installer="install": - if ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture -eq [System.Runtime.InteropServices.Architecture]::Arm64) { - Write-Host 'anvil-tool-cargo-spellcheck-install: ARM64 -- skipping (upstream tool incompatibility)' - exit 0 - } - & "{{just_executable()}}" _install-tool cargo-spellcheck {{cargo_spellcheck_version}} {{installer}} anvil-tool-cargo-spellcheck-source-deps-check - exit $LASTEXITCODE +anvil-tool-cargo-spellcheck-install installer="install": (_install-tool "cargo-spellcheck" cargo_spellcheck_version installer "anvil-tool-cargo-spellcheck-source-deps-check") # Validate that the pinned `cargo-spellcheck` tool is available. [group("anvil-setup")] -[script("pwsh", "-NoProfile")] -anvil-tool-cargo-spellcheck-validate-prereqs: - if ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture -eq [System.Runtime.InteropServices.Architecture]::Arm64) { - Write-Host 'anvil-tool-cargo-spellcheck-validate-prereqs: ARM64 -- skipping (upstream tool incompatibility)' - exit 0 - } - & "{{just_executable()}}" _check-tool cargo-spellcheck {{cargo_spellcheck_version}} - exit $LASTEXITCODE +anvil-tool-cargo-spellcheck-validate-prereqs: (_check-tool "cargo-spellcheck" cargo_spellcheck_version) # Install the pinned `cargo-udeps` tool. [group("anvil-setup")] diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap index 47b9a8f0..6da6632c 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap @@ -4474,10 +4474,6 @@ anvil-semver-check-validate-prereqs: anvil-tool-cargo-semver-checks-validate-pre [script("pwsh", "-NoProfile")] anvil-spellcheck: anvil-spellcheck-validate-prereqs $ErrorActionPreference = 'Stop' - if ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture -eq [System.Runtime.InteropServices.Architecture]::Arm64) { - Write-Host 'anvil-spellcheck: ARM64 -- skipping (upstream tool incompatibility)' - exit 0 - } if ($env:ANVIL_INCLUDE_MODIFIED -eq '--skip') { Write-Host 'anvil-spellcheck: no modified packages; skipping' exit 0 @@ -6169,32 +6165,13 @@ anvil-tool-cargo-sort-install installer="install": (_install-tool "cargo-sort" c [group("anvil-setup")] anvil-tool-cargo-sort-validate-prereqs: (_check-tool "cargo-sort" cargo_sort_version) -# cargo-spellcheck 0.15.1 is not usable on ARM64: its source dependency -# ra_ap_stdx no longer compiles with current Rust, and its Windows ARM64 binary -# segfaults. Spelling is architecture-independent and runs on the x64 matrix -# legs, so installation, validation, and execution all self-skip on ARM64. - # Install the pinned `cargo-spellcheck` tool. [group("anvil-setup")] -[script("pwsh", "-NoProfile")] -anvil-tool-cargo-spellcheck-install installer="install": - if ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture -eq [System.Runtime.InteropServices.Architecture]::Arm64) { - Write-Host 'anvil-tool-cargo-spellcheck-install: ARM64 -- skipping (upstream tool incompatibility)' - exit 0 - } - & "{{just_executable()}}" _install-tool cargo-spellcheck {{cargo_spellcheck_version}} {{installer}} anvil-tool-cargo-spellcheck-source-deps-check - exit $LASTEXITCODE +anvil-tool-cargo-spellcheck-install installer="install": (_install-tool "cargo-spellcheck" cargo_spellcheck_version installer "anvil-tool-cargo-spellcheck-source-deps-check") # Validate that the pinned `cargo-spellcheck` tool is available. [group("anvil-setup")] -[script("pwsh", "-NoProfile")] -anvil-tool-cargo-spellcheck-validate-prereqs: - if ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture -eq [System.Runtime.InteropServices.Architecture]::Arm64) { - Write-Host 'anvil-tool-cargo-spellcheck-validate-prereqs: ARM64 -- skipping (upstream tool incompatibility)' - exit 0 - } - & "{{just_executable()}}" _check-tool cargo-spellcheck {{cargo_spellcheck_version}} - exit $LASTEXITCODE +anvil-tool-cargo-spellcheck-validate-prereqs: (_check-tool "cargo-spellcheck" cargo_spellcheck_version) # Install the pinned `cargo-udeps` tool. [group("anvil-setup")] diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap index 359711c4..89bfb1c0 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap @@ -3198,10 +3198,6 @@ anvil-semver-check-validate-prereqs: anvil-tool-cargo-semver-checks-validate-pre [script("pwsh", "-NoProfile")] anvil-spellcheck: anvil-spellcheck-validate-prereqs $ErrorActionPreference = 'Stop' - if ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture -eq [System.Runtime.InteropServices.Architecture]::Arm64) { - Write-Host 'anvil-spellcheck: ARM64 -- skipping (upstream tool incompatibility)' - exit 0 - } if ($env:ANVIL_INCLUDE_MODIFIED -eq '--skip') { Write-Host 'anvil-spellcheck: no modified packages; skipping' exit 0 @@ -4893,32 +4889,13 @@ anvil-tool-cargo-sort-install installer="install": (_install-tool "cargo-sort" c [group("anvil-setup")] anvil-tool-cargo-sort-validate-prereqs: (_check-tool "cargo-sort" cargo_sort_version) -# cargo-spellcheck 0.15.1 is not usable on ARM64: its source dependency -# ra_ap_stdx no longer compiles with current Rust, and its Windows ARM64 binary -# segfaults. Spelling is architecture-independent and runs on the x64 matrix -# legs, so installation, validation, and execution all self-skip on ARM64. - # Install the pinned `cargo-spellcheck` tool. [group("anvil-setup")] -[script("pwsh", "-NoProfile")] -anvil-tool-cargo-spellcheck-install installer="install": - if ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture -eq [System.Runtime.InteropServices.Architecture]::Arm64) { - Write-Host 'anvil-tool-cargo-spellcheck-install: ARM64 -- skipping (upstream tool incompatibility)' - exit 0 - } - & "{{just_executable()}}" _install-tool cargo-spellcheck {{cargo_spellcheck_version}} {{installer}} anvil-tool-cargo-spellcheck-source-deps-check - exit $LASTEXITCODE +anvil-tool-cargo-spellcheck-install installer="install": (_install-tool "cargo-spellcheck" cargo_spellcheck_version installer "anvil-tool-cargo-spellcheck-source-deps-check") # Validate that the pinned `cargo-spellcheck` tool is available. [group("anvil-setup")] -[script("pwsh", "-NoProfile")] -anvil-tool-cargo-spellcheck-validate-prereqs: - if ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture -eq [System.Runtime.InteropServices.Architecture]::Arm64) { - Write-Host 'anvil-tool-cargo-spellcheck-validate-prereqs: ARM64 -- skipping (upstream tool incompatibility)' - exit 0 - } - & "{{just_executable()}}" _check-tool cargo-spellcheck {{cargo_spellcheck_version}} - exit $LASTEXITCODE +anvil-tool-cargo-spellcheck-validate-prereqs: (_check-tool "cargo-spellcheck" cargo_spellcheck_version) # Install the pinned `cargo-udeps` tool. [group("anvil-setup")] diff --git a/justfiles/anvil/checks/spellcheck.just b/justfiles/anvil/checks/spellcheck.just index f3690f3a..032aada6 100644 --- a/justfiles/anvil/checks/spellcheck.just +++ b/justfiles/anvil/checks/spellcheck.just @@ -24,10 +24,6 @@ [script("pwsh", "-NoProfile")] anvil-spellcheck: anvil-spellcheck-validate-prereqs $ErrorActionPreference = 'Stop' - if ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture -eq [System.Runtime.InteropServices.Architecture]::Arm64) { - Write-Host 'anvil-spellcheck: ARM64 -- skipping (upstream tool incompatibility)' - exit 0 - } if ($env:ANVIL_INCLUDE_MODIFIED -eq '--skip') { Write-Host 'anvil-spellcheck: no modified packages; skipping' exit 0 diff --git a/justfiles/anvil/tools.just b/justfiles/anvil/tools.just index c714b0ec..92de15d1 100644 --- a/justfiles/anvil/tools.just +++ b/justfiles/anvil/tools.just @@ -718,32 +718,13 @@ anvil-tool-cargo-sort-install installer="install": (_install-tool "cargo-sort" c [group("anvil-setup")] anvil-tool-cargo-sort-validate-prereqs: (_check-tool "cargo-sort" cargo_sort_version) -# cargo-spellcheck 0.15.1 is not usable on ARM64: its source dependency -# ra_ap_stdx no longer compiles with current Rust, and its Windows ARM64 binary -# segfaults. Spelling is architecture-independent and runs on the x64 matrix -# legs, so installation, validation, and execution all self-skip on ARM64. - # Install the pinned `cargo-spellcheck` tool. [group("anvil-setup")] -[script("pwsh", "-NoProfile")] -anvil-tool-cargo-spellcheck-install installer="install": - if ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture -eq [System.Runtime.InteropServices.Architecture]::Arm64) { - Write-Host 'anvil-tool-cargo-spellcheck-install: ARM64 -- skipping (upstream tool incompatibility)' - exit 0 - } - & "{{just_executable()}}" _install-tool cargo-spellcheck {{cargo_spellcheck_version}} {{installer}} anvil-tool-cargo-spellcheck-source-deps-check - exit $LASTEXITCODE +anvil-tool-cargo-spellcheck-install installer="install": (_install-tool "cargo-spellcheck" cargo_spellcheck_version installer "anvil-tool-cargo-spellcheck-source-deps-check") # Validate that the pinned `cargo-spellcheck` tool is available. [group("anvil-setup")] -[script("pwsh", "-NoProfile")] -anvil-tool-cargo-spellcheck-validate-prereqs: - if ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture -eq [System.Runtime.InteropServices.Architecture]::Arm64) { - Write-Host 'anvil-tool-cargo-spellcheck-validate-prereqs: ARM64 -- skipping (upstream tool incompatibility)' - exit 0 - } - & "{{just_executable()}}" _check-tool cargo-spellcheck {{cargo_spellcheck_version}} - exit $LASTEXITCODE +anvil-tool-cargo-spellcheck-validate-prereqs: (_check-tool "cargo-spellcheck" cargo_spellcheck_version) # Install the pinned `cargo-udeps` tool. [group("anvil-setup")] From 54e7d3e4d6b8a6ee08716b3d39525f2c65669f0a Mon Sep 17 00:00:00 2001 From: "Martin Kolinek (from Dev Box)" Date: Thu, 20 Aug 2026 18:53:12 +0200 Subject: [PATCH 18/20] docs: align exact-pin rationale with spellcheck 0.15.7 Explain that the 0.15.7 tokenization change is deliberately accommodated by spellcheck.toml instead of describing the now-pinned release as an unhandled regression. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1c90ffee-5127-4069-8bb1-cef1492a400c --- .anvil.lock | 4 ++-- crates/cargo-anvil/docs/design/local.md | 8 ++++---- .../cargo-anvil/templates/justfiles/anvil/versions.just | 7 +++---- .../tests/snapshots/snapshots__ado_backend.snap | 7 +++---- .../tests/snapshots/snapshots__github_backend.snap | 7 +++---- .../tests/snapshots/snapshots__local_only.snap | 7 +++---- justfiles/anvil/versions.just | 7 +++---- 7 files changed, 21 insertions(+), 26 deletions(-) diff --git a/.anvil.lock b/.anvil.lock index 3608af97..daa7d749 100644 --- a/.anvil.lock +++ b/.anvil.lock @@ -1,7 +1,7 @@ version = 1 tool = "anvil" tool_version = "0.4.0" -catalog_checksum = "sha256:9f1682fb6c917aa5be66e786f064900ba79a1715c3534a727a163ad2a0aeee34" +catalog_checksum = "sha256:97eb28be5a0fbd430872d9ce92cee3940d117f9bc2856926d821217d18947f90" [[file]] path = ".anvil/container/Containerfile" @@ -273,7 +273,7 @@ checksum = "sha256:69049ea8b26aed36e25055916bc333f303d85636850cb863096548e339f84 [[file]] path = "justfiles/anvil/versions.just" -checksum = "sha256:7f56852a5c4882fd535eeda7853e5cb45e32b79933a0cc46c62aeef289c9e69a" +checksum = "sha256:acbea93d5117db747537f4f7b9a5eb90b7d3e0dd3e8684cc0e4dc1dcb15ac93e" [[region]] host = ".delta.toml" diff --git a/crates/cargo-anvil/docs/design/local.md b/crates/cargo-anvil/docs/design/local.md index 4baa06c0..32c7f8ba 100644 --- a/crates/cargo-anvil/docs/design/local.md +++ b/crates/cargo-anvil/docs/design/local.md @@ -196,10 +196,10 @@ The catalog records, for each cargo subcommand, a **catalog version** (e.g. installs *exactly* that version (`--version '={{ pin }}'`), never `>=`. Pulling latest-matching at install time is a cloud-workflow reproducibility risk -- an upstream release between yesterday's green build and today's PR can break things, even though the - catalog hasn't moved. `cargo-spellcheck 0.15.7`'s em-dash word-boundary regression is - the canonical example: with `>=0.15.1` the catalog would have silently picked it up, - breaking every PR until the catalog was edited. With `=0.15.1` the catalog locks in - the version it was validated against. + catalog hasn't moved. Exact pins let the catalog accommodate behavior changes before + upgrading; for example, cargo-spellcheck 0.15.7 is paired with the explicit + `tokenization_splitchars` boundary list in `spellcheck.toml` rather than being selected + implicitly by a range. - **On runtime check** (`anvil-tool--validate-prereqs`): the recipe enforces `installed >= pin`. A local developer who has manually upgraded a tool for their own reasons (e.g. needing a bugfix the catalog hasn't pinned yet) is not downgraded by diff --git a/crates/cargo-anvil/templates/justfiles/anvil/versions.just b/crates/cargo-anvil/templates/justfiles/anvil/versions.just index 4e60c597..564a3b90 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/versions.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/versions.just @@ -11,10 +11,9 @@ # - On install (`-install` recipes): exactly this version (`=` for # cargo subcommands, exact ref for rustup toolchains). Pulling # "latest-matching" at install time is a cloud-workflow reproducibility risk -- -# an upstream release between yesterday's green build and today's -# PR can break things (cargo-spellcheck 0.15.7's em-dash regression -# is the canonical case). The `=` constraint locks the install to -# the version the catalog was validated against. +# an upstream release between yesterday's green build and today's PR can +# change behavior. The `=` constraint locks the install to the version the +# catalog was validated against. # - On validate-prereqs (`-validate-prereqs` recipes): the installed # version must be `>= `. A user who has manually upgraded a # tool for their own reasons (e.g. needing an unreleased bugfix) diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap index c6b37a6c..cae97ea6 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap @@ -6179,10 +6179,9 @@ anvil-tool-cargo-udeps-validate-prereqs: (_check-tool "cargo-udeps" cargo_udeps_ # - On install (`-install` recipes): exactly this version (`=` for # cargo subcommands, exact ref for rustup toolchains). Pulling # "latest-matching" at install time is a cloud-workflow reproducibility risk -- -# an upstream release between yesterday's green build and today's -# PR can break things (cargo-spellcheck 0.15.7's em-dash regression -# is the canonical case). The `=` constraint locks the install to -# the version the catalog was validated against. +# an upstream release between yesterday's green build and today's PR can +# change behavior. The `=` constraint locks the install to the version the +# catalog was validated against. # - On validate-prereqs (`-validate-prereqs` recipes): the installed # version must be `>= `. A user who has manually upgraded a # tool for their own reasons (e.g. needing an unreleased bugfix) diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap index 6da6632c..b54d34d9 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap @@ -6195,10 +6195,9 @@ anvil-tool-cargo-udeps-validate-prereqs: (_check-tool "cargo-udeps" cargo_udeps_ # - On install (`-install` recipes): exactly this version (`=` for # cargo subcommands, exact ref for rustup toolchains). Pulling # "latest-matching" at install time is a cloud-workflow reproducibility risk -- -# an upstream release between yesterday's green build and today's -# PR can break things (cargo-spellcheck 0.15.7's em-dash regression -# is the canonical case). The `=` constraint locks the install to -# the version the catalog was validated against. +# an upstream release between yesterday's green build and today's PR can +# change behavior. The `=` constraint locks the install to the version the +# catalog was validated against. # - On validate-prereqs (`-validate-prereqs` recipes): the installed # version must be `>= `. A user who has manually upgraded a # tool for their own reasons (e.g. needing an unreleased bugfix) diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap index 89bfb1c0..4afdf352 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap @@ -4919,10 +4919,9 @@ anvil-tool-cargo-udeps-validate-prereqs: (_check-tool "cargo-udeps" cargo_udeps_ # - On install (`-install` recipes): exactly this version (`=` for # cargo subcommands, exact ref for rustup toolchains). Pulling # "latest-matching" at install time is a cloud-workflow reproducibility risk -- -# an upstream release between yesterday's green build and today's -# PR can break things (cargo-spellcheck 0.15.7's em-dash regression -# is the canonical case). The `=` constraint locks the install to -# the version the catalog was validated against. +# an upstream release between yesterday's green build and today's PR can +# change behavior. The `=` constraint locks the install to the version the +# catalog was validated against. # - On validate-prereqs (`-validate-prereqs` recipes): the installed # version must be `>= `. A user who has manually upgraded a # tool for their own reasons (e.g. needing an unreleased bugfix) diff --git a/justfiles/anvil/versions.just b/justfiles/anvil/versions.just index 4e60c597..564a3b90 100644 --- a/justfiles/anvil/versions.just +++ b/justfiles/anvil/versions.just @@ -11,10 +11,9 @@ # - On install (`-install` recipes): exactly this version (`=` for # cargo subcommands, exact ref for rustup toolchains). Pulling # "latest-matching" at install time is a cloud-workflow reproducibility risk -- -# an upstream release between yesterday's green build and today's -# PR can break things (cargo-spellcheck 0.15.7's em-dash regression -# is the canonical case). The `=` constraint locks the install to -# the version the catalog was validated against. +# an upstream release between yesterday's green build and today's PR can +# change behavior. The `=` constraint locks the install to the version the +# catalog was validated against. # - On validate-prereqs (`-validate-prereqs` recipes): the installed # version must be `>= `. A user who has manually upgraded a # tool for their own reasons (e.g. needing an unreleased bugfix) From c4b173f9b6a0886186f7234b1f3df6388cbca68d Mon Sep 17 00:00:00 2001 From: "Martin Kolinek (from Dev Box)" Date: Thu, 20 Aug 2026 22:46:22 +0200 Subject: [PATCH 19/20] test: restore controlled installer fallback contract Disable binstall compilation only for tools with source prerequisites and restore executable coverage for fallback ordering, exact pins, prerequisite failures, and ordinary-tool strategy. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1c90ffee-5127-4069-8bb1-cef1492a400c --- .anvil.lock | 4 +- crates/cargo-anvil/docs/design/local.md | 12 +-- .../src/anvil/artifacts/justfile.rs | 6 +- .../templates/justfiles/anvil/tools.just | 16 ++- crates/cargo-anvil/tests/recipe_contracts.rs | 97 +++++++++++++++++++ .../snapshots/snapshots__ado_backend.snap | 16 ++- .../snapshots/snapshots__github_backend.snap | 16 ++- .../snapshots/snapshots__local_only.snap | 16 ++- justfiles/anvil/tools.just | 16 ++- 9 files changed, 165 insertions(+), 34 deletions(-) diff --git a/.anvil.lock b/.anvil.lock index daa7d749..3488aa5f 100644 --- a/.anvil.lock +++ b/.anvil.lock @@ -1,7 +1,7 @@ version = 1 tool = "anvil" tool_version = "0.4.0" -catalog_checksum = "sha256:97eb28be5a0fbd430872d9ce92cee3940d117f9bc2856926d821217d18947f90" +catalog_checksum = "sha256:8cbe093ffc07cb9f70af1fd24e1b7d81b6d68ae981eb820279619451b8908544" [[file]] path = ".anvil/container/Containerfile" @@ -269,7 +269,7 @@ checksum = "sha256:713c5a2ae28b6b5aa20dd38226278b3b7f71bbc5e6b84a16eaf5244713f27 [[file]] path = "justfiles/anvil/tools.just" -checksum = "sha256:69049ea8b26aed36e25055916bc333f303d85636850cb863096548e339f8426e" +checksum = "sha256:a1e44ca16f172b487afa3997f102512733d3b65a4418cf894cbd749a3abc17dc" [[file]] path = "justfiles/anvil/versions.just" diff --git a/crates/cargo-anvil/docs/design/local.md b/crates/cargo-anvil/docs/design/local.md index 32c7f8ba..930c496f 100644 --- a/crates/cargo-anvil/docs/design/local.md +++ b/crates/cargo-anvil/docs/design/local.md @@ -278,12 +278,12 @@ The `installer` argument: source builds; works in any cargo environment with no extra runtime dependency. Slow on a cold runner (~30 min for the full catalog) because every tool re-compiles common deps (`clap`, `syn`, `quote`, ...) from scratch independently. -- `binstall` -- `cargo binstall --no-confirm --locked --disable-strategies compile - --version '='`. Downloads a prebuilt binary from each tool's GitHub - Releases when available. If no binary is available, Anvil runs the tool's source - prerequisite before invoking `cargo install` itself; disabling binstall's compile - strategy prevents an uncontrolled source build from bypassing that check. The - binary path cuts the cold-runner install phase from ~30 min to ~1 min. +- `binstall` -- `cargo binstall --no-confirm --locked --version '='`. + Downloads a prebuilt binary from each tool's GitHub Releases when available. + When a tool declares a source prerequisite, Anvil disables binstall's compile + strategy and performs the source fallback itself after checking that prerequisite. + Tools without a source prerequisite retain binstall's normal compile strategy. + The binary path cuts the cold-runner install phase from ~30 min to ~1 min. `cargo-binstall` itself needs to be on PATH; the GH setup composite arranges this. The GitHub composite setup action calls `just anvil--setup binstall` diff --git a/crates/cargo-anvil/src/anvil/artifacts/justfile.rs b/crates/cargo-anvil/src/anvil/artifacts/justfile.rs index 0b11669e..4a880cb8 100644 --- a/crates/cargo-anvil/src/anvil/artifacts/justfile.rs +++ b/crates/cargo-anvil/src/anvil/artifacts/justfile.rs @@ -296,7 +296,11 @@ mod tests { #[test] fn spellcheck_checks_source_prerequisites_before_source_builds() { assert!( - TOOLS_JUST.contains("--disable-strategies compile"), + TOOLS_JUST.contains("if ($sourcePrereq)"), + "binstall compile strategy must only be disabled for tools with source prerequisites" + ); + assert!( + TOOLS_JUST.contains("$binstallArgs += @('--disable-strategies', 'compile')"), "binstall must not compile before Anvil checks source prerequisites" ); assert!( diff --git a/crates/cargo-anvil/templates/justfiles/anvil/tools.just b/crates/cargo-anvil/templates/justfiles/anvil/tools.just index 92de15d1..cb6aab14 100644 --- a/crates/cargo-anvil/templates/justfiles/anvil/tools.just +++ b/crates/cargo-anvil/templates/justfiles/anvil/tools.just @@ -173,9 +173,9 @@ anvil-tool-pwsh-validate-prereqs: # or no-op if it is already installed at or above that version. The # `installer` parameter selects between: # - "install" (cargo install --locked, pure-source). Default. -# - "binstall" (cargo binstall --no-confirm --locked with binstall's compile -# strategy disabled, followed by a controlled cargo install -# fallback). Bootstraps cargo-binstall itself if not on PATH. +# - "binstall" (cargo binstall --no-confirm --locked, with a controlled +# cargo install fallback for tools that declare source +# prerequisites). Bootstraps cargo-binstall itself if not on PATH. # `source_prereq`, when set, runs immediately before a source installation, # including a binstall fallback. [script("pwsh", "-NoProfile")] @@ -224,8 +224,14 @@ _install-tool name version installer source_prereq="": } } # Keep source builds behind Anvil's prerequisite check instead of - # allowing binstall to compile before that check can run. - cargo binstall --no-confirm --locked --disable-strategies compile $name --version "=$version" + # allowing binstall to compile before that check can run. Tools with + # no source prerequisite may still use binstall's compile strategy. + $binstallArgs = @('binstall', '--no-confirm', '--locked') + if ($sourcePrereq) { + $binstallArgs += @('--disable-strategies', 'compile') + } + $binstallArgs += @($name, '--version', "=$version") + cargo @binstallArgs if ($LASTEXITCODE -eq 0) { exit 0 } Write-Host ' binstall failed; falling back to cargo install' -ForegroundColor Yellow } diff --git a/crates/cargo-anvil/tests/recipe_contracts.rs b/crates/cargo-anvil/tests/recipe_contracts.rs index 5ca5a86b..f8d04a16 100644 --- a/crates/cargo-anvil/tests/recipe_contracts.rs +++ b/crates/cargo-anvil/tests/recipe_contracts.rs @@ -21,6 +21,7 @@ const BOLERO: &str = include_str!("../templates/justfiles/anvil/checks/bolero.ju const LLVM_COV: &str = include_str!("../templates/justfiles/anvil/checks/llvm-cov.just"); const SEMVER: &str = include_str!("../templates/justfiles/anvil/checks/semver-check.just"); const EXTERNAL_TYPES: &str = include_str!("../templates/justfiles/anvil/checks/external-types.just"); +const TOOLS: &str = include_str!("../templates/justfiles/anvil/tools.just"); const VERSIONS: &str = include_str!("../templates/justfiles/anvil/versions.just"); const FAKE_CARGO_PS1: &str = r#" $joined = $args -join ' ' @@ -89,6 +90,12 @@ if ($args -contains 'nextest') { } exit [int]$env:FAKE_NEXTEST_EXIT } +if ($args -contains 'binstall') { + exit [int]$env:FAKE_BINSTALL_EXIT +} +if ($args -contains 'install' -and $args -contains '--version') { + exit [int]$env:FAKE_INSTALL_EXIT +} exit 0 "#; @@ -370,6 +377,96 @@ fn semver_exit_code_contract_is_executed() { } } +#[test] +fn install_tool_controls_source_fallback_and_prerequisite_ordering() { + if !tools_available() { + return; + } + let tmp = fixture(&[("versions.just", VERSIONS), ("tools.just", TOOLS)], &[]); + let justfile_path = tmp.path().join("Justfile"); + let mut justfile = std::fs::read_to_string(&justfile_path).unwrap(); + justfile.push_str( + r#" +[script("pwsh", "-NoProfile")] +source-prereq: + Add-Content -LiteralPath $env:FAKE_CARGO_LOG -Value 'source-prereq' + exit [int]$env:FAKE_PREREQ_EXIT +"#, + ); + write(&justfile_path, &justfile); + let log = tmp.path().join("cargo.log"); + + let fallback = run_just( + tmp.path(), + &["_install-tool", "cargo-spellcheck", "0.15.7", "binstall", "source-prereq"], + &[ + ("FAKE_CARGO_LOG", log.as_os_str()), + ("FAKE_BINSTALL_EXIT", OsStr::new("7")), + ("FAKE_PREREQ_EXIT", OsStr::new("0")), + ("FAKE_INSTALL_EXIT", OsStr::new("0")), + ], + ); + assert!( + fallback.status.success(), + "controlled source fallback should succeed:\n{}", + String::from_utf8_lossy(&fallback.stderr) + ); + let log_contents = std::fs::read_to_string(&log).unwrap(); + let lines = log_contents.lines().collect::>(); + let binstall = lines + .iter() + .position(|line| line.contains("binstall --no-confirm --locked --disable-strategies compile")) + .expect("source-prerequisite tools must disable binstall compilation"); + let prerequisite = lines + .iter() + .position(|line| *line == "source-prereq") + .expect("source prerequisite must run after binary installation fails"); + let source_install = lines + .iter() + .position(|line| line.contains("install --locked cargo-spellcheck --version =0.15.7")) + .expect("Anvil must perform the controlled source install at the exact pin"); + assert!(binstall < prerequisite && prerequisite < source_install); + + std::fs::remove_file(&log).unwrap(); + let prerequisite_failure = run_just( + tmp.path(), + &["_install-tool", "cargo-spellcheck", "0.15.7", "binstall", "source-prereq"], + &[ + ("FAKE_CARGO_LOG", log.as_os_str()), + ("FAKE_BINSTALL_EXIT", OsStr::new("7")), + ("FAKE_PREREQ_EXIT", OsStr::new("9")), + ], + ); + assert_failed(&prerequisite_failure, "source prerequisite failure"); + let failed_log = std::fs::read_to_string(&log).unwrap(); + assert!(failed_log.contains("source-prereq")); + assert!( + !failed_log.contains("install --locked cargo-spellcheck --version =0.15.7"), + "source installation must not run after prerequisite failure" + ); + + std::fs::remove_file(&log).unwrap(); + let ordinary_tool = run_just( + tmp.path(), + &["_install-tool", "cargo-other", "1.2.3", "binstall", ""], + &[ + ("FAKE_CARGO_LOG", log.as_os_str()), + ("FAKE_BINSTALL_EXIT", OsStr::new("7")), + ("FAKE_INSTALL_EXIT", OsStr::new("0")), + ], + ); + assert!(ordinary_tool.status.success()); + let ordinary_log = std::fs::read_to_string(&log).unwrap(); + let ordinary_binstall = ordinary_log + .lines() + .find(|line| line.contains("binstall --no-confirm --locked")) + .expect("ordinary tool must attempt binstall"); + assert!( + !ordinary_binstall.contains("--disable-strategies compile"), + "tools without source prerequisites retain binstall's compile strategy" + ); +} + #[test] fn repository_constants_match_shared_anvil_versions() { let constants_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../constants.env"); diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap index cae97ea6..2a02a25c 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap @@ -5604,9 +5604,9 @@ anvil-tool-pwsh-validate-prereqs: # or no-op if it is already installed at or above that version. The # `installer` parameter selects between: # - "install" (cargo install --locked, pure-source). Default. -# - "binstall" (cargo binstall --no-confirm --locked with binstall's compile -# strategy disabled, followed by a controlled cargo install -# fallback). Bootstraps cargo-binstall itself if not on PATH. +# - "binstall" (cargo binstall --no-confirm --locked, with a controlled +# cargo install fallback for tools that declare source +# prerequisites). Bootstraps cargo-binstall itself if not on PATH. # `source_prereq`, when set, runs immediately before a source installation, # including a binstall fallback. [script("pwsh", "-NoProfile")] @@ -5655,8 +5655,14 @@ _install-tool name version installer source_prereq="": } } # Keep source builds behind Anvil's prerequisite check instead of - # allowing binstall to compile before that check can run. - cargo binstall --no-confirm --locked --disable-strategies compile $name --version "=$version" + # allowing binstall to compile before that check can run. Tools with + # no source prerequisite may still use binstall's compile strategy. + $binstallArgs = @('binstall', '--no-confirm', '--locked') + if ($sourcePrereq) { + $binstallArgs += @('--disable-strategies', 'compile') + } + $binstallArgs += @($name, '--version', "=$version") + cargo @binstallArgs if ($LASTEXITCODE -eq 0) { exit 0 } Write-Host ' binstall failed; falling back to cargo install' -ForegroundColor Yellow } diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap index b54d34d9..3d32aba8 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap @@ -5620,9 +5620,9 @@ anvil-tool-pwsh-validate-prereqs: # or no-op if it is already installed at or above that version. The # `installer` parameter selects between: # - "install" (cargo install --locked, pure-source). Default. -# - "binstall" (cargo binstall --no-confirm --locked with binstall's compile -# strategy disabled, followed by a controlled cargo install -# fallback). Bootstraps cargo-binstall itself if not on PATH. +# - "binstall" (cargo binstall --no-confirm --locked, with a controlled +# cargo install fallback for tools that declare source +# prerequisites). Bootstraps cargo-binstall itself if not on PATH. # `source_prereq`, when set, runs immediately before a source installation, # including a binstall fallback. [script("pwsh", "-NoProfile")] @@ -5671,8 +5671,14 @@ _install-tool name version installer source_prereq="": } } # Keep source builds behind Anvil's prerequisite check instead of - # allowing binstall to compile before that check can run. - cargo binstall --no-confirm --locked --disable-strategies compile $name --version "=$version" + # allowing binstall to compile before that check can run. Tools with + # no source prerequisite may still use binstall's compile strategy. + $binstallArgs = @('binstall', '--no-confirm', '--locked') + if ($sourcePrereq) { + $binstallArgs += @('--disable-strategies', 'compile') + } + $binstallArgs += @($name, '--version', "=$version") + cargo @binstallArgs if ($LASTEXITCODE -eq 0) { exit 0 } Write-Host ' binstall failed; falling back to cargo install' -ForegroundColor Yellow } diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap index 4afdf352..6ae533ee 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__local_only.snap @@ -4344,9 +4344,9 @@ anvil-tool-pwsh-validate-prereqs: # or no-op if it is already installed at or above that version. The # `installer` parameter selects between: # - "install" (cargo install --locked, pure-source). Default. -# - "binstall" (cargo binstall --no-confirm --locked with binstall's compile -# strategy disabled, followed by a controlled cargo install -# fallback). Bootstraps cargo-binstall itself if not on PATH. +# - "binstall" (cargo binstall --no-confirm --locked, with a controlled +# cargo install fallback for tools that declare source +# prerequisites). Bootstraps cargo-binstall itself if not on PATH. # `source_prereq`, when set, runs immediately before a source installation, # including a binstall fallback. [script("pwsh", "-NoProfile")] @@ -4395,8 +4395,14 @@ _install-tool name version installer source_prereq="": } } # Keep source builds behind Anvil's prerequisite check instead of - # allowing binstall to compile before that check can run. - cargo binstall --no-confirm --locked --disable-strategies compile $name --version "=$version" + # allowing binstall to compile before that check can run. Tools with + # no source prerequisite may still use binstall's compile strategy. + $binstallArgs = @('binstall', '--no-confirm', '--locked') + if ($sourcePrereq) { + $binstallArgs += @('--disable-strategies', 'compile') + } + $binstallArgs += @($name, '--version', "=$version") + cargo @binstallArgs if ($LASTEXITCODE -eq 0) { exit 0 } Write-Host ' binstall failed; falling back to cargo install' -ForegroundColor Yellow } diff --git a/justfiles/anvil/tools.just b/justfiles/anvil/tools.just index 92de15d1..cb6aab14 100644 --- a/justfiles/anvil/tools.just +++ b/justfiles/anvil/tools.just @@ -173,9 +173,9 @@ anvil-tool-pwsh-validate-prereqs: # or no-op if it is already installed at or above that version. The # `installer` parameter selects between: # - "install" (cargo install --locked, pure-source). Default. -# - "binstall" (cargo binstall --no-confirm --locked with binstall's compile -# strategy disabled, followed by a controlled cargo install -# fallback). Bootstraps cargo-binstall itself if not on PATH. +# - "binstall" (cargo binstall --no-confirm --locked, with a controlled +# cargo install fallback for tools that declare source +# prerequisites). Bootstraps cargo-binstall itself if not on PATH. # `source_prereq`, when set, runs immediately before a source installation, # including a binstall fallback. [script("pwsh", "-NoProfile")] @@ -224,8 +224,14 @@ _install-tool name version installer source_prereq="": } } # Keep source builds behind Anvil's prerequisite check instead of - # allowing binstall to compile before that check can run. - cargo binstall --no-confirm --locked --disable-strategies compile $name --version "=$version" + # allowing binstall to compile before that check can run. Tools with + # no source prerequisite may still use binstall's compile strategy. + $binstallArgs = @('binstall', '--no-confirm', '--locked') + if ($sourcePrereq) { + $binstallArgs += @('--disable-strategies', 'compile') + } + $binstallArgs += @($name, '--version', "=$version") + cargo @binstallArgs if ($LASTEXITCODE -eq 0) { exit 0 } Write-Host ' binstall failed; falling back to cargo install' -ForegroundColor Yellow } From 827314b77fad0360468ff7c7905f6c8cdd4fe01d Mon Sep 17 00:00:00 2001 From: "Martin Kolinek (from Dev Box)" Date: Fri, 21 Aug 2026 13:34:05 +0200 Subject: [PATCH 20/20] review: align scheduled failure notification contracts Clarify bounded issue identity and manual lifecycle, converge legacy mutation failures on the shared incident, tighten publisher permissions, document reusable-workflow boundaries, and explain focused test harness behavior. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1c90ffee-5127-4069-8bb1-cef1492a400c --- .anvil.lock | 10 +- .github/workflows/anvil-pr-impl.yml | 3 + .github/workflows/anvil-pr.yml | 3 + .github/workflows/anvil-scheduled-impl.yml | 4 +- .github/workflows/anvil-scheduled.yml | 3 + .github/workflows/nightly.yml | 30 +++-- crates/cargo-anvil/README.md | 12 +- crates/cargo-anvil/docs/design/github.md | 108 +++++++----------- crates/cargo-anvil/docs/design/local.md | 17 +-- .../cargo-anvil/src/anvil/artifacts/github.rs | 12 ++ crates/cargo-anvil/src/lib.rs | 10 +- .../templates/github/pr-impl-workflow.yml | 3 + .../templates/github/pr-root-workflow.yml | 3 + .../github/scheduled-impl-workflow.yml | 4 +- .../github/scheduled-root-workflow.yml | 3 + crates/cargo-anvil/tests/recipe_contracts.rs | 2 + .../snapshots/snapshots__github_backend.snap | 13 ++- 17 files changed, 139 insertions(+), 101 deletions(-) diff --git a/.anvil.lock b/.anvil.lock index 3488aa5f..c282120d 100644 --- a/.anvil.lock +++ b/.anvil.lock @@ -1,7 +1,7 @@ version = 1 tool = "anvil" tool_version = "0.4.0" -catalog_checksum = "sha256:8cbe093ffc07cb9f70af1fd24e1b7d81b6d68ae981eb820279619451b8908544" +catalog_checksum = "sha256:742b87bdd81a2398ffdd2369af1eb56cab92d249b407717be5191606956065e5" [[file]] path = ".anvil/container/Containerfile" @@ -77,19 +77,19 @@ checksum = "sha256:a153fa2ce4307ef9da91ff215e4f37e5a329d0bbb2c04c2b1f26096887f64 [[file]] path = ".github/workflows/anvil-pr-impl.yml" -checksum = "sha256:6920b089ab3b36ee754e949147990c037acafdb53ca229e43e401da310a9b4cd" +checksum = "sha256:8bf6f1c1b62901c53bf7a9fa5c0cf8fd50625731496e88f5ffa3a790bc3a3c56" [[file]] path = ".github/workflows/anvil-pr.yml" -checksum = "sha256:cb7996cc978eb3f6db6572a78eec1341bfbebb592c90f78f69d622eaacf6d44a" +checksum = "sha256:18350505aedb0d3e4bc0941016205acbd17b5619b83caa24689fd9d2ddcc14b9" [[file]] path = ".github/workflows/anvil-scheduled-impl.yml" -checksum = "sha256:a4bbf3fd9d2b757431856d8ad4cce0ba3dabbb3455e164521bdbd8da2bc84ba8" +checksum = "sha256:6c1cd79cff3660b8086cd57eef9f8e02b8145f7efb7236f0e97577e4588d6a94" [[file]] path = ".github/workflows/anvil-scheduled.yml" -checksum = "sha256:d31b9878b377bc9bbfe4c0156e75b279dc1aeb6a421ff5089aeae623fe2a8974" +checksum = "sha256:8be4848b851fa5b74702c87560fc628d0b90447ed27dc4dcdb655f92c55305c7" [[file]] path = "justfiles/anvil/checks/aprz.just" diff --git a/.github/workflows/anvil-pr-impl.yml b/.github/workflows/anvil-pr-impl.yml index 24f5ea61..5a914a9c 100644 --- a/.github/workflows/anvil-pr-impl.yml +++ b/.github/workflows/anvil-pr-impl.yml @@ -31,6 +31,9 @@ on: configured at Codecov; required for private repos. required: false +# The caller grants the maximum token scopes available to this reusable +# workflow. Reset jobs to read-only here, then restore only pr-fast's pull +# request scope below. See docs/design/github.md §9. permissions: contents: read diff --git a/.github/workflows/anvil-pr.yml b/.github/workflows/anvil-pr.yml index eb822683..e545a6ca 100644 --- a/.github/workflows/anvil-pr.yml +++ b/.github/workflows/anvil-pr.yml @@ -19,6 +19,9 @@ concurrency: jobs: anvil-pr: uses: ./.github/workflows/anvil-pr-impl.yml + # A called workflow cannot elevate beyond its caller. The implementation + # resets this upper bound to read-only and restores pull-requests:write + # only on pr-fast. See docs/design/github.md §9. permissions: contents: read # Write needed so the pr-fast job can upsert/clear the sticky PR diff --git a/.github/workflows/anvil-scheduled-impl.yml b/.github/workflows/anvil-scheduled-impl.yml index 556548d1..0e23c571 100644 --- a/.github/workflows/anvil-scheduled-impl.yml +++ b/.github/workflows/anvil-scheduled-impl.yml @@ -31,6 +31,9 @@ on: configured at Codecov; required for private repos. required: false +# The caller grants the maximum token scopes available to this reusable +# workflow. Reset jobs to read-only here, then restore only the publisher's +# issues scope below. See docs/design/github.md §9. permissions: contents: read @@ -137,7 +140,6 @@ jobs: && contains(needs.*.result, 'failure') }} runs-on: ${{ inputs.linux_runner }} permissions: - contents: read issues: write steps: - name: Create or update failure issue diff --git a/.github/workflows/anvil-scheduled.yml b/.github/workflows/anvil-scheduled.yml index 0c4e3812..4ecb2a60 100644 --- a/.github/workflows/anvil-scheduled.yml +++ b/.github/workflows/anvil-scheduled.yml @@ -16,6 +16,9 @@ permissions: jobs: anvil-scheduled: uses: ./.github/workflows/anvil-scheduled-impl.yml + # A called workflow cannot elevate beyond its caller. The implementation + # resets this upper bound to read-only and restores issues:write only on + # publish-failure. See docs/design/github.md §9. permissions: contents: read issues: write diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 99456e96..a796afac 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -35,6 +35,7 @@ jobs: if: ${{ needs.nightly-gatekeeper.outputs.should_skip != 'true' }} runs-on: ubuntu-latest permissions: + contents: read issues: write steps: # prep @@ -57,19 +58,26 @@ jobs: env: GH_TOKEN: ${{ github.token }} run: | - ISSUE_TITLE="🚨 Nightly Build Failed" - ISSUE_DATE="$(date +'%Y-%m-%d')" - ISSUE_FULL_TITLE="$ISSUE_TITLE: $ISSUE_DATE" - ISSUE_BODY="The nightly scheduled build failed. Please check the logs here: $GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" - # Search for open issues with the same base title and label - EXISTING_ISSUE=$(gh issue list --label "bug" --state open --search "$ISSUE_TITLE" --json number,title | jq -r '.[] | select(.title | startswith("'"$ISSUE_TITLE"'")) | .number' | head -n 1) + ISSUE_TITLE="[Anvil] Scheduled checks failed" + MARKER="" + ISSUE_BODY="$MARKER + + The legacy nightly mutation-testing workflow failed. + + [View workflow run]($GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID)" + # Share Anvil's marker-owned incident so overlapping mutation + # failures produce one durable issue and one notification stream. + EXISTING_ISSUE=$(gh issue list \ + --state open \ + --search '"anvil scheduled failure" in:body' \ + --limit 100 \ + --json number,body \ + --jq '.[] | select(.body | contains("")) | .number' \ + | head -n 1) if [[ -n "$EXISTING_ISSUE" ]]; then - # Add a comment to the existing issue gh issue comment "$EXISTING_ISSUE" --body "$ISSUE_BODY" else - # Create a new issue gh issue create \ - --title "$ISSUE_FULL_TITLE" \ - --body "$ISSUE_BODY" \ - --label "bug" + --title "$ISSUE_TITLE" \ + --body "$ISSUE_BODY" fi diff --git a/crates/cargo-anvil/README.md b/crates/cargo-anvil/README.md index 6443e105..232373e9 100644 --- a/crates/cargo-anvil/README.md +++ b/crates/cargo-anvil/README.md @@ -96,11 +96,11 @@ and run each check only over the affected packages, whereas a local `just anvil-pr` runs every check over the whole workspace. The generated GitHub scheduled workflow publishes failures as GitHub -issues. It creates one issue for an active failure and comments on that -issue when later scheduled runs also fail, providing a durable incident -record without creating one issue per run. Repositories can disable this -behavior by setting the `ANVIL_PUBLISH_FAILURE_ISSUE` Actions repository -variable to `false`. +issues. On failure, it best-effort reuses an open marker-owned issue and +comments when later scheduled runs also fail. A maintainer closes the issue +after resolving the incident; successful runs do not close it automatically. +Repositories can disable this behavior by setting the +`ANVIL_PUBLISH_FAILURE_ISSUE` Actions repository variable to `false`. ### Containerized local checks @@ -444,7 +444,7 @@ And `docs/verification.md` for the continuous-validation strategy. This crate was developed as part of The Oxidizer Project. Browse this crate's source code. - [__cargo_doc2readme_dependencies_info]: ggGmYW0CYXZlMC43LjJhdIQbFhzZ8rzWNNYbuRaDSGWynFgbH4PMdoT7GNcbVwNPtPjAhvFhYvRhcoQbRcIvCeEgxSQbz0a05iQ92jUbOvdahLnAmC8bqB4WbnXRqkVhZIGDa2NhcmdvLWFudmlsZTAuNC4wa2NhcmdvX2Fudmls + [__cargo_doc2readme_dependencies_info]: ggGmYW0CYXZlMC43LjJhdIQbFhzZ8rzWNNYbuRaDSGWynFgbH4PMdoT7GNcbVwNPtPjAhvFhYvRhcoQbwZ4st3e_65sbkyK8ewAQvKkbC226ePbvbHsbZyKr94ICgj9hZIGDa2NhcmdvLWFudmlsZTAuNC4wa2NhcmdvX2Fudmls [__link0]: https://crates.io/crates/cargo-delta [__link1]: https://crates.io/crates/cargo-spellcheck [__link2]: https://crates.io/crates/cargo-coverage-gate diff --git a/crates/cargo-anvil/docs/design/github.md b/crates/cargo-anvil/docs/design/github.md index 105b8489..92abdddc 100644 --- a/crates/cargo-anvil/docs/design/github.md +++ b/crates/cargo-anvil/docs/design/github.md @@ -393,56 +393,24 @@ contract. The scheduled reusable workflow is simpler — it omits the `impact` job and runs each group full-workspace. The include inputs default to empty strings, so recipes fall through to -their local-default behavior (`--workspace`): +their local-default behavior (`--workspace`). The following is deliberately a +non-executable schematic; the generated +[`scheduled-impl-workflow.yml`](../../templates/github/scheduled-impl-workflow.yml) +is the canonical YAML: -```yaml -# .github/workflows/anvil-scheduled-impl.yml (owned) -on: - workflow_call: - inputs: - linux_runner: { type: string, default: ubuntu-latest } - windows_runner: { type: string, default: windows-latest } - linux_arm_runner: { type: string, default: ubuntu-24.04-arm } - windows_arm_runner: { type: string, default: windows-11-arm } -jobs: - scheduled-test: - strategy: - fail-fast: false - matrix: - os: [linux, windows, linux-arm, windows-arm] - runs-on: ${{ matrix.os == 'linux' && inputs.linux_runner - || matrix.os == 'windows' && inputs.windows_runner - || matrix.os == 'linux-arm' && inputs.linux_arm_runner - || inputs.windows_arm_runner }} - steps: [ { uses: actions/checkout }, { uses: ./.github/actions/anvil-scheduled-test } ] - scheduled-advisories: - strategy: - fail-fast: false - matrix: - os: [linux, windows, linux-arm, windows-arm] - runs-on: ${{ matrix.os == 'linux' && inputs.linux_runner - || matrix.os == 'windows' && inputs.windows_runner - || matrix.os == 'linux-arm' && inputs.linux_arm_runner - || inputs.windows_arm_runner }} - steps: [ { uses: actions/checkout }, { uses: ./.github/actions/anvil-scheduled-advisories } ] - scheduled-exhaustive: - # x86_64 only -- cargo-mutants constraint. - strategy: - fail-fast: false - matrix: - os: [linux, windows] - runs-on: ${{ matrix.os == 'linux' && inputs.linux_runner || inputs.windows_runner }} - steps: [ { uses: actions/checkout }, { uses: ./.github/actions/anvil-scheduled-exhaustive } ] - - publish-failure: - needs: [scheduled-test, scheduled-advisories, scheduled-runtime-analysis, scheduled-exhaustive] - if: ${{ always() && vars.ANVIL_PUBLISH_FAILURE_ISSUE != 'false' - && contains(needs.*.result, 'failure') }} - runs-on: ${{ inputs.linux_runner }} - permissions: { contents: read, issues: write } - steps: - - uses: actions/github-script - # Upsert the stable "[Anvil] Scheduled checks failed" issue. +```text +caller anvil-scheduled.yml + permissions upper bound: contents:read + issues:write + └─ called anvil-scheduled-impl.yml + default reset: contents:read + ├─ scheduled-test (Linux/Windows × x64/ARM64) + ├─ scheduled-advisories (Linux/Windows × x64/ARM64) + ├─ scheduled-runtime-analysis (Linux/Windows × x64/ARM64) + ├─ scheduled-exhaustive (Linux/Windows x64) + └─ publish-failure + needs: all four scheduled groups + condition: at least one failure and publication not disabled + job override: issues:write only ``` Scheduled composite actions don't receive any `include_*` inputs at all — their inputs @@ -670,7 +638,8 @@ Recommended root workflow shape: - The scheduled reusable-workflow call grants `issues: write` at job scope so its publisher can create or comment on the failure issue. The called workflow resets its default permissions to `contents: read`, then restores `issues: write` only on the - publishing job; scheduled check jobs do not inherit write access. The PR workflow + publishing job. That job-level map omits `contents`, so the publisher cannot read + repository contents; scheduled check jobs retain read-only access. The PR workflow never receives this permission. - The PR reusable-workflow call grants `pull-requests: write` for advisory comments. The called workflow resets its default permissions to `contents: read`, then restores @@ -731,32 +700,43 @@ least one result is `failure`; successful, skipped, and cancelled runs do not cr issues. The issue title is `[Anvil] Scheduled checks failed`, while the stable hidden marker -`` identifies an issue owned by the publisher. The -publisher makes one repository-scoped Search API request for open issues whose bodies -match the marker terms, then verifies the exact marker client-side: +`` identifies the repository's shared scheduled-failure +incident. Anvil and any legacy scheduled publisher that adopts this identity converge on +the same open issue. Each publisher makes one repository-scoped Search API request for +open issues whose bodies match the marker terms, then verifies the exact marker +client-side: - If none exists, it creates one containing the failed group names and a link to the workflow run. - If one exists, it adds the new failure details as a comment instead of creating a duplicate. -This is a best-effort upsert: GitHub's search index is eventually consistent and the -single request considers at most 100 results, so closely overlapping failures can -occasionally create duplicate incident issues. Marker-based identity prevents a -human-authored issue with the same title from being reused and survives a maintainer -renaming an Anvil incident. +This is a bounded best-effort upsert, not a singleton guarantee. One Search request is +the selected boundary because a scheduled failure should spend a fixed, minimal amount +of API quota instead of paginating through repository issues; the marker is expected to +match at most one open incident. GitHub's search index is eventually consistent and the +request considers at most 100 results, so closely overlapping failures can occasionally +create duplicate incident issues. Marker-based identity prevents a human-authored issue +with the same title from being reused and survives a maintainer renaming an incident. No label is required because repositories can remove or rename their default labels. -The issue remains open until a maintainer resolves the underlying failure and closes it. -If a later run fails after closure, the publisher creates a new incident issue. +Here, "open incident" means an open marker-owned issue, not an automatically tracked +failure state. Successful runs do not close or update it. The issue remains open until a +maintainer resolves the underlying failure and closes it; a later failure after closure +creates a new incident issue. + +This repository's legacy nightly mutation workflow intentionally uses the same title and +marker. Its mutation configuration remains distinct from Anvil's exhaustive group, but +an overlapping failure updates the same durable incident instead of creating a second +notification owner and a duplicate Teams post. The publisher uses the workflow's short-lived `GITHUB_TOKEN`. The scheduled root call allows `issues: write`, while the reusable workflow defaults to `contents: read` and grants `issues: write` only to the publishing job. Scheduled check jobs therefore retain -read-only access. The publisher does not receive repository contents beyond read access -and does not forward logs or environment data into the issue. This narrow GitHub-native -path also lets GitHub's Teams app relay issue notifications without an external webhook -or additional secret. +read-only access. The publisher's job-level permission map omits `contents`, so it cannot +read repository contents, and it does not forward logs or environment data into the +issue. This narrow GitHub-native path also lets GitHub's Teams app relay issue +notifications without an external webhook or additional secret. The generated root and implementation workflows must be updated together. A repository that has taken ownership of the root workflow must retain `issues: write` on the reusable diff --git a/crates/cargo-anvil/docs/design/local.md b/crates/cargo-anvil/docs/design/local.md index 930c496f..69db4dbd 100644 --- a/crates/cargo-anvil/docs/design/local.md +++ b/crates/cargo-anvil/docs/design/local.md @@ -279,11 +279,13 @@ The `installer` argument: Slow on a cold runner (~30 min for the full catalog) because every tool re-compiles common deps (`clap`, `syn`, `quote`, ...) from scratch independently. - `binstall` -- `cargo binstall --no-confirm --locked --version '='`. - Downloads a prebuilt binary from each tool's GitHub Releases when available. - When a tool declares a source prerequisite, Anvil disables binstall's compile - strategy and performs the source fallback itself after checking that prerequisite. - Tools without a source prerequisite retain binstall's normal compile strategy. - The binary path cuts the cold-runner install phase from ~30 min to ~1 min. + This selects an ordered strategy, not a binary-only backend. Anvil first asks + cargo-binstall to install the exact pin. Tools without a source prerequisite retain + cargo-binstall's compile strategy. For tools that declare a source prerequisite, + Anvil disables that compile strategy so compilation cannot bypass the check. Any + nonzero binstall result then falls back to Anvil's exact-pin `cargo install`; the + declared prerequisite, when present, runs immediately before that fallback. + A successful binary path cuts the cold-runner install phase from ~30 min to ~1 min. `cargo-binstall` itself needs to be on PATH; the GH setup composite arranges this. The GitHub composite setup action calls `just anvil--setup binstall` @@ -326,8 +328,9 @@ channel, and `versions.just`. See A small set of catalog tools have non-Rust build dependencies that `cargo install` can't satisfy on its own. Today the only entry is `libclang`, needed by -`cargo-spellcheck` (via `clang-sys` / `hunspell-rs`) at build time. The `binstall` -install path sidesteps these entirely by downloading prebuilt binaries. +`cargo-spellcheck` (via `clang-sys` / `hunspell-rs`) at build time. A successful +prebuilt binstall sidesteps these; a failed binstall can reach the controlled source +fallback and therefore still requires them. Scope policy: only check for system libs that an anvil catalog tool **directly** requires. anvil is not a general-purpose dev-env doctor. Repository-specific diff --git a/crates/cargo-anvil/src/anvil/artifacts/github.rs b/crates/cargo-anvil/src/anvil/artifacts/github.rs index bbdfde6e..e08503d0 100644 --- a/crates/cargo-anvil/src/anvil/artifacts/github.rs +++ b/crates/cargo-anvil/src/anvil/artifacts/github.rs @@ -280,6 +280,15 @@ mod tests { assert!(SCHEDULED_IMPL_WORKFLOW.contains("github.rest.issues.create")); assert!(SCHEDULED_IMPL_WORKFLOW.contains("\npermissions:\n contents: read\n")); assert_eq!(SCHEDULED_IMPL_WORKFLOW.matches("issues: write").count(), 1); + let publisher_permissions = SCHEDULED_IMPL_WORKFLOW + .split_once("\n publish-failure:") + .expect("scheduled workflow should contain publish-failure") + .1 + .split_once("\n steps:") + .expect("publish-failure should contain steps") + .0; + assert!(publisher_permissions.contains("\n permissions:\n issues: write")); + assert!(!publisher_permissions.contains("contents: read")); assert_eq!( SCHEDULED_IMPL_WORKFLOW.matches("free-disk-space: true").count(), 1, @@ -301,6 +310,9 @@ mod tests { let harness = format!("const workflowScript = {script:?};\n") + r#" const assert = require("node:assert/strict"); +// github-script executes an asynchronous body with injected runtime values. +// Model only the github/context/process values this script uses; the API client +// remains mocked rather than recreating the complete action runtime or Node image. const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor; const run = new AsyncFunction("github", "context", "process", workflowScript); const marker = ""; diff --git a/crates/cargo-anvil/src/lib.rs b/crates/cargo-anvil/src/lib.rs index c410e5af..22b81115 100644 --- a/crates/cargo-anvil/src/lib.rs +++ b/crates/cargo-anvil/src/lib.rs @@ -97,11 +97,11 @@ //! `just anvil-pr` runs every check over the whole workspace. //! //! The generated GitHub scheduled workflow publishes failures as GitHub -//! issues. It creates one issue for an active failure and comments on that -//! issue when later scheduled runs also fail, providing a durable incident -//! record without creating one issue per run. Repositories can disable this -//! behavior by setting the `ANVIL_PUBLISH_FAILURE_ISSUE` Actions repository -//! variable to `false`. +//! issues. On failure, it best-effort reuses an open marker-owned issue and +//! comments when later scheduled runs also fail. A maintainer closes the issue +//! after resolving the incident; successful runs do not close it automatically. +//! Repositories can disable this behavior by setting the +//! `ANVIL_PUBLISH_FAILURE_ISSUE` Actions repository variable to `false`. //! //! ## Containerized local checks //! diff --git a/crates/cargo-anvil/templates/github/pr-impl-workflow.yml b/crates/cargo-anvil/templates/github/pr-impl-workflow.yml index 24f5ea61..5a914a9c 100644 --- a/crates/cargo-anvil/templates/github/pr-impl-workflow.yml +++ b/crates/cargo-anvil/templates/github/pr-impl-workflow.yml @@ -31,6 +31,9 @@ on: configured at Codecov; required for private repos. required: false +# The caller grants the maximum token scopes available to this reusable +# workflow. Reset jobs to read-only here, then restore only pr-fast's pull +# request scope below. See docs/design/github.md §9. permissions: contents: read diff --git a/crates/cargo-anvil/templates/github/pr-root-workflow.yml b/crates/cargo-anvil/templates/github/pr-root-workflow.yml index eb822683..e545a6ca 100644 --- a/crates/cargo-anvil/templates/github/pr-root-workflow.yml +++ b/crates/cargo-anvil/templates/github/pr-root-workflow.yml @@ -19,6 +19,9 @@ concurrency: jobs: anvil-pr: uses: ./.github/workflows/anvil-pr-impl.yml + # A called workflow cannot elevate beyond its caller. The implementation + # resets this upper bound to read-only and restores pull-requests:write + # only on pr-fast. See docs/design/github.md §9. permissions: contents: read # Write needed so the pr-fast job can upsert/clear the sticky PR diff --git a/crates/cargo-anvil/templates/github/scheduled-impl-workflow.yml b/crates/cargo-anvil/templates/github/scheduled-impl-workflow.yml index 556548d1..0e23c571 100644 --- a/crates/cargo-anvil/templates/github/scheduled-impl-workflow.yml +++ b/crates/cargo-anvil/templates/github/scheduled-impl-workflow.yml @@ -31,6 +31,9 @@ on: configured at Codecov; required for private repos. required: false +# The caller grants the maximum token scopes available to this reusable +# workflow. Reset jobs to read-only here, then restore only the publisher's +# issues scope below. See docs/design/github.md §9. permissions: contents: read @@ -137,7 +140,6 @@ jobs: && contains(needs.*.result, 'failure') }} runs-on: ${{ inputs.linux_runner }} permissions: - contents: read issues: write steps: - name: Create or update failure issue diff --git a/crates/cargo-anvil/templates/github/scheduled-root-workflow.yml b/crates/cargo-anvil/templates/github/scheduled-root-workflow.yml index 0c4e3812..4ecb2a60 100644 --- a/crates/cargo-anvil/templates/github/scheduled-root-workflow.yml +++ b/crates/cargo-anvil/templates/github/scheduled-root-workflow.yml @@ -16,6 +16,9 @@ permissions: jobs: anvil-scheduled: uses: ./.github/workflows/anvil-scheduled-impl.yml + # A called workflow cannot elevate beyond its caller. The implementation + # resets this upper bound to read-only and restores issues:write only on + # publish-failure. See docs/design/github.md §9. permissions: contents: read issues: write diff --git a/crates/cargo-anvil/tests/recipe_contracts.rs b/crates/cargo-anvil/tests/recipe_contracts.rs index f8d04a16..896fdd76 100644 --- a/crates/cargo-anvil/tests/recipe_contracts.rs +++ b/crates/cargo-anvil/tests/recipe_contracts.rs @@ -113,6 +113,8 @@ fn tools_available() -> bool { fn fixture(imports: &[(&str, &str)], dependency_recipes: &[&str]) -> TempDir { let tmp = TempDir::new().unwrap(); let mut justfile = String::from("set unstable\n\n"); + // Focused fixtures need this shared variable, but the real version catalog + // already defines it and Just rejects duplicate definitions. if !imports.iter().any(|(name, _)| *name == "versions.just") { justfile.push_str("rust_nightly := \"nightly-test\"\n\n"); } diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap index 3d32aba8..d689844d 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__github_backend.snap @@ -2043,6 +2043,9 @@ on: configured at Codecov; required for private repos. required: false +# The caller grants the maximum token scopes available to this reusable +# workflow. Reset jobs to read-only here, then restore only pr-fast's pull +# request scope below. See docs/design/github.md §9. permissions: contents: read @@ -2275,6 +2278,9 @@ concurrency: jobs: anvil-pr: uses: ./.github/workflows/anvil-pr-impl.yml + # A called workflow cannot elevate beyond its caller. The implementation + # resets this upper bound to read-only and restores pull-requests:write + # only on pr-fast. See docs/design/github.md §9. permissions: contents: read # Write needed so the pr-fast job can upsert/clear the sticky PR @@ -2317,6 +2323,9 @@ on: configured at Codecov; required for private repos. required: false +# The caller grants the maximum token scopes available to this reusable +# workflow. Reset jobs to read-only here, then restore only the publisher's +# issues scope below. See docs/design/github.md §9. permissions: contents: read @@ -2423,7 +2432,6 @@ jobs: && contains(needs.*.result, 'failure') }} runs-on: ${{ inputs.linux_runner }} permissions: - contents: read issues: write steps: - name: Create or update failure issue @@ -2500,6 +2508,9 @@ permissions: jobs: anvil-scheduled: uses: ./.github/workflows/anvil-scheduled-impl.yml + # A called workflow cannot elevate beyond its caller. The implementation + # resets this upper bound to read-only and restores issues:write only on + # publish-failure. See docs/design/github.md §9. permissions: contents: read issues: write