Fix VS Code release publish failing with npm E401 - #772
Fix VS Code release publish failing with npm E401#772Giulia Stocco (gfs) wants to merge 2 commits into
Conversation
The publish step ran `npx @vscode/vsce publish`. npx only reuses an already-installed binary when its first argument matches a bin name, so passing the package name `@vscode/vsce` always forces a manifest fetch from the npm registry. Inside the AzureCLI@2 task that fetch is unauthenticated against the Azure Artifacts feed and returns E401, even though the preceding step already installed vsce globally. Invoke the globally installed `vsce.cmd` by its full path instead, so publishing needs no registry access at all, and throw a clear error if the binary is missing or the publish exits non-zero. Drop the `npm_config_registry` env var that was added to make the npx fetch resolve. It is redundant with the `.npmrc` copied into the staging directory and has no bearing on the publish target, since `vsce publish` uploads to the Visual Studio Marketplace rather than an npm registry. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: a61244f3-4b6b-4a24-8e49-a12ce295259e
There was a problem hiding this comment.
Pull request overview
This PR fixes the DevSkim VS Code release pipeline publish failure (npm E401) by avoiding npx @vscode/vsce (which triggers an npm registry fetch) and directly invoking the globally installed vsce binary, removing dependence on npm registry authentication during the publish step.
Changes:
- Invoke the globally installed
vsce.cmdby full path during theAzureCLI@2publish step, and add explicit$LASTEXITCODEchecking. - Remove
npm_config_registryfrom the publish step environment since publishing goes to the VS Marketplace API, not an npm registry. - Add a changelog entry documenting the pipeline fix.
Show a summary per file
| File | Description |
|---|---|
| Pipelines/vscode/devskim-vscode-release.yml | Reworks the publish step to call the preinstalled vsce binary directly and removes the registry override that caused E401. |
| Changelog.md | Documents the pipeline publish/authentication fix. |
Review details
- Files reviewed: 2/2 changed files
- Comments generated: 2
- Review effort level: Lite
| @@ -184,18 +184,29 @@ | |||
| workingDirectory: '$(Build.StagingDirectory)' | |||
| inlineScript: | | |||
| $packPath = Resolve-Path $env:BUILD_STAGINGDIRECTORY\*.vsix | |||
There was a problem hiding this comment.
Good catch, fixed in d350a14.
Confirmed both failure modes in pwsh against staging dirs holding 0, 1, and 2 .vsix files:
| files | Resolve-Path result |
|---|---|
| 0 | $null (error is non-terminating, so the script continues and --packagePath gets nothing) |
| 1 | PathInfo, stringifies correctly |
| 2 | Object[], splats into two separate native arguments |
The zero case is the more likely one here, since it is what a silently failed sign or move step would produce, and it would have surfaced as a confusing vsce error rather than pointing at the real culprit.
Now enumerates explicitly and throws unless exactly one artifact is present:
$vsixFiles = @(Get-ChildItem -Path $env:BUILD_STAGINGDIRECTORY -Filter '*.vsix' -File)
if ($vsixFiles.Count -ne 1)
{
throw "Expected exactly one .vsix in $env:BUILD_STAGINGDIRECTORY, found $($vsixFiles.Count)"
}
$packPath = $vsixFiles[0].FullNameVerified it yields a plain String for the single-file case and throws with the count for 0 and 2.
| # `npx @vscode/vsce` cannot be used here: npx only short circuits to an already | ||
| # installed binary when the argument matches a bin name, so a package name always | ||
| # forces a manifest fetch from the npm registry, which fails with E401 in this task. | ||
| $vsce = Join-Path (npm prefix -g) 'vsce.cmd' |
There was a problem hiding this comment.
Not taking this one, since npm bin was removed in npm v9 and applying it would break the publish step outright:
$ npm --version
11.16.0
$ npm bin -g
Unknown command: "bin"
$ echo $?
1
The 1ES Windows agents ship npm well past v9, so this would swap a working lookup for a hard failure.
The general premise about prefix and bin diverging is fair, but it does not apply here because this pool is pinned to os: windows, and on Windows npm treats the global prefix as the global bin directory:
get globalBin () {
const b = this.globalPrefix
return process.platform !== 'win32' ? resolve(b, 'bin') : b
}So Join-Path (npm prefix -g) 'vsce.cmd' resolves to exactly the directory npm install -g writes the shim into. The Test-Path guard also means a wrong directory would fail with an explicit message rather than silently.
I added a comment in d350a14 recording this, so the suggestion does not get re-proposed later.
Address review feedback on the publish step. `Resolve-Path` returns `$null` when no `.vsix` is present and an array when more than one matches, and neither is safe to hand to `--packagePath`: the first silently passes no path, and the second splats into multiple native arguments. Enumerate the artifacts explicitly and throw unless exactly one is found, which keeps the failure attributable to signing or staging instead of surfacing as a confusing vsce error. Also record why `npm prefix -g` is used rather than `npm bin -g`, since `npm bin` was removed in npm v9 and the global bin directory is the global prefix itself on Windows. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: a61244f3-4b6b-4a24-8e49-a12ce295259e
Why
The
DevSkim-VSCode-Releasepipeline has been unable to publish for several runs. ThePublishing with Managed Identitystep fails immediately after printing the version:The suspicion was that
npm_config_registrypointing at the internal Azure Artifacts feed was sending the extension to the wrong place. That is not what is happening.vsce publishuploads to the Visual Studio Marketplace over its own API, authenticated by--azure-credentialusing the AAD token from the task'saz login. No npm registry is ever a publish target. The registry setting only controls where npm and npx download packages from, so it was never going to redirect the release.Root cause
The failure is
npx @vscode/vsce, introduced in #742 when the barevscecommand was not resolvable on the agent'sPATH.npx only short circuits to an already-installed binary when its first argument matches a bin name. From
libnpmexec/lib/index.js:@vscode/vsceis a package name, so npx checks for${globalBin}/@vscode/vsce, which does not exist because the bin is namedvsce. It falls through to apacote.manifest()call.npm_config_registry(added in #743 to get that fetch working) aimed it at the authenticated feed, where the publish task has no valid npm credential, producing E401. Thenpm install -g @vscode/vscefrom the preceding step was never consulted at all.This reproduces exactly. With a fake global prefix containing a
vsceshim and the registry pointed at the feed:npx vsce publish ...npx @vscode/vsce publish ...npm error code E401 / Unable to authenticate..., byte-identical to the pipeline logApproach
Skip npx entirely and invoke the binary that the previous step already installed, by its full path:
Publishing now needs no npm registry access whatsoever, which removes the whole class of feed-auth failures from this step.
npm prefix -galso sidesteps the original #742 problem, since it resolves the global prefix directly rather than relying on it being onPATH.Notes for reviewers
npx vsce. Changing the argument to the bin name would also fix the E401, but if the global install were ever missing, npx would silently fall back to installing the deprecated legacyvscepackage from the registry. That package tops out at 2.15.0 and predates--azure-credential, which would fail in a much more confusing way. The explicit path fails loudly instead.npm_config_registrydoes not loosen package sourcing. It is dead once npx is gone, and.npmrc.pipelineis already copied into$(Build.StagingDirectory), which is the step'sworkingDirectory, so the same feed stays pinned for anything else that shells out to npm.$LASTEXITCODEchecking. The task already propagated the exit code, but making it explicit keeps the failure attributable now that the publish call is no longer the last statement in each branch.1.0.96heading is the current git height (95) plus this commit. Per the repo's versioning guidance, this should be re-checked withnbgv get-version -v SimpleVersionbefore merging if other PRs land first. I could not runnbgvlocally because installing it requires auth against the private NuGet feed.npm installat the end of theInstall vsce and dependenciesstep is a no-op, since$(Build.StagingDirectory)has nopackage.json. It is unrelated to this failure so I did not touch it.Validation
The pipeline YAML parses, and the inline PowerShell parses cleanly via
[System.Management.Automation.Language.Parser]::ParseInput.nuget.configand.npmrc.pipelineare untouched.