Skip to content

Fix VS Code release publish failing with npm E401 - #772

Open
Giulia Stocco (gfs) wants to merge 2 commits into
mainfrom
gfs-fix-vscode-publish-npm-e401
Open

Fix VS Code release publish failing with npm E401#772
Giulia Stocco (gfs) wants to merge 2 commits into
mainfrom
gfs-fix-vscode-publish-npm-e401

Conversation

@gfs

Copy link
Copy Markdown
Contributor

Why

The DevSkim-VSCode-Release pipeline has been unable to publish for several runs. The Publishing with Managed Identity step fails immediately after printing the version:

Publishing as official release = 1.0.84
npm error code E401
npm error Unable to authenticate, your authentication token seems to be invalid.

The suspicion was that npm_config_registry pointing at the internal Azure Artifacts feed was sending the extension to the wrong place. That is not what is happening. vsce publish uploads to the Visual Studio Marketplace over its own API, authenticated by --azure-credential using the AAD token from the task's az 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 bare vsce command was not resolvable on the agent's PATH.

npx only short circuits to an already-installed binary when its first argument matches a bin name. From libnpmexec/lib/index.js:

} else if (globalPath && await fileExists(`${globalBin}/${args[0]}`)) {
  binPaths.push(globalBin)
  return await run()   // no registry call
}
packages.push(args[0])  // falls through -> pacote.manifest() -> registry

@vscode/vsce is a package name, so npx checks for ${globalBin}/@vscode/vsce, which does not exist because the bin is named vsce. It falls through to a pacote.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. The npm install -g @vscode/vsce from the preceding step was never consulted at all.

This reproduces exactly. With a fake global prefix containing a vsce shim and the registry pointed at the feed:

invocation result
npx vsce publish ... runs the pre-installed binary, exit 0, no network access
npx @vscode/vsce publish ... npm error code E401 / Unable to authenticate..., byte-identical to the pipeline log

Approach

Skip npx entirely and invoke the binary that the previous step already installed, by its full path:

$vsce = Join-Path (npm prefix -g) 'vsce.cmd'
if (-not (Test-Path $vsce)) { throw "vsce was not found at $vsce - ..." }
& $vsce publish --packagePath $packPath --azure-credential
if ($LASTEXITCODE -ne 0) { throw "vsce publish failed with exit code $LASTEXITCODE" }

Publishing now needs no npm registry access whatsoever, which removes the whole class of feed-auth failures from this step. npm prefix -g also sidesteps the original #742 problem, since it resolves the global prefix directly rather than relying on it being on PATH.

Notes for reviewers

  • Why the full path instead of just 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 legacy vsce package 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.
  • Removing npm_config_registry does not loosen package sourcing. It is dead once npx is gone, and .npmrc.pipeline is already copied into $(Build.StagingDirectory), which is the step's workingDirectory, so the same feed stays pinned for anything else that shells out to npm.
  • Added explicit $LASTEXITCODE checking. 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.
  • Changelog version. The 1.0.96 heading is the current git height (95) plus this commit. Per the repo's versioning guidance, this should be re-checked with nbgv get-version -v SimpleVersion before merging if other PRs land first. I could not run nbgv locally because installing it requires auth against the private NuGet feed.
  • Left alone: the bare npm install at the end of the Install vsce and dependencies step is a no-op, since $(Build.StagingDirectory) has no package.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.config and .npmrc.pipeline are untouched.

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

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 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.cmd by full path during the AzureCLI@2 publish step, and add explicit $LASTEXITCODE checking.
  • Remove npm_config_registry from 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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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].FullName

Verified 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'

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants