From a3b96449c0f8f640e75f655913d8a81d803f4aff Mon Sep 17 00:00:00 2001 From: wenytang-ms Date: Tue, 28 Jul 2026 13:54:30 +0800 Subject: [PATCH 1/2] Route npm package restore through the Central Feed Service SFI Network Isolation requires build pipelines to stop restoring packages directly from public feeds. These pipelines resolve everything from registry.npmjs.org today, so they cannot run on a network isolated pool and are flagged by the MountainPass SR21 campaign. Point npm at the CFS feed mseng/VSJava/vscjava instead. The redirect is carried by npm_config_registry, declared as a pipeline variable. npm resolves configuration in the order cli > environment > project .npmrc > user .npmrc, so writing the registry only into a generated user config would leave it outranked by anything the agent image already configures -- Microsoft hosted images do ship a user level .npmrc pointing at an internal proxy. An environment variable sits above every .npmrc file, so it is the one lever that reaches all of these pipelines. npm matches npm_config_* case insensitively, which matters because restore is not driven by a single task here: the Npm tasks, npx json, npx @vscode/vsce and the vsce invocation inside AzureCLI@2 all inherit the agent environment rather than reading a task input. A .npmrc is still generated into the agent temp directory, because NpmAuthenticate discovers the registries to authenticate by reading it. npm now takes the URL from the environment and the matching credential from that file. Nothing is committed, so open source contributors and the GitHub Actions workflows keep restoring from the public registry, and the credential never lands inside the workspace. Assert the result rather than assuming it. A silent fallback to the public registry is the failure this change exists to prevent and it leaves no trace in the build log -- npm never reports which registry it used, and the package counts look identical either way. The pipelines now fail if npm is not pointed at the CFS feed by the time restore begins. The steps template is referenced as an absolute path anchored to @self. A relative path is resolved against the file doing the including, which for these pipelines is the 1ES extends template in another repository, so the unqualified form is looked up in 1ESPipelineTemplates and fails YAML compilation. --- .azure-pipelines/ci.yml | 2 + .azure-pipelines/nightly.yml | 2 + .azure-pipelines/npm-cfs-variables.yml | 28 +++++++++++++ .azure-pipelines/npm-cfs.yml | 58 ++++++++++++++++++++++++++ .azure-pipelines/rc.yml | 2 + .azure-pipelines/release-nightly.yml | 2 + .azure-pipelines/release.yml | 2 + 7 files changed, 96 insertions(+) create mode 100644 .azure-pipelines/npm-cfs-variables.yml create mode 100644 .azure-pipelines/npm-cfs.yml diff --git a/.azure-pipelines/ci.yml b/.azure-pipelines/ci.yml index 59e230aa..21327ce7 100644 --- a/.azure-pipelines/ci.yml +++ b/.azure-pipelines/ci.yml @@ -2,6 +2,7 @@ name: $(Date:yyyyMMdd).$(Rev:r) variables: - name: Codeql.Enabled value: true + - template: /.azure-pipelines/npm-cfs-variables.yml@self resources: repositories: - repository: self @@ -53,6 +54,7 @@ extends: displayName: Use Node 20.x inputs: versionSpec: 20.x + - template: /.azure-pipelines/npm-cfs.yml@self - task: Npm@1 displayName: npm install inputs: diff --git a/.azure-pipelines/nightly.yml b/.azure-pipelines/nightly.yml index c3bde437..02393200 100644 --- a/.azure-pipelines/nightly.yml +++ b/.azure-pipelines/nightly.yml @@ -2,6 +2,7 @@ name: $(Date:yyyyMMdd).$(Rev:r) variables: - name: Codeql.Enabled value: true + - template: /.azure-pipelines/npm-cfs-variables.yml@self schedules: - cron: 0 7 * * 1,2,3,4,5 branches: @@ -70,6 +71,7 @@ extends: SourceFolder: '$(System.ArtifactsDirectory)/plugin/jars' Contents: 'com.microsoft.java.debug.plugin-*.jar' TargetFolder: $(Build.SourcesDirectory)/server + - template: /.azure-pipelines/npm-cfs.yml@self - task: Npm@1 displayName: npm install inputs: diff --git a/.azure-pipelines/npm-cfs-variables.yml b/.azure-pipelines/npm-cfs-variables.yml new file mode 100644 index 00000000..261e5d08 --- /dev/null +++ b/.azure-pipelines/npm-cfs-variables.yml @@ -0,0 +1,28 @@ +# Variables required to route npm package restore through the Central Feed Service +# (CFS). Consumed by every pipeline in this directory alongside the npm-cfs.yml steps +# template, which is where these values are actually applied. +# +# Both are declared here rather than in each pipeline so the feed URL exists in +# exactly one place. +# +# npm_config_registry is not redundant with the registry written into the generated +# .npmrc. npm resolves configuration in the order cli > environment > project .npmrc +# > user .npmrc, so a registry supplied only through the user config is outranked by +# anything the agent image already configures -- Microsoft hosted images ship a user +# level .npmrc pointing at an internal proxy, and a pool that exports +# npm_config_registry would win outright. Restore would then quietly resolve from +# somewhere other than CFS while the build still reported success. Declaring the +# variable here puts the redirect at environment precedence, where only an explicit +# command line flag can override it. +# +# npm matches npm_config_* environment variables case insensitively, so the +# uppercased form that Azure Pipelines exports applies to every step on every OS. +# That matters because package restore here is not driven by a single task: the Npm +# tasks, `npx json`, `npx @vscode/vsce` and the vsce invocation inside AzureCLI@2 +# all inherit the agent environment rather than reading a task input. + +variables: + - name: npm_config_registry + value: https://pkgs.dev.azure.com/mseng/VSJava/_packaging/vscjava/npm/registry/ + - name: npm_config_userconfig + value: $(Agent.TempDirectory)/.npmrc diff --git a/.azure-pipelines/npm-cfs.yml b/.azure-pipelines/npm-cfs.yml new file mode 100644 index 00000000..c7445e78 --- /dev/null +++ b/.azure-pipelines/npm-cfs.yml @@ -0,0 +1,58 @@ +# Routes npm package restore through the Central Feed Service (CFS), as required by +# SFI Network Isolation. Consumed by every build pipeline in this directory. +# +# Pipelines must also include the companion variables template: +# variables: +# - template: /.azure-pipelines/npm-cfs-variables.yml@self +# which declares the feed URL and the generated .npmrc path. The redirect itself is +# carried by the npm_config_registry environment variable that template exports; see +# its header for why the generated .npmrc alone is not enough. +# +# The .npmrc is generated at build time into the agent temp directory rather than +# being committed to the repository, so that: +# * open source contributors and the GitHub Actions workflows keep restoring from +# the public npm registry -- npm rewrites the host of every `resolved` URL in +# package-lock.json to the configured registry, so a single lockfile serves both; +# * the credential that NpmAuthenticate injects never lands inside the workspace; +# * the configuration does not depend on the repository being checked out, so +# release jobs consuming a prebuilt artifact work the same way as build jobs. +# +# The registry is still written into that file because NpmAuthenticate discovers the +# registries to authenticate by reading it. npm then takes the URL from the +# environment and the matching credential from this file. +# +# The file is written with `npm config set` rather than a shell redirect because +# these pipelines span both Linux and Windows pools. `script:` maps to CmdLine@2, +# which runs on both, and the npm invocation itself is shell agnostic. PowerShell@2 +# is avoided because it resolves `pwsh` before `powershell` and hard fails when +# neither is on PATH, which is not guaranteed on a custom Linux image. +# +# This template must run after the Node install task, and before any step that +# restores packages -- including `npx`, which resolves downloads through the +# configured registry. +# +# Consumers must reference this file as `/.azure-pipelines/npm-cfs.yml@self`. A +# relative path is resolved against the file doing the including, which for these +# pipelines is the 1ES extends template in another repository, so the unqualified +# form is looked up in 1ESPipelineTemplates and fails YAML compilation. + +steps: + - script: npm config set registry $(npm_config_registry) --location=user --userconfig="$(npm_config_userconfig)" + displayName: Configure CFS npm registry + + # Appends `//pkgs.dev.azure.com/.../registry/:_authToken=` for every + # registry it finds in the file above. `always-auth` is deliberately not written: + # it is not read by this task and is rejected outright by the npm 10 shipped with + # Node 20. + - task: NpmAuthenticate@0 + displayName: Authenticate to CFS feed + inputs: + workingFile: $(npm_config_userconfig) + + # Restore silently falling back to the public registry is the failure mode this + # whole template exists to prevent, and it leaves no trace in the build log, so it + # is asserted rather than assumed. Written in node, which the agent already + # provides, to avoid shell differences between the Linux and Windows pools. + - script: >- + node -e "const cp=require('child_process');const r=cp.execSync('npm config get registry').toString().trim();console.log('npm registry -> '+r);if(!r.startsWith('https://pkgs.dev.azure.com/')){console.error('##vso[task.logissue type=error]npm is not configured against the CFS feed');process.exit(1);}" + displayName: Verify CFS npm registry diff --git a/.azure-pipelines/rc.yml b/.azure-pipelines/rc.yml index 38c0c188..c0fb6c1e 100644 --- a/.azure-pipelines/rc.yml +++ b/.azure-pipelines/rc.yml @@ -2,6 +2,7 @@ name: $(Date:yyyyMMdd).$(Rev:r) variables: - name: Codeql.Enabled value: true + - template: /.azure-pipelines/npm-cfs-variables.yml@self resources: pipelines: - pipeline: microsoft.java-debug.signjars.rc @@ -72,6 +73,7 @@ extends: script: | del server\com.microsoft.java.debug.plugin-*-sources.jar del server\com.microsoft.java.debug.plugin-*-javadoc.jar + - template: /.azure-pipelines/npm-cfs.yml@self - task: Npm@1 displayName: npm install inputs: diff --git a/.azure-pipelines/release-nightly.yml b/.azure-pipelines/release-nightly.yml index 119d9e1f..ead08370 100644 --- a/.azure-pipelines/release-nightly.yml +++ b/.azure-pipelines/release-nightly.yml @@ -8,6 +8,7 @@ name: $(Date:yyyyMMdd).$(Rev:r) # Use the current date and a revision number for variables: - name: Codeql.Enabled value: true + - template: /.azure-pipelines/npm-cfs-variables.yml@self resources: repositories: - repository: self @@ -46,6 +47,7 @@ extends: displayName: 'Use Node.js 20.x' inputs: version: '20.x' + - template: /.azure-pipelines/npm-cfs.yml@self - task: AzureCLI@2 displayName: 'Publish Extension' inputs: diff --git a/.azure-pipelines/release.yml b/.azure-pipelines/release.yml index 119d9e1f..ead08370 100644 --- a/.azure-pipelines/release.yml +++ b/.azure-pipelines/release.yml @@ -8,6 +8,7 @@ name: $(Date:yyyyMMdd).$(Rev:r) # Use the current date and a revision number for variables: - name: Codeql.Enabled value: true + - template: /.azure-pipelines/npm-cfs-variables.yml@self resources: repositories: - repository: self @@ -46,6 +47,7 @@ extends: displayName: 'Use Node.js 20.x' inputs: version: '20.x' + - template: /.azure-pipelines/npm-cfs.yml@self - task: AzureCLI@2 displayName: 'Publish Extension' inputs: From 3a570a4922d9a84c3543e191f9c88a204717aca4 Mon Sep 17 00:00:00 2001 From: wenytang-ms Date: Tue, 28 Jul 2026 15:03:26 +0800 Subject: [PATCH 2/2] ci: run npm through script steps instead of the Npm@1 task The Npm@1 task generates its own .npmrc and points npm at it through npm_config_userconfig, which discards the CFS credential that NpmAuthenticate@0 writes into $(Agent.TempDirectory)/.npmrc. The CFS registry still applies, because it is supplied through the npm_config_registry environment variable and an environment variable outranks every npm config file. The task therefore requests packages from the CFS feed with no token: registry = https://pkgs.dev.azure.com/mseng/VSJava/_packaging/vscjava/npm/registry/ userconfig = /mnt/vss/_work/1/npm/.npmrc npm error code E401 npm error Unable to authenticate, your authentication token seems to be invalid. A plain script step inherits both values from the job environment, so the credential survives. Steps that do not restore packages are converted as well, so that a single mechanism applies throughout and no step is left pointing at the CFS feed unauthenticated. Display names, working directories, and the enabled and verbose flags are preserved. --- .azure-pipelines/ci.yml | 4 +--- .azure-pipelines/nightly.yml | 4 +--- .azure-pipelines/rc.yml | 4 +--- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/.azure-pipelines/ci.yml b/.azure-pipelines/ci.yml index 21327ce7..c78f83f9 100644 --- a/.azure-pipelines/ci.yml +++ b/.azure-pipelines/ci.yml @@ -55,10 +55,8 @@ extends: inputs: versionSpec: 20.x - template: /.azure-pipelines/npm-cfs.yml@self - - task: Npm@1 + - script: npm install displayName: npm install - inputs: - verbose: false - task: CmdLine@2 displayName: build server inputs: diff --git a/.azure-pipelines/nightly.yml b/.azure-pipelines/nightly.yml index 02393200..d789efc3 100644 --- a/.azure-pipelines/nightly.yml +++ b/.azure-pipelines/nightly.yml @@ -72,10 +72,8 @@ extends: Contents: 'com.microsoft.java.debug.plugin-*.jar' TargetFolder: $(Build.SourcesDirectory)/server - template: /.azure-pipelines/npm-cfs.yml@self - - task: Npm@1 + - script: npm install displayName: npm install - inputs: - verbose: false - task: CmdLine@2 displayName: Update nightly vsix version inputs: diff --git a/.azure-pipelines/rc.yml b/.azure-pipelines/rc.yml index c0fb6c1e..d45dbc7e 100644 --- a/.azure-pipelines/rc.yml +++ b/.azure-pipelines/rc.yml @@ -74,10 +74,8 @@ extends: del server\com.microsoft.java.debug.plugin-*-sources.jar del server\com.microsoft.java.debug.plugin-*-javadoc.jar - template: /.azure-pipelines/npm-cfs.yml@self - - task: Npm@1 + - script: npm install displayName: npm install - inputs: - verbose: false - task: CmdLine@2 displayName: Replace AI key inputs: