From 831605092d4375198e881ff73e1b3623bd177606 Mon Sep 17 00:00:00 2001 From: Vaiz <4908982+Vaiz@users.noreply.github.com> Date: Tue, 18 Aug 2026 13:44:30 +0100 Subject: [PATCH 1/2] feat(cargo-anvil): tell the ADO job wrapper which check group it renders The ADO stages templates name every per-OS job `linux` / `windows`, so a job wrapper cannot tell a `pr_mutants` job from a `pr_fast` one. That matters for extension-template wrappers (1ESPT, SubstratePT, M365PT), which take ownership of `steps/job.yml` and set a per-job `templateContext:` block: some groups need different SDL settings than others. `templateContext:` is consumed at template-expansion time, so a runtime condition on `System.StageName` is not an option either, and the job name is the only compile-time signal the wrapper gets. Add an optional `group` parameter to the wrapper contract and pass the check-group name from every `steps/job.yml` invocation in pr-stages.yml and scheduled-stages.yml. The default wrapper ignores it; it exists so downstream wrappers can discriminate. It defaults to '' so a wrapper that does not declare it keeps its current behaviour. Note for adopters: ADO rejects a parameter the target template does not declare, so a repository that already owns `steps/job.yml` must take the proposed wrapper update (which adds the `group` parameter) together with this stages-template update. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/cargo-anvil/src/anvil/artifacts/ado.rs | 38 +++++++++++++++++++ .../cargo-anvil/templates/ado/pr-stages.yml | 10 +++++ .../templates/ado/scheduled-stages.yml | 8 ++++ .../cargo-anvil/templates/ado/steps/job.yml | 14 +++++++ .../snapshots/snapshots__ado_backend.snap | 32 ++++++++++++++++ 5 files changed, 102 insertions(+) diff --git a/crates/cargo-anvil/src/anvil/artifacts/ado.rs b/crates/cargo-anvil/src/anvil/artifacts/ado.rs index b138f19d..4ff67960 100644 --- a/crates/cargo-anvil/src/anvil/artifacts/ado.rs +++ b/crates/cargo-anvil/src/anvil/artifacts/ado.rs @@ -254,6 +254,7 @@ mod tests { fn job_wrapper_declares_expected_contract() { for needle in [ "name: name", + "name: group", "name: pool", "name: steps", "type: stepList", @@ -264,6 +265,43 @@ mod tests { } } + #[test] + fn stages_identify_the_check_group_for_every_job() { + // Every per-OS job in the stages templates is named `linux` / `windows`, + // so `name` cannot tell an extension-template wrapper which group it is + // rendering. `group` carries that, letting a wrapper vary per-job + // `templateContext:` (which is evaluated at template-expansion time, so + // a runtime stage-name condition is not an option). + for (template, groups) in [ + ( + PR_STAGES, + &["impact", "pr-fast", "pr-test", "pr-runtime-analysis", "pr-mutants"][..], + ), + ( + SCHEDULED_STAGES, + &[ + "scheduled-test", + "scheduled-advisories", + "scheduled-runtime-analysis", + "scheduled-exhaustive", + ][..], + ), + ] { + for group in groups { + assert_eq!( + template.matches(&format!("group: {group}\n")).count(), + 2, + "group '{group}' must be declared on both per-OS jobs" + ); + } + assert_eq!( + template.matches("- template: steps/job.yml").count(), + template.matches("\n group: ").count(), + "every steps/job.yml invocation must pass a group" + ); + } + } + #[test] fn render_group_step_has_include_inputs_and_env() { let body = render_group_step("pr-fast"); diff --git a/crates/cargo-anvil/templates/ado/pr-stages.yml b/crates/cargo-anvil/templates/ado/pr-stages.yml index 6e1ac2f8..51bec51a 100644 --- a/crates/cargo-anvil/templates/ado/pr-stages.yml +++ b/crates/cargo-anvil/templates/ado/pr-stages.yml @@ -40,12 +40,14 @@ stages: - template: steps/job.yml parameters: name: compute_linux + group: impact pool: ${{ parameters.linuxPool }} steps: - template: steps/impact.yml - template: steps/job.yml parameters: name: compute_windows + group: impact pool: ${{ parameters.windowsPool }} steps: - template: steps/impact.yml @@ -77,6 +79,7 @@ stages: - template: steps/job.yml parameters: name: linux + group: pr-fast pool: ${{ parameters.linuxPool }} steps: - template: steps/pr-fast.yml @@ -97,6 +100,7 @@ stages: - template: steps/job.yml parameters: name: windows + group: pr-fast pool: ${{ parameters.windowsPool }} steps: - template: steps/pr-fast.yml @@ -137,6 +141,7 @@ stages: - template: steps/job.yml parameters: name: linux + group: pr-test pool: ${{ parameters.linuxPool }} steps: - template: steps/pr-test.yml @@ -153,6 +158,7 @@ stages: - template: steps/job.yml parameters: name: windows + group: pr-test pool: ${{ parameters.windowsPool }} steps: - template: steps/pr-test.yml @@ -181,6 +187,7 @@ stages: - template: steps/job.yml parameters: name: linux + group: pr-runtime-analysis pool: ${{ parameters.linuxPool }} steps: - template: steps/pr-runtime-analysis.yml @@ -191,6 +198,7 @@ stages: - template: steps/job.yml parameters: name: windows + group: pr-runtime-analysis pool: ${{ parameters.windowsPool }} steps: - template: steps/pr-runtime-analysis.yml @@ -215,6 +223,7 @@ stages: - template: steps/job.yml parameters: name: linux + group: pr-mutants pool: ${{ parameters.linuxPool }} steps: - template: steps/pr-mutants.yml @@ -225,6 +234,7 @@ stages: - template: steps/job.yml parameters: name: windows + group: pr-mutants pool: ${{ parameters.windowsPool }} steps: - template: steps/pr-mutants.yml diff --git a/crates/cargo-anvil/templates/ado/scheduled-stages.yml b/crates/cargo-anvil/templates/ado/scheduled-stages.yml index f1cb349d..d1c516cd 100644 --- a/crates/cargo-anvil/templates/ado/scheduled-stages.yml +++ b/crates/cargo-anvil/templates/ado/scheduled-stages.yml @@ -23,6 +23,7 @@ stages: - template: steps/job.yml parameters: name: linux + group: scheduled-test pool: ${{ parameters.linuxPool }} steps: - template: steps/scheduled-test.yml @@ -35,6 +36,7 @@ stages: - template: steps/job.yml parameters: name: windows + group: scheduled-test pool: ${{ parameters.windowsPool }} steps: - template: steps/scheduled-test.yml @@ -54,12 +56,14 @@ stages: - template: steps/job.yml parameters: name: linux + group: scheduled-advisories pool: ${{ parameters.linuxPool }} steps: - template: steps/scheduled-advisories.yml - template: steps/job.yml parameters: name: windows + group: scheduled-advisories pool: ${{ parameters.windowsPool }} steps: - template: steps/scheduled-advisories.yml @@ -75,12 +79,14 @@ stages: - template: steps/job.yml parameters: name: linux + group: scheduled-runtime-analysis pool: ${{ parameters.linuxPool }} steps: - template: steps/scheduled-runtime-analysis.yml - template: steps/job.yml parameters: name: windows + group: scheduled-runtime-analysis pool: ${{ parameters.windowsPool }} steps: - template: steps/scheduled-runtime-analysis.yml @@ -97,12 +103,14 @@ stages: - template: steps/job.yml parameters: name: linux + group: scheduled-exhaustive pool: ${{ parameters.linuxPool }} steps: - template: steps/scheduled-exhaustive.yml - template: steps/job.yml parameters: name: windows + group: scheduled-exhaustive pool: ${{ parameters.windowsPool }} steps: - template: steps/scheduled-exhaustive.yml diff --git a/crates/cargo-anvil/templates/ado/steps/job.yml b/crates/cargo-anvil/templates/ado/steps/job.yml index 028b1ccd..9328a94e 100644 --- a/crates/cargo-anvil/templates/ado/steps/job.yml +++ b/crates/cargo-anvil/templates/ado/steps/job.yml @@ -15,6 +15,17 @@ # # Contract (intentionally small and stable): # - name (string) Job name; ADO derives the display name from it. +# - group (string) Optional. The anvil check group this job runs +# (`impact`, `pr-fast`, `pr-mutants`, ...). The +# default wrapper ignores it; extension-template +# wrappers use it to vary per-job `templateContext:`, +# which cannot be expressed any other way: the stages +# templates name every per-OS job `linux` / `windows`, +# so `name` alone does not identify the group, and +# `templateContext:` is evaluated at template- +# expansion time so a runtime stage-name condition is +# not available either. Defaults to '' so wrappers +# written against the older contract keep working. # - pool (object) Pool block, passed verbatim to ADO's `pool:` key. # - steps (stepList) Body of the job. Templated step lists are fine. # - artifacts (object) Optional list of pipeline artifacts to publish. @@ -29,6 +40,9 @@ parameters: - name: name type: string + - name: group + type: string + default: '' - name: pool type: object - name: steps diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap index d0db7681..fef2cfb0 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap @@ -1357,12 +1357,14 @@ stages: - template: steps/job.yml parameters: name: compute_linux + group: impact pool: ${{ parameters.linuxPool }} steps: - template: steps/impact.yml - template: steps/job.yml parameters: name: compute_windows + group: impact pool: ${{ parameters.windowsPool }} steps: - template: steps/impact.yml @@ -1394,6 +1396,7 @@ stages: - template: steps/job.yml parameters: name: linux + group: pr-fast pool: ${{ parameters.linuxPool }} steps: - template: steps/pr-fast.yml @@ -1414,6 +1417,7 @@ stages: - template: steps/job.yml parameters: name: windows + group: pr-fast pool: ${{ parameters.windowsPool }} steps: - template: steps/pr-fast.yml @@ -1454,6 +1458,7 @@ stages: - template: steps/job.yml parameters: name: linux + group: pr-test pool: ${{ parameters.linuxPool }} steps: - template: steps/pr-test.yml @@ -1470,6 +1475,7 @@ stages: - template: steps/job.yml parameters: name: windows + group: pr-test pool: ${{ parameters.windowsPool }} steps: - template: steps/pr-test.yml @@ -1498,6 +1504,7 @@ stages: - template: steps/job.yml parameters: name: linux + group: pr-runtime-analysis pool: ${{ parameters.linuxPool }} steps: - template: steps/pr-runtime-analysis.yml @@ -1508,6 +1515,7 @@ stages: - template: steps/job.yml parameters: name: windows + group: pr-runtime-analysis pool: ${{ parameters.windowsPool }} steps: - template: steps/pr-runtime-analysis.yml @@ -1532,6 +1540,7 @@ stages: - template: steps/job.yml parameters: name: linux + group: pr-mutants pool: ${{ parameters.linuxPool }} steps: - template: steps/pr-mutants.yml @@ -1542,6 +1551,7 @@ stages: - template: steps/job.yml parameters: name: windows + group: pr-mutants pool: ${{ parameters.windowsPool }} steps: - template: steps/pr-mutants.yml @@ -1576,6 +1586,7 @@ stages: - template: steps/job.yml parameters: name: linux + group: scheduled-test pool: ${{ parameters.linuxPool }} steps: - template: steps/scheduled-test.yml @@ -1588,6 +1599,7 @@ stages: - template: steps/job.yml parameters: name: windows + group: scheduled-test pool: ${{ parameters.windowsPool }} steps: - template: steps/scheduled-test.yml @@ -1607,12 +1619,14 @@ stages: - template: steps/job.yml parameters: name: linux + group: scheduled-advisories pool: ${{ parameters.linuxPool }} steps: - template: steps/scheduled-advisories.yml - template: steps/job.yml parameters: name: windows + group: scheduled-advisories pool: ${{ parameters.windowsPool }} steps: - template: steps/scheduled-advisories.yml @@ -1628,12 +1642,14 @@ stages: - template: steps/job.yml parameters: name: linux + group: scheduled-runtime-analysis pool: ${{ parameters.linuxPool }} steps: - template: steps/scheduled-runtime-analysis.yml - template: steps/job.yml parameters: name: windows + group: scheduled-runtime-analysis pool: ${{ parameters.windowsPool }} steps: - template: steps/scheduled-runtime-analysis.yml @@ -1650,12 +1666,14 @@ stages: - template: steps/job.yml parameters: name: linux + group: scheduled-exhaustive pool: ${{ parameters.linuxPool }} steps: - template: steps/scheduled-exhaustive.yml - template: steps/job.yml parameters: name: windows + group: scheduled-exhaustive pool: ${{ parameters.windowsPool }} steps: - template: steps/scheduled-exhaustive.yml @@ -1876,6 +1894,17 @@ steps: # # Contract (intentionally small and stable): # - name (string) Job name; ADO derives the display name from it. +# - group (string) Optional. The anvil check group this job runs +# (`impact`, `pr-fast`, `pr-mutants`, ...). The +# default wrapper ignores it; extension-template +# wrappers use it to vary per-job `templateContext:`, +# which cannot be expressed any other way: the stages +# templates name every per-OS job `linux` / `windows`, +# so `name` alone does not identify the group, and +# `templateContext:` is evaluated at template- +# expansion time so a runtime stage-name condition is +# not available either. Defaults to '' so wrappers +# written against the older contract keep working. # - pool (object) Pool block, passed verbatim to ADO's `pool:` key. # - steps (stepList) Body of the job. Templated step lists are fine. # - artifacts (object) Optional list of pipeline artifacts to publish. @@ -1890,6 +1919,9 @@ steps: parameters: - name: name type: string + - name: group + type: string + default: '' - name: pool type: object - name: steps From 546f91b47de5f491103df0cfa9e97e9f0c594676 Mon Sep 17 00:00:00 2001 From: Vaiz <4908982+Vaiz@users.noreply.github.com> Date: Tue, 18 Aug 2026 14:00:55 +0100 Subject: [PATCH 2/2] docs(cargo-anvil): correct the `group` contract wording and document it The wrapper contract comment claimed the stages templates name every per-OS job `linux` / `windows`, but the impact stage renders `compute_linux` / `compute_windows`. The argument is unchanged -- per-OS names repeat across stages, so `name` cannot identify a check group -- but the wording now says that accurately, in `steps/job.yml`, in the test rationale and in the snapshot. It also claimed the `''` default lets wrappers written against the older contract keep working. ADO rejects a parameter the target template does not declare, so that is wrong and contradicted the adopter-compat note in the PR description: the default only lets a CALLER omit the value. Add `group` to the wrapper contract table and the default-wrapper snippet in docs/design/ado.md so the design stays in lockstep with the template. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/cargo-anvil/docs/design/ado.md | 2 ++ crates/cargo-anvil/src/anvil/artifacts/ado.rs | 11 ++++++----- .../cargo-anvil/templates/ado/steps/job.yml | 19 ++++++++++++------- .../snapshots/snapshots__ado_backend.snap | 19 ++++++++++++------- 4 files changed, 32 insertions(+), 19 deletions(-) diff --git a/crates/cargo-anvil/docs/design/ado.md b/crates/cargo-anvil/docs/design/ado.md index df3e7059..c8e45144 100644 --- a/crates/cargo-anvil/docs/design/ado.md +++ b/crates/cargo-anvil/docs/design/ado.md @@ -368,6 +368,7 @@ The contract is intentionally small and stable: | Parameter | Type | Required | Meaning | |-------------|------------|----------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `name` | `string` | yes | Job name; ADO derives the display name from it. | +| `group` | `string` | no | The anvil check group this job runs (`impact`, `pr-fast`, `pr-test`, `pr-runtime-analysis`, `pr-mutants`, `scheduled-*`). The default wrapper ignores it; extension-template wrappers use it to vary per-job `templateContext:`. Per-OS job names repeat across stages (`linux` / `windows` in every pr-\* and scheduled-\* stage, `compute_linux` / `compute_windows` in impact), so `name` alone cannot identify the group, and `templateContext:` is evaluated at template-expansion time so a runtime stage-name condition is not available either. Defaults to `''` so a caller may omit it — but a wrapper that does not *declare* the parameter is rejected by ADO once the stages templates pass it, so an adopter who owns this file takes the wrapper and stages updates together. | | `pool` | `object` | yes | Pool block, passed verbatim to ADO's `pool:` key. `linuxPool` and `windowsPool` at the stage level are object parameters, so users can override their shape (e.g. `{ name, os, image }` for 1ESPT). | | `steps` | `stepList` | yes | Body of the job. Templated step lists are fine — the wrapper splices them in via `${{ each step in parameters.steps }}: - ${{ step }}`. | | `artifacts` | `object` | no | List of pipeline artifacts to publish. Each item: `{ name: string, path: string }`. Default wrapper appends one `PublishPipelineArtifact@1` per entry; 1ESPT wrappers translate the same list into `templateContext.outputs.pipelineArtifact` blocks. The stages templates don't need to know which backend they're targeting. | @@ -377,6 +378,7 @@ The default wrapper anvil ships is six lines of logic: ```yaml parameters: - { name: name, type: string } + - { name: group, type: string, default: '' } - { name: pool, type: object } - { name: steps, type: stepList } - { name: artifacts, type: object, default: [] } diff --git a/crates/cargo-anvil/src/anvil/artifacts/ado.rs b/crates/cargo-anvil/src/anvil/artifacts/ado.rs index 4ff67960..26d37333 100644 --- a/crates/cargo-anvil/src/anvil/artifacts/ado.rs +++ b/crates/cargo-anvil/src/anvil/artifacts/ado.rs @@ -267,11 +267,12 @@ mod tests { #[test] fn stages_identify_the_check_group_for_every_job() { - // Every per-OS job in the stages templates is named `linux` / `windows`, - // so `name` cannot tell an extension-template wrapper which group it is - // rendering. `group` carries that, letting a wrapper vary per-job - // `templateContext:` (which is evaluated at template-expansion time, so - // a runtime stage-name condition is not an option). + // Per-OS job names repeat across stages (`linux` / `windows` in every + // pr-* and scheduled-* stage, `compute_linux` / `compute_windows` in + // impact), so `name` cannot tell an extension-template wrapper which + // group it is rendering. `group` carries that, letting a wrapper vary + // per-job `templateContext:` (which is evaluated at template-expansion + // time, so a runtime stage-name condition is not an option). for (template, groups) in [ ( PR_STAGES, diff --git a/crates/cargo-anvil/templates/ado/steps/job.yml b/crates/cargo-anvil/templates/ado/steps/job.yml index 9328a94e..975be926 100644 --- a/crates/cargo-anvil/templates/ado/steps/job.yml +++ b/crates/cargo-anvil/templates/ado/steps/job.yml @@ -19,13 +19,18 @@ # (`impact`, `pr-fast`, `pr-mutants`, ...). The # default wrapper ignores it; extension-template # wrappers use it to vary per-job `templateContext:`, -# which cannot be expressed any other way: the stages -# templates name every per-OS job `linux` / `windows`, -# so `name` alone does not identify the group, and -# `templateContext:` is evaluated at template- -# expansion time so a runtime stage-name condition is -# not available either. Defaults to '' so wrappers -# written against the older contract keep working. +# which cannot be expressed any other way: per-OS job +# names repeat across stages (`linux` / `windows` in +# every pr-* and scheduled-* stage, `compute_linux` / +# `compute_windows` in impact), so `name` alone does +# not identify the group, and `templateContext:` is +# evaluated at template-expansion time so a runtime +# stage-name condition is not available either. +# Defaults to '' so a caller may omit it. A wrapper +# that does not DECLARE the parameter is rejected by +# ADO once the stages templates pass it, so an adopter +# who owns this file takes the wrapper and stages +# updates together. # - pool (object) Pool block, passed verbatim to ADO's `pool:` key. # - steps (stepList) Body of the job. Templated step lists are fine. # - artifacts (object) Optional list of pipeline artifacts to publish. diff --git a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap index fef2cfb0..6086182d 100644 --- a/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap +++ b/crates/cargo-anvil/tests/snapshots/snapshots__ado_backend.snap @@ -1898,13 +1898,18 @@ steps: # (`impact`, `pr-fast`, `pr-mutants`, ...). The # default wrapper ignores it; extension-template # wrappers use it to vary per-job `templateContext:`, -# which cannot be expressed any other way: the stages -# templates name every per-OS job `linux` / `windows`, -# so `name` alone does not identify the group, and -# `templateContext:` is evaluated at template- -# expansion time so a runtime stage-name condition is -# not available either. Defaults to '' so wrappers -# written against the older contract keep working. +# which cannot be expressed any other way: per-OS job +# names repeat across stages (`linux` / `windows` in +# every pr-* and scheduled-* stage, `compute_linux` / +# `compute_windows` in impact), so `name` alone does +# not identify the group, and `templateContext:` is +# evaluated at template-expansion time so a runtime +# stage-name condition is not available either. +# Defaults to '' so a caller may omit it. A wrapper +# that does not DECLARE the parameter is rejected by +# ADO once the stages templates pass it, so an adopter +# who owns this file takes the wrapper and stages +# updates together. # - pool (object) Pool block, passed verbatim to ADO's `pool:` key. # - steps (stepList) Body of the job. Templated step lists are fine. # - artifacts (object) Optional list of pipeline artifacts to publish.