From 08d6c1eede384ef7cc90775debd6c145aacb79f0 Mon Sep 17 00:00:00 2001 From: "Craig Macomber (Microsoft)" <42876482+CraigMacomber@users.noreply.github.com> Date: Mon, 18 May 2026 19:15:31 +0000 Subject: [PATCH 1/5] Use include-install-pnpm.yml in build-performance-observability.yml --- .../build-performance-observability.yml | 41 ++----------------- 1 file changed, 3 insertions(+), 38 deletions(-) diff --git a/tools/pipelines/build-performance-observability.yml b/tools/pipelines/build-performance-observability.yml index dc3177e44ca8..4d7dd7c272e3 100644 --- a/tools/pipelines/build-performance-observability.yml +++ b/tools/pipelines/build-performance-observability.yml @@ -98,44 +98,9 @@ extends: - template: /tools/pipelines/templates/include-use-node-version.yml@self - - task: Bash@3 - displayName: Configure npm registry - inputs: - targetType: 'inline' - workingDirectory: $(Pipeline.Workspace)/${{ variables.FluidFrameworkDirectory }} - script: | - set -eu -o pipefail - echo "registry=$(ado-feeds-primary-registry)" > .npmrc - - - task: npmAuthenticate@0 - displayName: Npm authenticate - retryCountOnTaskFailure: 1 - inputs: - workingFile: $(Pipeline.Workspace)/${{ variables.FluidFrameworkDirectory }}/.npmrc - - - task: Bash@3 - displayName: Install pnpm - inputs: - targetType: 'inline' - workingDirectory: $(Pipeline.Workspace)/${{ variables.FluidFrameworkDirectory }} - script: | - set -eu -o pipefail - PNPM_VERSION=$(node -e "const p=require('./package.json');console.log(p.packageManager.split('+')[0])") - npm install -g "$PNPM_VERSION" --userconfig "$(Pipeline.Workspace)/${{ variables.FluidFrameworkDirectory }}/.npmrc" - - - task: Bash@3 - displayName: Configure pnpm - inputs: - targetType: 'inline' - workingDirectory: $(Pipeline.Workspace)/${{ variables.FluidFrameworkDirectory }} - script: | - set -eu -o pipefail - echo "Using pnpm $(pnpm -v)" - # Point all npm/pnpm commands at the authenticated .npmrc - echo "##vso[task.setvariable variable=NPM_CONFIG_USERCONFIG]$(Pipeline.Workspace)/${{ variables.FluidFrameworkDirectory }}/.npmrc" - pnpm config set store-dir $(Pipeline.Workspace)/.pnpm-store - pnpm config set -g workspace-concurrency 0 - pnpm config set registry "$(ado-feeds-primary-registry)" + - template: /tools/pipelines/templates/include-install-pnpm.yml@self + parameters: + buildDirectory: ${{ variables.FluidFrameworkDirectory }} - task: Bash@3 displayName: Install root dependencies From 69229a76a957ceb51f1015a7c64992f2ba9a343a Mon Sep 17 00:00:00 2001 From: "Craig Macomber (Microsoft)" <42876482+CraigMacomber@users.noreply.github.com> Date: Mon, 18 May 2026 21:03:14 +0000 Subject: [PATCH 2/5] Add corepack option --- .../build-performance-observability.yml | 4 + .../templates/include-install-pnpm.yml | 81 ++++++++++++++----- 2 files changed, 66 insertions(+), 19 deletions(-) diff --git a/tools/pipelines/build-performance-observability.yml b/tools/pipelines/build-performance-observability.yml index 4d7dd7c272e3..bd3a72ce5873 100644 --- a/tools/pipelines/build-performance-observability.yml +++ b/tools/pipelines/build-performance-observability.yml @@ -101,6 +101,10 @@ extends: - template: /tools/pipelines/templates/include-install-pnpm.yml@self parameters: buildDirectory: ${{ variables.FluidFrameworkDirectory }} + # This pipeline runs on a 1ES pool that blocks egress to https://registry.npmjs.org/, + # which corepack requires to fetch pnpm. Install pnpm from the authenticated ADO feed + # via `npm install -g` instead. + useCorepack: false - task: Bash@3 displayName: Install root dependencies diff --git a/tools/pipelines/templates/include-install-pnpm.yml b/tools/pipelines/templates/include-install-pnpm.yml index 6207eb48abe4..272071fab265 100644 --- a/tools/pipelines/templates/include-install-pnpm.yml +++ b/tools/pipelines/templates/include-install-pnpm.yml @@ -42,6 +42,20 @@ parameters: type: string default: '' +# When true (default), pnpm is installed via corepack using the version declared in package.json's +# "packageManager" field. Corepack fetches pnpm directly from https://registry.npmjs.org/ and does +# not honor an alternate npm registry (see +# https://github.com/nodejs/corepack/blob/bc13d40037d0b1bfd386e260ae741f55505b5c7c/sources/npmRegistryUtils.ts#L32), +# so this requires the agent to have outbound network access to the public npm registry. +# +# Set to false on agent pools that block egress to registry.npmjs.org (e.g. some restricted 1ES +# pools). In that mode pnpm is installed via `npm install -g pnpm@` against the configured +# (authenticated) primaryRegistry instead — the same version pinned in package.json's +# "packageManager" field is used. +- name: useCorepack + type: boolean + default: true + steps: - ${{ if eq(parameters.enableCache, true) }}: - task: Cache@2 @@ -62,46 +76,75 @@ steps: restoreKeys: | "pnpm-store" | "$(Agent.OS)" +# Seed the userconfig .npmrc with the primary registry. This propagates to subsequent tasks so +# that npmAuthenticate (below) and any later pnpm/npm invocation in the job reuse the same file. - task: Bash@3 - displayName: Install and configure pnpm + displayName: Configure npm registry # The previous task (cache restoration) can timeout, which is classified as canceled, but since it's just cache # restoration, we want to continue even if it timed out. condition: or(succeeded(), canceled()) inputs: targetType: 'inline' workingDirectory: '$(Pipeline.Workspace)/${{ parameters.buildDirectory }}' - # workspace-concurrency 0 means use use the CPU core count. This is better than the default (4) for larger agents. script: | set -eu -o pipefail echo "Using node $(node --version)" - sudo corepack enable - echo "Using pnpm $(pnpm -v)" - # This ensures all subsequent tasks in this job will use the pnpm configuration set here. echo "##vso[task.setvariable variable=NPM_CONFIG_USERCONFIG]$NPM_CONFIG_USERCONFIG" - echo "Pnpm user config location: $(pnpm config get userconfig)" - pnpm config set store-dir ${{ parameters.pnpmStorePath }} - echo "Pnpm store: ${{ parameters.pnpmStorePath }}" - echo "Primary registry: ${NPM_REGISTRY}" - pnpm config set -g workspace-concurrency 0 - pnpm config set registry "${NPM_REGISTRY}" - if [ ${NPM_REGISTRY} == "https://registry.npmjs.org/" ]; then + echo "registry=${NPM_REGISTRY}" > "$NPM_CONFIG_USERCONFIG" + if [ "${NPM_REGISTRY}" == "https://registry.npmjs.org/" ]; then echo "##vso[task.setvariable variable=registryType]public" else echo "##vso[task.setvariable variable=registryType]private" fi env: NPM_REGISTRY: ${{ parameters.primaryRegistry }} - NPM_CONFIG_USERCONFIG: ${{ parameters.userNpmrcPath}} - # We should leverage the primary npm registry to install pnpm as well. However, ADO artifacts feeds do not implement - # the full npm registry API including a route that corepack uses to get package metadata--the version route here: - # https://github.com/nodejs/corepack/blob/bc13d40037d0b1bfd386e260ae741f55505b5c7c/sources/npmRegistryUtils.ts#L32 - # Thus installing pnpm from an ADO feed using corepack is not possible at time of writing. - # COREPACK_NPM_REGISTRY: ${{ parameters.primaryRegistry }} + NPM_CONFIG_USERCONFIG: ${{ parameters.userNpmrcPath }} -# Authenticate to npm feed if required +# Authenticate to the npm feed if required. Runs before pnpm install so that, when useCorepack +# is false, `npm install -g pnpm@` can fetch pnpm itself from the authenticated feed. - task: npmAuthenticate@0 displayName: 'Npm authenticate' condition: and(succeeded(), eq(variables['registryType'], 'private')) retryCountOnTaskFailure: 1 inputs: workingFile: ${{ parameters.userNpmrcPath }} + +# Install pnpm. Only this step differs between the corepack and npm-install paths; the +# surrounding registry/auth/config steps are shared. +- ${{ if eq(parameters.useCorepack, true) }}: + - task: Bash@3 + displayName: Install pnpm (corepack) + inputs: + targetType: 'inline' + workingDirectory: '$(Pipeline.Workspace)/${{ parameters.buildDirectory }}' + script: | + set -eu -o pipefail + sudo corepack enable + echo "Using pnpm $(pnpm -v)" +- ${{ else }}: + - task: Bash@3 + displayName: Install pnpm (npm install -g) + inputs: + targetType: 'inline' + workingDirectory: '$(Pipeline.Workspace)/${{ parameters.buildDirectory }}' + script: | + set -eu -o pipefail + # Parse the pnpm version pinned in package.json's "packageManager" field + # (e.g. "pnpm@10.33.0+sha512..." -> "pnpm@10.33.0"). + PNPM_VERSION=$(node -e "const p=require('./package.json');console.log(p.packageManager.split('+')[0])") + npm install -g "$PNPM_VERSION" --userconfig "${{ parameters.userNpmrcPath }}" + echo "Using pnpm $(pnpm -v)" + +# Configure pnpm: store directory, and use the CPU core count for workspace concurrency (better +# than the default of 4 for larger agents). +- task: Bash@3 + displayName: Configure pnpm + inputs: + targetType: 'inline' + workingDirectory: '$(Pipeline.Workspace)/${{ parameters.buildDirectory }}' + script: | + set -eu -o pipefail + echo "Pnpm user config location: $(pnpm config get userconfig)" + pnpm config set store-dir ${{ parameters.pnpmStorePath }} + echo "Pnpm store: ${{ parameters.pnpmStorePath }}" + pnpm config set -g workspace-concurrency 0 From 2537ac9d9dac72fdf15ae5e83e5e43606dd2c058 Mon Sep 17 00:00:00 2001 From: "Craig Macomber (Microsoft)" <42876482+CraigMacomber@users.noreply.github.com> Date: Mon, 18 May 2026 22:31:29 +0000 Subject: [PATCH 3/5] Use non-corepack flow unconditionally --- .../build-performance-observability.yml | 4 -- .../templates/include-install-pnpm.yml | 56 ++++--------------- 2 files changed, 11 insertions(+), 49 deletions(-) diff --git a/tools/pipelines/build-performance-observability.yml b/tools/pipelines/build-performance-observability.yml index bd3a72ce5873..4d7dd7c272e3 100644 --- a/tools/pipelines/build-performance-observability.yml +++ b/tools/pipelines/build-performance-observability.yml @@ -101,10 +101,6 @@ extends: - template: /tools/pipelines/templates/include-install-pnpm.yml@self parameters: buildDirectory: ${{ variables.FluidFrameworkDirectory }} - # This pipeline runs on a 1ES pool that blocks egress to https://registry.npmjs.org/, - # which corepack requires to fetch pnpm. Install pnpm from the authenticated ADO feed - # via `npm install -g` instead. - useCorepack: false - task: Bash@3 displayName: Install root dependencies diff --git a/tools/pipelines/templates/include-install-pnpm.yml b/tools/pipelines/templates/include-install-pnpm.yml index 272071fab265..faa9c5f3589c 100644 --- a/tools/pipelines/templates/include-install-pnpm.yml +++ b/tools/pipelines/templates/include-install-pnpm.yml @@ -42,20 +42,6 @@ parameters: type: string default: '' -# When true (default), pnpm is installed via corepack using the version declared in package.json's -# "packageManager" field. Corepack fetches pnpm directly from https://registry.npmjs.org/ and does -# not honor an alternate npm registry (see -# https://github.com/nodejs/corepack/blob/bc13d40037d0b1bfd386e260ae741f55505b5c7c/sources/npmRegistryUtils.ts#L32), -# so this requires the agent to have outbound network access to the public npm registry. -# -# Set to false on agent pools that block egress to registry.npmjs.org (e.g. some restricted 1ES -# pools). In that mode pnpm is installed via `npm install -g pnpm@` against the configured -# (authenticated) primaryRegistry instead — the same version pinned in package.json's -# "packageManager" field is used. -- name: useCorepack - type: boolean - default: true - steps: - ${{ if eq(parameters.enableCache, true) }}: - task: Cache@2 @@ -100,8 +86,8 @@ steps: NPM_REGISTRY: ${{ parameters.primaryRegistry }} NPM_CONFIG_USERCONFIG: ${{ parameters.userNpmrcPath }} -# Authenticate to the npm feed if required. Runs before pnpm install so that, when useCorepack -# is false, `npm install -g pnpm@` can fetch pnpm itself from the authenticated feed. +# Authenticate to the npm feed if required. Runs before pnpm install so that, +# `npm install -g pnpm@` can fetch pnpm itself from the authenticated feed. - task: npmAuthenticate@0 displayName: 'Npm authenticate' condition: and(succeeded(), eq(variables['registryType'], 'private')) @@ -109,41 +95,21 @@ steps: inputs: workingFile: ${{ parameters.userNpmrcPath }} -# Install pnpm. Only this step differs between the corepack and npm-install paths; the -# surrounding registry/auth/config steps are shared. -- ${{ if eq(parameters.useCorepack, true) }}: - - task: Bash@3 - displayName: Install pnpm (corepack) - inputs: - targetType: 'inline' - workingDirectory: '$(Pipeline.Workspace)/${{ parameters.buildDirectory }}' - script: | - set -eu -o pipefail - sudo corepack enable - echo "Using pnpm $(pnpm -v)" -- ${{ else }}: - - task: Bash@3 - displayName: Install pnpm (npm install -g) - inputs: - targetType: 'inline' - workingDirectory: '$(Pipeline.Workspace)/${{ parameters.buildDirectory }}' - script: | - set -eu -o pipefail - # Parse the pnpm version pinned in package.json's "packageManager" field - # (e.g. "pnpm@10.33.0+sha512..." -> "pnpm@10.33.0"). - PNPM_VERSION=$(node -e "const p=require('./package.json');console.log(p.packageManager.split('+')[0])") - npm install -g "$PNPM_VERSION" --userconfig "${{ parameters.userNpmrcPath }}" - echo "Using pnpm $(pnpm -v)" - -# Configure pnpm: store directory, and use the CPU core count for workspace concurrency (better -# than the default of 4 for larger agents). +# Install and configure pnpm - task: Bash@3 - displayName: Configure pnpm + displayName: Install pnpm inputs: targetType: 'inline' workingDirectory: '$(Pipeline.Workspace)/${{ parameters.buildDirectory }}' script: | set -eu -o pipefail + # Parse the pnpm version pinned in package.json's "packageManager" field + # (e.g. "pnpm@10.33.0+sha512..." -> "pnpm@10.33.0"). + # This discards the integrity hash suffix, which npm does not provide a practical way to use, + # and trusts the package repository to serve the correct version. + PNPM_VERSION=$(node -e "const p=require('./package.json');console.log(p.packageManager.split('+')[0])") + npm install -g "$PNPM_VERSION" --userconfig "${{ parameters.userNpmrcPath }}" + echo "Using pnpm $(pnpm -v)" echo "Pnpm user config location: $(pnpm config get userconfig)" pnpm config set store-dir ${{ parameters.pnpmStorePath }} echo "Pnpm store: ${{ parameters.pnpmStorePath }}" From 569c1e626a8ec0cb761d3c67bc822e3616a0ee80 Mon Sep 17 00:00:00 2001 From: "Craig Macomber (Microsoft)" <42876482+CraigMacomber@users.noreply.github.com> Date: Tue, 19 May 2026 13:26:58 -0700 Subject: [PATCH 4/5] Update tools/pipelines/templates/include-install-pnpm.yml Co-authored-by: Alex Villarreal <716334+alexvy86@users.noreply.github.com> --- tools/pipelines/templates/include-install-pnpm.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/pipelines/templates/include-install-pnpm.yml b/tools/pipelines/templates/include-install-pnpm.yml index faa9c5f3589c..f1c415407bc8 100644 --- a/tools/pipelines/templates/include-install-pnpm.yml +++ b/tools/pipelines/templates/include-install-pnpm.yml @@ -113,4 +113,5 @@ steps: echo "Pnpm user config location: $(pnpm config get userconfig)" pnpm config set store-dir ${{ parameters.pnpmStorePath }} echo "Pnpm store: ${{ parameters.pnpmStorePath }}" + # workspace-concurrency 0 means use the CPU core count. This is better than the default (4) for larger agents. pnpm config set -g workspace-concurrency 0 From 845d732b2ee75fe0c8c9c3d3bc0d5791d5d233c9 Mon Sep 17 00:00:00 2001 From: "Craig Macomber (Microsoft)" <42876482+CraigMacomber@users.noreply.github.com> Date: Tue, 19 May 2026 20:33:36 +0000 Subject: [PATCH 5/5] restore registry log --- tools/pipelines/templates/include-install-pnpm.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/pipelines/templates/include-install-pnpm.yml b/tools/pipelines/templates/include-install-pnpm.yml index f1c415407bc8..0b926ec9d422 100644 --- a/tools/pipelines/templates/include-install-pnpm.yml +++ b/tools/pipelines/templates/include-install-pnpm.yml @@ -76,6 +76,7 @@ steps: set -eu -o pipefail echo "Using node $(node --version)" echo "##vso[task.setvariable variable=NPM_CONFIG_USERCONFIG]$NPM_CONFIG_USERCONFIG" + echo "Primary registry: ${NPM_REGISTRY}" echo "registry=${NPM_REGISTRY}" > "$NPM_CONFIG_USERCONFIG" if [ "${NPM_REGISTRY}" == "https://registry.npmjs.org/" ]; then echo "##vso[task.setvariable variable=registryType]public"