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) —
npmmust be on the PATH (it ships with Node.js). The build scripts (build.sh/build.ps1) install a pinned Corepack vianpm install -g corepack@<version>from the configured registry and seed Corepack's cache with the Yarn release pinned by thepackageManagerfield inextension/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-npmAzure Artifacts feed; every version pinned inyarn.lockis already cached and served anonymously, soyarn installworks 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/:
| Directory | Contents |
|---|---|
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 ExtensionorRun 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.tsandpackage.nls.json. package.jsoncontribution strings (command titles, setting descriptions, …): use a%placeholder%key defined inpackage.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-npmmirrors npmjs on a lag, and package metadata can appear before its tarball is anonymously available. CI then fails atyarn 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
.npmrcpointing at another registry letsyarn installsucceed on your machine while the rewrittenyarn.lockURL 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 newresolvedURLs, 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"; doneOnly
303confirms that the tarball is anonymously available and safe to pin.401means 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 thesupportedVersionsfield of both candidates before downgrading — 8.23.0 declares1.109.5–1.111.0while 8.24.0 declares1.129.1–1.131.0, so reverting the pin while usingmin/maxwould 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.
.npmrcand the build scripts'NPM_REGISTRYroute npm downloads through the dncengdotnet-public-npmAzure Artifacts feed. Anonymous reads of cached versions work without credentials, covering everything pinned inyarn.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 vianpm install --global --registry https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/ corepack@<version>ornpm pack --registry https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/ yarn@<version>. Don't pointCOREPACK_NPM_REGISTRYat 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.