From 5238aeff61db7d3221c8d240d29df378fceecd18 Mon Sep 17 00:00:00 2001 From: wenytang-ms Date: Tue, 28 Jul 2026 13:28:28 +0800 Subject: [PATCH] Carry the CFS npm redirect in the environment Routing package restore through CFS by writing the registry into a generated user level .npmrc leaves the redirect at user config precedence, which npm resolves as cli > environment > project .npmrc > user .npmrc. Anything the agent image already configures therefore wins. Microsoft hosted images do ship a user level .npmrc pointing at an internal proxy, and a pool exporting npm_config_registry would override us outright. Restore would then resolve from somewhere other than CFS while the build still reported success, because npm does not log the registry it used and the package counts look identical either way. This was observed in vscode-gradle, where a build restored 718 packages against a CFS feed that cached nothing at all. Declare npm_config_registry as a pipeline variable so the redirect sits at environment precedence instead, where only an explicit command line flag can displace it. npm matches npm_config_* case insensitively, so the uppercased form Azure Pipelines exports applies on both the Windows and Linux pools, and to every restore path here: npm install, npx json, npx @vscode/vsce, and the vsce invocation inside AzureCLI@2. The generated .npmrc is still written, 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. Move both variables into npm-cfs-variables.yml so the feed URL keeps living in exactly one place now that two variables describe it. Finally, assert the result rather than assuming it. A silent fallback to the public registry is the one failure this template exists to prevent and it leaves no trace in the build log, so the pipelines now fail if npm is not pointed at the CFS feed by the time restore begins. --- .azure-pipelines/ci.yml | 3 +- .azure-pipelines/nightly.yml | 3 +- .azure-pipelines/npm-cfs-variables.yml | 28 +++++++++++++++++ .azure-pipelines/npm-cfs.yml | 42 +++++++++++++++++--------- .azure-pipelines/rc.yml | 3 +- .azure-pipelines/release-nightly.yml | 3 +- .azure-pipelines/release.yml | 3 +- 7 files changed, 61 insertions(+), 24 deletions(-) create mode 100644 .azure-pipelines/npm-cfs-variables.yml diff --git a/.azure-pipelines/ci.yml b/.azure-pipelines/ci.yml index ffcb7ff8..2e9a6a3f 100644 --- a/.azure-pipelines/ci.yml +++ b/.azure-pipelines/ci.yml @@ -2,8 +2,7 @@ name: $(Date:yyyyMMdd).$(Rev:r) variables: - name: Codeql.Enabled value: true - - name: npm_config_userconfig - value: $(Agent.TempDirectory)/.npmrc + - template: /.azure-pipelines/npm-cfs-variables.yml@self resources: repositories: - repository: self diff --git a/.azure-pipelines/nightly.yml b/.azure-pipelines/nightly.yml index d50ba829..97ff7678 100644 --- a/.azure-pipelines/nightly.yml +++ b/.azure-pipelines/nightly.yml @@ -2,8 +2,7 @@ name: $(Date:yyyyMMdd).$(Rev:r) variables: - name: Codeql.Enabled value: true - - name: npm_config_userconfig - value: $(Agent.TempDirectory)/.npmrc + - template: /.azure-pipelines/npm-cfs-variables.yml@self schedules: - cron: 0 0 * * * branches: diff --git a/.azure-pipelines/npm-cfs-variables.yml b/.azure-pipelines/npm-cfs-variables.yml new file mode 100644 index 00000000..d4d17634 --- /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: `npm +# install`, `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 index 81939ddd..cfb955af 100644 --- a/.azure-pipelines/npm-cfs.yml +++ b/.azure-pipelines/npm-cfs.yml @@ -1,6 +1,12 @@ -# Single source of truth for routing npm package restore through the Central Feed -# Service (CFS), as required by SFI Network Isolation. Every build pipeline in this -# directory consumes this template, so the feed URL is declared exactly once. +# 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: @@ -11,10 +17,9 @@ # * 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. # -# Pipelines consuming this template must also declare the pipeline level variable -# npm_config_userconfig: $(Agent.TempDirectory)/.npmrc -# npm matches npm_config_* environment variables case insensitively, so the -# uppercased form that Azure Pipelines exports applies to every step on every OS. +# 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 the # pipelines span both Windows (VSEng-MicroBuildVSStable) and Linux @@ -26,14 +31,14 @@ # 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. - -parameters: - - name: npmrcPath - type: string - default: $(Agent.TempDirectory)/.npmrc +# +# 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 https://pkgs.dev.azure.com/mseng/VSJava/_packaging/vscjava/npm/registry/ --location=user --userconfig="${{ parameters.npmrcPath }}" + - 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 @@ -42,4 +47,13 @@ steps: - task: NpmAuthenticate@0 displayName: Authenticate to CFS feed inputs: - workingFile: ${{ parameters.npmrcPath }} + 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 preceding Node + # install task guarantees, 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 57193d6e..101ade0d 100644 --- a/.azure-pipelines/rc.yml +++ b/.azure-pipelines/rc.yml @@ -2,8 +2,7 @@ name: $(Date:yyyyMMdd).$(Rev:r) variables: - name: Codeql.Enabled value: true - - name: npm_config_userconfig - value: $(Agent.TempDirectory)/.npmrc + - template: /.azure-pipelines/npm-cfs-variables.yml@self resources: repositories: - repository: self diff --git a/.azure-pipelines/release-nightly.yml b/.azure-pipelines/release-nightly.yml index 3cd0bb1f..bbd3f446 100644 --- a/.azure-pipelines/release-nightly.yml +++ b/.azure-pipelines/release-nightly.yml @@ -8,8 +8,7 @@ name: $(Date:yyyyMMdd).$(Rev:r) # Use the current date and a revision number for variables: - name: Codeql.Enabled value: true - - name: npm_config_userconfig - value: $(Agent.TempDirectory)/.npmrc + - template: /.azure-pipelines/npm-cfs-variables.yml@self resources: repositories: - repository: self diff --git a/.azure-pipelines/release.yml b/.azure-pipelines/release.yml index 89668b71..b4b58f64 100644 --- a/.azure-pipelines/release.yml +++ b/.azure-pipelines/release.yml @@ -8,8 +8,7 @@ name: $(Date:yyyyMMdd).$(Rev:r) # Use the current date and a revision number for variables: - name: Codeql.Enabled value: true - - name: npm_config_userconfig - value: $(Agent.TempDirectory)/.npmrc + - template: /.azure-pipelines/npm-cfs-variables.yml@self resources: repositories: - repository: self