Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
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"

# 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
2 changes: 1 addition & 1 deletion .github/workflows/anvil-pr-impl.yml
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ jobs:
# 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') != ''
if: always() && matrix.os != 'windows-arm' && hashFiles('target/coverage/lcov-all-features.info', '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', '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
35 changes: 21 additions & 14 deletions crates/cargo-anvil/docs/design/github.md
Original file line number Diff line number Diff line change
Expand Up @@ -593,9 +593,10 @@ the following mechanisms, all driven by Just's existing terminal diagnostic:
1. The problem matcher registered by `anvil-setup` promotes
``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 +605,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 @@ -904,8 +907,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 report generation do not trigger an empty
upload. The windows-arm leg is excluded because its
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 +924,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', '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 +940,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.

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
32 changes: 17 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"));
// 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,10 @@ 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', \
'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 +454,10 @@ 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', \
'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
2 changes: 1 addition & 1 deletion crates/cargo-anvil/templates/github/pr-impl-workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ jobs:
# 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') != ''
if: always() && matrix.os != 'windows-arm' && hashFiles('target/coverage/lcov-all-features.info', '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
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', '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
Original file line number Diff line number Diff line change
Expand Up @@ -1508,6 +1508,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 @@ -1521,11 +1522,6 @@ runs:
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

=== .github/actions/anvil-setup/action.yml ===
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
Expand Down Expand Up @@ -1985,7 +1981,7 @@ jobs:
# 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') != ''
if: always() && matrix.os != 'windows-arm' && hashFiles('target/coverage/lcov-all-features.info', '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 Expand Up @@ -2188,7 +2184,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', '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
7 changes: 5 additions & 2 deletions crates/cargo-coverage-gate/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ by [`cargo-llvm-cov`][__link0] against per-package thresholds carried in
`Cargo.toml`. The accompanying `cargo-coverage-gate` binary reads the
coverage lcov tracefile, resolves each package鈥檚 threshold from a small
three-layer lookup, and emits a verdict table to stdout (and,
optionally, to a Markdown summary file for CI step summaries).
optionally, to a Markdown summary file for CI step summaries). A failing
verdict includes exact covered/coverable counts and the uncovered source
line ranges, so the output identifies what must be covered without relying
on a later coverage-service upload.

### Threshold resolution

Expand Down Expand Up @@ -105,7 +108,7 @@ plus the appropriate exit code.
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-coverage-gate">source code</a>.
</sub>

[__cargo_doc2readme_dependencies_info]: ggGmYW0CYXZlMC43LjJhdIQbFhzZ8rzWNNYbuRaDSGWynFgbH4PMdoT7GNcbVwNPtPjAhvFhYvRhcoQbDzRwf0qddWQbQiTzhu0-bE0bX-rutkvfDuYbITgXvtMXiRVhZIGDc2NhcmdvLWNvdmVyYWdlLWdhdGVlMC4zLjBzY2FyZ29fY292ZXJhZ2VfZ2F0ZQ
[__cargo_doc2readme_dependencies_info]: ggGmYW0CYXZlMC43LjJhdIQbFhzZ8rzWNNYbuRaDSGWynFgbH4PMdoT7GNcbVwNPtPjAhvFhYvRhcoQbUyBJhTlThKMb2KAsMNOODUYbjhYEhuJ2xScbr_AgFmRNpuZhZIGDc2NhcmdvLWNvdmVyYWdlLWdhdGVlMC4zLjBzY2FyZ29fY292ZXJhZ2VfZ2F0ZQ
[__link0]: https://github.com/taiki-e/cargo-llvm-cov
[__link1]: https://docs.rs/cargo-coverage-gate/0.3.0/cargo_coverage_gate/fn.evaluate.html
[__link2]: https://docs.rs/cargo-coverage-gate/0.3.0/cargo_coverage_gate/struct.EvaluatedReport.html
Expand Down
Loading
Loading