Add WinGet/Homebrew installer dogfood flows for PR builds#17115
Add WinGet/Homebrew installer dogfood flows for PR builds#17115radical wants to merge 20 commits into
Conversation
|
🚀 Dogfood this PR with:
curl -fsSL https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.sh | bash -s -- 17115Or
iex "& { $(irm https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.ps1) } 17115" |
|
Re-running the failed jobs in the CI workflow for this pull request because 1 job was identified as retry-safe transient failures in the CI run attempt.
Matched test failure patterns (1 test)
|
|
TODO: Make sure to test on azdo |
20d8ea4 to
6fec492
Compare
Stage manifest YAML files before invoking winget, validate the pristine manifest, and serve local PR archives through a loopback HTTP listener so winget can install them with refreshed hashes.\n\nAlso make verification robust when winget reports success before the expected aspire.exe is on PATH, surface useful diagnostics on install failures, and warn when the installed winget version is older than the Mark-of-the-Web fix. Co-authored-by: Copilot <[email protected]>
Teach the local Homebrew dogfood helper to rewrite casks to local PR archive files, verify aspire --version after install, and remove quarantine from local unsigned dogfood binaries before verification.\n\nThis keeps dogfood installs self-contained and fails loudly when the installed CLI cannot be executed. Co-authored-by: Copilot <[email protected]>
Add a reusable GitHub Actions workflow that prepares WinGet manifest and Homebrew cask artifacts from the native CLI archives, uploads those artifacts, and runs package-manager dogfood validation.\n\nMove the shared preparation logic into repo scripts so GitHub Actions and Azure Pipelines use the same generation and validation paths. Co-authored-by: Copilot <[email protected]>
Extend the PR acquisition scripts with WinGet and Homebrew install modes that download the generated installer artifacts plus their native archives, then invoke the bundled dogfood helpers.\n\nThe installer modes also populate the PR NuGet hive while avoiding persistent shell profile changes for temporary PR dogfood installs. Co-authored-by: Copilot <[email protected]>
Add acquisition coverage for installer-mode dispatch, local artifact dogfood helpers, generator output, hash handling, cleanup behavior, and WinGet local archive rewrite semantics.\n\nThe tests catch broken cask or manifest generation before relying on the full installer CI jobs. Co-authored-by: Copilot <[email protected]>
Run the installer artifact smoke-test commands with trace logging and always collect the Aspire CLI log directory into a workflow artifact. The console output usually shows the top-level failure, but the CLI writes the detailed exception and package/bundle diagnostics under .aspire/logs. Uploading those logs makes failures like bundle extraction issues debuggable without rerunning the job. Co-authored-by: Copilot <[email protected]>
WinGet launches Aspire through a Links shim while the real package payload and bundle sidecar live under the Packages directory. Bundle extraction already resolves the launcher target before choosing the extraction root, but layout discovery validated relative to the raw shim path and rejected the freshly extracted bundle. Resolve process-path links during relative layout discovery, then fall back to the raw path if the resolved target does not contain a layout. This keeps custom symlink layouts working while allowing WinGet and Homebrew-style launchers to validate the package-owned bundle layout. Co-authored-by: Copilot <[email protected]>
Move Homebrew cask validation behind a shared script so GitHub Actions and Azure DevOps run the same syntax, style, and audit checks during artifact preparation. PR artifacts are not notarized, so Offline validation rewrites the cask URLs to loopback archive URLs and runs the Homebrew audit with signing disabled while still exercising download and extraction. Full release validation keeps the normal signing audit and writes the summary consumed by Homebrew submission. Also keep the cask itself aligned with Homebrew style by declaring the minimum supported macOS version and using an explicit %Q string for the install-route sidecar. Do not add a zap stanza because the cask should not remove shared ~/.aspire state. Co-authored-by: Copilot <[email protected]>
Remove the Homebrew cask macOS dependency from the generated Aspire cask. The dependency is optional metadata, and GitHub's macOS 15 runner rejected local dogfood installs with "This software does not run on macOS versions other than Monterey." Keeping the cask dependency-free preserves the existing dogfood behavior while the shared cask validation still catches syntax, style, audit, download, and extraction issues. Also suppress benign connection reset traces from the loopback archive server used during offline Homebrew audit validation. Co-authored-by: Copilot <[email protected]>
The `🟣Submit to WinGet` and `🟣Submit PR to Homebrew/homebrew-cask` steps in `release-publish-nuget.yml` previously ran on any branch when `dryRun=false`. 1ES PT's branch validation logs a non-blocking warning when the source branch isn't a production branch, but the submission still goes through — so a misqueued run on a feature, fork, or dogfood branch could open a PR upstream on Homebrew/homebrew-cask or microsoft/winget-pkgs. Add a hard condition on each submit step that only allows execution when `Build.SourceBranch` is `refs/heads/main`, `refs/heads/release/*` or `refs/heads/internal/release/*` — matching the existing "production branch" predicate already used elsewhere in the pipeline YAML (`azure-pipelines.yml` `_RunAsInternalBuild` / `$isReleaseBranch`). On a non-production branch the steps now skip cleanly, with the displayed step name in the run summary making the gating visible. Co-authored-by: Copilot <[email protected]>
Upstream homebrew/cask labels generated PRs with `missing-zap` when no zap stanza is present. Add an explicit `zap trash: []` to silence the label without removing any user files. `~/.aspire` is shared with non-Homebrew Aspire state (PR hives, dev certs, telemetry, runtime sockets, manually installed CLIs) that must outlive `brew uninstall --zap aspire`. Co-authored-by: Copilot <[email protected]>
The set of branches that publish — main, release/*, internal/release/* —
was hard-coded in three places:
* publish-homebrew.yml condition guard
* publish-winget.yml condition guard
* _PackagesPublished in azure-pipelines.yml
Adding or removing a publishing branch required editing all three. The
two publish-template guards in particular drift independently because
they're far from the build-pipeline definition.
Hoist the branch set into _IsProductionBranch in common-variables.yml.
_PackagesPublished now composes it with the existing non-PR / internal
checks, and the publish-template guards reduce to one line:
eq(variables['_IsProductionBranch'], 'true')
release-publish-nuget.yml already imports common-variables.yml, so the
publish templates see the new variable. Cross-file template-variable
references inside ${{ }} expressions already work here — the previous
_PackagesPublished definition read _RunAsPublic and Build.Reason the
same way.
Co-authored-by: Copilot <[email protected]>
prepare-installer-artifacts.yml carried two near-identical inline smoke
tests — one pwsh, one bash — that scaffold an aspire-starter project and
run restore against the just-installed CLI. The two blocks drift
independently and there is no equivalent in AzDO: the only post-install
check inside validate-cask-artifact.sh and prepare-manifest-artifact.ps1
is `aspire --version`. If AzDO ever wants the same coverage there will
be a third copy.
Extract to eng/scripts/smoke-installed-cli.{sh,ps1}. The workflow now
calls them after the installer-specific PATH refresh, which stays inline
because it describes the GitHub Actions session state, not the smoke
logic. AzDO is not adopted in this change.
Co-authored-by: Copilot <[email protected]>
The "Prepare Homebrew cask artifact" job in prepare-installer-artifacts.yml
failed on PRs with:
Determining whether aspire is a new upstream cask...
Error: could not determine whether Casks/a/aspire.rb exists upstream (HTTP 403)
##[error]Process completed with exit code 1.
The probe in validate-cask-artifact.sh::detect_upstream_cask_is_new makes an
unauthenticated GET to api.github.com to classify the cask as new vs. update.
Unauthenticated requests share a 60/hour rate-limit pool across the runner
IP — easy to exhaust on GH-hosted macOS — and return 403 when throttled. The
script treats any non-200/404 as a fatal misclassification (the audit mode
and the eventual bot PR body both key off this signal), so the 403 fails
the whole job.
Send an `Authorization: Bearer <token>` header when GH_TOKEN or GITHUB_TOKEN
is present (auth raises the limit to 1000/h+). When neither is set, fall
back to unauthenticated — preserving prior behavior for callers without a
token in scope.
Pass `secrets.GITHUB_TOKEN` into the Homebrew step via GH_TOKEN env so the
script picks it up. AzDO callers of the same script have no GH token in
scope today and continue running unauthenticated; the limit is per-IP and
the AzDO Microsoft-hosted pool generally has plenty of headroom there.
Failing run for reference:
https://github.com/microsoft/aspire/actions/runs/26195583728/job/77075657321
Co-authored-by: Copilot <[email protected]>
The "Validate installer URLs from manifest", "Validate ReleaseNotesUrl", "Validate Homebrew validation summary", and "Validate download URLs from cask" steps in the publish-winget/publish-homebrew templates run inside the publish job to surface broken artifact URLs or missing prepare-stage checks before submitting an upstream PR. These checks only make sense when we're actually about to submit: - The URL probes hit canonical ci.dot.net paths that exist only after packages have been published on a production release build. On a non-production branch the prerelease artifacts go to different paths (or aren't published to ci.dot.net at all), so the probes fail with 404s before the publish-template's submit-step guard is reached. - The Homebrew validation-summary check requires brewInstall/brewUninstall to be marked 'passed', which only happens when the prepare stage runs with runInstallTest=true -- true only for production-publishing builds. On a non-prod prepare the required checks are absent and the validator fails before our Submit guard runs. Same effect as the existing dryRun=false clause that already gated the Homebrew summary validator: in dry-run we're not submitting, so URL reachability and summary completeness aren't useful signals. Reuse the existing Submit-step condition verbatim (dryRun=false AND _IsProductionBranch=true) so these checks run if and only if we'd actually submit a PR upstream. Observed in dogfood release-publish-nuget run on ankj/homebrew-release: both validators failed -> Submit step never reached -> no upstream PRs opened (which the existing Submit-step guard would have produced anyway), but the spurious red made the dogfood run unreadable. Co-authored-by: Copilot <[email protected]>
e298e86 to
eb507fd
Compare
The PR dogfood scripts only support x64 and arm64:
* eng/scripts/get-aspire-cli-pr.ps1 uses
ValidateSet("", "x64", "arm64") for -Architecture.
* eng/scripts/get-aspire-cli-pr.sh's
get_cli_architecture_from_architecture accepts only amd64/x64
and arm64; anything else hits the "Architecture ... not supported"
error path.
The reference table in docs/dogfooding-pull-requests.md previously
listed `x86` as an allowed --arch / -Architecture value, which
would mislead anyone trying to cross-target it: the scripts would
hard-fail rather than fall back.
Co-authored-by: Copilot <[email protected]>
There was a problem hiding this comment.
Pull request overview
This PR adds end-to-end “dogfood” generation and installation flows for Aspire CLI WinGet manifests and Homebrew casks produced from GitHub Actions PR builds, reusing shared repo scripts across GitHub Actions and Azure DevOps. It also fixes a WinGet first-run failure by updating CLI layout discovery to resolve launcher symlinks/reparse targets consistently.
Changes:
- Add reusable GitHub Actions workflow + jobs to prepare/upload WinGet/Homebrew installer artifacts (and smoke-test the installed CLI).
- Move WinGet/Homebrew artifact preparation and validation into shared
eng/*scripts, and update AzDO templates to call them. - Fix CLI layout discovery for WinGet “Links” shims by resolving process-path symlinks before relative layout probing; add tests and dogfood-mode tests for PR installer scripts.
Reviewed changes
Copilot reviewed 25 out of 25 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/Aspire.Cli.Tests/Layout/LayoutDiscoveryReparsePointTests.cs | Adds coverage for process-path symlink resolution and fallback behavior in layout discovery. |
| tests/Aspire.Acquisition.Tests/Scripts/PRScriptInstallerModeTests.cs | Adds end-to-end tests for PR installer modes (WinGet/Homebrew) and dogfood behaviors. |
| src/Aspire.Cli/Layout/LayoutDiscovery.cs | Updates relative layout discovery to resolve symlink targets first (with fallback) and adds a test hook. |
| eng/winget/README.md | Documents new artifact-preparation and dogfood flows for WinGet. |
| eng/winget/prepare-manifest-artifact.ps1 | New shared script to generate/validate/test WinGet manifests and stage dogfood helper. |
| eng/winget/dogfood.ps1 | Enhances dogfood install to support local archive serving + SHA refresh + improved verification/diagnostics. |
| eng/scripts/smoke-installed-cli.sh | New bash smoke test to validate an already-installed aspire CLI via new + restore. |
| eng/scripts/smoke-installed-cli.ps1 | New PowerShell smoke test to validate an already-installed aspire CLI via new + restore. |
| eng/scripts/README.md | Updates PR dogfood docs to include WinGet/Homebrew install modes. |
| eng/scripts/get-aspire-cli-pr.sh | Adds WinGet/Homebrew installer modes (download + install) for PR artifacts. |
| eng/scripts/get-aspire-cli-pr.ps1 | Adds WinGet/Homebrew installer modes (download + install) for PR artifacts. |
| eng/pipelines/templates/publish-winget.yml | Tightens submission gating to production branches; adds draft conversion and reduces unnecessary steps in non-submit scenarios. |
| eng/pipelines/templates/publish-homebrew.yml | Tightens submission gating to production branches; reuses draft-conversion mutation variable and avoids non-submit failures. |
| eng/pipelines/templates/prepare-winget-manifest.yml | Switches AzDO prepare to call shared WinGet artifact-prep script. |
| eng/pipelines/templates/prepare-homebrew-cask.yml | Switches AzDO prepare to call shared Homebrew artifact-prep script. |
| eng/pipelines/common-variables.yml | Introduces _IsProductionBranch as a single source of truth for “publishing branch” checks. |
| eng/pipelines/azure-pipelines.yml | Updates _PackagesPublished to depend on _IsProductionBranch. |
| eng/homebrew/validate-cask-artifact.sh | New shared validator for cask syntax/style/audit/install (supports offline/local-archive validation). |
| eng/homebrew/README.md | Documents new Homebrew preparation/validation scripts and GitHub Actions prerelease artifact flows. |
| eng/homebrew/prepare-cask-artifact.sh | New shared script to generate + validate cask artifacts and include dogfood helper. |
| eng/homebrew/dogfood.sh | Extends dogfood install to optionally use locally downloaded archives, plus stronger verification/shadow checks. |
| eng/homebrew/aspire.rb.template | Adjusts sidecar write string literal form and adds explicit empty zap to satisfy audit expectations. |
| docs/dogfooding-pull-requests.md | Updates PR dogfooding documentation to include WinGet/Homebrew modes and prerequisites. |
| .github/workflows/tests.yml | Adds macOS x64 archive build and installer-artifact preparation jobs; wires them into overall CI dependencies. |
| .github/workflows/prepare-installer-artifacts.yml | New reusable workflow to prepare, dogfood-install, smoke-test, and upload WinGet/Homebrew installer artifacts. |
…ask audit doc `smoke-installed-cli.sh` and `smoke-installed-cli.ps1` previously wiped a configurable `--work-dir` with `rm -rf` / `Remove-Item -Recurse -Force` to make reruns idempotent. The script is only ever invoked from `prepare-installer-artifacts.yml` with no arguments, so the destructive path only mattered as defense against caller misuse — but a passed-in sensitive path (e.g. `/`, `$HOME`, a drive root) would still be wiped without warning. Restructure both scripts so `--work-dir` / `-WorkDir` is documented as a *parent* directory under which a fresh `aspire-cli-smoke.XXXXXX` subdirectory is created via `mktemp` (or `[guid]::NewGuid()` on PowerShell). The destructive operation is removed entirely; nothing that the caller passes in is ever deleted. CI tears down `RUNNER_TEMP` between jobs, so the lack of an explicit cleanup is fine in CI and friendlier for local interactive use (the scaffolded project survives until the user removes it). Also corrects `eng/homebrew/README.md` to match the actual `validate-cask-artifact.sh` behavior: `brew audit` only passes `--new` when the cask is absent upstream (existing casks fail the additional `--new` checks), and `--no-signing` is added in offline mode (used for PR-artifact validation against unsigned local archives), not unconditionally. The previous wording would have mis-guided anyone reading the doc. Co-authored-by: Copilot <[email protected]>
`Show-LatestWinGetDiagLog` uses `Write-Host` (matching the rest of `eng/winget/dogfood.ps1`, where Write-Host is the prevailing convention), not stderr. The earlier comment claimed the newest diag log was printed to stderr, which was inaccurate and could mislead a future reader trying to capture / filter diagnostic output. Update the comment to match the code: the log is written to the host console via Write-Host, which GitHub Actions and the host capture into the same job log as the surrounding install output. Co-authored-by: Copilot <[email protected]>
…e misleading test Two distinct flakes were observed in `Tests / Cli / Cli (windows-latest)` on PR microsoft#17115 (CI run 26212960557, attempt 1): 1. `ProbeAsync_PeerOmitsPathStatus_DefaultsToNotOnPath` failed with `Expected PeerProbeResult.Ok, got PeerProbeResult.Failed. Reason: Peer produced no usable output (and --version fallback).` Under heavy CI load (the failing run logged 87.5% CPU and saturated disk via the HEARTBEAT line) the cmd.exe-based fake peer script takes longer than the production 5s probe timeout just to spawn, so the probe synthesizes `Failed: timed out after 5.0s` before the peer prints stdout. This is the same class of CI flake that commit 858825b fixed for the negative-path stderr tests via `ProbeFakeFailureAsync`; that fix did not cover the positive-path tests, which still used the default 5s ctor. 2. `ProbeAsync_PeerHangs_TimesOutAndKills` failed with `Expected probe to return within a few seconds after timing out; took 00:00:05.0820126.` The probe configured a 300ms internal timeout and the test asserted the wallclock came back in under 5s. On Windows under load the spawn -> ping -> terminate round trip alone ate 5.08s, exceeding the budget by 80ms. The 5s outer budget is too tight for what the test claims to catch: a probe that ignores its configured timeout would still trip a 15s bound, while the real production timeout path (which is what the `AndKills` suffix was meant to cover) is bounded by the 300ms ctor argument and synthesizes the `Failed` result regardless of the outer wallclock. The `AndKills` part of the name also overstates what the test asserts. The fake peer process cleanup is via `using var fakePeer` disposal, not a direct assertion against the production probe. The process-termination semantic is rigorously covered by `ProbeAsync_CallerCancels_KillsSpawnedProcess` in the same file, which uses a PID file + `Process.HasExited`. Rename to `ProbeAsync_PeerHangs_TimesOutAndReturnsFailed` to match what is actually checked. Changes: - Introduce `CreateProbeWithGenerousTimeout()` helper mirroring the pattern from `ProbeFakeFailureAsync` (30s internal timeout, well above the production 5s default). - Replace all 10 positive-path uses of the default `new PeerInstallProbe(ProbeLogger)` ctor with the helper. The timeout-path tests (`ProbeAsync_PeerHangs_TimesOutAndReturnsFailed` with the 300ms ctor, and `ProbeAsync_CallerCancels_KillsSpawnedProcess` with the 30s ctor) keep their dedicated constructors. - Widen the outer-wallclock budget in `ProbeAsync_PeerHangs_TimesOutAndReturnsFailed` from 5s to 15s, and rename it from the `...AndKills` form that overstated the assertion. - Update the cross-reference comment in `ProbeFakeFailureAsync` to the new test name. Co-authored-by: Copilot <[email protected]>
Description
Adds GitHub Actions generation for Aspire CLI installer artifacts so PR/main/release CI publishes dogfoodable WinGet and Homebrew outputs alongside the existing Azure DevOps prepare stages. The dogfood scripts then know how to consume these artifacts, so anyone can now test the WinGet/Homebrew install path of any open PR locally, before merge — previously this only worked end-to-end against the post-merge AzDO build, so installer breakage was caught after the fact.
Try it out
From any open PR, install the CLI from the actual installer the release pipeline will use:
Archive mode (default) and tool mode work everywhere; WinGet/Homebrew modes require the per-OS installers. Full reference:
docs/dogfooding-pull-requests.md.The implementation shares generation logic between AzDO and GitHub by moving WinGet and Homebrew artifact preparation into repo scripts. The AzDO templates now keep CI-specific setup/publishing while calling those shared scripts, and GitHub CI calls the same reusable installer-artifact workflow for each installer.
GitHub-produced installer artifacts are prerelease artifacts because the GitHub native CLI archives are PR/daily/staging builds, not stable release archives. The artifacts keep the normal
ci.dot.netURL shape while computing hashes from local native archive artifacts.This also fixes a WinGet-installed CLI first-run failure where bundle extraction wrote the package-owned layout under
WinGet\Packages, but layout discovery validated relative to the rawWinGet\Linkslauncher path:Layout discovery now resolves launcher symlinks/reparse targets the same way bundle extraction does, then falls back to the raw launcher path for custom layouts.
Also adds an
osx-x64native CLI archive build so Homebrew cask generation can hash both macOS architectures.Fixes #17305
Validation:
26145296164: WinGet installer job passed and uploadedwinget-manifests-prereleasepluswinget-aspire-cli-logs.26145296164: Homebrew installer job passed and uploadedhomebrew-cask-prereleaseplushomebrew-aspire-cli-logs.eng/winget/prepare-manifest-artifact.ps1with the PowerShell parser.eng/homebrew/prepare-cask-artifact.shwithbash -n.PRScriptInstallerModeTests.LayoutDiscoveryReparsePointTests,BundleServiceComputeDefaultExtractDirTests, andBundleServiceCrossRouteExtractionTests.Checklist
<remarks />and<code />elements on your triple slash comments?