Fix five aspire ls bugs from #17620 (L1–L5) - #17631
Conversation
|
🚀 Dogfood this PR with:
curl -fsSL https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.sh | bash -s -- 17631Or
iex "& { $(irm https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.ps1) } 17631" |
There was a problem hiding this comment.
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.jsonintoaspire.config.json. - Updates AppHost settings lookup to validate configured paths and dedupe symlink-equivalent candidates.
- Documents
aspire ls --streamas 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
8b34c07 to
3869490
Compare
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>
3869490 to
ee4ff12
Compare
🧪 Local validation resultsBuilt and tested the CLI from this branch ( Targeted regression: L1–L5
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)
Bug found and fixed mid-review (ATTACK C): Empty Test results
Filed follow-ups (out-of-scope for this PR)
Notes
|
James Newton-King (JamesNK)
left a comment
There was a problem hiding this comment.
Solid set of fixes. One minor nit about a missing diagnostic log in a silent catch path — otherwise LGTM.
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) => |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
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>
2d65ec5
into
microsoft:main
|
/backport to release/13.4 |
|
Started backporting to |
* 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>
Description
This PR fixes the five
aspire lsbugs catalogued in #17620 (L1–L5). They are all consequences of how the new settings/discovery pipeline interacts with the legacy.aspire/settings.json, the modernaspire.config.json, parallel discovery, and macOS-style symlinks.Fixes #17615
Fixes #17620
Fixes #17621
Fixes #17624
Fixes #17626
What changes for users
aspire ls(and any other read command) no longer silently creates anaspire.config.jsonnext to an existing.aspire/settings.json. Read commands no longer mutate the workspace; migration is performed only by the explicit write paths..aspire/settings.jsonnow references the file the user actually authored, instead of the auto-createdaspire.config.jsonthey have never seen.aspire ls --format json --streamnow correctly documents and emits candidates in arrival order from the parallel discovery walk (the previous dead post-emissionSort()had no effect and only confused readers).Path.GetInvalidPathChars()) inappHost.path/appHostPathnow surfaces a clear, localized error instead of crashing with the generic “An unexpected error occurred: Null character in path.”/tmp -> /private/tmp),aspire lsno 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 --streamis now documented as arrival-ordered:A bad
appHost.pathnow produces a useful, localized error rather than a stack-trace–style message:Implementation notes
ConfigurationHelper.RegisterSettingsFilesno longer eagerly migrates.aspire/settings.json→aspire.config.json. Migration is still available through the normal write paths.ProjectLocator.GetAppHostProjectFileFromSettingsAsyncdrops thesilentparameter; the legacy branch unconditionally surfaces the warning, and the message now uses the user-authored settings.json path.appHosts.Sort()inLsCommand.FindAppHostsWithJsonStreamAsync; updated the resx (LsStreamOptionDescription) anddocs/specs/cli-output-formats.md. xlf set refreshed viaUpdateXlf.IsValidConfiguredAppHostPathhelper inProjectLocatorvalidates against\0andPath.GetInvalidPathChars()beforePath.Combine/Path.IsPathRooted. Called in both the modern (appHost.pathfromaspire.config.json) and legacy (appHostPathfrom.aspire/settings.json) branches. Validation is intentionally at the consumption point rather than inAspireConfigFile.Load, which has 12+ unrelated callers that should not be impacted. NewConfiguredAppHostPathHasInvalidCharactersresource string.PathNormalizer.ResolveSymlinks(string path)insrc/Shared. It walks each path segment withDirectory.ResolveLinkTarget/File.ResolveLinkTarget(returnFinalTarget: true). The critical subtlety is thatResolveLinkTargetreturns the link target as stored on disk — so a link whose target is/var/.../appretains the un-canonical/varprefix, 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.AddSettingsAppHostCandidateAsyncuses the resolved paths as a comparison key only — the surfacedAppHostProjectCandidatekeeps its originalFileInfoso the displayed path matches what the user authored.Tests
PathNormalizer.ResolveSymlinks(idempotence, empty input, final-file symlink, intermediate-directory symlink, broken-link fallback).ConfigurationHelperTestsrewritten to verify noaspire.config.jsonis written when only legacy settings are present.ProjectLocatorTestsadds a regression that the legacy warning referencessettings.json, notaspire.config.json.LsCommandTestsadds an arrival-order test using non-alphabetical names (Z/A/M) so any incidental sort would fail.ProjectLocatorTestsadds two NUL-byte tests covering both the modern and the legacy branch.ProjectLocatorTestsadds an integration test that places the symlink innode_modules(excluded fromDefaultFiltereddiscovery) 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.
Checklist
<remarks />and<code />elements on your triple slash comments?