Skip to content

fix: clean-machine bootstrap (analyzer pack path) + winget auto-install + CI workflow#430

Merged
codemonkeychris merged 8 commits into
mainfrom
fix/clean-machine-bootstrap
May 27, 2026
Merged

fix: clean-machine bootstrap (analyzer pack path) + winget auto-install + CI workflow#430
codemonkeychris merged 8 commits into
mainfrom
fix/clean-machine-bootstrap

Conversation

@codemonkeychris
Copy link
Copy Markdown
Collaborator

Summary

User-reported failure on a freshly-cloned machine: mur pack-local blew up with NuGet.Build.Tasks.Pack.targets(222,5): error Could not find a part of the path '...\src\Reactor.Analyzers\bin\Debug\netstandard2.0\'. This PR fixes the underlying packaging bug, hardens bootstrap.ps1 so missing prerequisites get winget-installed instead of just being reported, and adds a CI workflow that would have caught the regression.

Three commits, three concerns

  1. fix(pack): analyzer DLL path on clean machinessrc/Reactor/Reactor.csproj packs the analyzer + localization-generator DLLs via <None Include="...\bin\$(Configuration)\netstandard2.0\..." /> (no $(Platform) segment). Both projects are <ProjectReference>s, so when dotnet pack -p:Platform=x64 runs, the inherited Platform cascades to the transitive analyzer build and the output lands in bin\x64\Debug\netstandard2.0\ instead. Pack reads the platform-less path, doesn't find it, dies. Masked on dev boxes by any prior AnyCPU build leaving a stale DLL behind. Fix: set <AppendPlatformToOutputPath>false</AppendPlatformToOutputPath> on both analyzer csprojs — they're netstandard2.0 IL with no platform-specific code, so output should always land at bin\$(Configuration)\netstandard2.0\ regardless of inherited Platform. Reproduced + verified locally on a wiped bin/obj tree.

  2. feat(bootstrap): winget auto-install for .NET 10 + optional WinAppSDK prompt

    • Install-WithWinget helper: runs winget with --silent --disable-interactivity --accept-source-agreements --accept-package-agreements, treats already-installed exit -1978335189 as success, and rebuilds $env:Path from Machine + User registry strings so freshly-installed binaries resolve in the same shell.
    • .NET 10 SDK missing → dump installed-SDK list, winget install Microsoft.DotNet.SDK.10, re-probe. Hard-fail if winget itself isn't on PATH (App Installer is an OS-level prereq this script doesn't try to repair).
    • Windows App SDK is tri-state via -InstallWinAppSdk. Unspecified → prompt the dev (default no) since the framework defaults to WindowsAppSDKSelfContained=true and the machine runtime is only needed for framework-dependent deployment. -InstallWinAppSdk → force-install non-interactively (for CI / fresh-dev-box automation). -InstallWinAppSdk:$false → skip silently. Preserves the existing self-contained happy path (no surprise 50MB install on every clean run) while making framework-dependent deployment one keystroke away.
  3. ci: bootstrap.yml — exercise bootstrap.ps1 end-to-end on a clean runner — new Windows-only workflow scoped to PRs touching bootstrap.ps1, the CLI source, the templates project, Directory.Build.props, or the workflow itself. Pipeline: pre-flight diagnostic dump → bootstrap.ps1 non-interactively → mur --version from a fresh shell → mur doctor → verify local-nupkgs/ content → dotnet new list reactorapp → scaffold + restore + build a TestApp → mur upgrade --skip-plugin → re-run bootstrap.ps1 for idempotence. Deliberately omits actions/setup-dotnet so bootstrap.ps1 itself is responsible for the SDK install path.

Test plan

  • Reproduced the clean-machine failure locally by wiping src/Reactor.Analyzers/{bin,obj} and src/Reactor.Localization.Generator/{bin,obj} and src/Reactor/{bin,obj}mur pack-local failed the same way as in the user's screenshot before the csproj fix, succeeded after.
  • Verified post-fix DLL layout: both analyzer DLLs land at bin/Debug/netstandard2.0/Reactor.Analyzers.dll (no x64/ARM64 segment).
  • Watch the new bootstrap.yml workflow fire on this PR and confirm green end-to-end (will show in CI; please block merge until that goes green).
  • x64 / framework-dependent deployment paths: not exercised in this PR (the workflow does -c Release build only). Worth adding dotnet publish -p:WindowsAppSDKSelfContained=false if we want the runtime-install path also gated.

🤖 Generated with Claude Code

codemonkeychris and others added 3 commits May 27, 2026 09:21
`mur pack-local` was failing on a freshly-cloned machine with:
  NuGet.Build.Tasks.Pack.targets(222,5): error
  Could not find a part of the path
    '...\src\Reactor.Analyzers\bin\Debug\netstandard2.0\'

Root cause: Reactor.csproj packs the analyzer + localization-generator
DLLs into the framework nupkg via `<None Include>` paths that hardcode
`bin\$(Configuration)\netstandard2.0\` — no `$(Platform)` segment. The
two projects are also `<ProjectReference>`s, so when `dotnet pack` is
invoked with `-p:Platform=x64` (or ARM64) the inherited Platform
cascades to the transitive analyzer build and the output actually
lands at `bin\x64\Debug\netstandard2.0\`. Pack then looks at the
platform-less path, doesn't find the file, and fails.

The bug was masked on dev machines by any prior AnyCPU build (VS
solution, bare `dotnet build src/Reactor.Analyzers`, etc.) that left
a stale DLL at the platform-less path.

Fix: set `AppendPlatformToOutputPath=false` on both analyzer csprojs.
These are netstandard2.0 IL with no platform-specific code, so output
should always land at `bin\$(Configuration)\netstandard2.0\` regardless
of what Platform the parent pack invocation inherited.

Reproduced + verified locally with a wiped bin/obj tree.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… prompt

Reduces clean-machine friction. Instead of failing with a doc link when a
prerequisite is missing, bootstrap.ps1 now drives a `winget install`
itself (hard-failing if winget itself isn't on PATH).

- New `Install-WithWinget` helper: single point for winget calls. Runs
  with `--silent --disable-interactivity --accept-source-agreements
  --accept-package-agreements`, treats "already installed" exit
  -1978335189 as success, and refreshes `$env:Path` from Machine + User
  registry strings so freshly-installed binaries resolve in this same
  shell (Path edits made by winget don't otherwise propagate to a
  running process).
- .NET 10 SDK: new `Test-DotnetSdk10` probe. When the SDK is missing or
  pre-10, dump the installed-SDK list (for debugging) and call
  `Install-WithWinget Microsoft.DotNet.SDK.10`. Re-probe after install
  and fail with a "open a new shell" hint if it still isn't visible.
- Windows App SDK: tri-state `-InstallWinAppSdk` parameter.
  Unspecified → prompt the dev (default no) since the framework defaults
  to WindowsAppSDKSelfContained=true and the runtime is only needed for
  framework-dependent deployment. `-InstallWinAppSdk` → force install
  non-interactively (use this from CI / fresh-dev-box automation).
  `-InstallWinAppSdk:$false` → skip the prompt silently.

The prompt-by-default for WinAppSDK preserves the existing
self-contained happy path (no surprise 50MB install) while making
framework-dependent deployment one keystroke away.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a Windows-only workflow that would have caught the clean-machine
pack-local regression fixed in a863f87. Runs on PRs that touch
bootstrap.ps1, the CLI source, the templates project, or
Directory.Build.props.

Pipeline (single job, ~25 min):
  1. Pre-flight diagnostic dump of dotnet/winget/WindowsAppRuntime state
     so failures are debuggable from the logs alone.
  2. `./bootstrap.ps1 -InstallWinAppSdk -SkipPlugin` non-interactively.
     Exercises both winget install paths when the runner image is bare.
  3. From a fresh-shell step, `mur --version` resolves on PATH (proves
     the global-tool install propagated to User PATH).
  4. `mur doctor` exits 0 with all checks PASS (plugin shows `info`
     when skipped, not FAIL).
  5. Verify `local-nupkgs/Microsoft.UI.Reactor.0.0.0-local.nupkg`,
     `Microsoft.UI.Reactor.ProjectTemplates.0.0.0-local.nupkg`, and any
     `Microsoft.UI.Reactor.Cli.*.nupkg` are produced.
  6. `dotnet new list reactorapp` finds the template.
  7. `dotnet new reactorapp -n TestApp` + restore + build -c Release.
  8. `mur upgrade --skip-plugin` succeeds against the bootstrapped tree.
  9. Re-run bootstrap.ps1 to validate idempotence (winget already-
     installed exit code handled, dotnet tool update is a no-op,
     dotnet new uninstall+install handles the template-engine cache).

Deliberately omits actions/setup-dotnet so bootstrap.ps1 itself is
responsible for the SDK install. If windows-latest already ships .NET
10, the install branch silently no-ops and the rest of the pipeline
still runs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@codemonkeychris codemonkeychris force-pushed the fix/clean-machine-bootstrap branch from d09b6a1 to 831d65c Compare May 27, 2026 16:26
@codemonkeychris
Copy link
Copy Markdown
Collaborator Author

Rebased onto current main (dac72fe3) to pick up @clintrutkas's #429 (Improving onboarding) which landed earlier today and touched the same two files.

Conflict resolution:

  • bootstrap.ps1Improving onboarding #429's inline winget install Microsoft.DotNet.SDK.10 + 2-line PATH refresh is a strict subset of this branch's Install-WithWinget helper + Test-DotnetSdk10 probe (which additionally handles the "dotnet present but pre-10" case and centralizes the Machine+User PATH rebuild for all winget calls). Resolved by keeping this branch's version.
  • README.mdImproving onboarding #429's & (Get-Process -Id $PID).Path -ExecutionPolicy Bypass -File .\bootstrap.ps1 invocation was preserved (this branch doesn't touch README, so it fast-forwarded cleanly).

bootstrap.ps1 parse-checked OK after resolution. Force-pushed.

The new bootstrap.ps1 (windows-latest) workflow (run 26524266103) is firing on this PR — which validates both the original pack fix (the analyzer DLL would land in the wrong directory on the clean runner without the csproj change) and the bootstrap auto-install path (windows-latest doesn't have WindowsAppRuntime 2.0 preinstalled). Will report back when it completes.

The first bootstrap.yml run failed in 23s, at the pre-flight diagnostic:

    The `msstore` source requires that you view the following agreements
    Do you agree to all the source agreements terms? [Y] Yes  [N] No:
      An unexpected error occurred while executing the command:
      0x8a150042 : Error reading input in prompt
    exit=-1978335166

`winget list` and `winget install` both gate on msstore source
acceptance the first time winget runs against an account/runner. On
non-interactive shells (GH Actions, CI in general) the prompt fails
with exit -1978335166, which pwsh's "propagate $LASTEXITCODE on the
final command" behavior then surfaces as the step exit.

Two places fixed:

- bootstrap.ps1 `Test-WindowsAppRuntime20`: add
  `--accept-source-agreements` to the existence probe, and reset
  $LASTEXITCODE on the way out so a "not installed" status doesn't
  leak past the probe into surrounding script logic.
- .github/workflows/bootstrap.yml pre-flight diagnostic step: same
  --accept-source-agreements flag on the `winget list`, plus a
  `$global:LASTEXITCODE = 0` at the end of the step so the diagnostic
  dump can't fail the workflow before bootstrap.ps1 even starts.

`Install-WithWinget` already passes --accept-source-agreements on the
install command, so that path was correct.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Fixes a clean-machine mur pack-local failure by stabilizing analyzer/source-generator output paths, improves bootstrap.ps1 to auto-install prerequisites via winget (with an optional Windows App Runtime install), and adds a GitHub Actions workflow to validate the end-to-end bootstrap flow on Windows runners.

Changes:

  • Pin analyzer/source-generator build output paths to exclude inherited Platform (prevents pack from looking for non-existent DLL paths on clean trees).
  • Enhance bootstrap.ps1 with Install-WithWinget, .NET 10 SDK detection/auto-install, and a tri-state -InstallWinAppSdk experience.
  • Add .github/workflows/bootstrap.yml to run bootstrap + template scaffolding + idempotence checks on windows-latest.
Show a summary per file
File Description
src/Reactor.Analyzers/Reactor.Analyzers.csproj Disables platform-suffixed output paths so packing can find analyzer DLLs reliably.
src/Reactor.Localization.Generator/Reactor.Localization.Generator.csproj Same output-path stabilization for the source generator DLL.
bootstrap.ps1 Adds winget-based installs, .NET 10 probing, and optional Windows App Runtime install prompting/forcing.
.github/workflows/bootstrap.yml New workflow that runs bootstrap end-to-end and validates template/tooling outputs.

Copilot's findings

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 4/4 changed files
  • Comments generated: 2

Comment thread bootstrap.ps1
Comment thread .github/workflows/bootstrap.yml
codemonkeychris and others added 4 commits May 27, 2026 09:39
`[Nullable[bool]]$Foo = $null` doesn't get PowerShell's switch-style
parameter binding — passing a bare `-InstallWinAppSdk` errors with
"Missing an argument for parameter 'InstallWinAppSdk'." which is what
the bootstrap.yml run hit. Tri-state via nullable bool would have
required every caller (including the .EXAMPLE block in the doc
header) to write `-InstallWinAppSdk:$true`, which is a footgun.

Refactor to two mutually-exclusive [switch] parameters:

  -InstallWinAppSdk   force install non-interactively (CI / automation)
  -NoWinAppSdk        skip the prompt silently
  (neither)           prompt the user (default no)

Mutual exclusion checked at the top of the script (early exit with a
clear error before any side effects). The "default no, prompt"
behavior is preserved for interactive dev runs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`dotnet new list reactorapp` writes a multi-line table; pwsh stores
the output as a [string[]]. PowerShell's -match/-notmatch on an array
returns the matching/non-matching elements rather than a single bool
— so `$listing -notmatch 'reactorapp'` returned the header + separator
lines (which don't contain "reactorapp"), evaluating truthy and
throwing the "template not found" error even though the template
WAS registered.

Join the array first so the comparison is a single substring check.

(Caught by run 3 of bootstrap.ps1 (windows-latest): bootstrap.ps1
itself + mur doctor + nupkg verification all PASS, just the workflow's
own validation logic was bugged.)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Scaffolded apps inherit Directory.Build.props:
  <WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>

The SelfContained target rejects AnyCPU (it needs a concrete arch to
embed the right runtime), so `dotnet build TestApp` with no `-p:Platform`
fails on a fresh clone:

  Microsoft.WindowsAppSDK.SelfContained.targets(74,9): error :
    WindowsAppSDKSelfContained requires a supported Windows architecture.

Detect $env:PROCESSOR_ARCHITECTURE and pass it through. This is the
same wart the user flagged earlier ("we don't want to REQUIRE devs to
do self contained") — addressing that properly means changing the
default in Directory.Build.props or in the template, which is bigger
than this PR's scope. For now, unblock CI.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…d path

Backs out the AppendPlatformToOutputPath=false approach (which broke
unit tests with CS2012 file-lock contention on
Reactor.Localization.Generator/obj/Debug/netstandard2.0/...) in favor
of teaching Reactor.csproj's <None Include> globs about the inherited-
Platform layout.

The cause of the original clean-machine pack failure is unchanged:
when mur pack-local runs `dotnet pack -p:Platform=x64` (or ARM64), the
inherited Platform cascades to the transitive Reactor.Analyzers and
Reactor.Localization.Generator builds, which then land at
bin\<Platform>\<Configuration>\netstandard2.0\. But Reactor.csproj's
pack glob hardcoded bin\<Configuration>\netstandard2.0\ (no Platform
segment), so pack couldn't find the file on a freshly-cloned machine.

Pinning AppendPlatformToOutputPath=false on the analyzers fixed
pack-local but removed bin/obj segregation across Platform values,
which let two concurrent transitive compiles (e.g. dotnet test +
ProjectReference deduplication race) collide on the same
obj\Debug\netstandard2.0\ output path. CI Unit Tests caught this with:

  CSC : error CS2012: Cannot open '...obj\Debug\netstandard2.0\
    Reactor.Localization.Generator.dll' for writing -- The process
    cannot access the file ... because it is being used by another
    process.

New approach: Reactor.csproj computes
$(_ReactorAnalyzerBinDir) = bin\<Configuration>\netstandard2.0
                            when Platform is empty or AnyCPU, OR
                          = bin\<Platform>\<Configuration>\netstandard2.0
                            when Platform is x64 / ARM64.

Pack now finds the analyzers in either layout. The analyzer csprojs
go back to their stock SDK behavior, preserving per-Platform obj/bin
segregation that prevents the build-graph race.

Verified locally:
- mur pack-local from a wiped bin/obj tree succeeds on ARM64 host
  (analyzers land in bin\ARM64\Debug\, Reactor.csproj finds them).
- Plain dotnet build src/Reactor (no -p:Platform) would land
  analyzers at bin\Debug\, also found by the conditional binding.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@codemonkeychris codemonkeychris merged commit e3039b0 into main May 27, 2026
25 of 26 checks passed
@codemonkeychris codemonkeychris deleted the fix/clean-machine-bootstrap branch May 27, 2026 17:48
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