Contributing to the Aspire VS Code extension

September 13, 2026 · View on GitHub

How to set up your machine, the code layout, and the fastest inner-loop for changes.

Bug fixes, new commands, debugger-language support, walkthrough content, settings, and docs are all welcome. To find a starting point, browse area-vscode-extension issues labeled good first issue or help wanted.

Install prerequisites

  • Node.js 22 or later (LTS recommended) — npm must be on the PATH (it ships with Node.js). The build scripts (build.sh / build.ps1) install a pinned Corepack via npm install -g corepack@<version> from the configured registry and seed Corepack's cache with the Yarn release pinned by the packageManager field in extension/package.json. You do not need to install Yarn yourself.
  • Visual Studio Code (latest) or Visual Studio Code Insiders
  • Aspire CLI must be installed and available in the PATH

No repository write access or credentials are needed to build. Dependencies come from the public dotnet-public-npm Azure Artifacts feed; every version pinned in yarn.lock is already cached and served anonymously, so yarn install works for everyone. See the npm mirror note for the one edge case maintainers hit when bumping pinned tool versions.

Quick start: extension-only changes

For TypeScript/UI changes that don't require debugging the Aspire CLI itself, skip the full repository build (and its .NET prerequisites) and use any Aspire CLI on your PATH (install one with the Aspire: Install Aspire CLI (stable) command).

From extension/:

corepack yarn install   # restore dependencies

Open extension/ in VS Code and launch Run Extension (F5) to start an Extension Development Host with your build. The launch config runs the tasks: watch extension preLaunchTask (which executes yarn watch) to keep dist/ up to date while you edit. After rebuilds, re-launch or run Developer: Reload Window in the host to pick up changes.

Project structure

Source lives under extension/src/:

DirectoryContents
commands/Command Palette commands (Aspire: …) and handlers
views/Aspire sidebar tree views and resource UI
debugger/Debug session orchestration; debugger/languages/ adds per-language support (C#, Python, Node.js, …)
dcp/Integration with the orchestrator (Developer Control Plane)
server/RPC server the Aspire CLI talks to
services/Long-lived services (CLI discovery, telemetry, settings, …)
mcp/Model Context Protocol server registration
editor/Editor features; editor/parsers/ parses apphost files for CodeLens and validation
loc/Localized string definitions (strings.ts)
utils/Shared helpers
test/*.test.ts unit tests run by @vscode/test-electron

Also: package.json declares commands, settings, and contribution points; walkthrough/ holds the Get Started Markdown; package.nls.json (+ package.nls.*.json) hold localized package.json strings.

Building the full repository (extension + CLI)

Run build.ps1 (Windows) or build.sh (Mac/Linux) from the repository root to compile the CLI, install extension dependencies, and localize. Use this when debugging the extension and CLI together. See docs/contributing.md and docs/machine-requirements.md for the .NET prerequisites.

Run extension locally

  • Open the extension folder in Visual Studio Code.
  • Launch either the Run Extension or Run Extension (cli stop on entry) launch configuration. The latter will set an environment variable that causes the CLI to wait until a debugger is attached to execute its logic.

Optional: set the CLI path

To debug the Aspire CLI together with the extension, set Aspire Cli Executable Path to the CLI output path. You can use an absolute path or a workspace token. If you opened the extension folder, the local build is:

"aspire.aspireCliExecutablePath": "${workspaceFolder}/../artifacts/bin/Aspire.Cli/Debug/net10.0/aspire"

${workspaceFolder} uses the folder that owns the current operation. In a multi-root workspace, ${workspaceFolder:name} selects the uniquely named folder. Plain relative paths are not resolved; use a workspace token when the path should move with the checkout.

Pointing at the raw build output makes the extension invoke your dev CLI, but that path is intentionally not forwarded as AspireCliPath (see Dogfooding below). To also dogfood bundle metadata, use an installed/bundled CLI layout instead.

You may also want to use the Run Extension (cli stop on entry) launch configuration, as Run Extension does not prevent the Aspire CLI from executing immediately.

You can use the Aspire: Extension settings command to open VS Code settings directly to the Aspire extension category.

Dogfooding a CLI build alongside the extension

When Aspire Cli Executable Path resolves to an existing absolute path that is not a raw framework-dependent local CLI build output, the extension forwards that value as the AspireCliPath MSBuild property/environment variable to terminals, tasks, and debug processes in that workspace folder. The Aspire SDK's ResolveAspireCliBundle task uses AspireCliPath (defined in src/Aspire.Hosting.Tasks/ResolveAspireCliBundle.cs) to locate the matching bundle layout — DCP, dashboard, and terminal-host binaries — and bakes those paths into the built AppHost as [AssemblyMetadata] attributes. Without this forwarding, MSBuild probes PATH and can stamp the stable CLI's bundle into the AppHost while the extension is launching it through the dev CLI, producing surprising runtime mismatches such as <unresolved-aspire-terminalhost> even though the new CLI is correctly invoked (tracked in issue #18073).

A tokenized setting is expanded to an absolute path before validation. A plain relative value (for example the bare aspire literal) or an absolute path that no longer exists is intentionally not forwarded because ResolveAspireCliBundle stops with a warning for invalid explicit AspireCliPath values instead of probing PATH. A raw framework-dependent local build output such as artifacts/bin/Aspire.Cli/Debug/net10.0/aspire is also not forwarded because it can make ResolveAspireCliBundle fall back to unrelated ASPIRE_HOME metadata. Symlinks to that raw local build output are filtered the same way. To dogfood bundle metadata end-to-end, point the setting at an installed/bundled CLI layout with a sidecar or adjacent bundle assets. Clear the setting to revert to default PATH/ASPIRE_HOME resolution.

Running tests

Unit tests are *.test.ts files under src/test/, run via @vscode/test-electron. From extension/:

corepack yarn test

This compiles tests and sources, lints, then runs the suite (corepack yarn lint lints only). Add or update tests for behavior changes, and ensure tests and lint pass before opening a PR.

To run a single unit-test file or a filtered subset, compile the tests first and pass Mocha selectors through unit-test:

corepack yarn compile-tests
corepack yarn compile
corepack yarn unit-test --run out/test/configInfoProvider.test.js
corepack yarn unit-test --grep "parseConfigInfoOutput"
corepack yarn unit-test --run out/test/configInfoProvider.test.js --run out/test/extensionApi.test.js

End-to-end tests

UI end-to-end tests live under src/test-e2e. They run a packaged VSIX in a real VS Code instance through ExTester, using a real Aspire CLI and a generated AppHost workspace.

Run one fixture-compatible E2E group from extension/. The default all-spec glob now spans the core C# suite, Java playground suites, and the Java starter suite, which require different workspaces and must run separately. Use one of the existing subset examples below.

On Linux, run the E2E command under xvfb-run -a when no desktop session is available. On Windows, ASPIRE_EXTENSION_E2E_CLI_PATH can point at an .exe or .cmd wrapper, including paths with spaces. Set ASPIRE_EXTENSION_E2E_VSIX=/path/to/aspire-extension.vsix to test an existing package instead of letting the runner create one. The runner defaults to VS Code 1.130.0 while ExTester 8.23.0 remains the newest version anonymously available from dotnet-public-npm. ExTester 8.23.0 launches the obsolete Contents/MacOS/Electron path, but VS Code 1.130.0 supplies only the canonical Contents/MacOS/Code executable; it does not supply an Electron -> Code compatibility symlink. The shared cache therefore validates Code. For each macOS run, the projection keeps Contents/MacOS run-local and adds the legacy Electron -> Code link there, so ExTester cleanup or replacement cannot mutate shared content. Override ASPIRE_EXTENSION_E2E_VSCODE_VERSION with another concrete version, or with ExTester's min/max, when you need to investigate package-supported behavior. On macOS, concrete versions at or above 1.131.0 are rejected until ExTester is upgraded; Linux and Windows use compatible executable paths. latest is rejected because it moves independently of the pinned dependency, so a cache key built from that literal would keep serving the first release downloaded after the alias changed. min and max are deterministic because the cache key also includes the pinned ExTester version whose support metadata resolves them. The runner also pins CODE_VERSION and CODE_TYPE for the ExTester child process, so an ambient value cannot make it download a version or a release stream the cache key does not describe. To investigate another ExTester version, update the pinned vscode-extension-tester package and regenerate yarn.lock from dotnet-public-npm. The VS Code user data is forced to English (locale.json plus VSCODE_NLS_CONFIG) so UI text assertions are deterministic across machines.

VS Code and its matching ChromeDriver are cached under <git-common-dir>/aspire-extension-e2e-cache, so the main checkout and every linked worktree reuse the same immutable downloads instead of re-acquiring them on each run. Settings, installed extensions, workspaces, screenshots, diagnostics, and ASPIRE_HOME stay isolated under the per-run temporary root, so concurrent runs cannot interfere with each other or mutate the shared cache. Cache entries are partitioned by OS, architecture, VS Code version, and ExTester version, so switching any of those acquires a separate entry rather than invalidating the existing one. Set ASPIRE_EXTENSION_E2E_CACHE_ROOT to override the cache location. To force a clean acquisition, first stop every E2E process using the cache, then delete the cache directory. A checkout under an awkward path works on every platform: ExTester interpolates the storage path into unquoted shell commands - unzip -qo <archive> when unpacking on macOS and Linux, and <chromedriver> -v when checking an already downloaded driver everywhere, Windows included - so any cache path containing something the command interpreter would act on is staged through an inert link under the per-run temporary root instead of being handed over verbatim. On macOS and Linux that means a space, (, &, $, ; or a quote; on Windows it means a space, &, %, ^, ! or any of the ,, ; and = that end a cmd.exe command token, and the link is a junction so no elevation is needed. Because the tools are already present, the test run itself passes --offline to ExTester.

The runner retries external VS Code and ChromeDriver downloads five times by default; override ASPIRE_EXTENSION_E2E_SETUP_DOWNLOAD_RETRY_ATTEMPTS, ASPIRE_EXTENSION_E2E_SETUP_DOWNLOAD_RETRY_DELAY_MS, or ASPIRE_EXTENSION_E2E_SETUP_DOWNLOAD_TIMEOUT_MS when diagnosing acquisition issues. A download that hits its timeout is not just signalled: ExTester shells out to unzip on macOS and Linux, so any process still writing under the staging path is found in ps output and killed, and the partly unpacked vscode-temp-* directory it abandoned is deleted before the retry starts. If those processes cannot be enumerated or confirmed dead, the run fails instead of retrying, because wiping and republishing a directory something may still be writing into is the corruption that cleanup exists to prevent. Downloads deliberately stay in the terminal's foreground process group so Ctrl-C still reaches them. Concurrent runs each populate a private candidate directory and publish it as the next numbered generation (entry-000001, entry-000002, ...) inside the entry's directory, so nothing on the hot path deletes or overwrites shared state and there is no lock to wait on. Readers pick the newest generation that validates, so a half-written or corrupt entry is stepped over rather than wedging the cache. Each cache-entry group reserves at most two generations: one normal generation plus one immutable replacement. If neither validates, the runner fails before another download or publication instead of creating a third generation. This also covers a concurrent publisher reserving the replacement during population: the in-flight candidate is discarded before rename. Stop every E2E process using the group before manually deleting it for recovery. Published generations are never deleted - an entry is not written to after it is published, so its timestamp cannot distinguish a warm entry from abandoned debris, and a concurrent run may be executing VS Code straight out of it. That also rules out any automatic repair for a group whose highest generation cannot be advanced, so publishing a name later runs could not read back is refused outright rather than done silently. Only leftover candidates are reclaimed, and only once the process that created one has exited (or a week has passed, since process ids get recycled) and it has been untouched for six hours - a candidate's timestamp stops moving while an extraction works deep inside a subdirectory, so age alone cannot tell an in-flight download from debris. The .zip/.tar.gz archives ExTester downloads are deleted once they have been unpacked, which keeps roughly 350 MB per entry off disk; ExTester only reads an archive when it is about to download, and it skips the download entirely once the unpacked binaries report a matching version.

Some extension E2E tests intentionally cover bugs fixed by the current repo-built CLI. When running the extension suite against an older published CLI to check backward compatibility, set ASPIRE_EXTENSION_E2E_SKIP_CURRENT_CLI_REGRESSIONS=true so those current-CLI-only regressions are skipped instead of failing on the older CLI bug.

The suite can be sharded by running separate VS Code windows/processes, which is how CI keeps the long UI paths parallel instead of relying on Mocha-level parallelism inside one extension host:

ASPIRE_EXTENSION_E2E_SHARD=command-palette ASPIRE_EXTENSION_E2E_SPEC=out/test-e2e/test-e2e/commandPalette.e2e.test.js ASPIRE_EXTENSION_E2E_CLI_PATH=/path/to/aspire corepack yarn test:e2e
ASPIRE_EXTENSION_E2E_SHARD=settings-files ASPIRE_EXTENSION_E2E_SPEC=out/test-e2e/test-e2e/settingsFiles.e2e.test.js ASPIRE_EXTENSION_E2E_CLI_PATH=/path/to/aspire corepack yarn test:e2e
ASPIRE_EXTENSION_E2E_SHARD=discovery-configuration ASPIRE_EXTENSION_E2E_SPEC=out/test-e2e/test-e2e/discoveryConfiguration.e2e.test.js ASPIRE_EXTENSION_E2E_CLI_PATH=/path/to/aspire corepack yarn test:e2e
ASPIRE_EXTENSION_E2E_SHARD=dynamic-debug-configuration ASPIRE_EXTENSION_E2E_SPEC=out/test-e2e/test-e2e/dynamicDebugConfiguration.e2e.test.js ASPIRE_EXTENSION_E2E_CLI_PATH=/path/to/aspire corepack yarn test:e2e
ASPIRE_EXTENSION_E2E_SHARD=apphost-tree ASPIRE_EXTENSION_E2E_SPEC=out/test-e2e/test-e2e/appHostTree.e2e.test.js ASPIRE_EXTENSION_E2E_CLI_PATH=/path/to/aspire corepack yarn test:e2e
ASPIRE_EXTENSION_E2E_SHARD=tree-actions ASPIRE_EXTENSION_E2E_SPEC=out/test-e2e/test-e2e/treeActions.e2e.test.js ASPIRE_EXTENSION_E2E_CLI_PATH=/path/to/aspire corepack yarn test:e2e
ASPIRE_EXTENSION_E2E_SHARD=debug-dashboard ASPIRE_EXTENSION_E2E_SPEC=out/test-e2e/test-e2e/debugDashboard.e2e.test.js ASPIRE_EXTENSION_E2E_CLI_PATH=/path/to/aspire corepack yarn test:e2e
ASPIRE_EXTENSION_E2E_SHARD=zero-to-running ASPIRE_EXTENSION_E2E_SPEC=out/test-e2e/test-e2e/zeroToRunning.e2e.test.js ASPIRE_EXTENSION_E2E_CLI_PATH=/path/to/aspire corepack yarn test:e2e
ASPIRE_EXTENSION_E2E_SHARD=package-surface ASPIRE_EXTENSION_E2E_SPEC=out/test-e2e/test-e2e/packageSurface.e2e.test.js ASPIRE_EXTENSION_E2E_CLI_PATH=/path/to/aspire corepack yarn test:e2e
ASPIRE_EXTENSION_E2E_SHARD=edge-cases ASPIRE_EXTENSION_E2E_SPEC=out/test-e2e/test-e2e/edgeCases.e2e.test.js ASPIRE_EXTENSION_E2E_CLI_PATH=/path/to/aspire corepack yarn test:e2e
ASPIRE_EXTENSION_E2E_ENABLE_AZURE_FUNCTIONS=true ASPIRE_EXTENSION_E2E_DOTNET_RUNTIME_VSIX=/path/to/vscode-dotnet-runtime.vsix ASPIRE_EXTENSION_E2E_CSHARP_VSIX=/path/to/vscode-csharp.vsix ASPIRE_EXTENSION_E2E_AZURE_RESOURCE_GROUPS_VSIX=/path/to/vscode-azureresourcegroups.vsix ASPIRE_EXTENSION_E2E_AZURE_FUNCTIONS_VSIX=/path/to/vscode-azurefunctions.vsix ASPIRE_EXTENSION_E2E_SHARD=azure-functions ASPIRE_EXTENSION_E2E_SPEC=out/test-e2e/test-e2e/azureFunctions.e2e.test.js ASPIRE_EXTENSION_E2E_CLI_PATH=/path/to/aspire corepack yarn test:e2e
ASPIRE_EXTENSION_E2E_ENABLE_WINUI=true ASPIRE_EXTENSION_E2E_DOTNET_RUNTIME_VSIX=/path/to/vscode-dotnet-runtime.vsix ASPIRE_EXTENSION_E2E_CSHARP_VSIX=/path/to/vscode-csharp-win32-x64.vsix ASPIRE_EXTENSION_E2E_SHARD=winui-debug ASPIRE_EXTENSION_E2E_SPEC=out/test-e2e/test-e2e/winUiDebug.e2e.test.js ASPIRE_EXTENSION_E2E_CLI_PATH=/path/to/aspire corepack yarn test:e2e

Blazor WebAssembly browser debugger E2E

Run the managed browser debugger proof with platform-specific .NET Install Tool and C# VSIX files:

ASPIRE_EXTENSION_E2E_SHARD=browser-debugger \
ASPIRE_EXTENSION_E2E_SPEC=out/test-e2e/test-e2e/browserDebugger.e2e.test.js \
ASPIRE_EXTENSION_E2E_DOTNET_RUNTIME_VSIX="$DOTNET_RUNTIME_VSIX" \
ASPIRE_EXTENSION_E2E_CSHARP_VSIX="$CSHARP_VSIX" \
corepack yarn test:e2e

Linux exercises Chrome, while Windows exercises Edge. The shard generates standalone, hosted-global, and hosted-per-page .NET 10 fixtures. Both VSIX files must match the current platform, and C# must be 2.145.15-prerelease or newer.

The E2E fixtures target .NET 10, matching the SDK pinned by the repository's global.json. The Azure Functions shard additionally requires Azure Functions Core Tools v4 (func) on PATH. It installs the real .NET Install Tool, C#, Azure Resource Groups, and Azure Functions extensions into the isolated VS Code instance, generates a dedicated HTTPS certificate with shell-sensitive arguments, and activates the Azure Functions extension so it registers its func task definition and listeners. Aspire then creates and runs a registered func: host start task for the generated .NET isolated Functions resource; the shard probes its HTTPS endpoint and verifies that stopping the Aspire resource ends the same VS Code task. CI runs this shard on Linux with pinned, checksum-verified copies of Core Tools 4.12.1, .NET Install Tool 3.1.0, C# 2.148.23 for Linux x64, Azure Resource Groups 0.12.7, and Azure Functions 1.22.0.

The winui-debug shard is Windows-only. It installs pinned .NET Install Tool and C# VSIX files into the offline VS Code instance, generates a self-contained unpackaged WinUI project, and debugs it as an Aspire project resource. Before starting Aspire, the shard waits for the C# definition provider to resolve the XAML-generated InitializeComponent method. This prevents the C# language server's design-time build and the Aspire CLI build from invoking XamlCompiler.exe against the same obj\...\input.json; a restore or prebuild alone is insufficient because the later build still runs the XAML compiler passes. The app then writes a readiness marker from Application.OnLaunched; reaching that marker proves WinUI passed the Application.Start failure point covered by the regression. Dependabot does not update the raw Marketplace VSIX URLs or checksums. Treat them as regression inputs rather than current-version dependencies: rebaseline them manually only when VS Code compatibility requires it or the fixture is deliberately moved, and before changing the C# version verify that the old TargetPath launch still reproduces the original crash while the RunCommand launch passes.

ASPIRE_EXTENSION_E2E_SPEC accepts either one compiled spec path or a glob, so local runs can target one spec or a small subset without editing the test runner:

corepack yarn compile-e2e
ASPIRE_EXTENSION_E2E_SHARD=debug-dashboard-local ASPIRE_EXTENSION_E2E_SPEC=out/test-e2e/test-e2e/debugDashboard.e2e.test.js ASPIRE_EXTENSION_E2E_CLI_PATH=/path/to/aspire corepack yarn test:e2e
ASPIRE_EXTENSION_E2E_SHARD=fast-subset ASPIRE_EXTENSION_E2E_SPEC='out/test-e2e/test-e2e/{commandPalette,settingsFiles}.e2e.test.js' ASPIRE_EXTENSION_E2E_CLI_PATH=/path/to/aspire corepack yarn test:e2e
ASPIRE_EXTENSION_E2E_SHARD=tree-subset ASPIRE_EXTENSION_E2E_SPEC='out/test-e2e/test-e2e/*Tree.e2e.test.js' ASPIRE_EXTENSION_E2E_CLI_PATH=/path/to/aspire corepack yarn test:e2e

The current shards cover command palette and terminal routing, settings-file creation/opening with an isolated Aspire home, workspace AppHost discovery/configuration changes, remembered multi-root dynamic debug configuration selection, AppHost run/stop/resource rendering, tree action commands for copy/open/log/resource operations, debug/dashboard lifecycle, unpackaged WinUI resource debugging, Azure Functions HTTPS startup and termination through the real Functions extension, a zero-to-running flow that routes the Aspire new/add terminal commands, creates a C# AppHost through the CLI, adds an integration package, registers a source breakpoint, and debugs the generated AppHost, the package.json contribution surface including exact activation events, command registration, menu/view/settings inventory, JSON validation, walkthrough command registration, CodeLens routing, and debug launch command routing, plus negative-path edge cases for invalid control payloads, missing tree targets, CLI-independent settings commands, and launch-state cleanup.

Diagnostics are left under extension/.test-results, extension/.test-storage, and extension/.test-workspaces, with shard-specific subdirectories when ASPIRE_EXTENSION_E2E_SHARD is set. The runner also sets ASPIRE_HOME to an isolated per-run directory and copies it into diagnostics before cleanup so settings-file failures do not touch or depend on the real user profile. These folders are ignored by git and are uploaded by CI when the E2E job runs.

Linux CI shards also record the Xvfb display with ffmpeg. The default workflow mode is ASPIRE_EXTENSION_E2E_RECORDING_MODE=always, which keeps and uploads extension/.test-recordings/<shard>/*.mp4 for both successful and failing Linux shards. Set ASPIRE_EXTENSION_E2E_RECORDING_MODE=failure when you only want failed-run videos, or off to disable recording. The default capture size is 1280x1024, matching the hosted Xvfb display; override with ASPIRE_EXTENSION_E2E_RECORDING_SIZE if you run under a larger display. Recording is intentionally Linux-only because the Windows E2E jobs do not run under Xvfb and hosted desktop capture is less reliable.

E2E tests should avoid fixed sleeps for readiness. Prefer the observation state written by the extension test bridge, ExTester wait APIs, unique generated workspaces, explicit per-phase timeouts, and cleanup through aspire stop --apphost. This is intentionally stricter than a normal smoke test because the suite runs a real VS Code, CLI, AppHost, terminal, and dashboard path. See https://github.com/microsoft/aspire/issues/17727 for the original tracking issue.

Localizing user-facing strings

All user-facing text must be localized:

  • Strings shown from extension code: add to both src/loc/strings.ts and package.nls.json.
  • package.json contribution strings (command titles, setting descriptions, …): use a %placeholder% key defined in package.nls.json.

Edit only the base package.nls.json / strings.ts. The translated package.nls.*.json files are generated by a separate workflow — don't hand-edit them.

Updating dependency overrides

The extension is built with yarn, pinned to the version recorded in packageManager of package.json. package.json uses resolutions for transitive dependency pins and yarn.lock is the authoritative lockfile.

When pinning a transitive dependency (e.g. to address a security advisory), add the pin to resolutions and regenerate yarn.lock in the same change:

corepack yarn install

The build rejects public registry URLs in yarn.lock; ensure regenerated entries resolve through the dotnet-public-npm feed (public, so no credentials are needed to consume it).

Check feed availability before pinning a just-published version. dotnet-public-npm mirrors npmjs on a lag, and package metadata can appear before its tarball is anonymously available. CI then fails at yarn install --frozen-lockfile; for example:

error Error: https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/vscode-extension-tester/-/vscode-extension-tester-8.24.0.tgz: Request failed "401 Unauthorized"

This is easy to miss locally, because a global .npmrc pointing at another registry lets yarn install succeed on your machine while the rewritten yarn.lock URL remains unavailable to anonymous CI. The options are to wait or to pin a version the approved feed already serves anonymously. Check the whole set of new resolved URLs, since a bump also drags in transitive dependencies published at the same time:

git diff origin/main -- yarn.lock | grep '^+.*resolved "' | sed 's/^+ *resolved "//; s/".*$//' | sort -u \
  | while read -r url; do echo "$(curl -s -o /dev/null -w '%{http_code}' "$url")  $url"; done

Only 303 confirms that the tarball is anonymously available and safe to pin. 401 means it is unavailable to anonymous consumers; treat any other status as inconclusive rather than assuming the package is ready.

Pinning back to the previous release is not automatically a neutral fallback for vscode-extension-tester, because consecutive releases can declare disjoint VS Code ranges. Compare the supportedVersions field of both candidates before downgrading — 8.23.0 declares 1.109.51.111.0 while 8.24.0 declares 1.129.11.131.0, so reverting the pin while using min/max would move the alias backwards. If the approved feed does not anonymously serve the newer package yet, keep the available dependency and use a concrete VS Code default until the package can be safely updated:

npm view vscode-extension-tester@8.24.0 supportedVersions

Updating the Yarn version

Edit the "packageManager": "yarn@x.y.z" field in extension/package.json. The next build.sh / build.ps1 run seeds Corepack's cache with that version before calling corepack yarn …. No further changes are required in build.sh, build.ps1, or extension/Extension.proj.

npm mirror note. .npmrc and the build scripts' NPM_REGISTRY route npm downloads through the dnceng dotnet-public-npm Azure Artifacts feed. Anonymous reads of cached versions work without credentials, covering everything pinned in yarn.lock. Exception: the first request for a never-cached version triggers a pull-through fetch that fails with HTTP 401 (subsequent reads succeed). This only affects maintainers bumping the pinned Corepack or Yarn version; pre-seed with credentials via npm install --global --registry https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/ corepack@<version> or npm pack --registry https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/ yarn@<version>. Don't point COREPACK_NPM_REGISTRY at this feed for Yarn: Corepack requests the /<package>/<version> route, which Azure Artifacts returns 404 for even when the package exists.

Troubleshooting

EACCES from npm install --global on Linux/macOS

The build scripts run npm install --global corepack@<version> to pin the Corepack version. On systems where Node.js is installed from a package manager (apt, yum, the official .pkg), the npm global prefix is typically /usr/lib/node_modules or /usr/local/lib/node_modules, which is root-owned. The install will fail with EACCES. The cleanest fix is to use a Node version manager that puts the npm prefix in your home directory:

  • nvm — installs Node and configures the npm prefix automatically.
  • fnm, asdf, volta — same idea, different tradeoffs.

Alternatively, point npm at a user-writable prefix without changing your Node install:

mkdir -p ~/.npm-global
npm config set prefix ~/.npm-global
export PATH="$HOME/.npm-global/bin:$PATH"   # add to ~/.bashrc or ~/.zshrc

corepack version mismatch from build.sh / build.ps1

This means the corepack resolved from PATH is not the one we just installed via npm install -g. Most often the system Node install (/usr/bin/corepack, %ProgramFiles%\nodejs\corepack.cmd) is sitting in front of the npm global bin directory. On Windows, ensure %APPDATA%\npm comes before %ProgramFiles%\nodejs on PATH. On Linux/macOS, follow the EACCES remediation above and the npm prefix will be on PATH ahead of the system Node directory.