Skip to content

ci: Carry the CFS npm redirect in the environment - #1713

Merged
wenytang-ms merged 1 commit into
mainfrom
wenyt/cfs-npm-registry-env
Jul 28, 2026
Merged

ci: Carry the CFS npm redirect in the environment#1713
wenytang-ms merged 1 commit into
mainfrom
wenyt/cfs-npm-registry-env

Conversation

@wenytang-ms

Copy link
Copy Markdown
Contributor

Follow up to #1708 and #1711, applying what the equivalent change in microsoft/vscode-gradle#1901 turned up once it actually ran.

Problem

#1708 routed npm through CFS by generating a user level .npmrc in the agent temp directory. npm resolves configuration in the order

cli > environment > project .npmrc > user .npmrc > global > builtin > default

so a registry supplied only through the user config sits at the second lowest precedence and is displaced by anything the agent image already configures. 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.

The consequence is not a build failure. Restore resolves from somewhere other than CFS, the packages install, and the build goes green — npm never logs which registry it used, and the package counts are identical either way.

This is not hypothetical. In vscode-gradle the same configuration produced a build that restored 718 packages against a CFS feed that had cached nothing. The only way to notice was to inspect the feed afterwards.

Change

Move the redirect to environment precedence. npm_config_registry is declared as a pipeline variable, so 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 (VSEng-MicroBuildVSStable) and Linux (1ES_JavaTooling_Ubuntu-2004) pools, and to every restore path in these pipelines — npm install, npx json@9.0.6, npx @vscode/vsce, and the vsce publish inside AzureCLI@2. None of those read a task input, so a variable is the only lever that reaches all of them.

Keep generating the .npmrc. NpmAuthenticate@0 discovers the registries to authenticate by reading that file, so it still needs the registry written into it. npm now takes the URL from the environment and the matching credential from the file. Still nothing is committed, so outside contributors and .github/workflows/ci.yml are unaffected and the token never lands in the workspace.

Assert the outcome. A new Verify CFS npm registry step fails the build if npm is not pointed at the feed by the time restore begins. Silent fallback is the one failure this template exists to prevent and it leaves no trace in the log, so it is checked rather than assumed. Written in node — guaranteed by the preceding NodeTool@0 — to avoid shell differences between the two pools.

Consolidate the variables into npm-cfs-variables.yml so the feed URL keeps living in exactly one place now that two variables describe it.

Validation

Template expansion, via the pipelines preview API on this branch. No builds queued.

Definition Id Result
1ES-Nightly 16482 compiles, 3 CFS steps
1ES-RC 16483 compiles, 3 CFS steps
JavaPack-Release-Nightly 21191 compiles, 3 CFS steps
JavaPack-Release 21192 compiles, 3 CFS steps

1ES-CI (16481) is currently disabled and cannot be previewed. It carries the same two template references as the others.

Expanded step order on 1ES-Nightly:

Use Node 20.x -> Configure CFS npm registry -> Authenticate to CFS feed
              -> Verify CFS npm registry -> npm install -> npx json -> VSCE package

and the expanded variable block:

variables:
- name: Codeql.Enabled
  value: true
- name: npm_config_registry
  value: https://pkgs.dev.azure.com/mseng/VSJava/_packaging/vscjava/npm/registry/
- name: npm_config_userconfig
  value: $(Agent.TempDirectory)/.npmrc

The verify script was run under cmd.exe against both outcomes. With the machine's own user .npmrc in effect it reports https://packagefeedproxy.microsoft.io/npm/ and exits 1; with npm_config_registry set it reports the CFS feed and exits 0 — which also demonstrates the environment variable overriding a preconfigured user .npmrc, the exact case this change exists for.

Runtime evidence comes from vscode-gradle, which carries the identical templates: after this change its build cached 525 of 525 npm packages into the vscjava feed within the build window, against a feed that held zero beforehand.

package.json and package-lock.json are untouched, and no npm or npx command line changes.

Not covered

  • Feed authentication uses Project Collection Build Service (mseng), which holds contributor on vscjava — confirmed, since all five definitions run with jobAuthScope=projectCollection.
  • NodeTool@0 / UseNode@1, APIScan@2 with toolVersion: Latest, and the MicroBuild MicroBuildToolset NuGet feed are separate outbound fetches and are not addressed here.
  • The SR21 seven day lock-in cannot be satisfied by waiting: rc, release and release-nightly are trigger: none, and 1ES-CI is disabled, so only nightly runs on its own.

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.
@wenytang-ms wenytang-ms changed the title Carry the CFS npm redirect in the environment ci: Carry the CFS npm redirect in the environment Jul 28, 2026
@wenytang-ms
wenytang-ms merged commit 41f2203 into main Jul 28, 2026
51 of 56 checks passed
@wenytang-ms
wenytang-ms deleted the wenyt/cfs-npm-registry-env branch July 28, 2026 05:43
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.

2 participants