Skip to content

test(workflows): fail a pwsh step that can mask a native failure - #658

Merged
rominf merged 3 commits into
mainfrom
test/workflow-pwsh-one-command-per-step
Oct 21, 2026
Merged

rominf merged 3 commits into
mainfrom
test/workflow-pwsh-one-command-per-step

Conversation

@rominf

@rominf rominf commented Oct 13, 2026

Copy link
Copy Markdown
Collaborator

Follows up #633 (juhovainio's note about a contract test).

GitHub runs pwsh bodies with $ErrorActionPreference = 'stop'. That stops on cmdlet errors but not on native ones, and only the last native command's exit code decides the step. The new workflow-contract test checks every shell: pwsh step in every workflow: a native command must be the last statement, or be followed by a $LASTEXITCODE check.

Three existing steps are allow-listed with reasons, and a stale allow-list entry fails the test:

  • ci.yml "Probe sccache cache backend": the unchecked sccache --stop-server is best-effort, and the step ends in exit 0.
  • nightly.yml and release.yml "Package Windows bundle": an unchecked cargo xtask package is only caught indirectly, when the following Copy-Item/throw fails on the missing outputs. An explicit $LASTEXITCODE check there would be the cleaner fix. It's a release-path change, so I left it out of this PR.

shell: powershell steps are not covered yet; there are 33 of them, mostly self-hosted.

Mutation checks:

Verified locally:

  • fmt
  • tests for xtask (249) and e2e-cucumber lib (142), plus feature_naming
  • both clippy invocations, on Linux and for x86_64-pc-windows-gnu

No scenario: this is a CI-only change.

@juhovainio juhovainio left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed this with two independent passes (one explicitly blind, since my own driving context had already read the PR description and the #633 thread).

The core logic checks out: all three allow-list entries, the "33 shell: powershell steps" count, and the described mutation checks all match the real workflow files exactly, and the powershell-exclusion / deferred nightly-release fix are reasonable, disclosed scope decisions rather than silent gaps.

One real issue I'd like fixed before merge, left inline below: pwsh_steps's step-name extraction duplicates the existing job_steps logic in this same file line-for-line. Worth pulling into a shared step_name helper per the repo's "reuse existing shared helpers" convention rather than maintaining two copies that can drift.

Two more inline notes on the new detection logic - neither is a live bug (no current workflow line is mis-classified), but both are real false-negative gaps worth knowing about for future edits.

Separately: "E2E tests" is currently failing on this PR (scenario chat-tool-definitions-accepted) - this isn't one of the known-flaky tests (comfyui status/cli_progress ticker/examine framework probe/therock tarball unpack/Strix Halo cancellation), and doesn't obviously trace back to this PR's own changed files (xtask test code + a workflow comment), but it's a live red X and worth checking before merging rather than assuming it's unrelated. "E2E tests (MI300X)" is also still pending.

Requesting changes mainly for the duplication - happy to re-review once that's split out and the E2E failure is sorted out or explained.

/// The step is found from its `shell: pwsh` line by walking up to the
/// nearest `- ` item shallower than it, so the key order inside the step
/// does not matter.
fn pwsh_steps(text: &str) -> Vec<(String, String)> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This step-name extraction (walk up to the nearest shallower - item, then pull the name: key) duplicates job_steps's existing logic in this same file. Could this call into a shared step_name helper instead, so the two don't drift independently? Not a correctness bug, but it's the one hard convention violation I'd want fixed before merge.

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.

Done in 378e1f83. The lookup now lives in one step_key(step, key) helper, and step_name is step_key(step, "name"); job_steps and pwsh_steps both use it. It reads the item indent from the step's first line, so neither caller passes it. To check nothing moved, I dumped every step name job_steps extracts from every workflow, and every pwsh_steps entry, before and after. The dumps are identical, and all workflow_contract tests pass.

Comment thread xtask/src/workflow_contract.rs Outdated
if line.ends_with("@'") || line.ends_with("@\"") {
in_here_string = true;
}
statements.push(line.to_string());

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Latent gap, not a live bug today: statements aren't split on ;, so a semicolon-chained line like Write-Output 'ok'; sccache --start-server is treated as one statement and the trailing native command's exit status would never be checked. No current workflow line hits this, but worth a follow-up if it's cheap - otherwise just flagging it for awareness.

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.

Fixed in 9ef57a17. Each logical line is now split at top-level ;, but not inside '…', "…", (…), $(…), {…} or @{…}, after a # comment, or when backtick-escaped. Backtick continuations are joined instead of dropped. One-line run: values are checked too, since they can also chain with ;. unchecked_native_commands_splits_statements_at_top_level_semicolons covers your example and the must-not-split cases, and removing any one guard fails it. On the real workflows the unchecked set is unchanged: the same three allow-listed steps. Still out of reach by design: a native command inside a one-line script block such as try { cargo a; cargo b }.

Comment thread xtask/src/workflow_contract.rs Outdated
for raw in body {
let line = raw.trim();
if in_here_string {
in_here_string = !(line.starts_with("'@") || line.starts_with("\"@"));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Latent gap: when a here-string's closing line ('@/"@) has real code appended after it on the same line, that whole line is dropped from analysis rather than just the closing marker. ci.yml's "Add install instructions to job summary" step already has this shape, and it's harmless today only because what gets dropped there is a cmdlet rather than a native command. Worth a narrower check (only the closing marker itself, not the whole line) if you're touching this again.

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.

Fixed in 9ef57a17. Only the opener's own closing marker ends a here-string, so "@ no longer closes @'. The here-string stands in as @'…'@ in the statement that opened it, and the rest of the closing line is read as that statement's tail. So '@ | Out-File x; cargo build yields cargo build. ci.yml's "Add install instructions to job summary" now parses as @'…'@ | Out-File …, still correctly not a native command. Test: unchecked_native_commands_reads_on_past_a_here_string_close. Making it drop the rest of the line, or letting any marker close, fails it.

@rominf

rominf commented Oct 14, 2026

Copy link
Copy Markdown
Collaborator Author

Pushed 378e1f83 and 9ef57a17, addressing all three inline notes:

  • one shared step_key/step_name helper; the step names from every real workflow are identical before and after;
  • statements split at top-level ;;
  • reading continues past a here-string's closing marker.

Each fix has a test, and 9 mutations of the new parsing are each caught. Re-running the contract over all workflows flags nothing new; the unchecked set is still the three allow-listed steps. cargo test -p xtask (251) and clippy -D warnings pass locally.

On the red E2E tests: the failing scenario is chat-06 (chat-tool-definitions-accepted), a timeout on GET /v1/models against the loopback mock. That is the harness problem in #661, which has failed this job on main and on every PR since #546. The fix, #662, is in the merge queue. This PR only touches xtask test code and a workflow comment.

@rominf
rominf dismissed juhovainio’s stale review October 14, 2026 08:44

Addressed on 9ef57a1: 378e1f8 shares one step_key/step_name helper; 9ef57a1 splits at top-level ; and reads past a here-string close. Both are tested and mutation-checked, and each inline thread has a reply. Reviewed commit 7c32a24.

GitHub runs a `shell: pwsh` body under `$ErrorActionPreference = 'stop'`,
which stops on a failing cmdlet but not on a failing native command; the step
then reports only the last command's exit code. #633 split the Windows
acceptance self-tests into one step each for that reason, but nothing stopped
them being merged back.

The new workflow-contract test walks every `shell: pwsh` step in every
workflow and fails on a native command that is neither the body's last
statement nor followed by a `$LASTEXITCODE` check. Three existing steps are
allow-listed with the reason their failures still surface (the sccache probe
decides for itself and ends in `exit 0`; the nightly and release packaging
steps act on `cargo xtask package`'s outputs, which throw when missing), and
a stale allow-list entry fails too.

Signed-off-by: Roman Inflianskas <Roman.Inflianskas@amd.com>
…h_steps

Signed-off-by: Roman Inflianskas <Roman.Inflianskas@amd.com>
…tring close

The pwsh contract treated each line as one statement, so a native command
chained before another with `;` was never checked, and a here-string's
closing line was dropped whole, hiding anything after the `'@`.

Split each logical line at `;`s outside quotes, `(...)`/`{...}` and
comments; join backtick continuations; consume only the closing marker
of a here-string and analyse the rest of its line; and check one-line
`run:` values too, since they can chain with `;` as well.

Signed-off-by: Roman Inflianskas <Roman.Inflianskas@amd.com>
@rominf
rominf force-pushed the test/workflow-pwsh-one-command-per-step branch from 9ef57a1 to c56db1a Compare October 15, 2026 08:35
@rominf

rominf commented Oct 15, 2026

Copy link
Copy Markdown
Collaborator Author

Rebased onto main af315e0a, which brings #454 and #672; the head is now c56db1ad.

@juhovainio juhovainio left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed the workflow-contract test change. Verified locally in a separate worktree: cargo test -p xtask workflow_contract (69/69 passing, including the 3 new tests) and cargo clippy -p xtask --all-targets --locked -- -D warnings (clean). The step_key refactor is behavior-preserving across job_steps and pwsh_steps, and the new tests cover the risk areas well (merge-back-into-one-step masking, top-level ; splitting with false-split traps, here-string open/close/mismatch).

A few non-blocking edge cases for future awareness, none of which are triggered by the current workflow files:

  • unchecked_native_commands only checks the statement immediately after a native command for $LASTEXITCODE; a step that inserts anything in between (even a Write-Output) before the check would false-positive.
  • run: |- / run: > / run: >- block-style keys aren't matched by the run: | detector, so a pwsh step authored that way would silently skip the contract check.
  • The $LASTEXITCODE match is case-sensitive, and PWSH_KEYWORDS is missing a few keywords (break, continue, begin, process, end, param, trap), which could misclassify a native command.

Leaving the nightly/release.yml $LASTEXITCODE cleanup out of scope seems reasonable given it's a release-path change and the current allow-list reasoning is sound and self-verifying.

CI is green on everything relevant (clippy, lint, builds, coverage, sign-off). The two failing checks (MI300X E2E and its downstream consolidated report) are self-hosted runner pool timeouts, unrelated to this change.

@rominf

rominf commented Oct 20, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks. All three edge cases are real, and I will cover them in one follow-up PR once this lands:

  • accept an exit-code check later in the step, not only in the very next statement;
  • match the run: |-, > and >- block forms;
  • match $LASTEXITCODE case-insensitively, and add break, continue, begin, process, end, param and trap to PWSH_KEYWORDS.

Each fix will come with a test that fails without it.

@rominf
rominf added this pull request to the merge queue Oct 20, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 20, 2026
@rominf
rominf added this pull request to the merge queue Oct 21, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 21, 2026
@rominf
rominf added this pull request to the merge queue Oct 21, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 21, 2026
@rominf
rominf added this pull request to the merge queue Oct 21, 2026
@rominf

rominf commented Oct 21, 2026

Copy link
Copy Markdown
Collaborator Author

The merge queue ejected this on dash-gen-tps-expiry-boundary and dash-esc-menu-quit-row-confirms-when-serving (merge-group run 39563547769). These are the intermittent dashboard-quit failures that have also hit #712, #630 and #437. This PR only touches xtask. Re-queued once; a fix for the race is being prepared against main.

Merged via the queue into main with commit acda519 Oct 21, 2026
30 of 32 checks passed
@rominf
rominf deleted the test/workflow-pwsh-one-command-per-step branch October 21, 2026 08:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants