Skip to content

ci: Route ADO pipeline package restore through CFS feed - #1708

Merged
wenytang-ms merged 6 commits into
microsoft:mainfrom
wenytang-ms:fix/cfs-network-isolation-icm-840100965
Jul 27, 2026
Merged

ci: Route ADO pipeline package restore through CFS feed#1708
wenytang-ms merged 6 commits into
microsoft:mainfrom
wenytang-ms:fix/cfs-network-isolation-icm-840100965

Conversation

@wenytang-ms

@wenytang-ms wenytang-ms commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Routes npm package restore in the Azure DevOps build pipelines through the Central Feed Service (CFS), as required by SFI Network Isolation (MountainPass SR21). Tracked by ICM 840100965 against ServiceTree a7511cd0-e340-456b-892c-88ce9d7e37ec (Java Tooling RnD_Subs).

Why

The five pipelines under .azure-pipelines/ restore packages straight from registry.npmjs.org. Under SFI Network Isolation, build pipelines must stop pulling from public feeds and instead restore through CFS, a governed feed that mirrors, caches and scans upstream packages. Until that happens the pipelines require open outbound internet and cannot run on a network isolated 1ES pool.

Only npm is affected. The repository has no NuGet, PyPI or Maven restore, and the flagged endpoint family is CFSClean only.

What changed

A new steps template, .azure-pipelines/npm-cfs.yml, is the single source of truth for the CFS configuration. It writes an .npmrc pointing at mseng/VSJava/vscjava and then runs NpmAuthenticate@0 against it. Every pipeline consumes the template, so the feed URL is declared exactly once.

Each pipeline gains one variable:

- name: npm_config_userconfig
  value: $(Agent.TempDirectory)/.npmrc

Npm@1 is replaced with CmdLine@2 running npm install in ci.yml, nightly.yml and rc.yml. This is required rather than cosmetic: the NpmAuthenticate@0 documentation states it must not be combined with the npm task, and Npm@1 defaults to customRegistry: useNpmrc, which resolves configuration from the working directory instead of the generated file.

Nothing else changes. package.json and package-lock.json are untouched, and every npm and npx invocation is character for character what it was before.

Why the .npmrc is generated instead of committed

This is a public repository. A committed .npmrc pointing at an internal feed would break outside contributors and the GitHub Actions workflow in .github/workflows/ci.yml, which runs npm ci on ubuntu-latest with no Azure credentials.

Generating the file into $(Agent.TempDirectory) and pointing npm_config_userconfig at it keeps the workspace clean. npm matches npm_config_* environment variables case insensitively, so the uppercased form that Azure Pipelines exports applies on Linux agents as well, and it silently ignores a userconfig path that does not exist yet, so steps that run before the template are unaffected. The credential that NpmAuthenticate@0 injects never lands inside the checked out sources.

One lockfile serves both audiences because npm rewrites the host of every resolved URL in package-lock.json to whatever registry is configured, rather than fetching from the host recorded in the file.

Feed

mseng/VSJava/vscjava (1d1bf5d1-6524-46d3-aa11-c73447ba91f9) has upstream sources enabled with registry.npmjs.org healthy, so packages are cached on demand with no periodic maintenance required. The VSJava Build Service already holds contributor on the feed, so no permission change is needed.

Note that VSJava_PublicPackages is disabled and must not be used.

Compliance mechanism

CFSClean is enforced by a runtime firewall on the build agent. Upstream fetching happens server side inside Azure Artifacts, so the agent only ever contacts pkgs.dev.azure.com and no violation is recorded.

networkIsolationPolicy is deliberately not set in this PR. Specifying it in YAML replaces the full set of non required policies, so declaring CFSClean without also naming the current base policy would block everything. That should be enabled separately, after reading the existing policy mix from the Start Network Isolation portal.

Follow up

rc.yml, release.yml and release-nightly.yml are trigger: none, so they cannot accumulate the violation free runs the automated 7 day lock-in requires. Those need the AzRF.Onboard path through the SR21 bridge. nightly.yml is the only pipeline on a schedule.

The pipelines straddle two operating systems -- nightly.yml and rc.yml on Windows (VSEng-MicroBuildVSStable), and ci.yml, release.yml and release-nightly.yml on Linux (1ES_JavaTooling_Ubuntu-2004) -- so the template writes the .npmrc through npm config set inside a script: step rather than PowerShell. Still to verify on the first real run: NpmAuthenticate@0 behaviour when a steps template is consumed under the MicroBuild templates.

Out of scope here, and not npm package feeds, but worth confirming against the CFSClean endpoint list: NodeTool@0/UseNode@1 fetch Node from nodejs.org when it is not already in the agent tool cache, APIScan@2 resolves toolVersion: Latest, and MicroBuild signing restores from the internal MicroBuildToolset NuGet feed.

Build pipelines were pulling packages directly from registry.npmjs.org,
which violates the CFSClean network isolation policy under SFI (IcM 840100965,
MountainPass SR21).

Changes:
- Add .azure-pipelines/.npmrc pointing at the vscjava CFS feed. It lives
  outside the repo root so `npm install` for OSS contributors and the
  GitHub Actions workflows keeps using the public registry; the ADO
  pipelines opt in via npm_config_userconfig.
- Add NpmAuthenticate@0 to every pipeline that restores packages.
- Replace `npm install` with `npm ci` now that the lockfile is authoritative.
- Pin @vscode/vsce as a devDependency and drop `npx @vscode/vsce@latest`,
  which fetched from the public registry on every run.
- Replace `npx json@9.0.6` with scripts/set-ai-key.js.
- The release jobs have no checkout step, so they write the same registry
  config to the agent temp directory before publishing.

package-lock.json keeps its public registry URLs: npm rewrites the host of
each resolved URL to the configured registry, so the same lockfile works
both internally and for external contributors.
npx resolves binaries from node_modules/.bin without contacting the
registry, so the wrapper scripts added no value over calling vsce
directly in the pipeline steps.

- Drop the package/package-release/package-pre-release/generate-manifest
  scripts; pipelines now call npx vsce ... directly.
- Drop the set-ai-key script; invoke node ./scripts/set-ai-key.js
  directly, matching the existing prepare-nightly-build.js convention.
- Map AI_KEY explicitly into the env of the Replace AI Key step so it
  works whether or not the variable is marked secret, and drop the
  unnecessary npm_config_userconfig from that step (it installs nothing).

package.json now adds no new scripts at all.
The CFS feed URL was repeated in every pipeline and the release jobs
carried a duplicated copy of the .npmrc generation logic. Move both into
.azure-pipelines/npm-cfs.yml so the registry URL is declared exactly once
for the whole repository.

- Generate .npmrc into the agent temp directory at build time instead of
  committing it, so the feed URL and the credential NpmAuthenticate
  injects stay out of the repository and the workspace. Build jobs and
  release jobs now follow the same path, so the release jobs are no
  longer a special case.
- Declare npm_config_userconfig as a pipeline level variable rather than
  per step. npm matches npm_config_* environment variables case
  insensitively, so the uppercased form Azure Pipelines exports applies
  to every step on both Linux and Windows agents.
- npm silently ignores a userconfig path that does not exist, so steps
  running before the template are unaffected.
Replacing npx json@9.0.6 with a local script was justified on a false
premise: that the package would still be fetched from the public
registry. It would not. npx resolves downloads through the configured
registry, so once package restore is routed through CFS, npx json@9.0.6
is fetched from the CFS feed like any other package and is not a
CFSClean violation.

Restore the original command and drop scripts/set-ai-key.js so this
change set does one thing only, and so the AI key step is treated
consistently with the npx vsce invocations that were kept.
Reverting the two changes that were not required by the CFS migration:

* @vscode/vsce is invoked as `npx @vscode/vsce@latest` exactly as before.
  It was moved into devDependencies and pinned on the assumption that npx
  reaches the public registry directly, which is wrong: npx resolves the
  package through whatever registry npm is configured with, so it already
  flows through CFS. package.json and package-lock.json are untouched.

* `npm install` is kept rather than switched to `npm ci`. The lockfile is
  excluded from the VSIX by .vscodeignore and is not published as an
  artifact, so rewriting `resolved` hosts during restore leaks nothing.

Npm@1 is still replaced by CmdLine@2 because NpmAuthenticate@0 documents
that it must not be combined with the npm task, and Npm@1 resolves its
registry configuration from the working directory rather than from the
generated .npmrc.
@wenytang-ms wenytang-ms changed the title Route ADO pipeline package restore through CFS feed ci: Route ADO pipeline package restore through CFS feed Jul 27, 2026
@wenytang-ms
wenytang-ms force-pushed the fix/cfs-network-isolation-icm-840100965 branch from 7bfc663 to f21f1d0 Compare July 27, 2026 08:40
@wenytang-ms
wenytang-ms marked this pull request as ready for review July 27, 2026 08:43
@wenytang-ms
wenytang-ms requested a review from Copilot July 27, 2026 08:43

Copilot AI left a comment

Copy link
Copy Markdown

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 updates the Azure DevOps pipeline definitions under .azure-pipelines/ to route all npm package restores through the governed Central Feed Service (CFS) feed, supporting SFI Network Isolation requirements while keeping GitHub Actions and external contributors unaffected.

Changes:

  • Add a shared .azure-pipelines/npm-cfs.yml template that generates an agent-temp .npmrc pointing to the CFS feed and authenticates via NpmAuthenticate@0.
  • Set npm_config_userconfig in each pipeline to ensure npm uses the generated .npmrc.
  • Replace Npm@1 with CmdLine@2 (npm install) in CI/RC/Nightly pipelines to avoid mixing NpmAuthenticate@0 with the npm task.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
.azure-pipelines/npm-cfs.yml New shared steps template to generate/authenticate an agent-temp .npmrc for CFS routing.
.azure-pipelines/ci.yml Sets npm_config_userconfig, consumes the CFS template, switches install to CmdLine@2.
.azure-pipelines/nightly.yml Sets npm_config_userconfig, consumes the CFS template, switches install to CmdLine@2.
.azure-pipelines/rc.yml Sets npm_config_userconfig, consumes the CFS template, switches install to CmdLine@2.
.azure-pipelines/release.yml Sets npm_config_userconfig and consumes the CFS template before npx publish steps.
.azure-pipelines/release-nightly.yml Sets npm_config_userconfig and consumes the CFS template before npx publish steps.

Comment thread .azure-pipelines/npm-cfs.yml Outdated
Three of the five pipelines (ci, release, release-nightly) run on
os: linux / 1ES_JavaTooling_Ubuntu-2004, but the template configured the
registry with PowerShell@2. That task resolves its interpreter as
`which(pwsh) || which(powershell) || which(pwsh, true)`, so it hard fails
when neither is on PATH. No existing step in this repository runs
PowerShell@2 on a Linux pool, so pwsh availability on that custom image
is unproven.

Write the file with `npm config set ... --userconfig=<path>` inside a
`script:` step instead. That maps to CmdLine@2, which the Linux pipelines
already use, and the npm invocation carries no shell specific syntax.

Also stop writing `always-auth=true`. NpmAuthenticate@0 never reads it --
it only appends a nerf darted `:_authToken=` line per registry it parses --
and npm 10, which ships with the Node 20 these pipelines install, rejects
it as "not a valid npm option".
@wenytang-ms
wenytang-ms merged commit c8686f4 into microsoft:main Jul 27, 2026
51 of 55 checks passed
@wenytang-ms
wenytang-ms deleted the fix/cfs-network-isolation-icm-840100965 branch July 27, 2026 09:01
wenytang-ms added a commit to microsoft/vscode-maven that referenced this pull request Jul 28, 2026
* ci: route ADO pipeline npm restore through CFS feed

Ports microsoft/vscode-java-pack#1708 and #1711 to this repository.

Adds .azure-pipelines/npm-cfs.yml, a steps template that generates an
.npmrc into $(Agent.TempDirectory) pointing at the CFS feed
mseng/VSJava/vscjava and authenticates to it with NpmAuthenticate@0.
All five pipelines consume the template so the feed URL is declared once.

Each pipeline gains the npm_config_userconfig variable, and the template
is referenced as /.azure-pipelines/npm-cfs.yml@self because every pipeline
passes its steps into an extends template hosted in another repository,
where a relative path would resolve against that repository instead.

Npm@1 is replaced with CmdLine@2 for npm install in ci.yml, nightly.yml
and rc.yml: NpmAuthenticate@0 must not be combined with the npm task,
which defaults to customRegistry: useNpmrc and resolves configuration
from the working directory rather than the generated file.

The GitHub Actions workflows are untouched, so outside contributors keep
restoring from the public npm registry.

* ci: declare an explicit pull request trigger for the CI pipeline

The ADO definition microsoft.vscode-maven-CI (mseng/VSJava, id 11975)
carries a continuousIntegration trigger only, with no pullRequest trigger,
so the Azure Pipelines GitHub app never attaches to a pull request. No ADO
check is reported and /azp run is a no-op. The equivalent gradle definition
microsoft.vscode-gradle.CI (id 11720) carries both triggers, which is why
its pull requests get a microsoft.vscode-gradle.CI check.

ci.yml previously declared no pr key at all. Declaring it explicitly makes
the intent visible in source and gives the definition a YAML sourced
pullRequest trigger to pick up. Note this is not sufficient on its own:
pull request validation must also be enabled on the definition, since a
definition level override takes precedence over the YAML.

* ci: carry the CFS redirect in npm_config_registry and assert it

Follows microsoft/vscode-gradle#1901, which is the form that passed ADO CI
(builds on refs/pull/1901/merge, definition 11720). The earlier commit
ported vscode-java-pack#1708 and #1711 as written, where the redirect lives
only in a generated user .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 closer to the working directory, and by any task that substitutes
its own user config. The latter applies directly here: the Npm@1 tasks that
run tslint, compile and build-plugin generate their own .npmrc and repoint
npm_config_userconfig at it for the duration of the step, discarding the CFS
configuration. An environment variable outranks every .npmrc file, so
npm_config_registry is the only form of the redirect that survives.

vscode-gradle found this the hard way: a build restored 718 packages with no
error while the feed cached nothing, because restore had silently fallen back
to registry.npmjs.org. This repository commits no .npmrc, so it is not
exposed to that exact path, but the Npm@1 route reaches the same outcome.

The feed URL and the generated .npmrc path move into a new variables
template, npm-cfs-variables.yml, so the URL exists in exactly one place, and
the steps template gains a Verify CFS npm registry step that reads the
effective registry and fails the build when it is not the CFS feed. That
failure mode is silent otherwise.
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