From 522a45dd428af6d87ef40e22cc771a7958b67eb8 Mon Sep 17 00:00:00 2001 From: bschnurr Date: Tue, 20 Jan 2026 15:10:06 -0800 Subject: [PATCH 01/11] pass .nmprc to publish --- build/templates/package.yml | 8 ++++++++ build/templates/publish-extension.yml | 18 +++++++++++++----- build/templates/setup.yml | 14 -------------- 3 files changed, 21 insertions(+), 19 deletions(-) diff --git a/build/templates/package.yml b/build/templates/package.yml index 1ed988eb..bba139d7 100644 --- a/build/templates/package.yml +++ b/build/templates/package.yml @@ -131,3 +131,11 @@ jobs: contents: '*.vsix' targetFolder: $(Build.ArtifactStagingDirectory)/drop displayName: 📦 Copy VSIX to staging + + - task: CopyFiles@2 + inputs: + sourceFolder: ${{ parameters.workingDirectory }} + contents: '.npmrc' + targetFolder: $(Build.ArtifactStagingDirectory)/drop + condition: and(succeeded(), exists('${{ parameters.workingDirectory }}/.npmrc')) + displayName: 📦 Copy .npmrc to staging diff --git a/build/templates/publish-extension.yml b/build/templates/publish-extension.yml index 163cb04d..186802f2 100644 --- a/build/templates/publish-extension.yml +++ b/build/templates/publish-extension.yml @@ -165,17 +165,25 @@ jobs: type: releaseJob # This makes a job a release job isProduction: true # Indicates a production release steps: - - template: setup.yml - parameters: - installNode: true - installPython: false - - task: 1ES.DownloadPipelineArtifact@1 inputs: artifactName: extension targetPath: $(Build.ArtifactStagingDirectory)/${{ parameters.publishFolder }} displayName: 🚛 Download signed extension + # setup.yml runs npmAuthenticate against a repo-root .npmrc. + # Pull the staged .npmrc from the VSIX build artifact and place it at the repo root. + - task: 1ES.DownloadPipelineArtifact@1 + inputs: + artifactName: .npmrc + targetPath: $(Build.SourcesDirectory) + displayName: 🚛 Download VSIX artifact (for .npmrc) + + - template: setup.yml + parameters: + installNode: true + installPython: false + # Extract VSIX to read publisher/version for GitHub release tagging. # Use Agent.TempDirectory to avoid reusing Build.ArtifactStagingDirectory which # is reserved for final artifact staging. diff --git a/build/templates/setup.yml b/build/templates/setup.yml index 4cd0aa80..01515e17 100644 --- a/build/templates/setup.yml +++ b/build/templates/setup.yml @@ -9,20 +9,6 @@ parameters: steps: - ${{ if eq(parameters.installNode, true) }}: - - pwsh: | - if (-not (Test-Path '.npmrc')) { - Write-Host 'No .npmrc found, creating one with public npm registry' - @" - # Force public npm registry to avoid CI auth (E401) when no token is provided - registry=https://registry.npmjs.org/ - # Do not require auth for public installs - always-auth=false - "@ | Out-File -FilePath '.npmrc' -Encoding utf8 - } else { - Write-Host '.npmrc already exists' - } - displayName: Ensure .npmrc exists - - task: npmAuthenticate@0 inputs: workingFile: .npmrc From f335e240f7745a3af8732ac296682bb991519c8e Mon Sep 17 00:00:00 2001 From: bschnurr Date: Tue, 20 Jan 2026 15:28:22 -0800 Subject: [PATCH 02/11] just always copy --- build/templates/package.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/build/templates/package.yml b/build/templates/package.yml index bba139d7..78864f7a 100644 --- a/build/templates/package.yml +++ b/build/templates/package.yml @@ -137,5 +137,4 @@ jobs: sourceFolder: ${{ parameters.workingDirectory }} contents: '.npmrc' targetFolder: $(Build.ArtifactStagingDirectory)/drop - condition: and(succeeded(), exists('${{ parameters.workingDirectory }}/.npmrc')) displayName: 📦 Copy .npmrc to staging From 388173d6fa2c26401f93e025d0f1e7f31b19c691 Mon Sep 17 00:00:00 2001 From: bschnurr Date: Tue, 20 Jan 2026 15:44:20 -0800 Subject: [PATCH 03/11] temp disable store publish --- build/templates/publish.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build/templates/publish.yml b/build/templates/publish.yml index 66ac7f9f..53ad3041 100644 --- a/build/templates/publish.yml +++ b/build/templates/publish.yml @@ -103,11 +103,11 @@ steps: if ('${{ parameters.preRelease }}' -eq 'True') { Write-Host 'Publishing as pre-release' Write-Host "Executing: npx vsce publish --pat *** --packagePath $vsixPath --manifestPath $manifestPath --signaturePath $signaturePath --pre-release" - npx vsce publish --pat $aadToken --packagePath $vsixPath --manifestPath $manifestPath --signaturePath $signaturePath --pre-release + # npx vsce publish --pat $aadToken --packagePath $vsixPath --manifestPath $manifestPath --signaturePath $signaturePath --pre-release } else { Write-Host 'Publishing as stable release' Write-Host "Executing: npx vsce publish --pat *** --packagePath $vsixPath --manifestPath $manifestPath --signaturePath $signaturePath" - npx vsce publish --pat $aadToken --packagePath $vsixPath --manifestPath $manifestPath --signaturePath $signaturePath + # npx vsce publish --pat $aadToken --packagePath $vsixPath --manifestPath $manifestPath --signaturePath $signaturePath } if ($LASTEXITCODE -ne 0) { From 7812cec7cd31bb89a62cbde767a74897bb55ba62 Mon Sep 17 00:00:00 2001 From: bschnurr Date: Tue, 20 Jan 2026 18:15:11 -0800 Subject: [PATCH 04/11] try uploading npmrc --- build/templates/package.yml | 26 ++++++++++++++++++++------ build/templates/publish-extension.yml | 9 ++++----- build/templates/setup.yml | 5 ++++- build/templates/sign.yml | 8 ++++++++ 4 files changed, 36 insertions(+), 12 deletions(-) diff --git a/build/templates/package.yml b/build/templates/package.yml index 78864f7a..25df5769 100644 --- a/build/templates/package.yml +++ b/build/templates/package.yml @@ -71,6 +71,11 @@ jobs: targetPath: '$(Build.ArtifactStagingDirectory)/drop' artifactName: vsix-${{ coalesce(platform.vsceTarget, 'universal') }} + - output: pipelineArtifact + displayName: Publish .npmrc + targetPath: '$(Build.SourcesDirectory)/.npmrc' + artifactName: npmrc + steps: - template: setup.yml@self @@ -132,9 +137,18 @@ jobs: targetFolder: $(Build.ArtifactStagingDirectory)/drop displayName: 📦 Copy VSIX to staging - - task: CopyFiles@2 - inputs: - sourceFolder: ${{ parameters.workingDirectory }} - contents: '.npmrc' - targetFolder: $(Build.ArtifactStagingDirectory)/drop - displayName: 📦 Copy .npmrc to staging + - ${{ if eq(coalesce(platform.vsceTarget, 'universal'), 'universal') }}: + - pwsh: | + $source = "${{ parameters.workingDirectory }}/.npmrc" + $destDir = "$(Build.ArtifactStagingDirectory)/npmrc" + $dest = Join-Path $destDir '.npmrc' + + New-Item -ItemType Directory -Path $destDir -Force | Out-Null + + if (-not (Test-Path $source)) { + throw "Expected .npmrc at: $source" + } + + Copy-Item -LiteralPath $source -Destination $dest -Force + Write-Host "Copied $source -> $dest" + displayName: 📦 Stage .npmrc for artifact diff --git a/build/templates/publish-extension.yml b/build/templates/publish-extension.yml index 186802f2..440bc585 100644 --- a/build/templates/publish-extension.yml +++ b/build/templates/publish-extension.yml @@ -171,18 +171,17 @@ jobs: targetPath: $(Build.ArtifactStagingDirectory)/${{ parameters.publishFolder }} displayName: 🚛 Download signed extension - # setup.yml runs npmAuthenticate against a repo-root .npmrc. - # Pull the staged .npmrc from the VSIX build artifact and place it at the repo root. - task: 1ES.DownloadPipelineArtifact@1 inputs: - artifactName: .npmrc - targetPath: $(Build.SourcesDirectory) - displayName: 🚛 Download VSIX artifact (for .npmrc) + artifactName: npmrc + targetPath: $(Pipeline.Workspace)/npmrc + displayName: 🚛 Download .npmrc artifact - template: setup.yml parameters: installNode: true installPython: false + npmrcPath: $(Pipeline.Workspace)/npmrc/.npmrc # Extract VSIX to read publisher/version for GitHub release tagging. # Use Agent.TempDirectory to avoid reusing Build.ArtifactStagingDirectory which diff --git a/build/templates/setup.yml b/build/templates/setup.yml index 01515e17..671525e2 100644 --- a/build/templates/setup.yml +++ b/build/templates/setup.yml @@ -6,12 +6,15 @@ parameters: - name: installPython type: boolean default: true + - name: npmrcPath + type: string + default: '$(Build.SourcesDirectory)/.npmrc' steps: - ${{ if eq(parameters.installNode, true) }}: - task: npmAuthenticate@0 inputs: - workingFile: .npmrc + workingFile: ${{ parameters.npmrcPath }} - script: npm config get registry displayName: Verify NPM Registry diff --git a/build/templates/sign.yml b/build/templates/sign.yml index 83948576..9392b324 100644 --- a/build/templates/sign.yml +++ b/build/templates/sign.yml @@ -74,7 +74,15 @@ steps: nugetConfigPath: '$(Build.SourcesDirectory)/build/NuGet.config' # Setup Node.js and npm authentication + - task: DownloadPipelineArtifact@2 + inputs: + artifactName: npmrc + targetPath: $(Pipeline.Workspace)/.npmrc + displayName: 🚛 Download .npmrc artifact + - template: setup.yml@self + parameters: + npmrcPath: $(Pipeline.Workspace)/.npmrc - task: Npm@1 displayName: 'npm ci (install vsce)' From a923b6c3acbe7b81eff502773dc5e878f7ae34c1 Mon Sep 17 00:00:00 2001 From: bschnurr Date: Tue, 20 Jan 2026 19:08:30 -0800 Subject: [PATCH 05/11] check .npmrc is a file --- build/templates/setup.yml | 45 +++++++++++++++++++++++++++++++++++++++ build/templates/sign.yml | 4 ++-- 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/build/templates/setup.yml b/build/templates/setup.yml index 671525e2..3361bce9 100644 --- a/build/templates/setup.yml +++ b/build/templates/setup.yml @@ -12,10 +12,55 @@ parameters: steps: - ${{ if eq(parameters.installNode, true) }}: + - pwsh: | + $npmrcPath = "${{ parameters.npmrcPath }}" + + if (Test-Path -LiteralPath $npmrcPath -PathType Container) { + throw "npmrcPath points to a directory (expected a file): $npmrcPath" + } + + $parent = Split-Path -Parent $npmrcPath + if ($parent -and -not (Test-Path -LiteralPath $parent)) { + New-Item -ItemType Directory -Path $parent -Force | Out-Null + } + + if (-not (Test-Path -LiteralPath $npmrcPath -PathType Leaf)) { + New-Item -ItemType File -Path $npmrcPath -Force | Out-Null + } + displayName: Ensure .npmrc is a file + - task: npmAuthenticate@0 inputs: workingFile: ${{ parameters.npmrcPath }} + - pwsh: | + $source = "${{ parameters.npmrcPath }}" + $dest = "$(Build.SourcesDirectory)/.npmrc" + + if (-not (Test-Path -LiteralPath $source -PathType Leaf)) { + throw "Expected authenticated .npmrc file at: $source" + } + + if (Test-Path -LiteralPath $dest -PathType Container) { + throw "Expected destination .npmrc to be a file, but it is a directory: $dest" + } + + $srcFull = [System.IO.Path]::GetFullPath($source) + $destFull = [System.IO.Path]::GetFullPath($dest) + + if ($srcFull -ieq $destFull) { + Write-Host "Authenticated .npmrc already in repo root: $dest" + return + } + + if (-not (Test-Path -LiteralPath $dest -PathType Leaf)) { + Copy-Item -LiteralPath $source -Destination $dest -Force + Write-Host "Copied authenticated .npmrc -> $dest" + } else { + Write-Host "Repo root .npmrc already exists; not overwriting: $dest" + } + displayName: Copy .npmrc to repo root (if missing) + - script: npm config get registry displayName: Verify NPM Registry diff --git a/build/templates/sign.yml b/build/templates/sign.yml index 9392b324..b198a23f 100644 --- a/build/templates/sign.yml +++ b/build/templates/sign.yml @@ -77,12 +77,12 @@ steps: - task: DownloadPipelineArtifact@2 inputs: artifactName: npmrc - targetPath: $(Pipeline.Workspace)/.npmrc + targetPath: $(Pipeline.Workspace)/npmrc displayName: 🚛 Download .npmrc artifact - template: setup.yml@self parameters: - npmrcPath: $(Pipeline.Workspace)/.npmrc + npmrcPath: $(Pipeline.Workspace)/npmrc/.npmrc - task: Npm@1 displayName: 'npm ci (install vsce)' From 83a3269d3969a733ab1489e454b74f167fdd111f Mon Sep 17 00:00:00 2001 From: bschnurr Date: Wed, 21 Jan 2026 13:25:57 -0800 Subject: [PATCH 06/11] Remove npmrc artifact; add custom registry setup Stop publishing/downloading .npmrc as a pipeline artifact. Instead, configure npm to use a temp user config (NPM_CONFIG_USERCONFIG=$(Agent.TempDirectory)/.npmrc) and support customNPMRegistry in the shared setup template, including auth (npmAuthenticate@0) and lockfile registry rewrites. Thread $(AZURE_ARTIFACTS_FEED) through DevDiv pipeline templates to enable the custom registry flow. --- build/azure-devdiv-pipeline.pre-release.yml | 2 + build/azure-devdiv-pipeline.stable.yml | 2 + build/templates/package.yml | 34 ++--- build/templates/publish-extension.yml | 21 ++- build/templates/setup.yml | 157 ++++++++++++++------ build/templates/sign.yml | 16 +- 6 files changed, 149 insertions(+), 83 deletions(-) diff --git a/build/azure-devdiv-pipeline.pre-release.yml b/build/azure-devdiv-pipeline.pre-release.yml index 3ac8ddf4..a3d96234 100644 --- a/build/azure-devdiv-pipeline.pre-release.yml +++ b/build/azure-devdiv-pipeline.pre-release.yml @@ -108,6 +108,7 @@ extends: buildSteps: ${{ parameters.buildSteps }} isPreRelease: true standardizedVersioning: true + customNPMRegistry: $(AZURE_ARTIFACTS_FEED) - stage: Publish displayName: Publish Extension @@ -122,3 +123,4 @@ extends: ghCreateTag: true ghCreateRelease: true ghReleaseAddChangeLog: true + customNPMRegistry: $(AZURE_ARTIFACTS_FEED) diff --git a/build/azure-devdiv-pipeline.stable.yml b/build/azure-devdiv-pipeline.stable.yml index d8219b3e..d3d000ed 100644 --- a/build/azure-devdiv-pipeline.stable.yml +++ b/build/azure-devdiv-pipeline.stable.yml @@ -105,6 +105,7 @@ extends: buildPlatforms: ${{ parameters.buildPlatforms }} buildSteps: ${{ parameters.buildSteps }} isPreRelease: false + customNPMRegistry: $(AZURE_ARTIFACTS_FEED) - stage: Publish displayName: Publish Extension @@ -116,3 +117,4 @@ extends: publishExtension: ${{ parameters.publishExtension }} preRelease: false teamName: $(TeamName) + customNPMRegistry: $(AZURE_ARTIFACTS_FEED) diff --git a/build/templates/package.yml b/build/templates/package.yml index 25df5769..b4d1e378 100644 --- a/build/templates/package.yml +++ b/build/templates/package.yml @@ -10,6 +10,16 @@ parameters: type: object displayName: 'List of platforms to build' + - name: customNPMRegistry + type: string + default: '' + displayName: 'Custom NPM registry (optional)' + + - name: nodeVersion + type: string + default: '22.17.0' + displayName: 'Node version to install' + - name: buildSteps type: stepList default: [] @@ -71,13 +81,11 @@ jobs: targetPath: '$(Build.ArtifactStagingDirectory)/drop' artifactName: vsix-${{ coalesce(platform.vsceTarget, 'universal') }} - - output: pipelineArtifact - displayName: Publish .npmrc - targetPath: '$(Build.SourcesDirectory)/.npmrc' - artifactName: npmrc - steps: - template: setup.yml@self + parameters: + customNPMRegistry: ${{ parameters.customNPMRegistry }} + nodeVersion: ${{ parameters.nodeVersion }} - ${{ if and(eq(parameters.isPreRelease, true), eq(parameters.standardizedVersioning, true)) }}: - template: modify-extension-version.yml@self @@ -136,19 +144,3 @@ jobs: contents: '*.vsix' targetFolder: $(Build.ArtifactStagingDirectory)/drop displayName: 📦 Copy VSIX to staging - - - ${{ if eq(coalesce(platform.vsceTarget, 'universal'), 'universal') }}: - - pwsh: | - $source = "${{ parameters.workingDirectory }}/.npmrc" - $destDir = "$(Build.ArtifactStagingDirectory)/npmrc" - $dest = Join-Path $destDir '.npmrc' - - New-Item -ItemType Directory -Path $destDir -Force | Out-Null - - if (-not (Test-Path $source)) { - throw "Expected .npmrc at: $source" - } - - Copy-Item -LiteralPath $source -Destination $dest -Force - Write-Host "Copied $source -> $dest" - displayName: 📦 Stage .npmrc for artifact diff --git a/build/templates/publish-extension.yml b/build/templates/publish-extension.yml index 440bc585..b1cc7c92 100644 --- a/build/templates/publish-extension.yml +++ b/build/templates/publish-extension.yml @@ -26,6 +26,16 @@ parameters: type: object displayName: 'List of platforms to sign and publish' + - name: customNPMRegistry + type: string + default: '' + displayName: 'Custom NPM registry (optional)' + + - name: nodeVersion + type: string + default: '22.17.0' + displayName: 'Node version to install' + # Signing parameters - name: signType type: string @@ -151,6 +161,8 @@ jobs: signType: ${{ parameters.signType }} verifySignature: ${{ parameters.verifySignature }} teamName: ${{ parameters.teamName }} + customNPMRegistry: ${{ parameters.customNPMRegistry }} + nodeVersion: ${{ parameters.nodeVersion }} # Job 2: Publish to marketplace - ${{ if eq(parameters.publishExtension, true) }}: @@ -171,17 +183,12 @@ jobs: targetPath: $(Build.ArtifactStagingDirectory)/${{ parameters.publishFolder }} displayName: 🚛 Download signed extension - - task: 1ES.DownloadPipelineArtifact@1 - inputs: - artifactName: npmrc - targetPath: $(Pipeline.Workspace)/npmrc - displayName: 🚛 Download .npmrc artifact - - template: setup.yml parameters: installNode: true installPython: false - npmrcPath: $(Pipeline.Workspace)/npmrc/.npmrc + customNPMRegistry: ${{ parameters.customNPMRegistry }} + nodeVersion: ${{ parameters.nodeVersion }} # Extract VSIX to read publisher/version for GitHub release tagging. # Use Agent.TempDirectory to avoid reusing Build.ArtifactStagingDirectory which diff --git a/build/templates/setup.yml b/build/templates/setup.yml index 3361bce9..7a21b891 100644 --- a/build/templates/setup.yml +++ b/build/templates/setup.yml @@ -6,70 +6,133 @@ parameters: - name: installPython type: boolean default: true + - name: customNPMRegistry + type: string + default: '' + - name: nodeVersion + type: string + default: '22.17.0' + - name: pythonVersion + type: string + default: '3.9' - name: npmrcPath type: string - default: '$(Build.SourcesDirectory)/.npmrc' + default: '$(Agent.TempDirectory)/.npmrc' steps: - ${{ if eq(parameters.installNode, true) }}: - - pwsh: | - $npmrcPath = "${{ parameters.npmrcPath }}" + - task: NodeTool@0 + inputs: + versionSpec: ${{ parameters.nodeVersion }} + checkLatest: true + displayName: 🛠 Install Node ${{ parameters.nodeVersion }} - if (Test-Path -LiteralPath $npmrcPath -PathType Container) { - throw "npmrcPath points to a directory (expected a file): $npmrcPath" - } + - ${{ if ne(parameters.customNPMRegistry, '') }}: + # When using a private/custom registry, configure npm to read auth/config from a temp user config + # instead of relying on a checked-in project .npmrc. + - pwsh: | + $path = "${{ parameters.npmrcPath }}" - $parent = Split-Path -Parent $npmrcPath - if ($parent -and -not (Test-Path -LiteralPath $parent)) { - New-Item -ItemType Directory -Path $parent -Force | Out-Null - } + if (Test-Path -LiteralPath $path -PathType Container) { + throw "npmrcPath points to a directory (expected a file): $path" + } - if (-not (Test-Path -LiteralPath $npmrcPath -PathType Leaf)) { - New-Item -ItemType File -Path $npmrcPath -Force | Out-Null - } - displayName: Ensure .npmrc is a file + $parent = Split-Path -Parent $path + if ($parent -and -not (Test-Path -LiteralPath $parent)) { + New-Item -ItemType Directory -Path $parent -Force | Out-Null + } - - task: npmAuthenticate@0 - inputs: - workingFile: ${{ parameters.npmrcPath }} + if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { + New-Item -ItemType File -Path $path -Force | Out-Null + } + + Write-Host "##vso[task.setvariable variable=NPM_CONFIG_USERCONFIG]$path" + Write-Host "##vso[task.setvariable variable=NPM_CONFIG_REGISTRY]${{ parameters.customNPMRegistry }}" + displayName: 📦 Setup NPM User Config - - pwsh: | - $source = "${{ parameters.npmrcPath }}" - $dest = "$(Build.SourcesDirectory)/.npmrc" + # Configure npm/yarn to use the custom registry and ensure auth headers are sent. + - pwsh: | + $path = "${{ parameters.npmrcPath }}" + $registry = "${{ parameters.customNPMRegistry }}" - if (-not (Test-Path -LiteralPath $source -PathType Leaf)) { - throw "Expected authenticated .npmrc file at: $source" - } + $env:NPM_CONFIG_USERCONFIG = $path + $env:NPM_CONFIG_REGISTRY = $registry - if (Test-Path -LiteralPath $dest -PathType Container) { - throw "Expected destination .npmrc to be a file, but it is a directory: $dest" - } + npm config set registry "$registry" - $srcFull = [System.IO.Path]::GetFullPath($source) - $destFull = [System.IO.Path]::GetFullPath($dest) + # npm >v7 deprecated the `always-auth` config option, refs npm/cli@72a7eeb + # following is a workaround for yarn-like clients to send authorization header for GET + # requests to the registry. + "always-auth=true" | Out-File -FilePath $path -Append -Encoding utf8 - if ($srcFull -ieq $destFull) { - Write-Host "Authenticated .npmrc already in repo root: $dest" - return - } + $yarn = Get-Command yarn -ErrorAction SilentlyContinue + if ($null -ne $yarn) { + yarn config set registry "$registry" + } else { + Write-Host "yarn not found; skipping yarn registry configuration" + } + displayName: 📦 Setup NPM & Yarn - if (-not (Test-Path -LiteralPath $dest -PathType Leaf)) { - Copy-Item -LiteralPath $source -Destination $dest -Force - Write-Host "Copied authenticated .npmrc -> $dest" - } else { - Write-Host "Repo root .npmrc already exists; not overwriting: $dest" - } - displayName: Copy .npmrc to repo root (if missing) + # Populate the temp .npmrc with auth for the configured registry. + - task: npmAuthenticate@0 + inputs: + workingFile: ${{ parameters.npmrcPath }} + displayName: 📦 Setup NPM Authentication + + # Some lockfiles contain hardcoded references to public registries. Rewrite them so installs + # and `npx` resolve from the custom registry consistently. + - pwsh: | + $registry = "${{ parameters.customNPMRegistry }}" + $scriptPath = Join-Path "$(Agent.TempDirectory)" 'setup-npm-registry.js' + + $lines = @( + "const fs = require('fs').promises;", + "const path = require('path');", + "", + "async function* getLockFiles(dir) {", + " const files = await fs.readdir(dir);", + "", + " for (const file of files) {", + " const fullPath = path.join(dir, file);", + " const stat = await fs.stat(fullPath);", + "", + " if (stat.isDirectory()) {", + " if (file === 'node_modules' || file === '.git') {", + " continue;", + " }", + " yield* getLockFiles(fullPath);", + " } else if (file === 'yarn.lock' || file === 'package-lock.json') {", + " yield fullPath;", + " }", + " }", + "}", + "", + "async function rewrite(file, registry) {", + " let contents = await fs.readFile(file, 'utf8');", + " contents = contents.replace(/https:\\/\\/registry\\.[^.]+\\.(com|org)\\//g, registry);", + " await fs.writeFile(file, contents);", + "}", + "", + "async function main() {", + " const root = process.cwd();", + " const registry = process.env.NPM_CONFIG_REGISTRY || " + JSON.stringify($registry) + ";", + "", + " for await (const file of getLockFiles(root)) {", + " await rewrite(file, registry);", + " console.log('Updated node registry:', file);", + " }", + "}", + "", + "main();" + ) + + Set-Content -LiteralPath $scriptPath -Value ($lines -join "`n") -Encoding utf8 + node $scriptPath + displayName: 📦 Setup NPM Registry - script: npm config get registry displayName: Verify NPM Registry - - task: NodeTool@0 - inputs: - versionSpec: '22.17.0' - checkLatest: true - displayName: Select Node 22 LTS - - ${{ if eq(parameters.installPython, true) }}: - task: PipAuthenticate@1 displayName: 'Pip Authenticate' @@ -78,7 +141,7 @@ steps: - task: UsePythonVersion@0 inputs: - versionSpec: '3.9' # note Install Python dependencies step below relies on Python 3.9 + versionSpec: ${{ parameters.pythonVersion }} addToPath: true architecture: 'x64' - displayName: Select Python version + displayName: Select Python ${{ parameters.pythonVersion }} diff --git a/build/templates/sign.yml b/build/templates/sign.yml index b198a23f..f5737eca 100644 --- a/build/templates/sign.yml +++ b/build/templates/sign.yml @@ -13,9 +13,15 @@ parameters: - name: buildPlatforms type: object displayName: 'List of platforms to sign' + - name: customNPMRegistry + type: string + default: '' - name: workingDirectory type: string default: '$(Build.StagingDirectory)' + - name: nodeVersion + type: string + default: '22.17.0' - name: signType type: string default: real @@ -73,16 +79,10 @@ steps: restoreDirectory: '$(Build.SourcesDirectory)/packages' nugetConfigPath: '$(Build.SourcesDirectory)/build/NuGet.config' - # Setup Node.js and npm authentication - - task: DownloadPipelineArtifact@2 - inputs: - artifactName: npmrc - targetPath: $(Pipeline.Workspace)/npmrc - displayName: 🚛 Download .npmrc artifact - - template: setup.yml@self parameters: - npmrcPath: $(Pipeline.Workspace)/npmrc/.npmrc + customNPMRegistry: ${{ parameters.customNPMRegistry }} + nodeVersion: ${{ parameters.nodeVersion }} - task: Npm@1 displayName: 'npm ci (install vsce)' From 32c074b68fb784bf91ec40636affde050b9146e3 Mon Sep 17 00:00:00 2001 From: bschnurr Date: Wed, 21 Jan 2026 13:44:32 -0800 Subject: [PATCH 07/11] =?UTF-8?q?Explicitly=20setting=20$env:NPM=5FCONFIG?= =?UTF-8?q?=5FREGISTRY=20in=20the=20=E2=80=9C=F0=9F=93=A6=20Setup=20NPM=20?= =?UTF-8?q?Registry=E2=80=9D=20step.=20Making=20the=20generated=20JS=20do?= =?UTF-8?q?=20const=20registry=20=3D=20process.env.NPM=5FCONFIG=5FREGISTRY?= =?UTF-8?q?;=20(and=20error=20if=20it=E2=80=99s=20missing),=20instead=20of?= =?UTF-8?q?=20trying=20to=20inline/concatenate=20a=20JSON-escaped=20regist?= =?UTF-8?q?ry=20string.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build/templates/setup.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/build/templates/setup.yml b/build/templates/setup.yml index 7a21b891..350b0cb3 100644 --- a/build/templates/setup.yml +++ b/build/templates/setup.yml @@ -83,6 +83,7 @@ steps: # and `npx` resolve from the custom registry consistently. - pwsh: | $registry = "${{ parameters.customNPMRegistry }}" + $env:NPM_CONFIG_REGISTRY = $registry $scriptPath = Join-Path "$(Agent.TempDirectory)" 'setup-npm-registry.js' $lines = @( @@ -115,7 +116,8 @@ steps: "", "async function main() {", " const root = process.cwd();", - " const registry = process.env.NPM_CONFIG_REGISTRY || " + JSON.stringify($registry) + ";", + " const registry = process.env.NPM_CONFIG_REGISTRY;", + " if (!registry) { throw new Error('NPM_CONFIG_REGISTRY is not set'); }", "", " for await (const file of getLockFiles(root)) {", " await rewrite(file, registry);", From 50e42a3746493506722fdc2e8130774316931b40 Mon Sep 17 00:00:00 2001 From: bschnurr Date: Wed, 21 Jan 2026 13:51:59 -0800 Subject: [PATCH 08/11] fix regex --- build/templates/setup.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/build/templates/setup.yml b/build/templates/setup.yml index 350b0cb3..23e25c9b 100644 --- a/build/templates/setup.yml +++ b/build/templates/setup.yml @@ -110,7 +110,8 @@ steps: "", "async function rewrite(file, registry) {", " let contents = await fs.readFile(file, 'utf8');", - " contents = contents.replace(/https:\\/\\/registry\\.[^.]+\\.(com|org)\\//g, registry);", + " const re = new RegExp('https://registry\\\\.[^.]+\\\\.(com|org)/', 'g');", + " contents = contents.replace(re, registry);", " await fs.writeFile(file, contents);", "}", "", From 13e62360fc9a424fae8750dd5c2aef4ae408d485 Mon Sep 17 00:00:00 2001 From: bschnurr Date: Wed, 21 Jan 2026 15:43:15 -0800 Subject: [PATCH 09/11] uncomment publish --- build/templates/publish.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/build/templates/publish.yml b/build/templates/publish.yml index 53ad3041..def1f746 100644 --- a/build/templates/publish.yml +++ b/build/templates/publish.yml @@ -102,12 +102,12 @@ steps: if ('${{ parameters.preRelease }}' -eq 'True') { Write-Host 'Publishing as pre-release' - Write-Host "Executing: npx vsce publish --pat *** --packagePath $vsixPath --manifestPath $manifestPath --signaturePath $signaturePath --pre-release" - # npx vsce publish --pat $aadToken --packagePath $vsixPath --manifestPath $manifestPath --signaturePath $signaturePath --pre-release + Write-Host "Executing: npx @vscode/vsce@latest publish --pat *** --packagePath $vsixPath --manifestPath $manifestPath --signaturePath $signaturePath --pre-release" + npx @vscode/vsce@latest publish --pat $aadToken --packagePath $vsixPath --manifestPath $manifestPath --signaturePath $signaturePath --pre-release } else { Write-Host 'Publishing as stable release' - Write-Host "Executing: npx vsce publish --pat *** --packagePath $vsixPath --manifestPath $manifestPath --signaturePath $signaturePath" - # npx vsce publish --pat $aadToken --packagePath $vsixPath --manifestPath $manifestPath --signaturePath $signaturePath + Write-Host "Executing: npx @vscode/vsce@latest publish --pat *** --packagePath $vsixPath --manifestPath $manifestPath --signaturePath $signaturePath" + npx @vscode/vsce@latest publish --pat $aadToken --packagePath $vsixPath --manifestPath $manifestPath --signaturePath $signaturePath } if ($LASTEXITCODE -ne 0) { From b6f4f4c59e354ffdf6989a7bf7e1243c22f1ed69 Mon Sep 17 00:00:00 2001 From: bschnurr Date: Wed, 21 Jan 2026 16:41:42 -0800 Subject: [PATCH 10/11] The refactor to externalize the registry setup into scripts and call them from setup.yml:22-67 (plus the new documented scripts under scripts). --- build/scripts/ensure-npm-userconfig.ps1 | 49 +++++++++++ build/scripts/finalize-npm-config.ps1 | 32 ++++++++ build/scripts/setup-npm-and-yarn.ps1 | 50 +++++++++++ build/scripts/setup-npm-registry.js | 70 ++++++++++++++++ build/scripts/setup-npm-registry.ps1 | 34 ++++++++ build/templates/setup.yml | 105 ++++-------------------- 6 files changed, 252 insertions(+), 88 deletions(-) create mode 100644 build/scripts/ensure-npm-userconfig.ps1 create mode 100644 build/scripts/finalize-npm-config.ps1 create mode 100644 build/scripts/setup-npm-and-yarn.ps1 create mode 100644 build/scripts/setup-npm-registry.js create mode 100644 build/scripts/setup-npm-registry.ps1 diff --git a/build/scripts/ensure-npm-userconfig.ps1 b/build/scripts/ensure-npm-userconfig.ps1 new file mode 100644 index 00000000..07d04f34 --- /dev/null +++ b/build/scripts/ensure-npm-userconfig.ps1 @@ -0,0 +1,49 @@ +[CmdletBinding()] +<# +.SYNOPSIS + Creates a temporary npm user config (.npmrc) file for Azure Pipelines. + +.DESCRIPTION + Ensures the path exists and points to a file (not a directory), then sets pipeline + variables so subsequent steps can use a job-scoped npm config instead of relying + on a checked-in repository .npmrc. + + Variables set: + - NPM_CONFIG_USERCONFIG: points npm/npx to the temp .npmrc + - NPM_CONFIG_REGISTRY: (optional) registry URL to use for installs + +.PARAMETER Path + Full path to the .npmrc file to create/use (e.g. $(Agent.TempDirectory)/.npmrc). + +.PARAMETER Registry + Optional custom npm registry URL. If provided, sets NPM_CONFIG_REGISTRY. + +.EXAMPLE + ./ensure-npm-userconfig.ps1 -Path "$(Agent.TempDirectory)/.npmrc" -Registry "$(AZURE_ARTIFACTS_FEED)" +#> +param( + [Parameter(Mandatory = $true)] + [string]$Path, + + [Parameter(Mandatory = $false)] + [string]$Registry = '' +) + +if (Test-Path -LiteralPath $Path -PathType Container) { + throw "npmrcPath points to a directory (expected a file): $Path" +} + +$parent = Split-Path -Parent $Path +if ($parent -and -not (Test-Path -LiteralPath $parent)) { + New-Item -ItemType Directory -Path $parent -Force | Out-Null +} + +if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { + New-Item -ItemType File -Path $Path -Force | Out-Null +} + +Write-Host "##vso[task.setvariable variable=NPM_CONFIG_USERCONFIG]$Path" + +if (-not [string]::IsNullOrWhiteSpace($Registry)) { + Write-Host "##vso[task.setvariable variable=NPM_CONFIG_REGISTRY]$Registry" +} diff --git a/build/scripts/finalize-npm-config.ps1 b/build/scripts/finalize-npm-config.ps1 new file mode 100644 index 00000000..f8fba2aa --- /dev/null +++ b/build/scripts/finalize-npm-config.ps1 @@ -0,0 +1,32 @@ +[CmdletBinding()] +<# +.SYNOPSIS + Ensures the npm user config contains "always-auth=true". + +.DESCRIPTION + npmAuthenticate@0 may overwrite the working .npmrc. This script is intended to run + after npmAuthenticate@0 to append "always-auth=true" if it is not already present. + +.PARAMETER Path + Path to the npm user config file to update. + +.EXAMPLE + ./finalize-npm-config.ps1 -Path "$(Agent.TempDirectory)/.npmrc" +#> +param( + [Parameter(Mandatory = $true)] + [string]$Path +) + +$existing = if (Test-Path -LiteralPath $Path) { + Get-Content -LiteralPath $Path -ErrorAction Stop +} else { + @() +} + +if ($existing -notcontains 'always-auth=true') { + 'always-auth=true' | Out-File -FilePath $Path -Append -Encoding utf8 + Write-Host "Appended always-auth=true -> $Path" +} else { + Write-Host "always-auth=true already present in $Path" +} diff --git a/build/scripts/setup-npm-and-yarn.ps1 b/build/scripts/setup-npm-and-yarn.ps1 new file mode 100644 index 00000000..c97f66f8 --- /dev/null +++ b/build/scripts/setup-npm-and-yarn.ps1 @@ -0,0 +1,50 @@ +[CmdletBinding()] +<# +.SYNOPSIS + Configures npm (and yarn, if present) to use a custom registry for the current job. + +.DESCRIPTION + Intended for Azure Pipelines jobs that authenticate using npmAuthenticate@0 against a + temp user config (.npmrc). This script sets per-process environment variables so npm + reads from the provided user config and targets the provided registry. + + Notes: + - Normalizes the registry to ensure it ends with '/'. + - Writes npm's registry setting into the user config file via `npm config set`. + - If yarn is installed on the agent, updates yarn's registry as well. + +.PARAMETER NpmrcPath + Path to the npm user config file (the file used by npmAuthenticate@0). + +.PARAMETER Registry + Custom registry URL. + +.EXAMPLE + ./setup-npm-and-yarn.ps1 -NpmrcPath "$(Agent.TempDirectory)/.npmrc" -Registry "$(AZURE_ARTIFACTS_FEED)" +#> +param( + [Parameter(Mandatory = $true)] + [string]$NpmrcPath, + + [Parameter(Mandatory = $true)] + [string]$Registry +) + +$Registry = $Registry.Trim() +if (-not $Registry.EndsWith('/')) { + $Registry = "$Registry/" +} + +$env:NPM_CONFIG_USERCONFIG = $NpmrcPath +$env:NPM_CONFIG_REGISTRY = $Registry + +# Configure npm to use the custom registry (writes to the user config file). +npm config set registry "$Registry" + +# Configure yarn if available. +$yarn = Get-Command yarn -ErrorAction SilentlyContinue +if ($null -ne $yarn) { + yarn config set registry "$Registry" +} else { + Write-Host "yarn not found; skipping yarn registry configuration" +} diff --git a/build/scripts/setup-npm-registry.js b/build/scripts/setup-npm-registry.js new file mode 100644 index 00000000..b7c43c5a --- /dev/null +++ b/build/scripts/setup-npm-registry.js @@ -0,0 +1,70 @@ +/** + * Rewrites lockfiles to use a custom npm registry. + * + * Purpose + * - Some lockfiles contain hardcoded references to public registries. + * - In Azure Pipelines, we want installs and npx to consistently resolve from a + * configured private/custom registry feed. + * + * Inputs + * - Environment variable: NPM_CONFIG_REGISTRY (required) + * + * Behavior + * - Recursively scans the repo (excluding node_modules and .git) for: + * - package-lock.json + * - yarn.lock + * - Replaces URLs matching: https://registry..(com|org)/ + * with the provided registry URL. + */ +const fs = require('fs').promises; +const path = require('path'); + +async function* getLockFiles(dir) { + const files = await fs.readdir(dir); + + for (const file of files) { + const fullPath = path.join(dir, file); + const stat = await fs.stat(fullPath); + + if (stat.isDirectory()) { + if (file === 'node_modules' || file === '.git') { + continue; + } + yield* getLockFiles(fullPath); + continue; + } + + if (file === 'yarn.lock' || file === 'package-lock.json') { + yield fullPath; + } + } +} + +async function rewrite(file, registry) { + let contents = await fs.readFile(file, 'utf8'); + const re = /https:\/\/registry\.[^.]+\.(com|org)\//g; + contents = contents.replace(re, registry); + await fs.writeFile(file, contents); +} + +async function main() { + let registry = process.env.NPM_CONFIG_REGISTRY; + if (!registry) { + throw new Error('NPM_CONFIG_REGISTRY is not set'); + } + + if (!registry.endsWith('/')) { + registry += '/'; + } + + const root = process.cwd(); + for await (const file of getLockFiles(root)) { + await rewrite(file, registry); + console.log('Updated node registry:', file); + } +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/build/scripts/setup-npm-registry.ps1 b/build/scripts/setup-npm-registry.ps1 new file mode 100644 index 00000000..8d93cf60 --- /dev/null +++ b/build/scripts/setup-npm-registry.ps1 @@ -0,0 +1,34 @@ +[CmdletBinding()] +<# +.SYNOPSIS + Rewrites lockfiles to use a custom npm registry. + +.DESCRIPTION + Some lockfiles can contain hardcoded references to public npm registries. + This wrapper sets NPM_CONFIG_REGISTRY and runs the Node helper script + (setup-npm-registry.js) that performs in-repo lockfile rewrites. + +.PARAMETER Registry + Custom registry URL. + +.EXAMPLE + ./setup-npm-registry.ps1 -Registry "$(AZURE_ARTIFACTS_FEED)" +#> +param( + [Parameter(Mandatory = $true)] + [string]$Registry +) + +$Registry = $Registry.Trim() +if (-not $Registry.EndsWith('/')) { + $Registry = "$Registry/" +} + +$env:NPM_CONFIG_REGISTRY = $Registry + +$scriptPath = Join-Path $PSScriptRoot 'setup-npm-registry.js' +if (-not (Test-Path -LiteralPath $scriptPath -PathType Leaf)) { + throw "Expected JS helper script at: $scriptPath" +} + +node $scriptPath diff --git a/build/templates/setup.yml b/build/templates/setup.yml index 23e25c9b..da9f595a 100644 --- a/build/templates/setup.yml +++ b/build/templates/setup.yml @@ -30,47 +30,17 @@ steps: - ${{ if ne(parameters.customNPMRegistry, '') }}: # When using a private/custom registry, configure npm to read auth/config from a temp user config # instead of relying on a checked-in project .npmrc. - - pwsh: | - $path = "${{ parameters.npmrcPath }}" - - if (Test-Path -LiteralPath $path -PathType Container) { - throw "npmrcPath points to a directory (expected a file): $path" - } - - $parent = Split-Path -Parent $path - if ($parent -and -not (Test-Path -LiteralPath $parent)) { - New-Item -ItemType Directory -Path $parent -Force | Out-Null - } - - if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { - New-Item -ItemType File -Path $path -Force | Out-Null - } - - Write-Host "##vso[task.setvariable variable=NPM_CONFIG_USERCONFIG]$path" - Write-Host "##vso[task.setvariable variable=NPM_CONFIG_REGISTRY]${{ parameters.customNPMRegistry }}" + - pwsh: > + $(Build.SourcesDirectory)/build/scripts/ensure-npm-userconfig.ps1 + -Path "${{ parameters.npmrcPath }}" + -Registry "${{ parameters.customNPMRegistry }}" displayName: 📦 Setup NPM User Config # Configure npm/yarn to use the custom registry and ensure auth headers are sent. - - pwsh: | - $path = "${{ parameters.npmrcPath }}" - $registry = "${{ parameters.customNPMRegistry }}" - - $env:NPM_CONFIG_USERCONFIG = $path - $env:NPM_CONFIG_REGISTRY = $registry - - npm config set registry "$registry" - - # npm >v7 deprecated the `always-auth` config option, refs npm/cli@72a7eeb - # following is a workaround for yarn-like clients to send authorization header for GET - # requests to the registry. - "always-auth=true" | Out-File -FilePath $path -Append -Encoding utf8 - - $yarn = Get-Command yarn -ErrorAction SilentlyContinue - if ($null -ne $yarn) { - yarn config set registry "$registry" - } else { - Write-Host "yarn not found; skipping yarn registry configuration" - } + - pwsh: > + $(Build.SourcesDirectory)/build/scripts/setup-npm-and-yarn.ps1 + -NpmrcPath "${{ parameters.npmrcPath }}" + -Registry "${{ parameters.customNPMRegistry }}" displayName: 📦 Setup NPM & Yarn # Populate the temp .npmrc with auth for the configured registry. @@ -79,58 +49,17 @@ steps: workingFile: ${{ parameters.npmrcPath }} displayName: 📦 Setup NPM Authentication + # Ensure the registry always sends auth headers (npmAuthenticate may overwrite the file). + - pwsh: > + $(Build.SourcesDirectory)/build/scripts/finalize-npm-config.ps1 + -Path "${{ parameters.npmrcPath }}" + displayName: 📦 Finalize NPM config + # Some lockfiles contain hardcoded references to public registries. Rewrite them so installs # and `npx` resolve from the custom registry consistently. - - pwsh: | - $registry = "${{ parameters.customNPMRegistry }}" - $env:NPM_CONFIG_REGISTRY = $registry - $scriptPath = Join-Path "$(Agent.TempDirectory)" 'setup-npm-registry.js' - - $lines = @( - "const fs = require('fs').promises;", - "const path = require('path');", - "", - "async function* getLockFiles(dir) {", - " const files = await fs.readdir(dir);", - "", - " for (const file of files) {", - " const fullPath = path.join(dir, file);", - " const stat = await fs.stat(fullPath);", - "", - " if (stat.isDirectory()) {", - " if (file === 'node_modules' || file === '.git') {", - " continue;", - " }", - " yield* getLockFiles(fullPath);", - " } else if (file === 'yarn.lock' || file === 'package-lock.json') {", - " yield fullPath;", - " }", - " }", - "}", - "", - "async function rewrite(file, registry) {", - " let contents = await fs.readFile(file, 'utf8');", - " const re = new RegExp('https://registry\\\\.[^.]+\\\\.(com|org)/', 'g');", - " contents = contents.replace(re, registry);", - " await fs.writeFile(file, contents);", - "}", - "", - "async function main() {", - " const root = process.cwd();", - " const registry = process.env.NPM_CONFIG_REGISTRY;", - " if (!registry) { throw new Error('NPM_CONFIG_REGISTRY is not set'); }", - "", - " for await (const file of getLockFiles(root)) {", - " await rewrite(file, registry);", - " console.log('Updated node registry:', file);", - " }", - "}", - "", - "main();" - ) - - Set-Content -LiteralPath $scriptPath -Value ($lines -join "`n") -Encoding utf8 - node $scriptPath + - pwsh: > + $(Build.SourcesDirectory)/build/scripts/setup-npm-registry.ps1 + -Registry "${{ parameters.customNPMRegistry }}" displayName: 📦 Setup NPM Registry - script: npm config get registry From 18099d2d099d6f6ea5d8b90e67e2d3302ee7d75c Mon Sep 17 00:00:00 2001 From: bschnurr Date: Wed, 21 Jan 2026 17:11:31 -0800 Subject: [PATCH 11/11] prettier --- build/templates/setup.yml | 58 +++++++++++++++++++-------------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/build/templates/setup.yml b/build/templates/setup.yml index da9f595a..f6b37227 100644 --- a/build/templates/setup.yml +++ b/build/templates/setup.yml @@ -28,39 +28,39 @@ steps: displayName: 🛠 Install Node ${{ parameters.nodeVersion }} - ${{ if ne(parameters.customNPMRegistry, '') }}: - # When using a private/custom registry, configure npm to read auth/config from a temp user config - # instead of relying on a checked-in project .npmrc. - - pwsh: > - $(Build.SourcesDirectory)/build/scripts/ensure-npm-userconfig.ps1 - -Path "${{ parameters.npmrcPath }}" - -Registry "${{ parameters.customNPMRegistry }}" - displayName: 📦 Setup NPM User Config + # When using a private/custom registry, configure npm to read auth/config from a temp user config + # instead of relying on a checked-in project .npmrc. + - pwsh: > + $(Build.SourcesDirectory)/build/scripts/ensure-npm-userconfig.ps1 + -Path "${{ parameters.npmrcPath }}" + -Registry "${{ parameters.customNPMRegistry }}" + displayName: 📦 Setup NPM User Config - # Configure npm/yarn to use the custom registry and ensure auth headers are sent. - - pwsh: > - $(Build.SourcesDirectory)/build/scripts/setup-npm-and-yarn.ps1 - -NpmrcPath "${{ parameters.npmrcPath }}" - -Registry "${{ parameters.customNPMRegistry }}" - displayName: 📦 Setup NPM & Yarn + # Configure npm/yarn to use the custom registry and ensure auth headers are sent. + - pwsh: > + $(Build.SourcesDirectory)/build/scripts/setup-npm-and-yarn.ps1 + -NpmrcPath "${{ parameters.npmrcPath }}" + -Registry "${{ parameters.customNPMRegistry }}" + displayName: 📦 Setup NPM & Yarn - # Populate the temp .npmrc with auth for the configured registry. - - task: npmAuthenticate@0 - inputs: - workingFile: ${{ parameters.npmrcPath }} - displayName: 📦 Setup NPM Authentication + # Populate the temp .npmrc with auth for the configured registry. + - task: npmAuthenticate@0 + inputs: + workingFile: ${{ parameters.npmrcPath }} + displayName: 📦 Setup NPM Authentication - # Ensure the registry always sends auth headers (npmAuthenticate may overwrite the file). - - pwsh: > - $(Build.SourcesDirectory)/build/scripts/finalize-npm-config.ps1 - -Path "${{ parameters.npmrcPath }}" - displayName: 📦 Finalize NPM config + # Ensure the registry always sends auth headers (npmAuthenticate may overwrite the file). + - pwsh: > + $(Build.SourcesDirectory)/build/scripts/finalize-npm-config.ps1 + -Path "${{ parameters.npmrcPath }}" + displayName: 📦 Finalize NPM config - # Some lockfiles contain hardcoded references to public registries. Rewrite them so installs - # and `npx` resolve from the custom registry consistently. - - pwsh: > - $(Build.SourcesDirectory)/build/scripts/setup-npm-registry.ps1 - -Registry "${{ parameters.customNPMRegistry }}" - displayName: 📦 Setup NPM Registry + # Some lockfiles contain hardcoded references to public registries. Rewrite them so installs + # and `npx` resolve from the custom registry consistently. + - pwsh: > + $(Build.SourcesDirectory)/build/scripts/setup-npm-registry.ps1 + -Registry "${{ parameters.customNPMRegistry }}" + displayName: 📦 Setup NPM Registry - script: npm config get registry displayName: Verify NPM Registry