Skip to content

Fix five aspire ls bugs from #17620 (L1–L5) - #17631

Merged
Jose Perez Rodriguez (joperezr) merged 8 commits into
microsoft:mainfrom
adamint:fix/aspire-ls-l1-l5
May 29, 2026
Merged

Fix five aspire ls bugs from #17620 (L1–L5)#17631
Jose Perez Rodriguez (joperezr) merged 8 commits into
microsoft:mainfrom
adamint:fix/aspire-ls-l1-l5

Conversation

@adamint

Copy link
Copy Markdown
Member

Description

This PR fixes the five aspire ls bugs catalogued in #17620 (L1–L5). They are all consequences of how the new settings/discovery pipeline interacts with the legacy .aspire/settings.json, the modern aspire.config.json, parallel discovery, and macOS-style symlinks.

Fixes #17615
Fixes #17620
Fixes #17621
Fixes #17624
Fixes #17626

What changes for users

# Issue User-visible behavior change
L1 #17615 aspire ls (and any other read command) no longer silently creates an aspire.config.json next to an existing .aspire/settings.json. Read commands no longer mutate the workspace; migration is performed only by the explicit write paths.
L2 #17620 The migration warning text for legacy .aspire/settings.json now references the file the user actually authored, instead of the auto-created aspire.config.json they have never seen.
L3 #17621 aspire ls --format json --stream now correctly documents and emits candidates in arrival order from the parallel discovery walk (the previous dead post-emission Sort() had no effect and only confused readers).
L4 #17624 A NUL byte (or any other character forbidden by Path.GetInvalidPathChars()) in appHost.path/appHostPath now surfaces a clear, localized error instead of crashing with the generic “An unexpected error occurred: Null character in path.”
L5 #17626 On macOS (and any platform with directory symlinks on the apphost path, e.g. /tmp -> /private/tmp), aspire ls no longer lists the same apphost twice — once from the discovery walk and once from the settings file pointing at it via the symlink.

User-facing usage

The CLI surface is unchanged; only behavior under existing scenarios is corrected.

aspire ls --stream is now documented as arrival-ordered:

--stream    Stream newline-delimited JSON discovery events in arrival order from parallel discovery (not sorted). Requires --format json

A bad appHost.path now produces a useful, localized error rather than a stack-trace–style message:

The configured AppHost path in '/path/to/aspire.config.json' ('appHost.path') contains characters that are not allowed in a file path.

Implementation notes

  • L1: ConfigurationHelper.RegisterSettingsFiles no longer eagerly migrates .aspire/settings.jsonaspire.config.json. Migration is still available through the normal write paths.
  • L2: ProjectLocator.GetAppHostProjectFileFromSettingsAsync drops the silent parameter; the legacy branch unconditionally surfaces the warning, and the message now uses the user-authored settings.json path.
  • L3: Removed dead appHosts.Sort() in LsCommand.FindAppHostsWithJsonStreamAsync; updated the resx (LsStreamOptionDescription) and docs/specs/cli-output-formats.md. xlf set refreshed via UpdateXlf.
  • L4: New IsValidConfiguredAppHostPath helper in ProjectLocator validates against \0 and Path.GetInvalidPathChars() before Path.Combine / Path.IsPathRooted. Called in both the modern (appHost.path from aspire.config.json) and legacy (appHostPath from .aspire/settings.json) branches. Validation is intentionally at the consumption point rather than in AspireConfigFile.Load, which has 12+ unrelated callers that should not be impacted. New ConfiguredAppHostPathHasInvalidCharacters resource string.
  • L5: New PathNormalizer.ResolveSymlinks(string path) in src/Shared. It walks each path segment with Directory.ResolveLinkTarget/File.ResolveLinkTarget(returnFinalTarget: true). The critical subtlety is that ResolveLinkTarget returns the link target as stored on disk — so a link whose target is /var/.../app retains the un-canonical /var prefix, even when the rest of the path has already been canonicalized through /var -> /private/var. To produce a canonical form regardless of which side of the comparison reached the file first, the helper recursively canonicalizes each resolved target, with a hard depth limit of 40 to defend against pathological chains, and falls back to the input on broken or circular links. AddSettingsAppHostCandidateAsync uses the resolved paths as a comparison key only — the surfaced AppHostProjectCandidate keeps its original FileInfo so the displayed path matches what the user authored.

Tests

  • New unit tests for PathNormalizer.ResolveSymlinks (idempotence, empty input, final-file symlink, intermediate-directory symlink, broken-link fallback).
  • L1: ConfigurationHelperTests rewritten to verify no aspire.config.json is written when only legacy settings are present.
  • L2: ProjectLocatorTests adds a regression that the legacy warning references settings.json, not aspire.config.json.
  • L3: LsCommandTests adds an arrival-order test using non-alphabetical names (Z/A/M) so any incidental sort would fail.
  • L4: ProjectLocatorTests adds two NUL-byte tests covering both the modern and the legacy branch.
  • L5: ProjectLocatorTests adds an integration test that places the symlink in node_modules (excluded from DefaultFiltered discovery) so the walk surfaces only the canonical path while the settings file references the symbolic path. The test verifies dedupe collapses them to a single entry.

Tests on a clean checkout: 158/160 targeted tests pass; 2 Windows-only tests are skipped on macOS as expected.

dotnet test --project tests/Aspire.Cli.Tests/Aspire.Cli.Tests.csproj --no-build --no-launch-profile \
  -- --filter-class "*.ProjectLocatorTests" --filter-class "*.LsCommandTests" \
     --filter-class "*.AspireConfigFileTests" --filter-class "*.ConfigurationHelperTests" \
     --filter-class "*.PathNormalizerTests" \
     --filter-not-trait "quarantined=true" --filter-not-trait "outerloop=true"

Checklist

  • Is this feature complete?
    • Yes. Ready to ship.
    • No. Follow-up changes expected.
  • Are you including unit tests for the changes and scenario tests if relevant?
    • Yes
    • No
  • Did you add public API?
    • Yes
      • If yes, did you have an API Review for it?
        • Yes
        • No
      • Did you add <remarks /> and <code /> elements on your triple slash comments?
        • Yes
        • No
    • No
  • Does the change make any security assumptions or guarantees?
    • Yes
      • If yes, have you done a threat model and had a security review?
        • Yes
        • No
    • No

@github-actions

github-actions Bot commented May 28, 2026

Copy link
Copy Markdown
Contributor

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.sh | bash -s -- 17631

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.ps1) } 17631"

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.

Pull request overview

This PR fixes several aspire ls settings/discovery regressions around legacy settings migration, missing-path warnings, streamed ordering, invalid configured paths, and symlink-aware duplicate detection.

Changes:

  • Stops startup settings registration from eagerly migrating legacy .aspire/settings.json into aspire.config.json.
  • Updates AppHost settings lookup to validate configured paths and dedupe symlink-equivalent candidates.
  • Documents aspire ls --stream as arrival-ordered and adds regression tests for the fixed cases.

Reviewed changes

Copilot reviewed 39 out of 40 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
src/Aspire.Cli/Utils/ConfigurationHelper.cs Removes eager migration during settings registration.
src/Aspire.Cli/Program.cs Updates call site for the new RegisterSettingsFiles signature.
src/Aspire.Cli/Projects/ProjectLocator.cs Adds invalid-path validation, warning behavior updates, and symlink-aware dedupe.
src/Shared/PathNormalizer.cs Adds symlink-resolution helper.
src/Aspire.Cli/Commands/LsCommand.cs Removes dead stream-mode sort and documents arrival-order behavior.
src/Aspire.Cli/Resources/ErrorStrings.resx Adds invalid AppHost path error string.
src/Aspire.Cli/Resources/ErrorStrings.Designer.cs Adds generated accessor for the new error string.
src/Aspire.Cli/Resources/SharedCommandStrings.resx Updates --stream option description.
src/Aspire.Cli/Resources/xlf/ErrorStrings.*.xlf Adds localized placeholders for the new error string.
src/Aspire.Cli/Resources/xlf/SharedCommandStrings.*.xlf Updates localized placeholders for the stream option description.
docs/specs/cli-output-formats.md Documents streamed output ordering.
tests/Aspire.Cli.Tests/Configuration/ConfigurationHelperTests.cs Updates tests for non-mutating startup registration.
tests/Aspire.Cli.Tests/Commands/LsCommandTests.cs Adds stream arrival-order regression coverage.
tests/Aspire.Cli.Tests/Projects/ProjectLocatorTests.cs Adds warning, invalid-path, and symlink-dedupe regression tests.
tests/Aspire.Cli.Tests/Utils/PathNormalizerTests.cs Adds symlink-resolution unit tests.
tests/Aspire.Cli.Tests/Utils/CliTestHelper.cs Updates test helper for the settings registration signature change.
Files not reviewed (1)
  • src/Aspire.Cli/Resources/ErrorStrings.Designer.cs: Language not supported

Comment thread src/Aspire.Cli/Projects/ProjectLocator.cs Outdated
Comment thread src/Aspire.Cli/Utils/ConfigurationHelper.cs
Fixes microsoft#17615, microsoft#17620, microsoft#17621, microsoft#17624, microsoft#17626.

- L1 (microsoft#17615): Remove the eager-migration block in
  ConfigurationHelper.RegisterSettingsFiles. Read commands like
  `aspire ls` no longer silently materialize an aspire.config.json
  next to a user's legacy .aspire/settings.json. Migration now happens
  lazily/explicitly via the existing write paths.

- L2 (microsoft#17620): Drop the `silent` parameter from
  ProjectLocator.GetAppHostProjectFileFromSettingsAsync so the legacy
  branch unconditionally surfaces the migration warning, and surface
  the actual user-authored `.aspire/settings.json` path in the warning
  text rather than the auto-created `aspire.config.json` path.

- L3 (microsoft#17621): Remove the dead post-emission `appHosts.Sort()` in
  LsCommand.FindAppHostsWithJsonStreamAsync (--stream emits candidates
  as they are discovered, so the sort had no effect on already-emitted
  output). Update the --stream option description and
  docs/specs/cli-output-formats.md to declare the arrival-ordered
  contract.

- L4 (microsoft#17624): Add an IsValidConfiguredAppHostPath helper in
  ProjectLocator that rejects `\0` and Path.GetInvalidPathChars()
  before the path is passed to Path.IsPathRooted / Path.Combine.
  Wired into both the modern `aspire.config.json` (`appHost.path`)
  branch and the legacy `.aspire/settings.json` (`appHostPath`)
  branch. Validation is intentionally at the consumption point rather
  than in AspireConfigFile.Load, which has 12+ unrelated callers.
  Adds a new ConfiguredAppHostPathHasInvalidCharacters resource string
  and refreshes the xlf set via UpdateXlf.

- L5 (microsoft#17626): Add PathNormalizer.ResolveSymlinks in src/Shared, a
  recursive segment-walker that canonicalizes intermediate symlinks
  (Directory.ResolveLinkTarget only reads exactly the path it is given,
  and returns the link target as stored on disk — so a single call on
  /tmp/x/y.cs does not unwrap /tmp -> /private/tmp, and following a
  link whose stored target is /var/.../app keeps the un-canonical
  /var prefix). The recursion has a hard depth limit of 40 and falls
  back to the un-resolved input on broken or circular links. Use it in
  AddSettingsAppHostCandidateAsync as a comparison key only — the
  surfaced AppHostProjectCandidate keeps its original FileInfo so the
  displayed path matches what the user authored in settings.

Tests: 158 of 160 targeted tests pass (2 Windows-only skipped on
macOS). New tests cover L1 (no migration on read), L2 (legacy warning
references settings.json), L3 (arrival-order under --stream), L4
(NUL byte in modern and legacy branches), L5 (symlink dedupe via a
node_modules-hosted link the discovery walk excludes), plus 5 unit
tests on PathNormalizer.ResolveSymlinks itself.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@adamint

Copy link
Copy Markdown
Member Author

🧪 Local validation results

Built and tested the CLI from this branch (ee4ff1236c) on macOS arm64. The PR build artifact is still in-flight in CI, so these results are from the locally-built binary at artifacts/bin/Aspire.Cli/Debug/net10.0/aspire against the same source.

Targeted regression: L1–L5

# Bug Manual check Result
L1 aspire ls silently writes aspire.config.json Run in a workspace with neither aspire.config.json nor .aspire/settings.json; assert no file is written ✅ no file written
L2 Warning claims path was specified in aspire.config.json when the file is actually legacy .aspire/settings.json Set legacy .aspire/settings.json with bogus path; run aspire ls; check warning text ✅ warning correctly names .aspire/settings.json
L3 --format json --stream ignores --stream aspire ls --format json --stream; assert NDJSON (one object per line) ✅ valid NDJSON; one JSON object per line
L4 NUL byte in appHost.path crashes with generic "unexpected error" Write {"appHost":{"path":"a\u0000b.csproj"}}; run aspire ls ✅ clean validation error naming the config file; exit 0
L5 Settings path via /tmp symlink produces a duplicate row alongside the canonical /private/tmp walk path Author settings with /tmp/... path on macOS; run aspire ls ✅ single row, no duplicate

Adversarial probe (15 attacks)

After addressing review feedback, I ran a focused adversarial pass against the same code paths. One additional in-scope bug was found and fixed in this PR.

Full attack table (click to expand)
Attack Scenario Result
A --format json with broken settings ✅ warning on stderr, clean [] on stdout
B --format json --stream with NUL byte ✅ clean NDJSON, error on stderr
C Empty appHostPath string ❌ → ✅ FIXED in this PR (was misleading "contains invalid characters" message)
D Whitespace-only path ✅ falls through as "not found" (matches pre-PR)
E Path with embedded newline ✅ legal on Unix, treated as filename
F Root / as appHost path ✅ rejected as directory not file
G Directory not file ✅ same as F
H Self-referential symlink ResolveSymlinks depth limit holds
I Nested symlink chain (3 deep) ⚠️ walk-vs-walk dedupe out-of-scope — pre-existing #17618
J Relative path ./AppHost.csproj in settings ✅ L5 dedupe canonicalizes correctly
K Invalid JSON in aspire.config.json ✅ clean error, exit 20
L Empty file aspire.config.json ✅ clean error, exit 20
M appHost section with no path ✅ falls through, walk works
N appHost.path as a number ✅ JSON-typing error + walk fallback
O appHost.path as an array ✅ same as N
P BOTH legacy appHostPath + modern appHost.path keys in legacy file ✅ legacy appHostPath wins (existing semantics)
Q Read-permission denied on settings ✅ clean access-denied error
R Case mismatch on macOS APFS (APPHOST vs AppHost) ⚠️ pre-existing case-sensitive comparison — filed #17635
S Very long path (1500 chars) ✅ clean "not found" warning
T Empty workspace --format json ✅ valid empty JSON []
U Empty workspace --format json --stream ✅ valid empty NDJSON (0 lines)

Bug found and fixed mid-review (ATTACK C): Empty appHost.path ("") was rejected by the L4 validator but emitted the misleading error string "... contains characters that are not allowed in a file path" — confusing for a string with no characters. Widened ConfiguredAppHostPathHasInvalidCharacters to "... is empty or contains characters that are not allowed in a file path" and added two regression tests (modern + legacy paths).

Test results

  • ProjectLocatorTests: 80/80 pass (78 existing + 2 new empty-path regression tests).
    Test run summary: Passed!
      total: 81, failed: 0, succeeded: 80, skipped: 1, duration: 5s
    
    (Skipped test is UseOrFindAppHostProjectFileResultUsesOnDiskCasingForExplicitPath — Windows-only.)

Filed follow-ups (out-of-scope for this PR)

Notes

Comment thread src/Aspire.Cli/Projects/ProjectLocator.cs Outdated
Comment thread src/Aspire.Cli/Resources/SharedCommandStrings.resx Outdated
@davidfowl David Fowler (davidfowl) added this to the 13.4 milestone May 29, 2026
Comment thread src/Aspire.Cli/Projects/ProjectLocator.cs Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Solid set of fixes. One minor nit about a missing diagnostic log in a silent catch path — otherwise LGTM.

Comment thread src/Aspire.Cli/Projects/ProjectLocator.cs
Adam Ratzman (adamint) and others added 4 commits May 29, 2026 11:56
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
capabilities.Add(capability.CapabilityId, capability);
}

private static bool CapabilitiesAreEquivalent(AtsCapability left, AtsCapability right) =>

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.

I actually think we should fail in every case the same id is found, even if the types are the same. We want to fix it, not keep these collisions

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.

I understand it's not your PR that introduced it (a recent Foundry one). Other PRs are blocked by the same issue. We can just wait for #17671 to be merged

Adam Ratzman (adamint) added a commit to adamint/aspire that referenced this pull request May 29, 2026
Match the duplicate ATS capability parser/test/baseline shape from PR microsoft#17631 to avoid merge conflicts between the branches.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
PR microsoft#17671 owns the Foundry ATS baseline update; this branch should only contain the AppHost and CLI fixes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@joperezr
Jose Perez Rodriguez (joperezr) merged commit 2d65ec5 into microsoft:main May 29, 2026
616 of 619 checks passed
@joperezr

Copy link
Copy Markdown
Member

/backport to release/13.4

@microsoft-github-policy-service microsoft-github-policy-service Bot modified the milestones: 13.4, 13.5 May 29, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Started backporting to release/13.4 (link to workflow run)

Adam Ratzman (adamint) added a commit that referenced this pull request May 30, 2026
* Pin Corepack explicitly for the VS Code extension build

Replaces the implicit 'corepack is somewhere on PATH' assumption with an
explicit 'npm install -g corepack@0.34.7' step in extension/build.sh,
extension/build.ps1, and the three AzDO pipelines that build the
extension. The Yarn version is now pinned in extension/package.json via
the standard 'packageManager' field (yarn@1.22.22), removing duplicate
@1.22.22 pins from build.sh, build.ps1, and Extension.proj.

The build scripts default COREPACK_NPM_REGISTRY to the dnceng
dotnet-public-npm mirror and disable the Corepack download prompt, and
the same defaults are set as AzDO pipeline variables in
common-variables.yml and public-pipeline-template.yml so Corepack
downloads Yarn from an approved internal feed rather than npmjs.org.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address code review for Corepack pin

- Add npmAuthenticate@0 + NPM_CONFIG_USERCONFIG setup to the CodeQL
  pipeline before the Install Corepack step. The previous CodeQL pipeline
  worked anonymously against dnceng dotnet-public-npm only because
  yarn@1.22.22 was already cached there; corepack@0.34.7 is not, and the
  pipeline would have started failing on the first run.
- After 'npm install -g corepack@<pin>' in build.sh, build.ps1, and all
  three AzDO pipeline PowerShell steps, run 'corepack --version' and
  fail loudly if the version doesn't match the pin. On Windows the
  bundled corepack.cmd under %ProgramFiles%\nodejs can shadow the
  npm-global shim under %APPDATA%\npm, so a successful install does not
  guarantee the pinned Corepack is what 'corepack enable' actually runs.
- In build.sh and build.ps1 only (not the pipelines), force the public
  npm registry for the Corepack install via
  --registry=https://registry.npmjs.org so first-time OSS contributors
  are not blocked on dnceng cache misses. Corepack is build tooling and
  never ships in the extension VSIX, so registry choice is local-dev
  ergonomics only.
- Document the 'EACCES from npm install --global' and 'corepack version
  mismatch' troubleshooting steps in extension/CONTRIBUTING.MD, plus a
  note that bumping Yarn requires the new tarball to be pulled through
  dotnet-public-npm at least once with credentials.
- Drop the misleading 'update Extension.proj inline pin' comment from
  the build scripts (no such inline pin remains).
- Drop the redundant DependsOnTargets='ValidateYarnLockRegistries' from
  CheckYarnInstalled; the parent BuildAndPackageExtension target already
  declares the same dependency.
- Normalize the workingDirectory path separator across the three AzDO
  pipelines to backslash, matching the convention used elsewhere in
  those Windows-only files.
- Add trailing newline to extension/CONTRIBUTING.MD.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Drop unnecessary --registry override on corepack install

corepack@0.34.7 is already cached in the dnceng dotnet-public-npm feed
and serves anonymously, so the build scripts don't need to bypass the
internal mirror to install Corepack. Verified anonymously:

  GET .../dotnet-public-npm/.../corepack/-/corepack-0.34.7.tgz
  -> 200 OK, 229 KB

The earlier comment overstated the problem: only versions that have
never been requested from the feed return 401 (the feed's pull-through
behavior requires auth for the very first fetch, then anyone can read
the cached copy). The same caveat applies to bumping the pinned Yarn
or Corepack version, so the heads-up about pre-seeding the feed now
lives in extension/CONTRIBUTING.MD rather than the build-script comments.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix Corepack registry handling

Ensure Corepack and Yarn setup use the configured npm registry and authenticated Azure Artifacts credentials across local scripts and CI.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Avoid Corepack registry override for Azure Artifacts

Azure Artifacts does not support the npm /package/version metadata endpoint Corepack uses when COREPACK_NPM_REGISTRY is set. Keep the internal feed for npm's Corepack install, but let Corepack prepare Yarn without that registry override.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Force Corepack shim install in CI

Hosted Windows images can already have a Yarn shim in npm's global prefix, and the npm Corepack package owns that shim. Use --force only in CI tool setup so the pinned Corepack install can replace ephemeral runner shims without changing local developer scripts.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Prepend npm Corepack shim path in CI

After installing the pinned Corepack package, hosted Windows runners can still resolve the bundled Corepack first. Prepend npm's global prefix for the current CI step and subsequent steps so Corepack 0.34.7 is the shim that prepares and runs Yarn.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Seed Corepack Yarn cache via npm pack

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Invoke npm CLI directly for Corepack Yarn seed

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Handle duplicate equivalent ATS capabilities

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Align duplicate ATS compatibility fix

Match the duplicate ATS capability parser/test/baseline shape from PR #17631 to avoid merge conflicts between the branches.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Remove duplicate ATS compatibility fix

Keep the TypeScript API compatibility fix in the dedicated Foundry API PR instead of duplicating it in this Corepack CI fix.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Export NPM_REGISTRY in build.sh and document Corepack cache-path coupling

- extension/build.sh: export NPM_REGISTRY so the child
  scripts/prepareCorepackYarn.mjs process actually inherits it. Without
  the export, the script silently fell back to its DefaultNpmRegistry
  constant and any user override of NPM_REGISTRY would be ignored when
  seeding Corepack's Yarn cache. COREPACK_ENABLE_DOWNLOAD_PROMPT on the
  next line was already exported; this restores symmetry.

- extension/scripts/prepareCorepackYarn.mjs: add a comment in
  getCorepackHome() documenting the implicit coupling to corepack
  0.34.x's own cache-path resolution. If COREPACK_VERSION is later bumped
  to a release that switches schemes (e.g., env-paths, which would
  relocate the macOS cache to ~/Library/Caches/node/corepack), this
  fallback would silently seed the wrong directory. The AzDO pipelines
  already set COREPACK_HOME explicitly to avoid this; this comment flags
  the same hardening as the simplest fix when the pin is updated.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address PR review feedback for Corepack bootstrap

Six follow-ups from the review on #17630:

* Add --force to the local 'npm install --global corepack@<version>' in
  extension/build.sh and extension/build.ps1. Without --force, npm refuses
  to overwrite the yarn / yarnpkg / pnpm / pnpx bin entries owned by any
  pre-existing global yarn or pnpm install (the state this repo itself
  shipped before the bootstrap existed), aborting with EEXIST. The CI
  pipelines already pass --force for the same reason.

* Switch the GitHub Actions 'extension_tests_win' job to use
  prepareCorepackYarn.mjs instead of 'corepack prepare --activate'. The
  built-in Corepack prepare path downloads Yarn 1.x from
  registry.yarnpkg.com (hardcoded in Corepack 0.34's config.json and not
  redirectable via COREPACK_NPM_REGISTRY), bypassing the dnceng feed this
  workflow exists to validate. Also scope COREPACK_HOME to runner.temp.

* Add an 'extension_bootstrap_linux' GH Actions job so the non-Windows
  branches of prepareCorepackYarn.mjs (POSIX npm invocation, no node.exe
  wrapping) are exercised on a fresh CI image, not only on contributor
  machines.

* Update the 'CheckYarnInstalled' error in extension/Extension.proj to
  name the actual supported entry points (the root Arcade flow
  './build.sh -build-extension' and direct 'dotnet build
  extension/Extension.proj' both require running extension/build.sh or
  extension/build.ps1 first) instead of pointing developers at a path
  that isn't part of the documented root build flow.

* Scope COREPACK_HOME to '$SCRIPT_DIR/.corepack-cache' in the local
  build entrypoints. prepareCorepackYarn.mjs rewrites the cache in place
  via rmSync + renameSync, so concurrent builds (multiple worktrees,
  parallel invocations) sharing the user's default cache can corrupt
  each other. The CI pipelines already scope this per-job via
  Agent.TempDirectory / runner.temp; do the same locally. New cache
  directory is gitignored.

* Loosen PackageManagerPattern in prepareCorepackYarn.mjs to accept the
  optional integrity suffix that 'corepack use yarn@<v>' writes
  ('yarn@1.22.22+sha512.<hex>'). CONTRIBUTING.MD points contributors at
  'corepack use' for updating the pin, so rejecting the canonical
  spec-conformant value would have broken that flow.

* Centralize the pinned Corepack version in
  extension/scripts/corepack-version.txt. The bash and PowerShell build
  scripts, the GitHub Actions workflow, and all three AzDO pipelines
  (azure-pipelines.yml, azure-pipelines-unofficial.yml,
  azure-pipelines-codeql.yml) now read from this single file, removing
  the six-place version-drift hazard.

Validated by running extension/build.sh end-to-end from a clean state:
npm install (with --force), corepack enable, prepareCorepackYarn.mjs
against $SCRIPT_DIR/.corepack-cache, corepack yarn install
--frozen-lockfile, corepack yarn compile, dotnet build Aspire.Cli.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Set COREPACK_HOME via $GITHUB_ENV instead of job env

The runner context is not available in job-level env evaluation, so
'COREPACK_HOME: ${{ runner.temp }}/corepack' caused the workflow file
to be rejected before any job could run ('This run likely failed because
of a workflow file issue', latest_check_runs_count: 0).

Forward COREPACK_HOME from inside the Install Corepack step using
$RUNNER_TEMP (which is exposed as an env var on the runner) and
$GITHUB_ENV. The value reaches all subsequent steps the same way a
job-level env entry would have, so the corepack cache stays isolated
to the job.

Verified with actionlint: previously two 'context "runner" is not
allowed here' errors at lines 320 and 395; now clean.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address PR #17630 round-3 review feedback

- prepareCorepackYarn.mjs: correct the misleading Corepack hash-verification comment to
  explain that integrity rests on the npm pack fetch from the dnceng feed, not on Corepack
  re-verifying a pre-seeded cache. Cite the corepackUtils.ts source at v0.34.7.
- prepareCorepackYarn.mjs: catch both EEXIST and ENOTEMPTY in the renameSync race handler
  (platform-specific rename(2) collision codes) and document why both are needed.
- prepareCorepackYarn.mjs: bump source citation v0.34.0 -> v0.34.7 in getCorepackHome to
  match the version pin in corepack-version.txt.
- extension/build.sh, extension/build.ps1: tighten the over-claimed isolation comment to
  cover only multi-worktree setups; same-worktree concurrent builds still race.
- extension/Extension.proj: set EnvironmentVariables=COREPACK_HOME=<extension>/.corepack-cache
  on every Exec that invokes corepack, so the documented recovery path (./build.sh
  -build-extension, or direct dotnet build extension/Extension.proj after running
  extension/build.sh once) works without depending on parent-shell env propagation.
- extension/Extension.proj and .github/workflows/tests.yml: switch lockfile registry
  validation from a denylist of npmjs.org/yarnpkg.com to an allowlist requiring the
  internal dotnet-public-npm feed, scoped to lines starting with 'resolved' (yarn.lock's
  only URL-bearing lines).
- .github/workflows/tests.yml: add the same allowlist validation step to the
  extension_bootstrap_linux job before Install dependencies.
- eng/pipelines/templates/install-corepack.yml: extract the triplicated 'Install Corepack'
  PowerShell block; parameterize displayPrefix. azure-pipelines.yml,
  azure-pipelines-unofficial.yml, and azure-pipelines-codeql.yml now reference the template.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address PR #17630 Mitch review feedback

- HIGH: Extension.proj _CorepackHome now inherits COREPACK_HOME from the
  parent environment when set (AzDO install-corepack.yml + GitHub Actions
  extension jobs both export it), falling back to <extension>/.corepack-cache
  only for local recovery. Previously the unconditional override defeated
  the AzDO seed and forced corepack to fall back to registry.yarnpkg.com.

- LOW: prepareCorepackYarn.mjs now guards the rmSync with an immediate
  re-check of the .corepack metadata path, narrowing the race window where
  a concurrent winner's just-renamed cache could be destroyed.

- NIT: Removed the single-use corepackMetadataPathFor helper; inlined the
  join(stagingDirectory, '.corepack') call.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions github-actions Bot locked and limited conversation to collaborators Jun 29, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

6 participants