Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
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
8 changes: 4 additions & 4 deletions .anvil.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
version = 1
tool = "anvil"
tool_version = "0.5.0"
catalog_checksum = "sha256:b9e76e5dab6d2cd8cd2eaea04bd1b4457ea8839a688795981dc4cc9a415f8092"
catalog_checksum = "sha256:b2da3c025ce50918fae38da58f80625205984a4804a3c770c0cc3e66d005ebf0"

[[file]]
path = ".anvil/container/Containerfile"
Expand Down Expand Up @@ -45,7 +45,7 @@ checksum = "sha256:9940d1947482150ac08fcb9b4150da99f5ae60642f4caeea137577ce0e709

[[file]]
path = ".github/actions/anvil-run-group/action.yml"
checksum = "sha256:ff8def6c0786b6e146c4b633dfe38cb9b8ede398345516cca32bbcd5586087af"
checksum = "sha256:d564a0ce424cda58c8f5b2476cf0c8b36e759afbdf8227608bd1f3fc74913fdf"

[[file]]
path = ".github/actions/anvil-setup/action.yml"
Expand All @@ -57,15 +57,15 @@ checksum = "sha256:ea44d5e1a2cb1471cf2cef05eceab846d8afa45cf691be800f21fec1218da

[[file]]
path = ".github/workflows/anvil-pr-impl.yml"
checksum = "sha256:518d267b6def1d2549800df52fbe32c5f0848b2b2d9c00d9480b86e07b54ee3a"
checksum = "sha256:de66fea0b7340f2dc6b2f7bd70d5230e0c0c67187b3cd6870214b1f4139a1616"

[[file]]
path = ".github/workflows/anvil-pr.yml"
checksum = "sha256:0c2530d9a38e6a74e0a7fd4f999b4a1790f97de30b58b68c6c2344600da196f2"

[[file]]
path = ".github/workflows/anvil-scheduled-impl.yml"
checksum = "sha256:ac70061acf594c8c212c45ed97c3b653e7b8de68f4e1dcc9695a2628e4e2596d"
checksum = "sha256:da730340e961a0d7520856ce8e2a947e767d88cc8dbe3f534cf9a79452320244"

[[file]]
path = ".github/workflows/anvil-scheduled.yml"
Expand Down
6 changes: 1 addition & 5 deletions .github/actions/anvil-run-group/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ runs:
failed_recipe="$(sed -n 's/^error: recipe `\([^`]*\)` failed\( on line [0-9][0-9]*\)\{0,1\} with exit code [0-9][0-9]*$/\1/p' "$log" | tail -n 1)"
echo "failed_recipe=${failed_recipe:-anvil-$ANVIL_GROUP}" >> "$GITHUB_OUTPUT"
echo "exit_code=$status" >> "$GITHUB_OUTPUT"
exit "$status"
Comment thread
martin-kolinek marked this conversation as resolved.

# Reporting is supplemental: run after success or failure, but never let
# an API outage determine the authoritative workflow-job result.
Expand All @@ -84,8 +85,3 @@ runs:
setup_outcome: ${{ steps.setup.outcome }}
exit_code: ${{ steps.run.outputs.exit_code }}
failed_recipe: ${{ steps.run.outputs.failed_recipe }}

- name: "Failed Just recipe: ${{ steps.run.outputs.failed_recipe }}"
if: always() && steps.run.outputs.exit_code != '' && steps.run.outputs.exit_code != '0'
shell: bash
run: exit 1
9 changes: 5 additions & 4 deletions .github/workflows/anvil-pr-impl.yml
Original file line number Diff line number Diff line change
Expand Up @@ -211,10 +211,11 @@ jobs:
# Codecov UI without changing the union total.
# lcov-*.info are produced by the anvil-llvm-cov recipe inside
# anvil-pr-test (one per feature config); if the affected set was
# empty the recipe no-ops and there is no file to upload, so we gate
# on the files existing (impact scoping lives in the downloaded cache
# now, not a job output). Codecov coalesces the two per-config files.
if: matrix.os != 'windows-arm' && hashFiles('target/coverage/lcov-all-features.info', 'target/coverage/lcov-no-default.info') != ''
# empty the recipe no-ops and there are no files to upload, so we gate
# on both files existing (impact scoping lives in the downloaded cache
# now, not a job output). Requiring both avoids publishing a partial
# result if the second feature configuration fails.
if: always() && matrix.os != 'windows-arm' && hashFiles('target/coverage/lcov-all-features.info') != '' && hashFiles('target/coverage/lcov-no-default.info') != ''
uses: codecov/codecov-action@v7.0.0 # immutable release, the tag cannot be moved
with:
files: target/coverage/lcov-all-features.info,target/coverage/lcov-no-default.info
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/anvil-scheduled-impl.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ jobs:
# Multi-flag tag combines the OS with a "scheduled" marker so
# the Codecov UI can distinguish PR-tier uploads from scheduled
# uploads while still tracking each platform separately.
if: matrix.os != 'windows-arm'
if: always() && matrix.os != 'windows-arm' && hashFiles('target/coverage/lcov-all-features.info') != '' && hashFiles('target/coverage/lcov-no-default.info') != ''
uses: codecov/codecov-action@v7.0.0 # immutable release, the tag cannot be moved
with:
files: target/coverage/lcov-all-features.info,target/coverage/lcov-no-default.info
Expand Down
42 changes: 23 additions & 19 deletions crates/cargo-anvil/docs/design/github.md
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,7 @@ runs:
failed_recipe="$(sed -n 's/^error: recipe `\([^`]*\)` failed\( on line [0-9][0-9]*\)\{0,1\} with exit code [0-9][0-9]*$/\1/p' "$log" | tail -n 1)"
echo "failed_recipe=${failed_recipe:-anvil-$ANVIL_GROUP}" >> "$GITHUB_OUTPUT"
echo "exit_code=$status" >> "$GITHUB_OUTPUT"
exit "$status"
- name: Publish supplemental Anvil commit status
if: always() && inputs.publish_commit_statuses == 'true' && github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
continue-on-error: true
Expand All @@ -543,10 +544,6 @@ runs:
setup_outcome: ${{ steps.setup.outcome }}
exit_code: ${{ steps.run.outputs.exit_code }}
failed_recipe: ${{ steps.run.outputs.failed_recipe }}
- name: "Failed Just recipe: ${{ steps.run.outputs.failed_recipe }}"
if: always() && steps.run.outputs.exit_code != '' && steps.run.outputs.exit_code != '0'
shell: bash
run: exit 1
```

Input set on the shared group action:
Expand Down Expand Up @@ -593,9 +590,10 @@ the following mechanisms, all driven by Just's existing terminal diagnostic:
1. The problem matcher registered by `anvil-setup` promotes
Comment thread
martin-kolinek marked this conversation as resolved.
``error: recipe `anvil-license-headers` failed with exit code 1`` to a
GitHub annotation.
2. The group composite ends with a failing step named
`Failed Just recipe: anvil-license-headers`, putting the recipe name in the
job's step list.
2. The `Run Anvil group` step itself returns Just's exit status after recording
the recipe name and exit code for supplemental reporting. The failed step is
therefore the step containing the complete, live recipe output; no
synthetic failure step can displace or truncate the underlying diagnostic.
3. On eligible pull requests, `anvil-report-status` publishes a commit status
whose reserved context namespace names the failed recipe and runner:

Expand All @@ -604,10 +602,12 @@ the following mechanisms, all driven by Just's existing terminal diagnostic:
```

The group action streams normal Just output, captures the terminal failed
recipe, reports supplemental presentation on a best-effort basis, and then
propagates Just's result to the authoritative workflow job. The reporter
neither invokes checks nor contains group membership. Internal capture,
parsing, status reconciliation, and test-harness details are documented in the
recipe, writes its outputs, and returns Just's status from that same step.
Subsequent reporting uses `always()` and is supplemental and best-effort, so it
still runs after a recipe failure without replacing the authoritative failed
step. The reporter neither invokes checks nor contains group membership.
Internal capture, parsing, status reconciliation, and test-harness details are
documented in the
[implementation guide](../implementation.md#github-group-execution-and-status-reporting).

When `publish_commit_statuses` is enabled, the shared reporter manages statuses
Expand Down Expand Up @@ -801,7 +801,7 @@ The wiring never gates jobs on the impact result — every job runs regardless o
status. This is intentional: unscoped checks (`deny`, `audit`, `aprz`, `pr-title`,
`mutants-full`) must run on every PR even when every tier reports `--skip`. Steps that
need a per-tier side decision read the downloaded cache file directly (e.g. the Codecov
upload is gated on the coverage files existing via `hashFiles(...)`), never on a job
upload is gated on both coverage files existing via `hashFiles(...)`), never on a job
output.


Expand Down Expand Up @@ -904,8 +904,11 @@ Recommended root workflow shape:
## 10. Coverage upload

After `pr-test` (and `scheduled-test`) runs the `anvil-llvm-cov` recipe, the reusable
workflow uploads the resulting `target/coverage/lcov.info` to Codecov from every leg of
the matrix except `windows-11-arm`. The windows-arm leg is excluded because its
workflow uploads the resulting coverage files to Codecov from every leg of the matrix
except `windows-11-arm`. The upload condition uses `always()` plus a file-existence
guard: completed coverage reports are retained even when the coverage gate or a later
group recipe fails, while failures before both feature configurations complete do not
trigger an empty or partial upload. The windows-arm leg is excluded because its
Comment thread
martin-kolinek marked this conversation as resolved.
Outdated
LLVM-coverage instrumentation produces `malformed instrumentation profile data: symbol
name is empty` errors that make the profile unusable. Coverage from every other leg is
necessary because OS/arch-gated code (`cfg(target_os = ...)`, `cfg(target_arch = ...)`)
Expand All @@ -918,10 +921,10 @@ The upload step:

```yaml
- name: Upload coverage to Codecov
if: matrix.os != 'windows-arm' && hashFiles('target/coverage/lcov-all-features.info', 'target/coverage/lcov-no-default.info') != ''
if: always() && matrix.os != 'windows-arm' && hashFiles('target/coverage/lcov-all-features.info') != '' && hashFiles('target/coverage/lcov-no-default.info') != ''
uses: codecov/codecov-action@v7.0.0 # immutable release, the tag cannot be moved
with:
files: target/coverage/lcov.info
files: target/coverage/lcov-all-features.info,target/coverage/lcov-no-default.info
flags: ${{ matrix.os }}
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: false
Expand All @@ -934,9 +937,10 @@ all; private repos set `CODECOV_TOKEN` at the repo level. `fail_ci_if_error: fal
keeps the build green when Codecov is unreachable (typical for internal repos that
can't reach `codecov.io`).

On the scheduled upload the step additionally combines the OS flag with a `scheduled`
marker (`flags: scheduled,${{ matrix.os }}`) so PR vs scheduled streams stay
distinguishable in the Codecov UI while still being queryable per-OS.
The scheduled upload has the same `always()` and file-existence semantics. It
additionally combines the OS flag with a `scheduled` marker
(`flags: scheduled,${{ matrix.os }}`) so PR vs scheduled streams stay distinguishable
in the Codecov UI while still being queryable per-OS.

anvil does not gate the PR on coverage. The lcov upload is informational; Codecov's
own status check is the gating layer when the adopter wants one (configured in Codecov,
Expand Down
24 changes: 16 additions & 8 deletions crates/cargo-anvil/docs/implementation.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,14 +120,22 @@ tests in `tests/impact.rs` exercise the real recipe rather than a CI-only path.

## GitHub group execution and status reporting

The generated `anvil-run-group` composite action owns the capture-before-failure
protocol. Its inline Bash step invokes Just through `tee`, temporarily disables
immediate exit, and reads `PIPESTATUS[0]` so the saved result belongs to Just
rather than `tee`. It selects the final standard Just failed-recipe diagnostic,
including the optional line-number form, and falls back to the group recipe
when a tool exits without that diagnostic. The step writes the recipe and exit
code as outputs without failing so the reporter can consume them. After
best-effort reporting, a final guarded step propagates the captured failure.
The generated `anvil-run-group` composite action owns the
capture-before-propagation protocol. Its inline Bash step invokes Just through
`tee`, temporarily disables immediate exit, and reads `PIPESTATUS[0]` so the
saved result belongs to Just rather than `tee`. It selects the final standard
Just failed-recipe diagnostic, including the optional line-number form, and
falls back to the group recipe when a tool exits without that diagnostic. The
step writes the recipe and exit code as outputs, then returns the captured
status itself. This is a correctness constraint for diagnostics: the GitHub
step marked failed must be the step containing the complete recipe output.
Moving propagation to a later synthetic step would make GitHub focus that
empty step and hide the useful output behind a successful predecessor.
Comment thread
martin-kolinek marked this conversation as resolved.
Outdated

The reporter uses `always()`, so GitHub runs it after the group step fails and
the outputs written before propagation remain available to it. Its
`continue-on-error` remains necessary because supplemental API reporting must
not replace or obscure the authoritative recipe result.

The status reporter is an inline `actions/github-script` body. It validates the
pull-request head SHA, reads same-commit status history newest-first, and keeps
Expand Down
34 changes: 19 additions & 15 deletions crates/cargo-anvil/src/anvil/artifacts/github.rs
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,11 @@ mod tests {
assert!(RUN_GROUP_ACTION.contains("group: ${{ inputs.group }}"));
assert!(RUN_GROUP_ACTION.contains("free-disk-space: ${{ inputs.free-disk-space }}"));
assert!(RUN_GROUP_ACTION.contains("status=${PIPESTATUS[0]}"));
assert!(RUN_GROUP_ACTION.contains("Failed Just recipe: ${{ steps.run.outputs.failed_recipe }}"));
assert!(RUN_GROUP_ACTION.contains("exit \"$status\""));
assert!(
!RUN_GROUP_ACTION.contains("Failed Just recipe:"),
"failure propagation must stay in the step containing the recipe output"
);
assert!(RUN_GROUP_ACTION.contains("uses: ./.github/actions/anvil-report-status"));
Comment thread
martin-kolinek marked this conversation as resolved.
Outdated
// Impact reaches scoped checks through the downloaded impact cache
// (read via `_anvil-impact-include`), not threaded --package env vars;
Expand Down Expand Up @@ -285,7 +289,7 @@ export -f just

assert!(
status.success(),
"the capture script must defer group failure to the named action step"
"a successful group must return success after exporting its result"
);
assert!(outputs.contains("failed_recipe=anvil-pr-fast"));
assert!(outputs.contains("exit_code=0"));
Expand All @@ -297,10 +301,7 @@ export -f just
let diagnostic = "error: recipe `anvil-license-headers` failed with exit code 17";
let (status, outputs) = run_group_step(diagnostic, 17);

assert!(
status.success(),
"the capture script must defer group failure to the named action step"
);
assert_eq!(status.code(), Some(17), "the recipe-running step must return Just's status");
assert!(outputs.contains("failed_recipe=anvil-license-headers"));
assert!(outputs.contains("exit_code=17"));
}
Expand All @@ -311,10 +312,7 @@ export -f just
let diagnostic = "error: recipe `anvil-license-headers` failed on line 42 with exit code 17";
let (status, outputs) = run_group_step(diagnostic, 17);

assert!(
status.success(),
"the capture script must defer group failure to the named action step"
);
assert_eq!(status.code(), Some(17), "the recipe-running step must return Just's status");
assert!(outputs.contains("failed_recipe=anvil-license-headers"));
assert!(outputs.contains("exit_code=17"));
}
Expand All @@ -324,10 +322,7 @@ export -f just
fn run_group_step_falls_back_to_group_without_terminal_diagnostic() {
let (status, outputs) = run_group_step("unexpected tool failure", 9);

assert!(
status.success(),
"the capture script must defer group failure to the named action step"
);
assert_eq!(status.code(), Some(9), "the recipe-running step must return Just's status");
assert!(outputs.contains("failed_recipe=anvil-pr-fast"));
assert!(outputs.contains("exit_code=9"));
}
Expand Down Expand Up @@ -427,7 +422,11 @@ export -f just
1,
"Codecov upload step should be declared exactly once (gated per-leg via `if:`)"
);
assert!(PR_IMPL_WORKFLOW.contains("matrix.os != 'windows-arm'"));
assert!(PR_IMPL_WORKFLOW.contains(
"if: always() && matrix.os != 'windows-arm' && \
hashFiles('target/coverage/lcov-all-features.info') != '' && \
hashFiles('target/coverage/lcov-no-default.info') != ''"
));
assert!(PR_IMPL_WORKFLOW.contains("flags: ${{ matrix.os }}"));
assert_eq!(
PR_IMPL_WORKFLOW.matches("permissions:").count(),
Expand Down Expand Up @@ -456,6 +455,11 @@ export -f just
}
assert!(SCHEDULED_IMPL_WORKFLOW.contains("publish-failure:"));
assert!(SCHEDULED_IMPL_WORKFLOW.contains("codecov/codecov-action"));
assert!(SCHEDULED_IMPL_WORKFLOW.contains(
"if: always() && matrix.os != 'windows-arm' && \
hashFiles('target/coverage/lcov-all-features.info') != '' && \
hashFiles('target/coverage/lcov-no-default.info') != ''"
));
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"));
Expand Down
9 changes: 5 additions & 4 deletions crates/cargo-anvil/templates/github/pr-impl-workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -211,10 +211,11 @@ jobs:
# Codecov UI without changing the union total.
# lcov-*.info are produced by the anvil-llvm-cov recipe inside
# anvil-pr-test (one per feature config); if the affected set was
# empty the recipe no-ops and there is no file to upload, so we gate
# on the files existing (impact scoping lives in the downloaded cache
# now, not a job output). Codecov coalesces the two per-config files.
if: matrix.os != 'windows-arm' && hashFiles('target/coverage/lcov-all-features.info', 'target/coverage/lcov-no-default.info') != ''
# empty the recipe no-ops and there are no files to upload, so we gate
# on both files existing (impact scoping lives in the downloaded cache
# now, not a job output). Requiring both avoids publishing a partial
# result if the second feature configuration fails.
if: always() && matrix.os != 'windows-arm' && hashFiles('target/coverage/lcov-all-features.info') != '' && hashFiles('target/coverage/lcov-no-default.info') != ''
Comment thread
martin-kolinek marked this conversation as resolved.
uses: codecov/codecov-action@v7.0.0 # immutable release, the tag cannot be moved
with:
files: target/coverage/lcov-all-features.info,target/coverage/lcov-no-default.info
Expand Down
6 changes: 1 addition & 5 deletions crates/cargo-anvil/templates/github/run-group-action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ runs:
failed_recipe="$(sed -n 's/^error: recipe `\([^`]*\)` failed\( on line [0-9][0-9]*\)\{0,1\} with exit code [0-9][0-9]*$/\1/p' "$log" | tail -n 1)"
echo "failed_recipe=${failed_recipe:-anvil-$ANVIL_GROUP}" >> "$GITHUB_OUTPUT"
echo "exit_code=$status" >> "$GITHUB_OUTPUT"
exit "$status"

# Reporting is supplemental: run after success or failure, but never let
# an API outage determine the authoritative workflow-job result.
Expand All @@ -84,8 +85,3 @@ runs:
setup_outcome: ${{ steps.setup.outcome }}
exit_code: ${{ steps.run.outputs.exit_code }}
failed_recipe: ${{ steps.run.outputs.failed_recipe }}

- name: "Failed Just recipe: ${{ steps.run.outputs.failed_recipe }}"
if: always() && steps.run.outputs.exit_code != '' && steps.run.outputs.exit_code != '0'
shell: bash
run: exit 1
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ jobs:
# Multi-flag tag combines the OS with a "scheduled" marker so
# the Codecov UI can distinguish PR-tier uploads from scheduled
# uploads while still tracking each platform separately.
if: matrix.os != 'windows-arm'
if: always() && matrix.os != 'windows-arm' && hashFiles('target/coverage/lcov-all-features.info') != '' && hashFiles('target/coverage/lcov-no-default.info') != ''
uses: codecov/codecov-action@v7.0.0 # immutable release, the tag cannot be moved
with:
files: target/coverage/lcov-all-features.info,target/coverage/lcov-no-default.info
Expand Down
Loading
Loading