Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
08a6f33
feat(anvil): publish scheduled failures as issues
martin-kolinek Jul 31, 2026
0f0ab97
fix(anvil): configure issue publishing without workflow edits
martin-kolinek Jul 31, 2026
c91cc44
docs: document scheduled issue publishing opt-out
martin-kolinek Jul 31, 2026
3cdd505
fix: use scoped search for failure issues
martin-kolinek Jul 31, 2026
3c5b6d5
review: harden scheduled failure issue upsert
martin-kolinek Aug 3, 2026
d1e77c9
fix: align failure marker with search phrase
martin-kolinek Aug 3, 2026
f34b241
fix: match nightly rustfmt import layout
martin-kolinek Aug 3, 2026
3a64a64
fix: skip subprocess test under Miri isolation
martin-kolinek Aug 3, 2026
f64bf6c
test: harden scheduled issue script harness
martin-kolinek Aug 3, 2026
d43587e
fix: keep scheduled issue test within lint limit
martin-kolinek Aug 3, 2026
77456af
security: scope issue write to failure publisher
martin-kolinek Aug 3, 2026
9ba74a7
security: scope PR comment write permission
martin-kolinek Aug 3, 2026
95fa86c
fix: provision spellcheck dependency on ARM runners
martin-kolinek Aug 5, 2026
1767cf4
test: refresh shared installer snapshots
martin-kolinek Aug 5, 2026
9aa3aab
fix: skip incompatible spellcheck on ARM64
martin-kolinek Aug 5, 2026
0fd8954
fix: keep recipe fixture within lint limits
martin-kolinek Aug 5, 2026
3d22490
fix: run cargo-spellcheck on ARM64
martin-kolinek Aug 19, 2026
54e7d3e
docs: align exact-pin rationale with spellcheck 0.15.7
martin-kolinek Aug 20, 2026
c4b173f
test: restore controlled installer fallback contract
martin-kolinek Aug 20, 2026
827314b
review: align scheduled failure notification contracts
martin-kolinek Aug 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions .anvil.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
version = 1
tool = "anvil"
tool_version = "0.4.0"
catalog_checksum = "sha256:df5327676750f40b3d34bea369cd683e40628dcfcab79d6c8fa3a3e209c33828"
catalog_checksum = "sha256:742b87bdd81a2398ffdd2369af1eb56cab92d249b407717be5191606956065e5"

[[file]]
path = ".anvil/container/Containerfile"
Expand Down Expand Up @@ -77,19 +77,19 @@ checksum = "sha256:a153fa2ce4307ef9da91ff215e4f37e5a329d0bbb2c04c2b1f26096887f64

[[file]]
path = ".github/workflows/anvil-pr-impl.yml"
checksum = "sha256:bf8b39c2ccb0cb6a682b68f5055df08d944913f863414202454ea75dafe4a839"
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:c0404d734d90a3d4cb184fcb40536f61417c735c497212e39c156c40fcd374c8"
checksum = "sha256:6c1cd79cff3660b8086cd57eef9f8e02b8145f7efb7236f0e97577e4588d6a94"

[[file]]
path = ".github/workflows/anvil-scheduled.yml"
checksum = "sha256:91408602dc3ee274b593e234841934c749ff03bba0ee7846ab88247c06f20cae"
checksum = "sha256:8be4848b851fa5b74702c87560fc628d0b90447ed27dc4dcdb655f92c55305c7"

[[file]]
path = "justfiles/anvil/checks/aprz.just"
Expand Down Expand Up @@ -269,11 +269,11 @@ checksum = "sha256:713c5a2ae28b6b5aa20dd38226278b3b7f71bbc5e6b84a16eaf5244713f27

[[file]]
path = "justfiles/anvil/tools.just"
checksum = "sha256:662ad55792b05981ada347a4ff4a8026ad0969a61ae9bd046b8e5a03cc7e820c"
checksum = "sha256:a1e44ca16f172b487afa3997f102512733d3b65a4418cf894cbd749a3abc17dc"

[[file]]
path = "justfiles/anvil/versions.just"
checksum = "sha256:7f56852a5c4882fd535eeda7853e5cb45e32b79933a0cc46c62aeef289c9e69a"
checksum = "sha256:acbea93d5117db747537f4f7b9a5eb90b7d3e0dd3e8684cc0e4dc1dcb15ac93e"

[[region]]
host = ".delta.toml"
Expand Down
9 changes: 9 additions & 0 deletions .github/workflows/anvil-pr-impl.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ 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

# 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
Expand Down Expand Up @@ -79,6 +85,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
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/anvil-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
74 changes: 74 additions & 0 deletions .github/workflows/anvil-scheduled-impl.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ 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

# Note on matrices: see pr-impl-workflow.yml for the rationale. OS
# matrices are hardcoded; per-leg runner labels are inputs.

Expand Down Expand Up @@ -122,3 +128,71 @@ 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() && vars.ANVIL_PUBLISH_FAILURE_ISSUE != 'false'
&& contains(needs.*.result, 'failure') }}
runs-on: ${{ inputs.linux_runner }}
permissions:
issues: write
Comment thread
martin-kolinek marked this conversation as resolved.
steps:
- name: Create or update failure issue
Comment thread
martin-kolinek marked this conversation as resolved.
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
env:
ANVIL_JOB_RESULTS: ${{ toJSON(needs) }}
with:
script: |
const title = "[Anvil] Scheduled checks failed";
const marker = "<!-- anvil scheduled failure -->";
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 = [
marker,
"",
"The Anvil scheduled workflow failed.",
"",
"Failed jobs:",
failedJobs,
"",
`[View workflow run](${runUrl})`,
].join("\n");

const query =
`repo:${context.repo.owner}/${context.repo.repo} ` +
`is:issue is:open in:body "anvil scheduled failure"`;
Comment thread
martin-kolinek marked this conversation as resolved.
const { data: search } =
await github.rest.search.issuesAndPullRequests({
q: query,
per_page: 100,
});
const existing = search.items.find(
issue => issue.body?.includes(marker),
);

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,
});
}
4 changes: 4 additions & 0 deletions .github/workflows/anvil-scheduled.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ 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
secrets: inherit
30 changes: 19 additions & 11 deletions .github/workflows/nightly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ jobs:
if: ${{ needs.nightly-gatekeeper.outputs.should_skip != 'true' }}
runs-on: ubuntu-latest
permissions:
contents: read
issues: write
steps:
# prep
Expand All @@ -57,19 +58,26 @@ jobs:
env:

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

🤖: Non-blocking — this is about .github/workflows/nightly.yml:56, the if: on this step (nearest commentable line). Now that this job publishes into Anvil's marker-owned incident (same title, same <!-- anvil scheduled failure --> marker), it should mirror the publisher's kill switch. anvil-scheduled-impl.yml:139 gates publish-failure on vars.ANVIL_PUBLISH_FAILURE_ISSUE != 'false', and the README / src/lib.rs / docs/design/github.md §11 all advertise that variable as the way to disable failure-issue publication. With the two publishers sharing one identity, setting it to false no longer stops the incident stream in this repo — nightly keeps creating/commenting on the same issue. Suggest adding && vars.ANVIL_PUBLISH_FAILURE_ISSUE != 'false' to this step's if: so both halves of the shared contract respect the same switch.

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="<!-- anvil scheduled failure -->"
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 \

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

🤖: Non-blocking — I think the goal is right: a nightly mutation failure and an Anvil scheduled failure are one incident operationally, so converging on one durable issue and one notification owner beats two Teams posts.

The part worth weighing is the mechanism's other effect: <!-- anvil scheduled failure --> stops being an Anvil-private key and becomes a repo-wide protocol with two independent implementations — the github-script publisher in templates/github/scheduled-impl-workflow.yml:145+, which is executably tested (src/anvil/artifacts/github.rs runs the extracted script under node and pins the exact search query), and this gh-CLI version, which is hand-maintained, not emitted from a template, not tracked in .anvil.lock, and has no test at all. Three things have to stay identical across the pair — the marker text, the title, and the effective query semantics — and nothing enforces that. If a later change to the template's marker or title lands (it is generated artifact text, so it can move with an anvil update), this file keeps publishing under the old identity and the incident stream silently splits back into two, which is precisely the outcome this change exists to prevent. The failure mode is quiet: two open issues rather than an error.

Two cheap guards would close it, either or both:

  • assert in the existing contract test that SCHEDULED_IMPL_WORKFLOW's marker/title literals also appear in this workflow's text — or at minimum a comment here naming the template as the canonical owner of the marker, so the next person editing either side knows the other exists;
  • extract this failure step and run it against a fake gh: in the convergence case, gh issue list exposes an open issue carrying Anvil's exact marker and the step must comment on it without creating another; in the absent-marker case it must create with the shared title/marker and the current run URL. Have the fake assert the marker-scoped list arguments too — otherwise the two implementations can drift while each stays internally plausible.

Separately, and out of scope for this PR: sharing the output while deliberately keeping the inputs distinct means the repo now runs mutation testing twice (scripts/mutants.rs here, Anvil's scheduled-exhaustive there) under one incident identity. Is folding this config into scheduled-exhaustive and retiring nightly.yml the intended end-state, or is a second hand-maintained publisher of Anvil's protocol something this repo carries permanently? Worth writing the answer down, because it decides whether the duplication above is temporary or not.

--state open \
--search '"anvil scheduled failure" in:body' \
--limit 100 \
--json number,body \
--jq '.[] | select(.body | contains("<!-- anvil scheduled failure -->")) | .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
19 changes: 18 additions & 1 deletion crates/cargo-anvil/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,13 @@ 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. 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

Anvil can run any generated recipe in a content-addressed Linux container.
Expand Down Expand Up @@ -292,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
Expand Down Expand Up @@ -427,7 +444,7 @@ And `docs/verification.md` for the continuous-validation strategy.
This crate was developed as part of <a href="../..">The Oxidizer Project</a>. Browse this crate's <a href="https://github.com/microsoft/ox-tools/tree/main/crates/cargo-anvil">source code</a>.
</sub>

[__cargo_doc2readme_dependencies_info]: ggGmYW0CYXZlMC43LjJhdIQbFhzZ8rzWNNYbuRaDSGWynFgbH4PMdoT7GNcbVwNPtPjAhvFhYvRhcoQbRQpVpEjw3x0b7FHf_9HBExgbfia0zvhKdz8bZ7R_zqIR8z1hZIGDa2NhcmdvLWFudmlsZTAuNC4wa2NhcmdvX2Fudmls
[__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
Expand Down
Loading
Loading