Skip to content

ci: make sample-test Pester install resilient to PSGallery flake - #701

Merged
Nikola Metulev (nmetulev) merged 1 commit into
mainfrom
nmetulev-fix-pester-install-flake
Jul 30, 2026
Merged

ci: make sample-test Pester install resilient to PSGallery flake#701
Nikola Metulev (nmetulev) merged 1 commit into
mainfrom
nmetulev-fix-pester-install-flake

Conversation

@nmetulev

Copy link
Copy Markdown
Member

The flake

The Install Pester step in .github/workflows/test-samples.yml fails intermittently on GitHub-hosted Windows runners:

Missing option value for: '-source'
NuGet.Commands.CommandException: Missing option value for: '-source'
   at NuGet.CommandLine.CommandLineParser.ExtractOptions(...)
Set-PSRepository: No repository with the name 'PSGallery' was found.
##[error]Process completed with exit code 1.

Register-PSRepository -Default hits a transient error inside the bundled NuGet provider, so PSGallery never gets registered. The next line then fails because the repository doesn't exist and the step exits 1 — the sample test never runs.

Evidence it's a flake, not a real break

  • It fails in CI setup, before any repo code executes.
  • Observed on run 30570937690, job winui-app. All 11 other sample matrix jobs passed on the same commit, same runner image, same step.
  • Re-running just that job succeeded with no code change.

Approach

Two changes to the one step (it appears once in the YAML — the matrix duplicates it at runtime, so no composite action is needed):

  1. Check first, install only if missing. Hosted Windows images ship Pester 5.x, so the PSGallery dance is skipped entirely in the common case — which removes the flake's window altogether. Uses the same detection idiom already in scripts/test-samples.ps1 and scripts/build-cli.ps1.
  2. Retry acquisition up to 3 times with backoff (10s, 20s) when Pester genuinely isn't present. Failed attempts surface as ::warning:: so the flake stays visible rather than being silently swallowed.

The failure signal is preserved: if Pester 5.x still isn't available after all attempts, the step Write-Errors and exits 1.

Verification

  • YAML parses cleanly.
  • Extracted the step body and ran it locally across all three paths:
    • Pester already present → short-circuits, Using Pester 5.7.1, exit 0
    • Install fails on attempt 1, succeeds on attempt 2 → warning emitted, exit 0
    • Install fails all 3 attempts → 3 warnings + Write-Error, exit 1

Skip the PSGallery registration entirely when Pester 5.x is already on the
runner image, and retry acquisition up to 3 times with backoff when it is
not. A genuinely missing Pester still fails the job.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5b5228f5-d804-4288-bf04-a00cdfd27560
Copilot AI review requested due to automatic review settings July 30, 2026 19:03

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 Ready to approve

The focused workflow change correctly addresses the verified CI flake without masking persistent failures.

This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.

Pull request overview

Makes sample-test CI resilient to transient PSGallery failures.

Changes:

  • Reuses an available Pester 5.x installation.
  • Retries installation three times with backoff and preserves failure reporting.
File summaries
File Description
.github/workflows/test-samples.yml Adds Pester detection and resilient installation retries.
Review details
  • Files reviewed: 1/1 changed files
  • Comments generated: 0
  • Review effort level: Medium

We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.

@github-actions

Copy link
Copy Markdown
Contributor

Build Metrics Report

Binary Sizes

Artifact Baseline Current Delta
CLI (ARM64) 36.63 MB 36.63 MB ✅ 0.0 KB (0.00%)
CLI (x64) 36.82 MB 36.82 MB ✅ 0.0 KB (0.00%)
MSIX (ARM64) 15.26 MB 15.26 MB 📈 +0.3 KB (+0.00%)
MSIX (x64) 16.20 MB 16.20 MB 📈 +0.6 KB (+0.00%)
NPM Package 31.83 MB 31.83 MB 📈 +0.7 KB (+0.00%)
NuGet Package 31.86 MB 31.86 MB 📈 +0.5 KB (+0.00%)

Test Results

3719 passed, 4 skipped out of 3723 tests in 625.3s (+15.5s vs. baseline)

Test Coverage

94.5% line coverage, 88.6% branch coverage · ✅ no change vs. baseline

CLI Startup Time

42ms median (x64, winapp --version) · ✅ no change vs. baseline

Try This Build

Installs the MSIX for your architecture, replacing any previously installed build. Needs the GitHub CLI — the command offers to install it and sign you in if it is missing.

& ([scriptblock]::Create((irm https://raw.githubusercontent.com/microsoft/winappCli/main/scripts/winapp-pr.ps1))) 701
Switching between builds often?

Put the tool on your PATH once:

& ([scriptblock]::Create((irm https://raw.githubusercontent.com/microsoft/winappCli/main/scripts/winapp-pr.ps1))) -AddToPath

Then this build is just:

winapp-pr 701

Run winapp-pr with no arguments to pick from a list of open PRs.


Updated 2026-07-30 19:24:37 UTC · commit 42c98da · workflow run

@nmetulev
Nikola Metulev (nmetulev) merged commit 0029a76 into main Jul 30, 2026
25 checks passed
@nmetulev
Nikola Metulev (nmetulev) deleted the nmetulev-fix-pester-install-flake branch July 30, 2026 22:06
Nikola Metulev (nmetulev) added a commit that referenced this pull request Jul 30, 2026
Based directly on `main`. Single commit, one file.

Picking up newer commits on the build you are already running was more
work than it should be. You had to remember which PR or branch it came
from — `-Status` would tell you, but that is an extra step — and name it
again. Worse, re-resolving the same target silently uninstalled and
reinstalled an identical package with no indication you were already
current.

## Changes

**`-Update`** installs the newest build for whatever is installed:

```powershell
winapp-pr -Update
```

It resolves by the PR the build came from where there was one. Fork PRs
run in the *base* repo, so a branch name alone can match another fork's
runs — `main` being the obvious case — and picking "the newest run on
that branch" could hand you an unrelated contributor's build. Where only
a branch is known, resolution is pinned to that build's head repository
instead.

Only an explicit `-Repo` overrides the recorded source. `WINAPP_PR_REPO`
is a default for *new* installs, not a redirect for an existing one;
otherwise having it point at the public repo while running a
private-fork build would send `-Update` looking for that branch in the
wrong place.

Updating the tool itself moves from `-Update` to **`-UpdateTool`**.
Updating the *build* is by far the more common intent, so it should own
the obvious name.

**Already-installed builds are no longer reinstalled.** Any resolution
that lands on the run you are already on stops early:

```
   [OK] Run 30544907744  [success]  de22c73
   [OK] Already on this build -- nothing to do.
   Use -Force to reinstall it anyway.
```

This applies to naming a target explicitly too, so `winapp-pr 681` when
you are on 681's newest build is a no-op instead of a ~10 second
uninstall/reinstall cycle.

**The install record is only trusted while it still describes the
installed package.** A package installed by other means —
double-clicking an MSIX, `setup-winapprun.ps1` — used to leave the
record describing a build that was no longer there, which would have let
the no-op above skip a genuinely needed install. The record is now
validated against the live package first:

```
>> Installed winapp package
   winapp-dev 0.5.1.40  [CN=runneradmin]
   [WARN] This package was not installed by winapp-pr; its source is unknown.
```

`-Update` refuses rather than guessing, and points at a concrete
alternative:

```
[ERROR] The installed package (winapp-dev 0.5.1.40) was not installed
by winapp-pr, so there is no way to tell which build it came from.

Name what you want instead, for example:
  winapp-pr jay/find-ui-port
```

The record now tracks architecture as well, so requesting a different
`-Arch` for a run you already have is not mistaken for a no-op. Records
written before this change lack the newer fields and fall back to the
old behaviour, so nobody has to reinstall to get a working record.

**The picker shows build freshness**, distinguishing "you have this"
from "there is something newer":

```
   * installed   ^ newer build available   . current branch

  1. . #702  Add winapp-pr -Update and show build freshness in the picker      nmetulev - 2m
  2. ^ #681  feat: add winapp find-ui — WinUI control & sample search     Jaylyn-Barbee - 4m
  3.   #701  ci: make sample-test Pester install resilient to PSGallery       nmetulev - 2h
```

Freshness is resolved the same way an install would resolve it — the
newest run that actually has an artifact, pinned to the same head
repository — so neither an in-progress build nor another fork's run
produces a phantom `^`. It costs two extra API calls, only for the one
installed branch.

**`-Help`** lists everything. PowerShell's built-in `-?` already covers
the on-disk case, but it throws when the script is run from the web as a
scriptblock:

```
InvalidArgument: Cannot process argument because the value of argument "helpTarget" is null.
```

That is exactly how the documented installer runs it, so `-Help` uses
`Get-Help` when there is a file and falls back to the script's own
comment-based help block otherwise.

## Also carries the #700 review fixes

Those review comments were written after #700 had already merged, so
they never landed. They are folded in here rather than left on a dead
branch:

- The gh readiness check is scoped with `--active --hostname
github.com`. A bare `gh auth status` fails when *any* configured account
on *any* host is stale, so anyone with a second account would have been
pushed into a pointless sign-in.
- When an invalid `GH_TOKEN` or `GITHUB_TOKEN` is the cause, say so
instead of offering to sign in. `gh auth login` refuses to store
credentials while an env token is set, so that prompt could only ever
dead-end.

## Validation

The fork-collision fix was reproduced directly: two `Build and Package`
runs on a branch named `feature` from different head repositories, the
unrelated one newer. Unfiltered resolution selects the stranger's run;
pinned to the head repository it selects the right one.

`-Update` was checked with `WINAPP_PR_REPO` pointing at a different
repo, confirming it stays on the recorded source; with a record carrying
a PR number, confirming it resolves by PR; and with a legacy record
lacking the new fields, confirming the branch path still works. A real
install was run end to end to confirm `Pr`, `HeadRepo`, and `Arch` are
persisted.

Also exercised against real branch builds: `-Update` when current
reports it and exits 0; `-Update -Force` performs a full reinstall;
`-Update` with no record fails cleanly; `-UpdateTool` still targets the
tool. Record validation was covered as a table — matching version and
architecture, mismatched architecture, legacy record, and a version
mismatch standing in for a manual install — and end to end by pointing
the record at a version that is not installed. Picker markers were
checked in all three states, and `-Help` both from a file and through a
scriptblock.

---------

Co-authored-by: Nikola Metulev <711864+nmetulev@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: db5ddd3d-7017-4d4a-9b45-d7f8dc70deaf
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