Repository navigation
Rehearse the release weekly from the release pipeline itself - #783
Conversation
ES/1ES policy changes keep surfacing mid-release, which is the most expensive time to find them. This adds a scheduled pipeline that rehearses a release every Monday so a policy or credential problem shows up days before anyone cuts rel/v*. .pipelines/dryrun.yml runs on the 1ES *Unofficial* template since it produces nothing shippable. It does the full stable build with telemetry unstubbed, all test suites, MSIX/NuGet/npm packaging, and the release-only steps CI never touches: release-notes generation (as a hard gate rather than continueOnError), MS Learn doc porting run from a copy of release.yml's mslearn-source file list, and release asset renaming plus zip archiving verified against the names the WinGet manifest URLs hardcode. It signs nothing, creates no GitHub release, and performs no fork operations. Credentials are validated read-only by scripts/check-release-credentials.ps1: PAT scopes and expiry, fork push permission, GitHub Models reachability, ADO service connection readiness, and the symbol publishing federated identity. Also: - Extract the shared agent setup into templates/build-env.yaml so an ES policy change to feeds or auth is one edit instead of three. - Stop echoing generated release notes to stdout. They are built from PR titles and model output, and Azure Pipelines executes any stdout line starting with ##vso[...] as a logging command. The scripts/tests suite runs during the real release build, so the new tests are deliberately offline-only. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: dbafe00e-a7b1-48f9-8065-453bd752ac9b
There was a problem hiding this comment.
Pull request overview
Adds a weekly, non-publishing rehearsal of the release pipeline.
Changes:
- Adds release-fidelity build and credential validation.
- Extracts shared pipeline environment setup.
- Adds asset-staging checks and release-script tests.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
.pipelines/dryrun.yml |
Defines the weekly rehearsal pipeline. |
.pipelines/templates/build-env.yaml |
Centralizes agent and feed setup. |
.pipelines/ci.yml |
Uses shared environment setup. |
.pipelines/release.yml |
Uses shared setup and documents staging parity. |
.pipelines/README.md |
Documents operation, security, and setup. |
AGENTS.md |
Adds pipeline maintenance guidance. |
scripts/check-release-credentials.ps1 |
Validates release credentials and connections. |
scripts/stage-release-assets.ps1 |
Renames and verifies release assets. |
scripts/generate-release-notes.ps1 |
Prevents untrusted notes reaching pipeline stdout. |
scripts/tests/check-release-credentials.Tests.ps1 |
Tests offline credential-check behavior. |
scripts/tests/stage-release-assets.Tests.ps1 |
Tests asset naming and verification. |
Suppressed comments (1)
scripts/check-release-credentials.ps1:294
- The repository probe has the same false-failure path: any non-404 response, including a timeout or GitHub 5xx, is marked FAIL even though it does not prove the fork or token is unusable. Preserve 404 as the definitive failure for these public repositories and classify other request failures as WARN.
if (-not $result.Ok) {
if ($result.StatusCode -eq 404) {
Add-Result -Status 'FAIL' -Check $Label -Detail "'$Repo' was not found, or the token cannot see it. If the fork was deleted, recreate it - wingetcreate and the docs PR both depend on it."
}
else {
Add-Result -Status 'FAIL' -Check $Label -Detail "GET /repos/$Repo failed with status $($result.StatusCode)."
}
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Build Metrics ReportBinary Sizes
Test Results✅ 4619 passed, 5 skipped out of 4624 tests in 643.0s (+0.7s vs. baseline) Test Coverage✅ 89.1% line coverage, 82.5% branch coverage · ✅ no change vs. baseline CLI Startup Time51ms median (x64, Try This BuildInstalls 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))) 783Switching between builds often?Put the tool on your PATH once: & ([scriptblock]::Create((irm https://raw.githubusercontent.com/microsoft/winappCli/main/scripts/winapp-pr.ps1))) -AddToPathThen this build is just: winapp-pr 783Run Updated 2026-09-01 21:33:17 UTC · commit |
1ES release jobs cannot check out the repo, so release.yml inlined its asset renames and the dry run kept a second copy in a script. That is exactly the drift the dry run exists to prevent. YAML `- template:` references are expanded at compile time, so a release job CAN share logic - just as YAML rather than as a .ps1. Move the renames into templates/release-assets.yaml and have both consume it, then keep verification (which only the dry run needs, and which has a checkout) in verify-release-assets.ps1. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: dbafe00e-a7b1-48f9-8065-453bd752ac9b
The dry run was a separate pipeline that reproduced the release's shape. A lookalike can only approximate the thing it is rehearsing, and it needed its own ADO registration, variable groups and service-connection authorization. Instead, run the REAL release every Monday from main with only the final publishing actions omitted: same 1ES Official template, same stages, same jobs, same tasks, same connections. This also closes the SDL gap the separate pipeline had, since it stays on Official rather than dropping to Unofficial. The mode is derived from the branch, never from a parameter. Azure DevOps compiles scheduled AND branch-triggered runs with parameter DEFAULTS, so a dryRun parameter cannot tell a Monday rehearsal from a real release - defaulting it false would publish weekly, defaulting it true would make every release ship nothing. Each gate is a positive test for rel/v*, so an unexpected branch fails closed. Six things are skipped, and nothing else: ESRP signing (DoEsrp ANDed with the branch, since it defaults to true and would otherwise sign), the GitHub release, the symbol stage, the npm stage, the WinGet fork sync and --submit, and the MS Learn push and PR. Where the step is a script rather than a task the gate is a YAML-conditional env var the script branches on, so an absent variable means "do not publish". nuget.org needs no gate: release-nuget.yml only triggers on rel/v*. WinGet rehearses against the last published release, because no release exists yet for the in-flight version and wingetcreate downloads the installers to hash them. Also promote the asset-name check from rehearsal-only to a preflight on every build. A real release could previously publish versioned asset names and silently break the WinGet URLs, which is what #568 was. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: dbafe00e-a7b1-48f9-8065-453bd752ac9b
main reverted the NuGet-publish split (#795), which reintroduced a Release_NuGet stage into release.yml gated only on DoEsrp. DoEsrp defaults to true and a scheduled run compiles with parameter defaults, so merging main would have made the weekly rehearsal push to nuget.org. Gate it on the release branch like every other publishing stage. Also from the review: - Restrict the credentialed rehearsal steps to `main` with a positive condition, instead of "any branch that is not rel/v*". Production tokens were reachable from any branch someone could queue; now an unrelated branch gets the build and asset preflight and nothing else. - Validate WingetPkgsFork through env rather than ADO macro expansion. It was interpolated unquoted into an inline PowerShell body, where a ';' in the value is a statement separator. The MS Learn step already did this correctly; the WinGet step now matches it. - Define WINGET_SUBMIT and MSLEARN_PUBLISH explicitly as 'false' in a rehearsal. Azure injects pipeline variables into the task environment, so an absent variable could be inherited rather than simply meaning "do not publish". - Make the credential check advisory. It ran as a hard gate early in Build, so a dead Models token would abort before the WinGet and MS Learn rehearsals - the stages this exists to run, and where releases have actually broken. - Rehearse WinGet against the release list rather than /releases/latest, which excludes prereleases. Every release here is published as a prerelease, so the rehearsal was validating an ever-staler version and would have 404'd once no stable release remained. - Verify the portable winappcli-<arch>.zip names in Release_GitHub, where they are created. The Build preflight cannot see them, the upload glob is winappcli-*.zip, and their URLs are hardcoded in the WinGet submission - so a rename would have uploaded fine and 404'd later. Cut what the review found unearning: the dead -SkipZip branch (the only caller always passed it), -ExpectedArchitectures, -TreatWarningsAsErrors, the rate-limit check, the product-repo reachability check, and a release-notes gate that could not fire. Fixed stale dryrun.yml references left behind when that pipeline was removed. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: dbafe00e-a7b1-48f9-8065-453bd752ac9b
… into azchohfi-weekly-release-dry-run
scripts/build-cli.ps1 regenerates src/winapp-npm/src/winapp-commands.ts, which bumped its recorded schema version from 0.6.1 to 0.6.3 when I ran the build to validate this branch. That staleness predates this PR and has nothing to do with the release pipeline, so restore main's version rather than carrying an unrelated change. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: dbafe00e-a7b1-48f9-8065-453bd752ac9b
AGENTS.md called the bulk NuGet test failures "a known limitation, not a flaky test", which is wrong and led me to accept 69 failures as environmental. Setting the feed variables it already documents takes the unit suite from 69 failures to 7 - measured, not estimated. Document the remaining 7 precisely: they shell out to `dotnet add package Microsoft.WindowsAppSDK`, which goes through the NuGet client and reads nuget.config rather than WINAPP_NUGET_*, so the feed variables cannot help them. pde-oss_Internal does carry that package, and CI resolves this by copying release-nuget.config over the user-level NuGet.Config - noted, along with the fact that VSS_NUGET_ACCESSTOKEN alone does not authenticate the service index. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: dbafe00e-a7b1-48f9-8065-453bd752ac9b
The previous guidance was wrong in both directions. It called the NuGet test failures "a known limitation, not a flaky test", and it documented only half the setup - so following it left 7 failures that still looked environmental. They were not. With the complete configuration the entire unit suite passes locally on a corp machine: 4621 tests, 0 failures. The missing half is that two different mechanisms resolve packages. NugetService reads the WINAPP_NUGET_* variables, which the file already documented. The end-to-end tests instead shell out to `dotnet add package Microsoft.WindowsAppSDK`, which goes through the NuGet client, reads nuget.config, and ignores those variables entirely - so it needs the internal feed registered as a source plus VSS_NUGET_EXTERNAL_FEED_ENDPOINTS for the Azure Artifacts credential provider. VSS_NUGET_ACCESSTOKEN alone returns 401 on the service index, which is the trap. Registering the source stores no password; the credential provider supplies it at runtime. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: dbafe00e-a7b1-48f9-8065-453bd752ac9b
The previous revision told you to register the internal feed as a global package source. That is a persistent change to a shared dev box to make a test run, and it duplicates a feed URL the repo already stores in .pipelines/release-nuget.config. Redirect APPDATA at a throwaway directory holding that same config instead. NuGet resolves its user-level config from %APPDATA%\NuGet\NuGet.Config, so this swaps the feed list for the session and leaves the real one untouched. Verified: dotnet add package Microsoft.WindowsAppSDK resolves from the internal feed with no global source registered. A repo-local nuget.config cannot work here, which is the non-obvious part: the end-to-end tests run in %TEMP%, outside the repo, so NuGet never walks up to it. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: dbafe00e-a7b1-48f9-8065-453bd752ac9b
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: dbafe00e-a7b1-48f9-8065-453bd752ac9b
A rehearsal that publishes nothing looks like it belongs on Unofficial, and that was the original design, so this will read like a mistake to the next person. It is not: Unofficial enforces a weaker SDL subset, and TSA, CodeQL TSA integration, SBOM and signing validation - Official-only - are exactly the policies that keep changing under us. Rehearsing on Unofficial would be blind to the failures the rehearsal exists to catch. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: dbafe00e-a7b1-48f9-8065-453bd752ac9b
Re-review caught that the previous round's cut removed -SkipZip from verify-release-assets.ps1 but left it in the release.yml call site. The script is an advanced function, so an unknown parameter is a terminating binding error - the ungated Build preflight would have failed EVERY run, including a real rel/v* release, before any artifact shipped. Reproduced locally (exit 1), fixed by deleting the argument. Two safety mechanisms were also documented as working when they were not: - `Generate Release Notes` was made a hard gate on the rehearsal branch to surface a dead token. It cannot: generate-release-notes.ps1 catches its own API and model failures and falls back to a git-log changelog, exiting 0 either way. Restored unconditional continueOnError and corrected the claim; the Models probe in check-release-credentials.ps1 is what actually detects this. - The credential check ran with continueOnError, so a FAIL only rendered the run orange with no watcher - and phase 2, the thing that would read it, is explicitly unbuilt. Moved it into a trailing Verify_Release_Credentials stage that depends on the rehearsal stages, so a dead credential turns the run red without costing the WinGet and MS Learn coverage. Also gate Release_WinGet and Release_MSLearn on rel/v* or main. Both hand GITHUB_TOKEN_2 to scripts read off the source branch, and the WINGET_SUBMIT / MSLEARN_PUBLISH gates stop the publishing but not the token exposure - so an unrelated branch someone queued could read the PAT. The README claimed this was already handled; now it is, and the docs say plainly that it is defense in depth rather than a boundary. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: dbafe00e-a7b1-48f9-8065-453bd752ac9b
…r pass Copilot review flagged that GitHub Models was fully retired on 2026-07-30; a live probe confirms 410 Gone for every token. The check I added would therefore have failed the weekly rehearsal every single week, forever - and I had just moved it into a stage without continueOnError, so the Monday build would have been permanently red for a reason nobody could fix. That is precisely the alert fatigue this pipeline exists to avoid. Remove the probe and its parameters, and document that AI release notes are permanently dead and every release now ships the git-log fallback. Also stop classifying transport failures as credential failures. Any non-401 response from GET /user was reported FAIL, so a DNS blip, proxy hiccup or GitHub 5xx would send someone rotating a live PAT. Those are now WARN and say plainly that the result is inconclusive; 403 is called out separately as likely SSO de-authorization. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: dbafe00e-a7b1-48f9-8065-453bd752ac9b
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 11 out of 11 changed files in this pull request and generated 4 comments.
Suppressed comments (1)
scripts/check-release-credentials.ps1:192
- A transient GitHub failure is classified as a definitive credential failure. For example, a simulated network outage produces status
0, adds[FAIL] GitHub token, and makes the final rehearsal stage fail, although the script’s contract says an inconclusive probe isWARN. ReserveFAILfor the explicit 401 branch and warn for other statuses so GitHub outages do not create false credential alarms.
$hasRepoWrite = ($scopeList -contains 'repo') -or ($scopeList -contains 'public_repo')
if (-not $hasRepoWrite) {
Add-Result -Status 'FAIL' -Check 'GitHub token scopes' -Detail "Neither 'repo' nor 'public_repo' is granted. WinGet submission and the MS Learn docs PR both push branches and will fail."
…vs-failed split Copilot review caught that #795's revert dropped useDotNetTask: false when folding Release_NuGet back into release.yml. The deleted pipeline carried it deliberately - DotNetCoreCLI cannot consume the encrypted API key from the NuGet-WinAppCLI service connection - so the nuget.org push would fail to authenticate on the next real release. Restored along with the retry it also lost. Two fixes from the previous round turned out not to have applied; the string replaces silently missed. Test-RepoAccess still reported every non-404 as FAIL, and the on-demand docs still claimed any non-rel/v* branch gives a rehearsal, which stopped being true when Release_WinGet, Release_MSLearn and Verify_Release_Credentials were restricted to main. Both are now correct, and the docs say plainly that a green feature-branch run is not evidence the release path works. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: dbafe00e-a7b1-48f9-8065-453bd752ac9b
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 11 out of 11 changed files in this pull request and generated 2 comments.
Suppressed comments (2)
Previously missed (1) — in code that hasn't changed since the last review.
scripts/verify-release-assets.ps1:82
- This does not verify the NuGet asset's expected name; any unversioned package passes. For example, a staging set containing only
Wrong.Package.nupkgprints “All expected release assets are present,” so a PackageId/naming regression reaches the release stages undetected. Require exactlyMicrosoft.Windows.SDK.BuildTools.WinApp.nupkgand update the fixture to use that real contract name.
$nuget = @(Get-AssetNames -Sub 'nuget-packages' -Filter '*.nupkg')
if (-not $nuget) {
$errors.Add('No .nupkg assets were produced.')
}
.pipelines/release.yml:813
- The rehearsal skips the fork sync above but still clones the fork, so it can validate a stale docs tree. A real release syncs first and then runs against current upstream content; clone
$docsUpstreamon the rehearsal path so Monday's run exercises the same input without mutating the fork.
# Clone the fork
Write-Host "Cloning docs repo fork..."
$docsDir = "$tempDir\windows-dev-docs-pr"
& $ghExe repo clone $docsFork $docsDir -- --depth 1
…tion failure GitHub returns 403 for both authorization failures and rate limiting, so the status alone is not a verdict. Throttling the pipeline PAT would have been reported as a dead credential and sent someone rotating a working token. Distinguish them by the x-ratelimit-remaining and retry-after headers. That needed two supporting fixes: Invoke-GitHubApi discarded headers on the error path, so the rate-limit signal never reached the caller, and Get-HeaderValue assumed the IDictionary shape from a successful response - an exception's HttpResponseHeaders has no .Keys and would have thrown under StrictMode. Also drop a stale comment pointing at the GitHub Models probe removed in the previous commit. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: dbafe00e-a7b1-48f9-8065-453bd752ac9b
An authenticated releases list includes drafts, and the rehearsal took the first entry. A maintainer creating a draft after the last prerelease would make the Monday run pick that draft tag, whose assets are not downloadable, so wingetcreate would fail for a reason unrelated to anything the rehearsal is meant to detect. Filter out drafts and take the first real release, keeping prereleases eligible - this pipeline publishes every release as a prerelease, which is why /releases/latest cannot be used here. Verified against the live API: returns v0.6.1, the actual newest release, where /releases/latest returns the two-versions-stale v0.6.0. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: dbafe00e-a7b1-48f9-8065-453bd752ac9b
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 11 out of 11 changed files in this pull request and generated no new comments.
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
.pipelines/release.yml:806
- The rehearsal skips syncing the fork but then clones that fork, so it validates whatever stale
mainthe fork last had rather than this week's upstream docs. For example, an upstream change after the last real release is absent from Monday's rehearsal, yet the next real release syncs it and can fail. Keep the production sync, but clone$docsUpstreamon the non-publishing path so the rehearsal uses current docs without mutating the fork.
# Sync the fork with upstream. Skipped in a rehearsal: it mutates the fork, and
# nothing gets pushed anyway. Push access is asserted by
# check-release-credentials.ps1 in Build, which checks the permission rather than
# just reachability.
if ($env:MSLEARN_PUBLISH -eq 'true') {
… cannot see them (#850) ## What happened The first real weekly rehearsal ([build 20260914.1](https://microsoft.visualstudio.com/pde-oss/_build/results?buildId=157509154)) failed with four verdicts like: ``` [FAIL] Service connection 'github-service-connection': Not found in project 'pde-oss'. ``` **All four connections exist and are ready.** Verified against the same API with an interactive identity: | Connection | count | type | isReady | |---|---|---|---| | `github-service-connection` | 1 | github | True | | `WinSDKCLI - ESRP Code Signing MI` | 1 | azurerm | True | | `WinAppCLI - Symbol Publishing` | 1 | azurerm | True | | `NuGet-WinAppCLI` | 1 | externalnugetfeed | True | ## Root cause Azure DevOps does **not** return 401/403 when the caller lacks endpoint read permission. It returns **HTTP 200 with an empty list** — byte-identical to "this connection does not exist". The build service identity has no such permission by default. `check-release-credentials.ps1` treated an empty result as definitive absence, so it accused four healthy connections and failed the run. This is the same definitive-vs-inconclusive inversion review already caught twice on the `/user` and repository probes in #783. I didn't apply the lesson to this third call site. ## Fix Probe visibility before trusting an empty result: - **Can enumerate any endpoint** → visibility works, so a missing name is meaningful and still `FAIL`s. - **Can enumerate none** → one inconclusive `WARN` naming the permission to grant, instead of one false `FAIL` per connection. Verified both paths against the live API: ``` CASE A (can enumerate): [PASS] github-service-connection ... [PASS] NuGet-WinAppCLI ... [PASS] WinAppCLI - Symbol Publishing [FAIL] this-does-not-exist: Not found in project 'pde-oss'. ← still has teeth CASE B (blind identity): [WARN] Service connections: The build identity cannot enumerate service endpoints ... Grant the build service 'Read' on the service connections to enable this check. ← one WARN, not four FAILs ``` ## Also - **Regression test** using an unroutable address (`127.0.0.1:1`), so it stays offline — `build-cli.ps1` runs `scripts/tests` during a real release build, and a test that reached the network could let an outage block a release. - **`.pipelines/README.md`** documents the required grant (connection → Security → add `<project> Build Service` as Reader) and explains why the empty-list behavior makes it non-obvious. 86 Pester tests pass; YAML validates. ## Worth noting: the rest of the rehearsal worked Every other stage succeeded in rehearsal mode — `Build`, `Create GitHub Release`, `Create WinGet Release`, `Update MS Learn Docs` — and nothing was published. The design held up; only this one check was wrong. It also surfaced a **genuine** finding that needs separate action: `GITHUB_TOKEN_2` expires **2026-09-17**, and the run flagged it with 3 days' notice. That is exactly what the check was built for. --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: dbafe00e-a7b1-48f9-8065-453bd752ac9b
Why
ES/1ES policy changes keep surfacing mid-release — the most expensive possible moment. The release stalls, someone edits the pipeline under time pressure, and we ship late or ship twice.
This makes
release.ymlrehearse a release every Monday frommain, so a policy or credential failure shows up days before anyone cutsrel/v*.How
There is no separate dry-run pipeline. The real release pipeline runs weekly with only the final publishing actions omitted — same 1ES Official template, same stages, same jobs, same tasks, same service connections. A lookalike pipeline could only ever approximate those.
The mode is derived from the branch, never from a parameter. This is the load-bearing detail: Azure DevOps compiles both scheduled and branch-triggered runs with the default values of
parameters:. AdryRunparameter therefore cannot distinguish a Monday rehearsal from a real release — defaulting itfalsemakes the weekly run publish for real; defaulting ittruemakes every real release silently ship nothing.Every gate is a positive test for
rel/v*, so an unexpected branch fails closed. Greprefs/heads/rel/v.What the rehearsal skips
DoEsrpANDed with the branch${{ if }}--submitWINGET_SUBMITenv,'false'unlessrel/v*MSLEARN_PUBLISHenv,'false'unlessrel/v*Everything else runs for real: the full stable build with telemetry unstubbed, all tests, packaging, release-notes generation, the real
Release_GitHubjob up to the upload, WinGet manifest generation (installers downloaded and hashed, stopping at--submit), and MS Learn porting/validation/commit (stopping at the push).Review findings addressed
A full multi-dimension review ran on this branch. The headline catch came from the different-model cross-check:
Critical —
mainmoved under this branch. #795 reverted the NuGet-publish split, putting aRelease_NuGet+1ES.PublishNuget@1stage back intorelease.ymlgated only onDoEsrp. SinceDoEsrpdefaults totrueand scheduled runs compile with defaults, mergingmainwould have made the weekly rehearsal push to nuget.org. Now branch-gated like every other publishing stage.Also fixed:
mainwith a positive condition, rather than "any branch that isn'trel/v*". Production tokens were reachable from any branch someone could queue.WingetPkgsForkvalidated via env instead of ADO macro expansion — it was interpolated unquoted into an inline PowerShell body, where;is a statement separator.WINGET_SUBMIT/MSLEARN_PUBLISHdefined explicitly as'false'in a rehearsal. Azure injects pipeline variables into the task environment, so an absent variable could be inherited rather than meaning "do not publish".continueOnError). It was a hard gate early inBuild, so a dead Models token would abort before the WinGet and MS Learn rehearsals — the stages this exists to run./releases/latest, which excludes prereleases. Every release here ships as a prerelease, so it was validating an ever-staler version.winappcli-<arch>.zipnames verified inRelease_GitHub, where they're created. The upload glob iswinappcli-*.zipand their URLs are hardcoded in the WinGet submission, so a rename would have uploaded fine and 404'd later.Plus a cut list, removing ~90 lines the review found weren't earning their keep: a dead
-SkipZipbranch (the only caller always passed it),-ExpectedArchitectures,-TreatWarningsAsErrors, the rate-limit check, the redundant product-repo check, and a release-notes gate that structurally could not fire.Separately,
generate-release-notes.ps1no longer echoes its output to stdout — that text is built from PR titles and model output, and Azure Pipelines executes any##vso[...]line, so a crafted PR title could set pipeline variables.AGENTS.md: the corp-machine test guidance was wrong
Unrelated to the pipeline but found while validating this branch. AGENTS.md called the local NuGet test failures "a known limitation, not a flaky test" — so I initially wrote off 69 failing tests as environmental.
They weren't. With the right configuration the full suite passes: 4621 tests, 0 failures.
The reason it was easy to get wrong is that two different mechanisms resolve packages:
NugetServicereads theWINAPP_NUGET_*variables AGENTS.md already documented → fixed 62.dotnet add package, which goes through the NuGet client, readsnuget.config, and ignores those variables entirely → the last 7. Those needVSS_NUGET_EXTERNAL_FEED_ENDPOINTS;VSS_NUGET_ACCESSTOKENalone returns 401 on the service index.A repo-local
nuget.configcan't help, because those tests run in%TEMP%outside the repo. AGENTS.md now documents a session-scoped fix (redirectAPPDATAat a temp dir holding.pipelines/release-nuget.config) and explicitly says not to register a global package source.Validation
build-cli.ps1runsscripts/testsduring a real release build, so a test reachingapi.github.comwould let a GitHub outage block a release.Setup required before this does anything
No new pipeline is needed — the schedule runs on the existing WinDevCLI - Release definition, which already has its variable groups and connections authorized. Two things to check in the ADO UI, both documented in
.pipelines/README.md:refs/heads/rel/v*andrefs/heads/main— amain-only filter would block real releases.Known limitations (documented, not papered over)
Signing is never exercised in a rehearsal, so an ESRP-side break still surfaces only during a real release. Service-connection checks prove authorization, not that the stored secret works. The rehearsal validates the publish steps' preconditions, not the final API calls.
Phase 2 — a scheduled agent prompt that reads each weekly run and posts a summary to the team — is intentionally a follow-up, to be written once there are real runs to summarize.