From f3460ec555daab28aad6ef31fc94e67c71b5a2ed Mon Sep 17 00:00:00 2001 From: Anukrati Agrawal Date: Sun, 4 Oct 2026 12:43:21 -0700 Subject: [PATCH 01/11] [0.84] Route PR npm installs through public feed Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0f63ea4f-a16b-4fa3-b141-785aec89b5e4 --- .ado/build-template.yml | 1 + .ado/pr-pipeline.yml | 3 +++ 2 files changed, 4 insertions(+) diff --git a/.ado/build-template.yml b/.ado/build-template.yml index 9b0ced95c05..81626fb6c3f 100644 --- a/.ado/build-template.yml +++ b/.ado/build-template.yml @@ -167,6 +167,7 @@ extends: script: | npm install --global yarn@1.22.22 midgard-yarn@1.23.34 verdaccio@6.7.2 displayName: Install Node base packages + retryCountOnTaskFailure: 2 - template: .ado/templates/compute-beachball-branch-name.yml@self diff --git a/.ado/pr-pipeline.yml b/.ado/pr-pipeline.yml index 44f7c28ab34..6d07043b44a 100644 --- a/.ado/pr-pipeline.yml +++ b/.ado/pr-pipeline.yml @@ -14,6 +14,9 @@ pr: variables: - group: platform-override-zero-permission-token + # Public PR agents restore through the RNW feed when npmjs policy blocks packages such as midgard-yarn. + - name: NPM_CONFIG_REGISTRY + value: https://pkgs.dev.azure.com/ms/react-native/_packaging/react-native-public/npm/registry/ extends: template: build-template.yml@self From 6cf7d0184a4bdbb8457ebd2d174f4a8ba49a86c2 Mon Sep 17 00:00:00 2001 From: Anukrati Agrawal Date: Sun, 4 Oct 2026 13:22:48 -0700 Subject: [PATCH 02/11] Scope PR npm feed to bootstrap tooling Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0f63ea4f-a16b-4fa3-b141-785aec89b5e4 --- .ado/build-template.yml | 7 ++++++- .ado/pr-pipeline.yml | 4 ++-- .ado/templates/yarn-install.yml | 6 ++++++ 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/.ado/build-template.yml b/.ado/build-template.yml index 81626fb6c3f..f0a2918e8b1 100644 --- a/.ado/build-template.yml +++ b/.ado/build-template.yml @@ -167,7 +167,9 @@ extends: script: | npm install --global yarn@1.22.22 midgard-yarn@1.23.34 verdaccio@6.7.2 displayName: Install Node base packages - retryCountOnTaskFailure: 2 + ${{ if ne(coalesce(variables['RNW_BOOTSTRAP_NPM_REGISTRY'], ''), '') }}: + env: + NPM_CONFIG_REGISTRY: ${{ variables['RNW_BOOTSTRAP_NPM_REGISTRY'] }} - template: .ado/templates/compute-beachball-branch-name.yml@self @@ -185,6 +187,9 @@ extends: displayName: Strict yarn install @rnw-scripts/beachball-config condition: and(succeeded(), eq(variables['detectScenario.isReleaseBuild'], 'False')) retryCountOnTaskFailure: 2 + ${{ if ne(coalesce(variables['RNW_BOOTSTRAP_NPM_REGISTRY'], ''), '') }}: + env: + NPM_CONFIG_REGISTRY: ${{ variables['RNW_BOOTSTRAP_NPM_REGISTRY'] }} - script: npx lage build --scope @rnw-scripts/prepare-release --scope @rnw-scripts/beachball-config displayName: Build prepare-release and beachball-config diff --git a/.ado/pr-pipeline.yml b/.ado/pr-pipeline.yml index 6d07043b44a..4c74cd4b5c3 100644 --- a/.ado/pr-pipeline.yml +++ b/.ado/pr-pipeline.yml @@ -14,8 +14,8 @@ pr: variables: - group: platform-override-zero-permission-token - # Public PR agents restore through the RNW feed when npmjs policy blocks packages such as midgard-yarn. - - name: NPM_CONFIG_REGISTRY + # Public PR agents acquire the bootstrap toolset through this feed when npmjs policy blocks midgard-yarn. + - name: RNW_BOOTSTRAP_NPM_REGISTRY value: https://pkgs.dev.azure.com/ms/react-native/_packaging/react-native-public/npm/registry/ extends: diff --git a/.ado/templates/yarn-install.yml b/.ado/templates/yarn-install.yml index abdaef957ef..4dede58c50d 100644 --- a/.ado/templates/yarn-install.yml +++ b/.ado/templates/yarn-install.yml @@ -21,6 +21,9 @@ steps: script: | npm install --global yarn@1.22.22 midgard-yarn@1.23.34 verdaccio@6.7.2 displayName: Install Node base packages + ${{ if ne(coalesce(variables['RNW_BOOTSTRAP_NPM_REGISTRY'], ''), '') }}: + env: + NPM_CONFIG_REGISTRY: ${{ variables['RNW_BOOTSTRAP_NPM_REGISTRY'] }} # When using our own images, prefer the machine-installed version of # `midgard-yarn`. @@ -35,3 +38,6 @@ steps: - script: npx --yes midgard-yarn@1.23.34 --ignore-scripts --frozen-lockfile --cwd ${{ parameters.workingDirectory }} displayName: midgard-yarn (faster yarn install) retryCountOnTaskFailure: 2 + ${{ if ne(coalesce(variables['RNW_BOOTSTRAP_NPM_REGISTRY'], ''), '') }}: + env: + NPM_CONFIG_REGISTRY: ${{ variables['RNW_BOOTSTRAP_NPM_REGISTRY'] }} From b002e8a7418328b430d42a79c66b3210e658a0bb Mon Sep 17 00:00:00 2001 From: Anukrati Agrawal Date: Sun, 4 Oct 2026 13:25:16 -0700 Subject: [PATCH 03/11] Gate bootstrap feed to PR tasks Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0f63ea4f-a16b-4fa3-b141-785aec89b5e4 --- .ado/build-template.yml | 25 ++++++++++++++++++------- .ado/pr-pipeline.yml | 3 --- .ado/templates/yarn-install.yml | 24 ++++++++++++++++++------ 3 files changed, 36 insertions(+), 16 deletions(-) diff --git a/.ado/build-template.yml b/.ado/build-template.yml index f0a2918e8b1..b5eb5ea5d9c 100644 --- a/.ado/build-template.yml +++ b/.ado/build-template.yml @@ -167,9 +167,16 @@ extends: script: | npm install --global yarn@1.22.22 midgard-yarn@1.23.34 verdaccio@6.7.2 displayName: Install Node base packages - ${{ if ne(coalesce(variables['RNW_BOOTSTRAP_NPM_REGISTRY'], ''), '') }}: - env: - NPM_CONFIG_REGISTRY: ${{ variables['RNW_BOOTSTRAP_NPM_REGISTRY'] }} + condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest')) + + - task: CmdLine@2 + inputs: + script: | + npm install --global yarn@1.22.22 midgard-yarn@1.23.34 verdaccio@6.7.2 + displayName: Install Node base packages + condition: and(succeeded(), eq(variables['Build.Reason'], 'PullRequest')) + env: + NPM_CONFIG_REGISTRY: https://pkgs.dev.azure.com/ms/react-native/_packaging/react-native-public/npm/registry/ - template: .ado/templates/compute-beachball-branch-name.yml@self @@ -185,11 +192,15 @@ extends: - script: npx --yes midgard-yarn-strict@1.2.4 @rnw-scripts/beachball-config displayName: Strict yarn install @rnw-scripts/beachball-config - condition: and(succeeded(), eq(variables['detectScenario.isReleaseBuild'], 'False')) + condition: and(succeeded(), eq(variables['detectScenario.isReleaseBuild'], 'False'), ne(variables['Build.Reason'], 'PullRequest')) retryCountOnTaskFailure: 2 - ${{ if ne(coalesce(variables['RNW_BOOTSTRAP_NPM_REGISTRY'], ''), '') }}: - env: - NPM_CONFIG_REGISTRY: ${{ variables['RNW_BOOTSTRAP_NPM_REGISTRY'] }} + + - script: npx --yes midgard-yarn-strict@1.2.4 @rnw-scripts/beachball-config + displayName: Strict yarn install @rnw-scripts/beachball-config + condition: and(succeeded(), eq(variables['detectScenario.isReleaseBuild'], 'False'), eq(variables['Build.Reason'], 'PullRequest')) + retryCountOnTaskFailure: 2 + env: + NPM_CONFIG_REGISTRY: https://pkgs.dev.azure.com/ms/react-native/_packaging/react-native-public/npm/registry/ - script: npx lage build --scope @rnw-scripts/prepare-release --scope @rnw-scripts/beachball-config displayName: Build prepare-release and beachball-config diff --git a/.ado/pr-pipeline.yml b/.ado/pr-pipeline.yml index 4c74cd4b5c3..44f7c28ab34 100644 --- a/.ado/pr-pipeline.yml +++ b/.ado/pr-pipeline.yml @@ -14,9 +14,6 @@ pr: variables: - group: platform-override-zero-permission-token - # Public PR agents acquire the bootstrap toolset through this feed when npmjs policy blocks midgard-yarn. - - name: RNW_BOOTSTRAP_NPM_REGISTRY - value: https://pkgs.dev.azure.com/ms/react-native/_packaging/react-native-public/npm/registry/ extends: template: build-template.yml@self diff --git a/.ado/templates/yarn-install.yml b/.ado/templates/yarn-install.yml index 4dede58c50d..ddc5445d306 100644 --- a/.ado/templates/yarn-install.yml +++ b/.ado/templates/yarn-install.yml @@ -21,9 +21,16 @@ steps: script: | npm install --global yarn@1.22.22 midgard-yarn@1.23.34 verdaccio@6.7.2 displayName: Install Node base packages - ${{ if ne(coalesce(variables['RNW_BOOTSTRAP_NPM_REGISTRY'], ''), '') }}: - env: - NPM_CONFIG_REGISTRY: ${{ variables['RNW_BOOTSTRAP_NPM_REGISTRY'] }} + condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest')) + + - task: CmdLine@2 + inputs: + script: | + npm install --global yarn@1.22.22 midgard-yarn@1.23.34 verdaccio@6.7.2 + displayName: Install Node base packages + condition: and(succeeded(), eq(variables['Build.Reason'], 'PullRequest')) + env: + NPM_CONFIG_REGISTRY: https://pkgs.dev.azure.com/ms/react-native/_packaging/react-native-public/npm/registry/ # When using our own images, prefer the machine-installed version of # `midgard-yarn`. @@ -38,6 +45,11 @@ steps: - script: npx --yes midgard-yarn@1.23.34 --ignore-scripts --frozen-lockfile --cwd ${{ parameters.workingDirectory }} displayName: midgard-yarn (faster yarn install) retryCountOnTaskFailure: 2 - ${{ if ne(coalesce(variables['RNW_BOOTSTRAP_NPM_REGISTRY'], ''), '') }}: - env: - NPM_CONFIG_REGISTRY: ${{ variables['RNW_BOOTSTRAP_NPM_REGISTRY'] }} + condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest')) + + - script: npx --yes midgard-yarn@1.23.34 --ignore-scripts --frozen-lockfile --cwd ${{ parameters.workingDirectory }} + displayName: midgard-yarn (faster yarn install) + retryCountOnTaskFailure: 2 + condition: and(succeeded(), eq(variables['Build.Reason'], 'PullRequest')) + env: + NPM_CONFIG_REGISTRY: https://pkgs.dev.azure.com/ms/react-native/_packaging/react-native-public/npm/registry/ From 28755c79ebc573c04b03871d74756cb19be6ac6b Mon Sep 17 00:00:00 2001 From: Anukrati Agrawal Date: Sun, 4 Oct 2026 22:12:42 -0700 Subject: [PATCH 04/11] Route Yarn lock downloads through PR proxy Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0f63ea4f-a16b-4fa3-b141-785aec89b5e4 --- .ado/build-template.yml | 15 +---- .ado/pr-pipeline.yml | 3 + .ado/scripts/install-yarn-dependencies.ps1 | 66 ++++++++++++++++++++++ .ado/templates/yarn-install.yml | 21 +++---- .ado/windows-vs-pr.yml | 3 + 5 files changed, 82 insertions(+), 26 deletions(-) create mode 100644 .ado/scripts/install-yarn-dependencies.ps1 diff --git a/.ado/build-template.yml b/.ado/build-template.yml index b5eb5ea5d9c..fdf809ff236 100644 --- a/.ado/build-template.yml +++ b/.ado/build-template.yml @@ -167,16 +167,7 @@ extends: script: | npm install --global yarn@1.22.22 midgard-yarn@1.23.34 verdaccio@6.7.2 displayName: Install Node base packages - condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest')) - - - task: CmdLine@2 - inputs: - script: | - npm install --global yarn@1.22.22 midgard-yarn@1.23.34 verdaccio@6.7.2 - displayName: Install Node base packages - condition: and(succeeded(), eq(variables['Build.Reason'], 'PullRequest')) - env: - NPM_CONFIG_REGISTRY: https://pkgs.dev.azure.com/ms/react-native/_packaging/react-native-public/npm/registry/ + retryCountOnTaskFailure: 2 - template: .ado/templates/compute-beachball-branch-name.yml@self @@ -195,12 +186,10 @@ extends: condition: and(succeeded(), eq(variables['detectScenario.isReleaseBuild'], 'False'), ne(variables['Build.Reason'], 'PullRequest')) retryCountOnTaskFailure: 2 - - script: npx --yes midgard-yarn-strict@1.2.4 @rnw-scripts/beachball-config + - pwsh: .ado/scripts/install-yarn-dependencies.ps1 -Installer Strict -Scope '@rnw-scripts/beachball-config' displayName: Strict yarn install @rnw-scripts/beachball-config condition: and(succeeded(), eq(variables['detectScenario.isReleaseBuild'], 'False'), eq(variables['Build.Reason'], 'PullRequest')) retryCountOnTaskFailure: 2 - env: - NPM_CONFIG_REGISTRY: https://pkgs.dev.azure.com/ms/react-native/_packaging/react-native-public/npm/registry/ - script: npx lage build --scope @rnw-scripts/prepare-release --scope @rnw-scripts/beachball-config displayName: Build prepare-release and beachball-config diff --git a/.ado/pr-pipeline.yml b/.ado/pr-pipeline.yml index 44f7c28ab34..a977113ff11 100644 --- a/.ado/pr-pipeline.yml +++ b/.ado/pr-pipeline.yml @@ -14,6 +14,9 @@ pr: variables: - group: platform-override-zero-permission-token + # PR builds use the anonymous package proxy because 1ES network isolation blocks public registries. + - name: NPM_CONFIG_REGISTRY + value: https://packagefeedproxy.microsoft.io/npm/ extends: template: build-template.yml@self diff --git a/.ado/scripts/install-yarn-dependencies.ps1 b/.ado/scripts/install-yarn-dependencies.ps1 new file mode 100644 index 00000000000..e63cd94ea42 --- /dev/null +++ b/.ado/scripts/install-yarn-dependencies.ps1 @@ -0,0 +1,66 @@ +param( + [Parameter(Mandatory = $true)] + [ValidateSet('Strict', 'Midgard', 'MidgardNpx')] + [string] $Installer, + + [string] $WorkingDirectory = '.', + + [string] $Scope +) + +$resolvedWorkingDirectory = (Resolve-Path $WorkingDirectory).Path +$lockFile = Join-Path $resolvedWorkingDirectory 'yarn.lock' +$originalLockFile = $null + +if ($env:BUILD_REASON -eq 'PullRequest') { + if (-not (Test-Path -LiteralPath $lockFile -PathType Leaf)) { + throw "Could not find yarn.lock in '$resolvedWorkingDirectory'." + } + + $registry = $env:NPM_CONFIG_REGISTRY + if ([string]::IsNullOrWhiteSpace($registry)) { + throw 'NPM_CONFIG_REGISTRY must be set for the network-isolated PR install.' + } + + $registry = $registry.TrimEnd('/') + '/' + $originalLockFile = [System.IO.File]::ReadAllBytes($lockFile) + $lockFileText = [System.Text.Encoding]::UTF8.GetString($originalLockFile) + $remappedLockFileText = $lockFileText. + Replace('https://registry.yarnpkg.com/', $registry). + Replace('https://registry.npmjs.org/', $registry) + + Write-Host "Temporarily routing yarn.lock package URLs through $registry" + [System.IO.File]::WriteAllText( + $lockFile, + $remappedLockFileText, + [System.Text.UTF8Encoding]::new($false) + ) +} + +try { + switch ($Installer) { + 'Strict' { + $arguments = @('--yes', 'midgard-yarn-strict@1.2.4') + if (-not [string]::IsNullOrWhiteSpace($Scope)) { + $arguments += $Scope + } + & npx @arguments + } + 'Midgard' { + & midgard-yarn --ignore-scripts --frozen-lockfile --cwd $resolvedWorkingDirectory + } + 'MidgardNpx' { + & npx --yes midgard-yarn@1.23.34 --ignore-scripts --frozen-lockfile --cwd $resolvedWorkingDirectory + } + } + + if ($LASTEXITCODE -ne 0) { + throw "$Installer dependency installation exited with code $LASTEXITCODE." + } +} +finally { + if ($null -ne $originalLockFile) { + [System.IO.File]::WriteAllBytes($lockFile, $originalLockFile) + Write-Host 'Restored the original yarn.lock.' + } +} diff --git a/.ado/templates/yarn-install.yml b/.ado/templates/yarn-install.yml index ddc5445d306..d05776c55dd 100644 --- a/.ado/templates/yarn-install.yml +++ b/.ado/templates/yarn-install.yml @@ -21,16 +21,7 @@ steps: script: | npm install --global yarn@1.22.22 midgard-yarn@1.23.34 verdaccio@6.7.2 displayName: Install Node base packages - condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest')) - - - task: CmdLine@2 - inputs: - script: | - npm install --global yarn@1.22.22 midgard-yarn@1.23.34 verdaccio@6.7.2 - displayName: Install Node base packages - condition: and(succeeded(), eq(variables['Build.Reason'], 'PullRequest')) - env: - NPM_CONFIG_REGISTRY: https://pkgs.dev.azure.com/ms/react-native/_packaging/react-native-public/npm/registry/ + retryCountOnTaskFailure: 2 # When using our own images, prefer the machine-installed version of # `midgard-yarn`. @@ -38,6 +29,12 @@ steps: - script: midgard-yarn --ignore-scripts --frozen-lockfile --cwd ${{ parameters.workingDirectory }} displayName: midgard-yarn (faster yarn install) retryCountOnTaskFailure: 2 + condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest')) + + - pwsh: .ado/scripts/install-yarn-dependencies.ps1 -Installer Midgard -WorkingDirectory '${{ parameters.workingDirectory }}' + displayName: midgard-yarn (faster yarn install) + retryCountOnTaskFailure: 2 + condition: and(succeeded(), eq(variables['Build.Reason'], 'PullRequest')) # If using an image we don't control, acquire a fixed version of midgard-yarn # before install @@ -47,9 +44,7 @@ steps: retryCountOnTaskFailure: 2 condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest')) - - script: npx --yes midgard-yarn@1.23.34 --ignore-scripts --frozen-lockfile --cwd ${{ parameters.workingDirectory }} + - pwsh: .ado/scripts/install-yarn-dependencies.ps1 -Installer MidgardNpx -WorkingDirectory '${{ parameters.workingDirectory }}' displayName: midgard-yarn (faster yarn install) retryCountOnTaskFailure: 2 condition: and(succeeded(), eq(variables['Build.Reason'], 'PullRequest')) - env: - NPM_CONFIG_REGISTRY: https://pkgs.dev.azure.com/ms/react-native/_packaging/react-native-public/npm/registry/ diff --git a/.ado/windows-vs-pr.yml b/.ado/windows-vs-pr.yml index 52958f7121b..42be702e5df 100644 --- a/.ado/windows-vs-pr.yml +++ b/.ado/windows-vs-pr.yml @@ -16,6 +16,9 @@ pr: variables: - group: platform-override-zero-permission-token + # PR builds use the anonymous package proxy because 1ES network isolation blocks public registries. + - name: NPM_CONFIG_REGISTRY + value: https://packagefeedproxy.microsoft.io/npm/ extends: template: build-template.yml@self From fa4a653284a3aa2bae9e362faae5afc2aab861ec Mon Sep 17 00:00:00 2001 From: Anukrati Agrawal Date: Mon, 5 Oct 2026 05:59:04 -0700 Subject: [PATCH 05/11] Route Verdaccio uplink through PR proxy Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0f63ea4f-a16b-4fa3-b141-785aec89b5e4 --- .ado/verdaccio/config.yaml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.ado/verdaccio/config.yaml b/.ado/verdaccio/config.yaml index 6309a4660c8..0353275fc5d 100644 --- a/.ado/verdaccio/config.yaml +++ b/.ado/verdaccio/config.yaml @@ -3,8 +3,8 @@ auth: htpasswd: file: ./htpasswd uplinks: - npmjs: - url: https://registry.npmjs.org/ + npmFeed: + url: https://packagefeedproxy.microsoft.io/npm/ max_fails: 40 maxage: 30m timeout: 60s @@ -14,14 +14,16 @@ uplinks: keepAlive: true maxSockets: 40 maxFreeSockets: 10 +publish: + allow_offline: true packages: '@*/*': access: $all publish: $all - proxy: npmjs + proxy: npmFeed '**': access: $all publish: $all - proxy: npmjs + proxy: npmFeed logs: - {type: file, path: verdaccio.log, format: pretty, level: debug} From 9591c63ae5ac9920261b5bcf413f3e85a47714a3 Mon Sep 17 00:00:00 2001 From: Anukrati Agrawal Date: Mon, 5 Oct 2026 07:17:26 -0700 Subject: [PATCH 06/11] Route CLI validation through local Verdaccio Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0f63ea4f-a16b-4fa3-b141-785aec89b5e4 --- .ado/templates/react-native-init-windows.yml | 8 ++++++-- .ado/templates/verdaccio-start.yml | 2 ++ .ado/templates/verdaccio-stop.yml | 1 + 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.ado/templates/react-native-init-windows.yml b/.ado/templates/react-native-init-windows.yml index df8113acc30..30da9376581 100644 --- a/.ado/templates/react-native-init-windows.yml +++ b/.ado/templates/react-native-init-windows.yml @@ -46,7 +46,9 @@ steps: - ${{ if endsWith(parameters.template, '-app') }}: - script: | - $(Build.SourcesDirectory)\vnext\Scripts\creaternwapp.cmd /rn $(reactNativeDevDependency) /rnw $(npmVersion) /t ${{ parameters.template }} /verdaccio testcli + if exist testcli rmdir /s /q testcli + if exist testcli exit /b 1 + call $(Build.SourcesDirectory)\vnext\Scripts\creaternwapp.cmd /rn $(reactNativeDevDependency) /rnw $(npmVersion) /t ${{ parameters.template }} /verdaccio testcli displayName: Init new app project with creaternwapp.cmd workingDirectory: $(Agent.BuildDirectory) retryCountOnTaskFailure: 2 @@ -55,7 +57,9 @@ steps: - ${{ if endsWith(parameters.template, '-lib') }}: - script: | - $(Build.SourcesDirectory)\vnext\Scripts\creaternwlib.cmd /rn $(reactNativeDevDependency) /rnw $(npmVersion) /t ${{ parameters.template }} /verdaccio testcli + if exist testcli rmdir /s /q testcli + if exist testcli exit /b 1 + call $(Build.SourcesDirectory)\vnext\Scripts\creaternwlib.cmd /rn $(reactNativeDevDependency) /rnw $(npmVersion) /t ${{ parameters.template }} /verdaccio testcli displayName: Init new lib project with creaternwlib.cmd workingDirectory: $(Agent.BuildDirectory) retryCountOnTaskFailure: 2 diff --git a/.ado/templates/verdaccio-start.yml b/.ado/templates/verdaccio-start.yml index 3a59d61c378..54d8d6a4e1f 100644 --- a/.ado/templates/verdaccio-start.yml +++ b/.ado/templates/verdaccio-start.yml @@ -40,6 +40,8 @@ steps: call yarn config set registry http://localhost:4873 call yarn config set npmRegistryServer http://localhost:4873 call yarn config set unsafeHttpWhitelist --json "[\"localhost\"]" + echo ##vso[task.setvariable variable=RNW_UPSTREAM_NPM_REGISTRY]%NPM_CONFIG_REGISTRY% + echo ##vso[task.setvariable variable=NPM_CONFIG_REGISTRY]http://localhost:4873 displayName: Modify npm/yarn config to point to local verdaccio server env: YARN_ENABLE_IMMUTABLE_INSTALLS: false diff --git a/.ado/templates/verdaccio-stop.yml b/.ado/templates/verdaccio-stop.yml index 17e4107396a..ecaaeda3712 100644 --- a/.ado/templates/verdaccio-stop.yml +++ b/.ado/templates/verdaccio-stop.yml @@ -17,6 +17,7 @@ steps: call yarn config delete registry call yarn config delete npmRegistryServer call yarn config delete unsafeHttpWhitelist + echo ##vso[task.setvariable variable=NPM_CONFIG_REGISTRY]$(RNW_UPSTREAM_NPM_REGISTRY) displayName: Reset npm/yarn config to stop poiting at local verdaccio server env: YARN_ENABLE_IMMUTABLE_INSTALLS: false \ No newline at end of file From 8915ffbccb66dbc4ea99c5361571a70a91e77f7a Mon Sep 17 00:00:00 2001 From: Anukrati Agrawal Date: Mon, 5 Oct 2026 09:43:04 -0700 Subject: [PATCH 07/11] Stabilize 0.84 PR validation Use request-specific IDs and callbacks in the HTTP OPTIONS integration test, and scope the local Verdaccio registry override to CLI creation scripts. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0f63ea4f-a16b-4fa3-b141-785aec89b5e4 --- .ado/templates/verdaccio-start.yml | 2 - .ado/templates/verdaccio-stop.yml | 2 +- .../HttpResourceIntegrationTests.cpp | 90 +++++++++++++------ vnext/Scripts/creaternwapp.cmd | 3 + vnext/Scripts/creaternwlib.cmd | 3 + 5 files changed, 71 insertions(+), 29 deletions(-) diff --git a/.ado/templates/verdaccio-start.yml b/.ado/templates/verdaccio-start.yml index 54d8d6a4e1f..3a59d61c378 100644 --- a/.ado/templates/verdaccio-start.yml +++ b/.ado/templates/verdaccio-start.yml @@ -40,8 +40,6 @@ steps: call yarn config set registry http://localhost:4873 call yarn config set npmRegistryServer http://localhost:4873 call yarn config set unsafeHttpWhitelist --json "[\"localhost\"]" - echo ##vso[task.setvariable variable=RNW_UPSTREAM_NPM_REGISTRY]%NPM_CONFIG_REGISTRY% - echo ##vso[task.setvariable variable=NPM_CONFIG_REGISTRY]http://localhost:4873 displayName: Modify npm/yarn config to point to local verdaccio server env: YARN_ENABLE_IMMUTABLE_INSTALLS: false diff --git a/.ado/templates/verdaccio-stop.yml b/.ado/templates/verdaccio-stop.yml index ecaaeda3712..90c35f35141 100644 --- a/.ado/templates/verdaccio-stop.yml +++ b/.ado/templates/verdaccio-stop.yml @@ -17,7 +17,7 @@ steps: call yarn config delete registry call yarn config delete npmRegistryServer call yarn config delete unsafeHttpWhitelist - echo ##vso[task.setvariable variable=NPM_CONFIG_REGISTRY]$(RNW_UPSTREAM_NPM_REGISTRY) displayName: Reset npm/yarn config to stop poiting at local verdaccio server + condition: succeededOrFailed() env: YARN_ENABLE_IMMUTABLE_INSTALLS: false \ No newline at end of file diff --git a/vnext/Desktop.IntegrationTests/HttpResourceIntegrationTests.cpp b/vnext/Desktop.IntegrationTests/HttpResourceIntegrationTests.cpp index c20c173e86e..effc9dd2917 100644 --- a/vnext/Desktop.IntegrationTests/HttpResourceIntegrationTests.cpp +++ b/vnext/Desktop.IntegrationTests/HttpResourceIntegrationTests.cpp @@ -15,6 +15,7 @@ #include // Standard Library +#include #include #include "HttpServer.h" @@ -302,12 +303,18 @@ TEST_CLASS (HttpResourceIntegrationTest) { } TEST_METHOD(RequestOptionsSucceeds) { + constexpr int64_t optionsRequestId = 1; + constexpr int64_t getRequestId = 2; string url = "http://localhost:" + std::to_string(s_port); promise getResponsePromise; promise getDataPromise; promise optionsPromise; - string error; + std::atomic_bool getResponseCompleted{false}; + std::atomic_bool getDataCompleted{false}; + std::atomic_bool optionsCompleted{false}; + string getError; + string optionsError; IHttpResource::Response getResponse; IHttpResource::Response optionsResponse; string content; @@ -330,38 +337,66 @@ TEST_CLASS (HttpResourceIntegrationTest) { server->Start(); auto resource = IHttpResource::Make(); - resource->SetOnResponse([&getResponse, &getResponsePromise, &optionsResponse, &optionsPromise]( - int64_t, IHttpResource::Response callbackResponse) { - if (callbackResponse.StatusCode == static_cast(http::status::ok)) { - getResponse = callbackResponse; - getResponsePromise.set_value(); - } else if (callbackResponse.StatusCode == static_cast(http::status::partial_content)) { - optionsResponse = callbackResponse; - optionsPromise.set_value(); + resource->SetOnResponse([&getResponse, + &getResponsePromise, + &getResponseCompleted, + &optionsResponse, + &optionsPromise, + &optionsCompleted, + getRequestId, + optionsRequestId](int64_t requestId, IHttpResource::Response callbackResponse) { + if (requestId == getRequestId) { + if (!getResponseCompleted.exchange(true)) { + getResponse = callbackResponse; + getResponsePromise.set_value(); + } + } else if (requestId == optionsRequestId) { + if (!optionsCompleted.exchange(true)) { + optionsResponse = callbackResponse; + optionsPromise.set_value(); + } } }); - resource->SetOnData([&getDataPromise, &content](int64_t, string &&responseData) { - content = std::move(responseData); - - if (!content.empty()) - getDataPromise.set_value(); - }); - resource->SetOnError( - [&optionsPromise, &getResponsePromise, &getDataPromise, &error, &server](int64_t, string &&message, bool) { - error = std::move(message); - - optionsPromise.set_value(); + resource->SetOnData( + [&getDataPromise, &getDataCompleted, &content, getRequestId](int64_t requestId, string &&responseData) { + if (requestId == getRequestId && !getDataCompleted.exchange(true)) { + content = std::move(responseData); + getDataPromise.set_value(); + } + }); + resource->SetOnError([&optionsPromise, + &optionsCompleted, + &getResponsePromise, + &getResponseCompleted, + &getDataPromise, + &getDataCompleted, + &getError, + &optionsError, + getRequestId, + optionsRequestId](int64_t requestId, string &&message, bool) { + if (requestId == optionsRequestId && !optionsCompleted.exchange(true)) { + optionsError = std::move(message); + optionsPromise.set_value(); + } else if (requestId == getRequestId) { + const bool completeResponse = !getResponseCompleted.exchange(true); + const bool completeData = !getDataCompleted.exchange(true); + if (completeResponse || completeData) { + getError = std::move(message); + } + if (completeResponse) { getResponsePromise.set_value(); + } + if (completeData) { getDataPromise.set_value(); - - server->Stop(); - }); + } + } + }); //clang-format off resource->SendRequest( "OPTIONS", string{url}, - 0, /*requestId*/ + optionsRequestId, {}, /*headers*/ {}, /*data*/ "text", @@ -372,7 +407,7 @@ TEST_CLASS (HttpResourceIntegrationTest) { resource->SendRequest( "GET", std::move(url), - 0, /*requestId*/ + getRequestId, {}, /*headers*/ {}, /*data*/ "text", @@ -387,7 +422,10 @@ TEST_CLASS (HttpResourceIntegrationTest) { getDataPromise.get_future().wait(); server->Stop(); - Assert::AreEqual({}, error, L"Error encountered"); + Assert::AreEqual({}, optionsError, L"OPTIONS error encountered"); + Assert::AreEqual({}, getError, L"GET error encountered"); + Assert::AreEqual(static_cast(http::status::partial_content), optionsResponse.StatusCode); + Assert::AreEqual(static_cast(http::status::ok), getResponse.StatusCode); Assert::AreEqual(static_cast(1), optionsResponse.Headers.size()); for (auto header : optionsResponse.Headers) { if (header.first == "PreflightName") { diff --git a/vnext/Scripts/creaternwapp.cmd b/vnext/Scripts/creaternwapp.cmd index f5de6fbc61a..b335ff466ac 100644 --- a/vnext/Scripts/creaternwapp.cmd +++ b/vnext/Scripts/creaternwapp.cmd @@ -72,6 +72,9 @@ if not "%part%"=="" ( if %USE_VERDACCIO% equ 1 ( @echo creaternwapp.cmd: Setting npm to use verdaccio at http://localhost:4873 + set NPM_CONFIG_REGISTRY=http://localhost:4873 + set YARN_NPM_REGISTRY_SERVER=http://localhost:4873 + set YARN_UNSAFE_HTTP_WHITELIST=localhost call npm config set registry http://localhost:4873 ) diff --git a/vnext/Scripts/creaternwlib.cmd b/vnext/Scripts/creaternwlib.cmd index b2bab04879d..6e45f3bd0af 100644 --- a/vnext/Scripts/creaternwlib.cmd +++ b/vnext/Scripts/creaternwlib.cmd @@ -77,6 +77,9 @@ if not "%part%"=="" ( if %USE_VERDACCIO% equ 1 ( @echo creaternwlib.cmd: Setting npm to use verdaccio at http://localhost:4873 + set NPM_CONFIG_REGISTRY=http://localhost:4873 + set YARN_NPM_REGISTRY_SERVER=http://localhost:4873 + set YARN_UNSAFE_HTTP_WHITELIST=localhost call npm config set registry http://localhost:4873 ) From 5650f1249825ad5e6e8f339c2c217b571b22ab01 Mon Sep 17 00:00:00 2001 From: Anukrati Agrawal Date: Mon, 5 Oct 2026 09:55:19 -0700 Subject: [PATCH 08/11] Add change file for PR validation fixes Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0f63ea4f-a16b-4fa3-b141-785aec89b5e4 --- ...ative-windows-0b30812f-d640-4e29-9a0e-fc3fc73cf5f7.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 change/react-native-windows-0b30812f-d640-4e29-9a0e-fc3fc73cf5f7.json diff --git a/change/react-native-windows-0b30812f-d640-4e29-9a0e-fc3fc73cf5f7.json b/change/react-native-windows-0b30812f-d640-4e29-9a0e-fc3fc73cf5f7.json new file mode 100644 index 00000000000..c3f194078b6 --- /dev/null +++ b/change/react-native-windows-0b30812f-d640-4e29-9a0e-fc3fc73cf5f7.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "Stabilize HTTP OPTIONS integration testing and local Verdaccio CLI validation", + "packageName": "react-native-windows", + "email": "anuagra@microsoft.com", + "dependentChangeType": "patch" +} From f3e3425632781fa6e76a8e050c5f8787b87cb9ae Mon Sep 17 00:00:00 2001 From: Anukrati Agrawal Date: Mon, 5 Oct 2026 11:23:36 -0700 Subject: [PATCH 09/11] Route CLI NuGet restore through compliant feed Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0f63ea4f-a16b-4fa3-b141-785aec89b5e4 --- .ado/templates/react-native-init-windows.yml | 35 ++++++++++++++++--- ...-0b30812f-d640-4e29-9a0e-fc3fc73cf5f7.json | 2 +- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/.ado/templates/react-native-init-windows.yml b/.ado/templates/react-native-init-windows.yml index 30da9376581..eb23ffa6db0 100644 --- a/.ado/templates/react-native-init-windows.yml +++ b/.ado/templates/react-native-init-windows.yml @@ -103,11 +103,36 @@ steps: workingDirectory: $(Agent.BuildDirectory)\testcli - pwsh: | - nuget.exe sources add -name TestFeed -source $(System.DefaultWorkingDirectory)\NugetTestFeed - nuget.exe sources remove -name react-native - nuget.exe sources remove -name Nuget.org - nuget.exe sources add -name Nuget.org -source https://api.nuget.org/v3/index.json - displayName: Add local NuGet test feed + $configPaths = @('NuGet.config') + if ('${{ parameters.template }}'.EndsWith('-lib')) { + $configPaths += 'example\NuGet.config' + } + + $sources = [ordered]@{ + TestFeed = '$(System.DefaultWorkingDirectory)\NugetTestFeed' + 'react-native' = 'https://pkgs.dev.azure.com/ms/react-native/_packaging/react-native-public/nuget/v3/index.json' + } + + foreach ($configPath in $configPaths) { + [xml] $config = Get-Content -LiteralPath $configPath + $packageSources = $config.configuration.packageSources + if (-not $packageSources) { + # The lib example inherits feeds from the root config. + Write-Host "Skipping $configPath (no packageSources element)" + continue + } + $packageSources.RemoveAll() + [void] $packageSources.AppendChild($config.CreateElement('clear')) + foreach ($source in $sources.GetEnumerator()) { + $element = $config.CreateElement('add') + $element.SetAttribute('key', $source.Key) + $element.SetAttribute('value', $source.Value) + [void] $packageSources.AppendChild($element) + } + $config.Save((Resolve-Path -LiteralPath $configPath)) + Write-Host "Updated $configPath" + } + displayName: Add compliant NuGet test feeds workingDirectory: $(Agent.BuildDirectory)\testcli - template: ../templates/run-windows-with-certificates.yml diff --git a/change/react-native-windows-0b30812f-d640-4e29-9a0e-fc3fc73cf5f7.json b/change/react-native-windows-0b30812f-d640-4e29-9a0e-fc3fc73cf5f7.json index c3f194078b6..49c9ff24cec 100644 --- a/change/react-native-windows-0b30812f-d640-4e29-9a0e-fc3fc73cf5f7.json +++ b/change/react-native-windows-0b30812f-d640-4e29-9a0e-fc3fc73cf5f7.json @@ -1,6 +1,6 @@ { "type": "patch", - "comment": "Stabilize HTTP OPTIONS integration testing and local Verdaccio CLI validation", + "comment": "Stabilize HTTP OPTIONS integration testing and network-isolated CLI validation", "packageName": "react-native-windows", "email": "anuagra@microsoft.com", "dependentChangeType": "patch" From b5b416da4c88c085f476c25ab82713f61f2cb6f4 Mon Sep 17 00:00:00 2001 From: Anukrati Agrawal Date: Mon, 5 Oct 2026 13:02:24 -0700 Subject: [PATCH 10/11] Scope Verdaccio proxy to PR validation Keep the anonymous proxy and offline publication behavior used by network-isolated PR jobs while preserving the existing npmjs uplink for continuous validation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0f63ea4f-a16b-4fa3-b141-785aec89b5e4 --- .ado/templates/react-native-init-windows.yml | 2 ++ .ado/templates/verdaccio-start.yml | 15 +++++++++- .ado/verdaccio/config.pr.yaml | 30 ++++++++++++++++++++ .ado/verdaccio/config.yaml | 11 ++++--- 4 files changed, 51 insertions(+), 7 deletions(-) create mode 100644 .ado/verdaccio/config.pr.yaml diff --git a/.ado/templates/react-native-init-windows.yml b/.ado/templates/react-native-init-windows.yml index eb23ffa6db0..1846d15939f 100644 --- a/.ado/templates/react-native-init-windows.yml +++ b/.ado/templates/react-native-init-windows.yml @@ -32,6 +32,8 @@ parameters: steps: # Start npm test server - template: verdaccio-start.yml + parameters: + buildEnvironment: ${{ parameters.buildEnvironment }} - template: set-version-vars.yml parameters: diff --git a/.ado/templates/verdaccio-start.yml b/.ado/templates/verdaccio-start.yml index 3a59d61c378..8619bd5c50d 100644 --- a/.ado/templates/verdaccio-start.yml +++ b/.ado/templates/verdaccio-start.yml @@ -3,9 +3,22 @@ parameters: - name: beachballPublish type: boolean default: true + - name: buildEnvironment + type: string + values: + - PullRequest + - Continuous steps: - - pwsh: start-process verdaccio.cmd -ArgumentList @('--config', './.ado/verdaccio/config.yaml') + - pwsh: | + $configPath = './.ado/verdaccio/config.yaml' + if ('${{ parameters.buildEnvironment }}' -eq 'PullRequest') { + $configPath = './.ado/verdaccio/config.pr.yaml' + } + if (-not (Test-Path -LiteralPath $configPath)) { + throw "Verdaccio config not found: $configPath" + } + start-process verdaccio.cmd -ArgumentList @('--config', $configPath) displayName: Launch test npm server (verdaccio) - script: node .ado/scripts/waitForVerdaccio.js diff --git a/.ado/verdaccio/config.pr.yaml b/.ado/verdaccio/config.pr.yaml new file mode 100644 index 00000000000..5ce8caf6f11 --- /dev/null +++ b/.ado/verdaccio/config.pr.yaml @@ -0,0 +1,30 @@ +# PR network-isolation variant; keep shared settings in sync with config.yaml. +storage: ./storage +auth: + htpasswd: + file: ./htpasswd +uplinks: + npmFeed: + url: https://packagefeedproxy.microsoft.io/npm/ + max_fails: 40 + maxage: 30m + timeout: 60s + fail_timeout: 10m + cache: false + agent_options: + keepAlive: true + maxSockets: 40 + maxFreeSockets: 10 +publish: + allow_offline: true +packages: + '@*/*': + access: $all + publish: $all + proxy: npmFeed + '**': + access: $all + publish: $all + proxy: npmFeed +logs: + - {type: file, path: verdaccio.log, format: pretty, level: debug} diff --git a/.ado/verdaccio/config.yaml b/.ado/verdaccio/config.yaml index 0353275fc5d..da98ba3d7df 100644 --- a/.ado/verdaccio/config.yaml +++ b/.ado/verdaccio/config.yaml @@ -1,10 +1,11 @@ +# Keep shared Verdaccio settings in sync with config.pr.yaml. storage: ./storage auth: htpasswd: file: ./htpasswd uplinks: - npmFeed: - url: https://packagefeedproxy.microsoft.io/npm/ + npmjs: + url: https://registry.npmjs.org/ max_fails: 40 maxage: 30m timeout: 60s @@ -14,16 +15,14 @@ uplinks: keepAlive: true maxSockets: 40 maxFreeSockets: 10 -publish: - allow_offline: true packages: '@*/*': access: $all publish: $all - proxy: npmFeed + proxy: npmjs '**': access: $all publish: $all - proxy: npmFeed + proxy: npmjs logs: - {type: file, path: verdaccio.log, format: pretty, level: debug} From b57d2c9ee5054b5ba75f4794f5d68e0fe8240f6a Mon Sep 17 00:00:00 2001 From: Anukrati Agrawal Date: Mon, 5 Oct 2026 14:11:07 -0700 Subject: [PATCH 11/11] Wait for filtered FlatList rendering in E2E test Backport the proven synchronization guard so the visual-tree snapshot is captured only after the filtered item replaces the transient list header. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0f63ea4f-a16b-4fa3-b141-785aec89b5e4 --- .../test/FlatListComponentTest.test.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/packages/e2e-test-app-fabric/test/FlatListComponentTest.test.ts b/packages/e2e-test-app-fabric/test/FlatListComponentTest.test.ts index b52afe565a3..44e0c8018e6 100644 --- a/packages/e2e-test-app-fabric/test/FlatListComponentTest.test.ts +++ b/packages/e2e-test-app-fabric/test/FlatListComponentTest.test.ts @@ -74,7 +74,22 @@ describe('FlatList Tests', () => { await searchBoxBasic('555'); const component = await app.findElementByTestID('flatlist-basic'); await component.waitForDisplayed({timeout: 5000}); - const dump = await dumpVisualTree('flatlist-basic'); + let dump = await dumpVisualTree('flatlist-basic'); + await app.waitUntil( + async () => { + dump = await dumpVisualTree('flatlist-basic'); + const automationTree = JSON.stringify(dump['Automation Tree']); + return ( + automationTree.includes('Item 555 -') && + !automationTree.includes('LIST HEADER') + ); + }, + { + interval: 250, + timeout: 20000, + timeoutMsg: 'Filtered FlatList did not finish rendering.', + }, + ); expect(dump).toMatchSnapshot(); }); test('A FlatList has an onStartReached event', async () => {