From cd8961a242c304128d3d39001959feb51c7c049a Mon Sep 17 00:00:00 2001 From: Vladimir Morozov Date: Wed, 19 Aug 2026 20:33:19 -0700 Subject: [PATCH 1/3] Fix CI pipeline and add warm-feed --- .ado/release-pipeline.yml | 36 +++ .ado/templates/msbuild-sln.yml | 1 - .ado/templates/prepare-build-env.yml | 13 - .ado/templates/publish-npm-to-ado-feed.yml | 51 ++++ .../run-windows-with-certificates.yml | 2 +- .ado/warm-feed-cache-pipeline.yml | 117 ++++---- .ado/warm-feed-pipeline.yml | 100 +++++++ .nuget/empty-uwp-fallback/.gitkeep | 7 + Directory.Build.props | 9 +- package.json | 1 + .../AutomationChannel/packages.lock.json | 6 +- .../cli/src/e2etest/createRnwApp.test.ts | 38 ++- .../tester/js/examples/Image/ImageExample.js | 4 +- .../js/examples/Layout/LayoutEventsExample.js | 2 +- .../js/examples/Pressable/PressableExample.js | 2 +- .../js/examples/Touchable/TouchableExample.js | 2 +- packages/@react-native/tester/overrides.json | 54 +++- .../prepare-release/src/beachballBump.ts | 16 ++ .../prepare-release/src/prepareRelease.ts | 7 +- packages/@rnw-scripts/warm-feed/.eslintrc.js | 4 + packages/@rnw-scripts/warm-feed/.gitignore | 2 + packages/@rnw-scripts/warm-feed/README.md | 88 ++++++ packages/@rnw-scripts/warm-feed/bin.js | 11 + packages/@rnw-scripts/warm-feed/package.json | 43 +++ packages/@rnw-scripts/warm-feed/src/auth.ts | 79 ++++++ packages/@rnw-scripts/warm-feed/src/config.ts | 61 ++++ packages/@rnw-scripts/warm-feed/src/expand.ts | 147 ++++++++++ .../warm-feed/src/feedPackages.ts | 66 +++++ packages/@rnw-scripts/warm-feed/src/http.ts | 146 ++++++++++ packages/@rnw-scripts/warm-feed/src/logger.ts | 29 ++ packages/@rnw-scripts/warm-feed/src/pool.ts | 26 ++ .../@rnw-scripts/warm-feed/src/registries.ts | 146 ++++++++++ packages/@rnw-scripts/warm-feed/src/run.ts | 266 ++++++++++++++++++ packages/@rnw-scripts/warm-feed/src/types.ts | 103 +++++++ .../@rnw-scripts/warm-feed/src/versions.ts | 103 +++++++ .../@rnw-scripts/warm-feed/src/warmFeed.ts | 76 +++++ .../@rnw-scripts/warm-feed/src/warmers.ts | 79 ++++++ packages/@rnw-scripts/warm-feed/tsconfig.json | 5 + .../warm-feed/warm-feed.config.json | 23 ++ .../__snapshots__/snapshotPages.test.js.snap | 2 +- .../packages.lock.json | 8 +- .../RNTesterApp-Fabric/packages.lock.json | 8 +- .../packages.experimentalwinui3.lock.json | 8 +- .../packages.lock.json | 8 +- .../packages.experimentalwinui3.lock.json | 8 +- .../playground-composition/packages.lock.json | 8 +- .../packages.lock.json | 8 +- .../SampleAppFabric/packages.lock.json | 8 +- .../packages.experimentalwinui3.lock.json | 6 +- .../SampleCustomComponent/packages.lock.json | 6 +- .../packages.experimentalwinui3.lock.json | 8 +- vnext/Desktop.DLL/packages.lock.json | 8 +- .../packages.experimentalwinui3.lock.json | 10 +- .../packages.lock.json | 10 +- .../packages.experimentalwinui3.lock.json | 6 +- vnext/Desktop.UnitTests/packages.lock.json | 6 +- .../packages.experimentalwinui3.lock.json | 6 +- vnext/Desktop/packages.lock.json | 6 +- .../packages.experimentalwinui3.lock.json | 6 +- .../packages.lock.json | 6 +- .../packages.experimentalwinui3.lock.json | 6 +- .../packages.lock.json | 6 +- .../packages.experimentalwinui3.lock.json | 6 +- .../Microsoft.ReactNative/packages.lock.json | 6 +- vnext/PropertySheets/JSEngine.props | 2 +- vnext/PropertySheets/NuGet.LockFile.props | 11 +- .../packages.experimentalwinui3.lock.json | 6 +- .../ReactCommon.UnitTests/packages.lock.json | 6 +- .../NuGetRestoreForceEvaluateAllSolutions.ps1 | 59 +++- vnext/Scripts/Warm-RnwFeedCache.ps1 | 95 ++++++- yarn.lock | 19 ++ 71 files changed, 2125 insertions(+), 218 deletions(-) create mode 100644 .ado/templates/publish-npm-to-ado-feed.yml create mode 100644 .ado/warm-feed-pipeline.yml create mode 100644 .nuget/empty-uwp-fallback/.gitkeep create mode 100644 packages/@rnw-scripts/warm-feed/.eslintrc.js create mode 100644 packages/@rnw-scripts/warm-feed/.gitignore create mode 100644 packages/@rnw-scripts/warm-feed/README.md create mode 100644 packages/@rnw-scripts/warm-feed/bin.js create mode 100644 packages/@rnw-scripts/warm-feed/package.json create mode 100644 packages/@rnw-scripts/warm-feed/src/auth.ts create mode 100644 packages/@rnw-scripts/warm-feed/src/config.ts create mode 100644 packages/@rnw-scripts/warm-feed/src/expand.ts create mode 100644 packages/@rnw-scripts/warm-feed/src/feedPackages.ts create mode 100644 packages/@rnw-scripts/warm-feed/src/http.ts create mode 100644 packages/@rnw-scripts/warm-feed/src/logger.ts create mode 100644 packages/@rnw-scripts/warm-feed/src/pool.ts create mode 100644 packages/@rnw-scripts/warm-feed/src/registries.ts create mode 100644 packages/@rnw-scripts/warm-feed/src/run.ts create mode 100644 packages/@rnw-scripts/warm-feed/src/types.ts create mode 100644 packages/@rnw-scripts/warm-feed/src/versions.ts create mode 100644 packages/@rnw-scripts/warm-feed/src/warmFeed.ts create mode 100644 packages/@rnw-scripts/warm-feed/src/warmers.ts create mode 100644 packages/@rnw-scripts/warm-feed/tsconfig.json create mode 100644 packages/@rnw-scripts/warm-feed/warm-feed.config.json diff --git a/.ado/release-pipeline.yml b/.ado/release-pipeline.yml index e317a4bda50..f8bcd0817a7 100644 --- a/.ado/release-pipeline.yml +++ b/.ado/release-pipeline.yml @@ -229,6 +229,42 @@ extends: owners: 'vmorozov@microsoft.com' approvers: 'khosany@microsoft.com' + - job: PushNpmPublicAdo + displayName: ADO - npm - react-native-public + timeoutInMinutes: 30 + templateContext: + type: releaseJob + isProduction: true + inputs: + - input: pipelineArtifact + pipeline: 'CI' + artifactName: 'NpmPackedTarballs' + targetPath: '$(Pipeline.Workspace)/npm-feed-packages' + steps: + - template: .ado/templates/publish-npm-to-ado-feed.yml@self + parameters: + npmFeedRegistry: 'https://pkgs.dev.azure.com/ms/react-native/_packaging/react-native-public/npm/registry/' + packagesPath: '$(Pipeline.Workspace)/npm-feed-packages' + feedDisplayName: 'ms/react-native-public' + + - job: PushNpmPrivateAdo + displayName: ADO - npm - react-native + timeoutInMinutes: 30 + templateContext: + type: releaseJob + isProduction: true + inputs: + - input: pipelineArtifact + pipeline: 'CI' + artifactName: 'NpmPackedTarballs' + targetPath: '$(Pipeline.Workspace)/npm-feed-packages' + steps: + - template: .ado/templates/publish-npm-to-ado-feed.yml@self + parameters: + npmFeedRegistry: 'https://pkgs.dev.azure.com/ms/_packaging/react-native/npm/registry/' + packagesPath: '$(Pipeline.Workspace)/npm-feed-packages' + feedDisplayName: 'ms/react-native' + - job: PushPrivateAdo displayName: ADO - nuget - react-native timeoutInMinutes: 30 diff --git a/.ado/templates/msbuild-sln.yml b/.ado/templates/msbuild-sln.yml index 3cfbce25ec7..ae303bc3358 100644 --- a/.ado/templates/msbuild-sln.yml +++ b/.ado/templates/msbuild-sln.yml @@ -47,7 +47,6 @@ steps: /p:PlatformToolset=${{parameters.platformToolset}} /p:PublishToolDuringBuild=true /p:RestoreLockedMode=true - /p:RestoreForceEvaluate=true /bl:$(BuildLogDirectory)\MsBuild.binlog /flp1:errorsonly;logfile=$(BuildLogDirectory)\MsBuild.err.log /flp2:warningsonly;logfile=$(BuildLogDirectory)\MsBuild.wrn.log diff --git a/.ado/templates/prepare-build-env.yml b/.ado/templates/prepare-build-env.yml index 72ed3acdafb..8e6ea5449c4 100644 --- a/.ado/templates/prepare-build-env.yml +++ b/.ado/templates/prepare-build-env.yml @@ -31,19 +31,6 @@ parameters: # invoked. Example: ['RNTesterApp-Fabric', 'Playground']. steps: - # The VS Installer's background auto-update service otherwise wakes up mid-build and - # downloads VS updates from the MS CDN, which trips the network isolation policy. - # Follow-up: bake this into the agent image so it doesn't have to run per job. - - pwsh: | - foreach ($key in @( - 'HKLM:\SOFTWARE\Microsoft\VisualStudio\Setup', - 'HKLM:\SOFTWARE\Policies\Microsoft\VisualStudio\Setup')) { - New-Item -Path $key -Force | Out-Null - New-ItemProperty -Path $key -Name BackgroundDownload -PropertyType DWord -Value 0 -Force | Out-Null - } - Get-Process -Name BackgroundDownload -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue - displayName: Disable VS Installer background download - # The commit tag in the nuspec requires that we use at least nuget 5.8 (because things break with nuget versions before and Vs 16.8 or later) - task: NuGetToolInstaller@1 displayName: Set NuGet version diff --git a/.ado/templates/publish-npm-to-ado-feed.yml b/.ado/templates/publish-npm-to-ado-feed.yml new file mode 100644 index 00000000000..de0ca684124 --- /dev/null +++ b/.ado/templates/publish-npm-to-ado-feed.yml @@ -0,0 +1,51 @@ +# Publishes packed npm tarballs to an Azure Artifacts feed's npm registry, mirroring +# publish-nuget-to-ado-feed.yml. Auth uses the shared managed identity (same identity/ +# resource the NuGet feed publish uses). +parameters: +- name: azureSubscription + type: string + default: 'Office-Hermes-Windows-Bot' +- name: npmFeedRegistry + type: string +- name: packagesPath + type: string +- name: feedDisplayName + type: string + +steps: +- task: AzureCLI@2 + displayName: Acquire ${{ parameters.feedDisplayName }} feed token + inputs: + azureSubscription: ${{ parameters.azureSubscription }} + visibleAzLogin: false + scriptType: pscore + scriptLocation: inlineScript + inlineScript: | + $token = az account get-access-token --query accessToken --resource 499b84ac-1321-427f-aa17-267ca6975798 -o tsv + if ([string]::IsNullOrWhiteSpace($token)) { throw 'Failed to acquire a feed access token.' } + Write-Host "##vso[task.setsecret]$token" + Write-Host "##vso[task.setvariable variable=AdoNpmFeedToken;issecret=true]$token" + +- pwsh: | + # The .npmrc holds only the ${NPM_FEED_TOKEN} placeholder; npm expands it from the masked env + # var at run time, so the raw token never lands in a file. A version already present in the feed + # (locally or via its npmjs upstream) returns 409, which we treat as success. + $registry = '${{ parameters.npmFeedRegistry }}' + $key = ($registry -replace '^https?:', '') + Set-Content -Path (Join-Path $env:USERPROFILE '.npmrc') -Encoding ascii -Value @( + "registry=$registry" + "${key}:_authToken=`${NPM_FEED_TOKEN}" + ) + $tgzs = @(Get-ChildItem -Path '${{ parameters.packagesPath }}' -Filter *.tgz -Recurse) + Write-Host "Publishing $($tgzs.Count) package(s) to ${{ parameters.feedDisplayName }}" + $failed = @() + foreach ($tgz in $tgzs) { + $out = & npm publish $tgz.FullName --registry $registry 2>&1 | Out-String + if ($LASTEXITCODE -eq 0) { Write-Host "published $($tgz.Name)" } + elseif ($out -match 'already exists|EPUBLISHCONFLICT|cannot publish over|\b409\b') { Write-Host "skipped (already in feed): $($tgz.Name)" } + else { Write-Host "##[error]Failed to publish $($tgz.Name): $out"; $failed += $tgz.Name } + } + if ($failed.Count -gt 0) { throw "Failed to publish $($failed.Count) package(s) to ${{ parameters.feedDisplayName }}." } + displayName: Publish npm packages to ${{ parameters.feedDisplayName }} + env: + NPM_FEED_TOKEN: $(AdoNpmFeedToken) diff --git a/.ado/templates/run-windows-with-certificates.yml b/.ado/templates/run-windows-with-certificates.yml index c05eef784e8..e7ecb53f08f 100644 --- a/.ado/templates/run-windows-with-certificates.yml +++ b/.ado/templates/run-windows-with-certificates.yml @@ -24,7 +24,7 @@ parameters: default: true - name: restoreForceEvaluate type: boolean - default: true + default: false - name: errorOnNuGetLockChanges type: boolean default : true diff --git a/.ado/warm-feed-cache-pipeline.yml b/.ado/warm-feed-cache-pipeline.yml index b692209ef0b..88e9b435b0d 100644 --- a/.ado/warm-feed-cache-pipeline.yml +++ b/.ado/warm-feed-cache-pipeline.yml @@ -1,81 +1,100 @@ +# Scheduled feed-warming pipeline (office/ISS). # -# Scheduled feed-warming pipeline (office/ISS, non-production). -# -# Runs Warm-RnwFeedCache.ps1 to save the CLI-init toolchain closure into the -# ms/react-native-public feed with an authenticated identity, so anonymous PR -# builds restore cleanly instead of 404ing on a not-yet-cached transitive package. -# -# Runs every 6 hours; drop to hourly later if it stays light. +# Enumerates the ms/react-native-public feed and re-pulls, with the pipeline's +# managed identity, the latest patch of every npm/NuGet major.minor line already +# in use, so anonymous network-isolated PR/CI builds can restore them. # +# Runs out of band (never in a PR build) because saving into the feed needs the +# managed identity. A maintainer can also queue it with the `packages` parameter +# to warm a specific set of versions on demand. -name: 0.0.$(Date:yyMM.d)$(Rev:rrr) +name: $(Date:yyyyMMdd).$(Rev:r) trigger: none pr: none +parameters: + - name: packages + displayName: 'One-off warm (space-separated): npm:foo@1.2.3 nuget:Bar@4.0.0' + type: string + default: ' ' + schedules: - - cron: "0 0,6,12,18 * * *" + - cron: '0 0,6,12,18 * * *' displayName: Every 6 hours branches: include: - main always: true +# Route npm/Yarn/NuGet through the ms/react-native-public feed (matches CI) under network isolation. +variables: + - template: variables/shared.yml + resources: repositories: - - repository: OfficePipelineTemplates - type: git - name: 1ESPipelineTemplates/OfficePipelineTemplates - ref: refs/tags/release + - repository: OfficePipelineTemplates + type: git + name: 1ESPipelineTemplates/OfficePipelineTemplates + ref: refs/tags/release extends: template: v1/Office.Unofficial.PipelineTemplate.yml@OfficePipelineTemplates parameters: pool: - name: fabric-internal-pool-large - demands: ImageOverride -equals rnw-img-vs2026-node24 + name: Azure-Pipelines-1ESPT-ExDShared + vmImage: windows-latest + os: windows sdl: bandit: enabled: false + # Skip ESLint SDL on this utility pipeline (CI/Release run it on the code); the Unofficial + # template's --exit-on-fatal-error trips on repo-wide Guardian ES5-parser parse noise. eslint: - enableExclusions: true + enabled: false suppression: suppressionFile: $(Build.SourcesDirectory)\.ado\guardian\sdl\.gdnsuppress stages: - - stage: Warm - displayName: Warm feed cache - jobs: - - job: WarmFeed - displayName: Warm npm and NuGet feed cache - timeoutInMinutes: 60 - steps: - - checkout: self - fetchDepth: 1 + - stage: Warm + displayName: Warm feed cache + jobs: + - job: WarmFeed + displayName: Warm npm and NuGet feed cache + timeoutInMinutes: 60 + steps: + - checkout: self + fetchDepth: 1 + + - task: UseNode@1 + displayName: Use Node.js 24.x + inputs: + version: '24.x' - - task: UseNode@1 - displayName: Use Node.js 24.x - inputs: - version: '24.x' + # Authenticate npm/Yarn to the feed before install (same MI as CI). + - template: .ado/templates/auth-npm-feed.yml@self - # The agent image does not guarantee Yarn (build-template.yml installs it - # explicitly), and the warm script runs `yarn install`. Authenticate npm to - # the feed, then install the same pinned Yarn from it. - - template: .ado/templates/auth-npm-feed.yml@self + - script: yarn install --immutable + displayName: yarn install + retryCountOnTaskFailure: 2 - - task: CmdLine@2 - displayName: Install pinned Yarn from the feed - inputs: - script: npm install --global yarn@1.22.22 --registry https://pkgs.dev.azure.com/ms/react-native/_packaging/react-native-public/npm/registry/ + - script: npx lage build --scope @rnw-scripts/warm-feed + displayName: Build warm-feed + retryCountOnTaskFailure: 2 - # Interim identity (shared with auth-npm-feed.yml); swap to the RNW managed - # identity once it is provisioned. AzureCLI@2 logs in az as this identity, so - # the script's `az account get-access-token` authenticates to the feed. - - task: AzureCLI@2 - displayName: Warm ms/react-native-public feed - inputs: - azureSubscription: Office-Hermes-Windows-Bot - scriptType: pscore - scriptLocation: inlineScript - inlineScript: | - $ErrorActionPreference = 'Stop' - & "$(Build.SourcesDirectory)/vnext/Scripts/Warm-RnwFeedCache.ps1" + # AzureCLI logs `az` in as the managed identity, so + # `az account get-access-token` mints the feed token the tool reads + # from WARM_FEED_TOKEN. + - task: AzureCLI@2 + displayName: Warm ms/react-native-public feed + inputs: + azureSubscription: Office-Hermes-Windows-Bot + scriptType: pscore + scriptLocation: inlineScript + inlineScript: | + $ErrorActionPreference = 'Stop' + $env:WARM_FEED_TOKEN = az account get-access-token ` + --resource 499b84ac-1321-427f-aa17-267ca6975798 --query accessToken -o tsv + $pkgs = '${{ parameters.packages }}'.Trim() + $warmArgs = @() + if ($pkgs) { foreach ($p in ($pkgs -split '\s+')) { $warmArgs += @('--packages', $p) } } + npx warm-feed @warmArgs diff --git a/.ado/warm-feed-pipeline.yml b/.ado/warm-feed-pipeline.yml new file mode 100644 index 00000000000..88e9b435b0d --- /dev/null +++ b/.ado/warm-feed-pipeline.yml @@ -0,0 +1,100 @@ +# Scheduled feed-warming pipeline (office/ISS). +# +# Enumerates the ms/react-native-public feed and re-pulls, with the pipeline's +# managed identity, the latest patch of every npm/NuGet major.minor line already +# in use, so anonymous network-isolated PR/CI builds can restore them. +# +# Runs out of band (never in a PR build) because saving into the feed needs the +# managed identity. A maintainer can also queue it with the `packages` parameter +# to warm a specific set of versions on demand. + +name: $(Date:yyyyMMdd).$(Rev:r) + +trigger: none +pr: none + +parameters: + - name: packages + displayName: 'One-off warm (space-separated): npm:foo@1.2.3 nuget:Bar@4.0.0' + type: string + default: ' ' + +schedules: + - cron: '0 0,6,12,18 * * *' + displayName: Every 6 hours + branches: + include: + - main + always: true + +# Route npm/Yarn/NuGet through the ms/react-native-public feed (matches CI) under network isolation. +variables: + - template: variables/shared.yml + +resources: + repositories: + - repository: OfficePipelineTemplates + type: git + name: 1ESPipelineTemplates/OfficePipelineTemplates + ref: refs/tags/release + +extends: + template: v1/Office.Unofficial.PipelineTemplate.yml@OfficePipelineTemplates + parameters: + pool: + name: Azure-Pipelines-1ESPT-ExDShared + vmImage: windows-latest + os: windows + sdl: + bandit: + enabled: false + # Skip ESLint SDL on this utility pipeline (CI/Release run it on the code); the Unofficial + # template's --exit-on-fatal-error trips on repo-wide Guardian ES5-parser parse noise. + eslint: + enabled: false + suppression: + suppressionFile: $(Build.SourcesDirectory)\.ado\guardian\sdl\.gdnsuppress + stages: + - stage: Warm + displayName: Warm feed cache + jobs: + - job: WarmFeed + displayName: Warm npm and NuGet feed cache + timeoutInMinutes: 60 + steps: + - checkout: self + fetchDepth: 1 + + - task: UseNode@1 + displayName: Use Node.js 24.x + inputs: + version: '24.x' + + # Authenticate npm/Yarn to the feed before install (same MI as CI). + - template: .ado/templates/auth-npm-feed.yml@self + + - script: yarn install --immutable + displayName: yarn install + retryCountOnTaskFailure: 2 + + - script: npx lage build --scope @rnw-scripts/warm-feed + displayName: Build warm-feed + retryCountOnTaskFailure: 2 + + # AzureCLI logs `az` in as the managed identity, so + # `az account get-access-token` mints the feed token the tool reads + # from WARM_FEED_TOKEN. + - task: AzureCLI@2 + displayName: Warm ms/react-native-public feed + inputs: + azureSubscription: Office-Hermes-Windows-Bot + scriptType: pscore + scriptLocation: inlineScript + inlineScript: | + $ErrorActionPreference = 'Stop' + $env:WARM_FEED_TOKEN = az account get-access-token ` + --resource 499b84ac-1321-427f-aa17-267ca6975798 --query accessToken -o tsv + $pkgs = '${{ parameters.packages }}'.Trim() + $warmArgs = @() + if ($pkgs) { foreach ($p in ($pkgs -split '\s+')) { $warmArgs += @('--packages', $p) } } + npx warm-feed @warmArgs diff --git a/.nuget/empty-uwp-fallback/.gitkeep b/.nuget/empty-uwp-fallback/.gitkeep new file mode 100644 index 00000000000..62a6bbb5455 --- /dev/null +++ b/.nuget/empty-uwp-fallback/.gitkeep @@ -0,0 +1,7 @@ +# Intentionally-empty NuGet fallback folder. +# +# Directory.Build.props points UWPNugetRepo here so that +# Microsoft.NETCore.UniversalWindowsPlatform / runtime.win10-* resolve from the +# ADO feed instead of the machine-local, SDK-versioned Windows SDK copy (whose +# per-machine signature makes committed lock hashes mismatch across dev/CI -> NU1403). +# Build-time .NET Native injection is unaffected; it reads the SDK folder directly. diff --git a/Directory.Build.props b/Directory.Build.props index 6bf36e02259..3fb892634c0 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -29,8 +29,15 @@ $(RootIntDir)\ProjectExtensions\$(ProjectName)\ $(RootIntDir)\ProjectExtensions\$(MSBuildProjectName)\ true - true + + $(MSBuildThisFileDirectory).nuget\empty-uwp-fallback true + + false + false true diff --git a/package.json b/package.json index 6b4d250a438..51b33946d9c 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "format:verify": "format-files -i -style=file -verify", "postinstall": "yarn build", "release-notes": "yarn workspace @rnw-scripts/generate-release-notes release-notes", + "warm-feed": "yarn workspace @rnw-scripts/warm-feed warm-feed", "spellcheck": "npx cspell", "test": "lage test --verbose --passWithNoTests", "validate-overrides": "react-native-platform-override validate", diff --git a/packages/@react-native-windows/automation-channel/windows/AutomationChannel/packages.lock.json b/packages/@react-native-windows/automation-channel/windows/AutomationChannel/packages.lock.json index 2d4d412900a..8dabefc19c4 100644 --- a/packages/@react-native-windows/automation-channel/windows/AutomationChannel/packages.lock.json +++ b/packages/@react-native-windows/automation-channel/windows/AutomationChannel/packages.lock.json @@ -44,8 +44,8 @@ }, "Microsoft.JavaScript.Hermes": { "type": "Transitive", - "resolved": "0.0.0-2605.6002-2279da22", - "contentHash": "E59URq24UdJMpLMkMY92h3hs91EHelicbUsBRETn+FJ19uQbzU3gqkh2NPyBtMj4IVYK5K9FW+hIUTLg8SG25g==" + "resolved": "0.0.0-2608.12001-35d34796", + "contentHash": "6EirG0swfzCbfajtXtUUUFJJqi0yYzYOMRjkx0XcLB1a8qYjAhRcFdAfsNDvxPbs0ErI+6++EjBZquf3YFQZDA==" }, "Microsoft.SourceLink.Common": { "type": "Transitive", @@ -165,7 +165,7 @@ "type": "Project", "dependencies": { "Common": "[1.0.0, )", - "Microsoft.JavaScript.Hermes": "[0.0.0-2605.6002-2279da22, )", + "Microsoft.JavaScript.Hermes": "[0.0.0-2608.12001-35d34796, )", "Microsoft.SourceLink.GitHub": "[1.1.1, )", "Microsoft.WindowsAppSDK": "[1.8.260508005, )", "ReactCommon": "[1.0.0, )", diff --git a/packages/@react-native-windows/cli/src/e2etest/createRnwApp.test.ts b/packages/@react-native-windows/cli/src/e2etest/createRnwApp.test.ts index aac78624436..fc7d58e4d20 100644 --- a/packages/@react-native-windows/cli/src/e2etest/createRnwApp.test.ts +++ b/packages/@react-native-windows/cli/src/e2etest/createRnwApp.test.ts @@ -8,23 +8,36 @@ import fs from '@react-native-windows/fs'; import * as path from 'path'; import {execSync} from 'child_process'; +// The creaternwapp tests below only validate command-string construction against a +// mocked npm registry (mockNpmShow); they never install a real package. Resolve versions +// best-effort with deterministic fallbacks instead of failing the suite when one isn't +// published: `preview` versions are cut by in-progress *-stable branches (not main) and +// are absent from the network-isolated CI feed, so requiring one made main's CI depend on +// an unrelated branch. +const FALLBACK_STABLE_VERSION = '0.0.0'; +const FALLBACK_PREVIEW_VERSION = '0.0.0-preview.1'; + /** - * Get latest stable version from npm + * Get latest stable version from npm, or a deterministic fallback. */ function getLatestStableVersion(): string { try { - return execSync('npm view react-native-windows version', { + const version = execSync('npm view react-native-windows version', { encoding: 'utf8', }).trim(); + if (version && !version.includes('preview')) { + return version; + } } catch (error) { - throw new Error(`Could not fetch latest stable version from npm: ${error}`); + console.warn('Could not fetch latest stable version from npm:', error); } + return FALLBACK_STABLE_VERSION; } /** - * Get latest preview version from npm + * Get latest preview version from npm, or a deterministic fallback. */ -function getLatestPreviewVersion(): string | undefined { +function getLatestPreviewVersion(): string { try { const versions = JSON.parse( execSync('npm view react-native-windows versions --json', { @@ -32,24 +45,19 @@ function getLatestPreviewVersion(): string | undefined { }), ) as string[]; // Preview versions usually have "preview" in the string - return versions.reverse().find(v => v.includes('preview')); + const preview = versions.reverse().find(v => v.includes('preview')); + if (preview) { + return preview; + } } catch (error) { console.warn('Could not fetch preview versions from npm:', error); - return undefined; } + return FALLBACK_PREVIEW_VERSION; } const LATEST_STABLE_VERSION = getLatestStableVersion(); const LATEST_PREVIEW_VERSION = getLatestPreviewVersion(); -// Ensure we have valid versions for testing -if (!LATEST_STABLE_VERSION) { - throw new Error('Could not fetch latest stable version from npm'); -} -if (!LATEST_PREVIEW_VERSION) { - throw new Error('Could not fetch latest preview version from npm'); -} - /** * Mock NPM registry response for version check */ diff --git a/packages/@react-native/tester/js/examples/Image/ImageExample.js b/packages/@react-native/tester/js/examples/Image/ImageExample.js index d403a35dbed..9d287054352 100644 --- a/packages/@react-native/tester/js/examples/Image/ImageExample.js +++ b/packages/@react-native/tester/js/examples/Image/ImageExample.js @@ -21,9 +21,9 @@ import {useEffect, useState} from 'react'; import {Image, ImageBackground, StyleSheet, Text, View} from 'react-native'; const IMAGE1 = - 'https://www.facebook.com/assets/fb_lite_messaging/E2EE-settings@3x.png'; + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVR4nGNgAAIAAAUAAen63NgAAAAASUVORK5CYII='; const IMAGE2 = - 'https://www.facebook.com/ar_effect/external_textures/648609739826677.png'; + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVR4nGNgAAIAAAUAAen63NgAAAAASUVORK5CYII='; const base64Icon = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEsAAABLCAQAAACSR7JhAAADtUlEQVR4Ac3YA2Bj6QLH0XPT1Fzbtm29tW3btm3bfLZtv7e2ObZnms7d8Uw098tuetPzrxv8wiISrtVudrG2JXQZ4VOv+qUfmqCGGl1mqLhoA52oZlb0mrjsnhKpgeUNEs91Z0pd1kvihA3ULGVHiQO2narKSHKkEMulm9VgUyE60s1aWoMQUbpZOWE+kaqs4eLEjdIlZTcFZB0ndc1+lhB1lZrIuk5P2aib1NBpZaL+JaOGIt0ls47SKzLC7CqrlGF6RZ09HGoNy1lYl2aRSWL5GuzqWU1KafRdoRp0iOQEiDzgZPnG6DbldcomadViflnl/cL93tOoVbsOLVM2jylvdWjXolWX1hmfZbGR/wjypDjFLSZIRov09BgYmtUqPQPlQrPapecLgTIy0jMgPKtTeob2zWtrGH3xvjUkPCtNg/tm1rjwrMa+mdUkPd3hWbH0jArPGiU9ufCsNNWFZ40wpwn+62/66R2RUtoso1OB34tnLOcy7YB1fUdc9e0q3yru8PGM773vXsuZ5YIZX+5xmHwHGVvlrGPN6ZSiP1smOsMMde40wKv2VmwPPVXNut4sVpUreZiLBHi0qln/VQeI/LTMYXpsJtFiclUN+5HVZazim+Ky+7sAvxWnvjXrJFneVtLWLyPJu9K3cXLWeOlbMTlrIelbMDlrLenrjEQOtIF+fuI9xRp9ZBFp6+b6WT8RrxEpdK64BuvHgDk+vUy+b5hYk6zfyfs051gRoNO1usU12WWRWL73/MMEy9pMi9qIrR4ZpV16Rrvduxazmy1FSvuFXRkqTnE7m2kdb5U8xGjLw/spRr1uTov4uOgQE+0N/DvFrG/Jt7i/FzwxbA9kDanhf2w+t4V97G8lrT7wc08aA2QNUkuTfW/KimT01wdlfK4yEw030VfT0RtZbzjeMprNq8m8tnSTASrTLti64oBNdpmMQm0eEwvfPwRbUBywG5TzjPCsdwk3IeAXjQblLCoXnDVeoAz6SfJNk5TTzytCNZk/POtTSV40NwOFWzw86wNJRpubpXsn60NJFlHeqlYRbslqZm2jnEZ3qcSKgm0kTli3zZVS7y/iivZTweYXJ26Y+RTbV1zh3hYkgyFGSTKPfRVbRqWWVReaxYeSLarYv1Qqsmh1s95S7G+eEWK0f3jYKTbV6bOwepjfhtafsvUsqrQvrGC8YhmnO9cSCk3yuY984F1vesdHYhWJ5FvASlacshUsajFt2mUM9pqzvKGcyNJW0arTKN1GGGzQlH0tXwLDgQTurS8eIQAAAABJRU5ErkJggg=='; diff --git a/packages/@react-native/tester/js/examples/Layout/LayoutEventsExample.js b/packages/@react-native/tester/js/examples/Layout/LayoutEventsExample.js index b07b8300636..90a544eef86 100644 --- a/packages/@react-native/tester/js/examples/Layout/LayoutEventsExample.js +++ b/packages/@react-native/tester/js/examples/Layout/LayoutEventsExample.js @@ -99,7 +99,7 @@ class LayoutEventExample extends React.Component { onLayout={this.onImageLayout} style={styles.image} source={{ - uri: 'https://www.facebook.com/favicon.ico', + uri: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVR4nGNgAAIAAAUAAen63NgAAAAASUVORK5CYII=', }} /> diff --git a/packages/@react-native/tester/js/examples/Pressable/PressableExample.js b/packages/@react-native/tester/js/examples/Pressable/PressableExample.js index 648fe4d5e6b..99a2595ebb1 100644 --- a/packages/@react-native/tester/js/examples/Pressable/PressableExample.js +++ b/packages/@react-native/tester/js/examples/Pressable/PressableExample.js @@ -499,7 +499,7 @@ const examples = [ }}> { cwd: repoRoot, }); + // 10b. Refresh yarn.lock to match the bumped versions so the release + // commit stays installable under `yarn install --immutable`. + console.log(colorize('Updating yarn.lock...', ansi.bright)); + await updateLockfile({cwd: repoRoot}); + // 11. Check if beachball actually changed anything const status = await git.statusPorcelain(); if (!status) { diff --git a/packages/@rnw-scripts/warm-feed/.eslintrc.js b/packages/@rnw-scripts/warm-feed/.eslintrc.js new file mode 100644 index 00000000000..1e2ec12e2be --- /dev/null +++ b/packages/@rnw-scripts/warm-feed/.eslintrc.js @@ -0,0 +1,4 @@ +module.exports = { + extends: ['@rnw-scripts'], + parserOptions: {tsconfigRootDir: __dirname}, +}; diff --git a/packages/@rnw-scripts/warm-feed/.gitignore b/packages/@rnw-scripts/warm-feed/.gitignore new file mode 100644 index 00000000000..f42efbb9f7c --- /dev/null +++ b/packages/@rnw-scripts/warm-feed/.gitignore @@ -0,0 +1,2 @@ +lib/ +lib-commonjs/ diff --git a/packages/@rnw-scripts/warm-feed/README.md b/packages/@rnw-scripts/warm-feed/README.md new file mode 100644 index 00000000000..b45f075ba3d --- /dev/null +++ b/packages/@rnw-scripts/warm-feed/README.md @@ -0,0 +1,88 @@ +# @rnw-scripts/warm-feed + +Warms the `ms/react-native-public` Azure DevOps upstream feed so that +network-isolated PR/CI builds — which read the feed **anonymously** — can restore +the package versions they need. + +An Azure Artifacts upstream feed only serves a version once an **authenticated** +identity has pulled it from upstream (which *saves* it into the feed). This tool +performs those authenticated pulls. + +## How it works + +The warmer is **feed-centric** — it needs no repo checkouts or lockfiles, so one +run serves every repo that shares the feed: + +1. **Enumerate** the feed with the ADO Get Packages API — every npm and NuGet + package already in the feed, plus each package's already-saved versions. +2. **Expand** each package to the latest stable patch of every `major.minor` + line already in use (and, by default, the newest stable version overall). The + candidate versions come from the authenticated packument / flat2 index, which + includes not-yet-saved upstream versions. +3. **Skip** versions the feed already has (the enumeration result is the + authoritative "already cached" set — no separate cache is needed). +4. **Warm** the rest with an authenticated GET of the tarball / `.nupkg`, which + triggers the upstream save. The response body is streamed and discarded. + +## Local usage + +Auth is resolved in order: `--pat`, `$env:ADO_PAT` / `$env:AZURE_DEVOPS_EXT_PAT`, +`$env:WARM_FEED_TOKEN`, then an AAD token from `az account get-access-token` +(run `az login` first). + +```powershell +# Preview what a full warm would do (no writes): +yarn warm-feed --dry-run + +# Preview just one ecosystem: +yarn warm-feed --dry-run --only nuget + +# Warm a specific version on demand (one-off; skips enumeration): +yarn warm-feed --packages "npm:left-pad@1.3.0" +yarn warm-feed --packages "nuget:Newtonsoft.Json@13.0.3" + +# Warm everything the feed needs (mutates the feed): +yarn warm-feed +``` + +`yarn warm-feed` builds the package and then runs it. Common flags: `--only +npm|nuget`, `--dry-run`, `--verify` (warm even already-cached targets), +`--concurrency `, `-v` / `--verbose`. See `--help` for the full list. + +## Pipeline usage + +`.ado/warm-feed-pipeline.yml` runs the tool on a schedule (and on manual queue) +in the office/ISS project, on the isolated pool, extending the Office Unofficial +1ES template. An `AzureCLI@2` step logs in as the managed identity and mints its +feed token into `WARM_FEED_TOKEN`, then runs `npx warm-feed`. + +Warming runs **out of band** — never in a PR build — because saving into the feed +needs the managed identity. To warm a version a PR/Dependabot introduced, a +maintainer queues the pipeline with the `packages` parameter +(`npm:foo@1.2.3 nuget:Bar@4.0.0`); the rights gate is ADO "Queue builds". + +## Config + +`warm-feed.config.json`: + +| Key | Meaning | +| --- | --- | +| `feeds.npm.registry` / `feeds.nuget.index` | Feed endpoints to warm. | +| `enumerate.feedManagementBase` | Feed Management base (`https://feeds.dev.azure.com///_apis/packaging/Feeds/`). | +| `enumerate.apiVersion` / `enumerate.pageSize` | Get Packages API version and page size. | +| `expand.scope` | `in-use-lines` (latest patch per `major.minor`) or `in-use-majors` (latest per major). | +| `expand.includeLatest` | Also warm the newest stable version overall. | +| `expand.includePrerelease` | Include prerelease versions. | +| `expand.maxMajorsBack` | Limit to the N most-recent majors already in use (0 = no limit). | +| `concurrency` | Parallel requests. | +| `ignore` | `id`, `id@version`, or `eco:id@version` entries to skip. | + +## Scope and limitations + +- Warms **latest patch per in-use line**, not a specific build's exact + lockfile-pinned closure. A build pinning an older patch, or a version whose + transitive graph differs, is not guaranteed by this pass alone. +- Cannot introduce a **brand-new package name** the feed has never seen (that name + is not in the feed's list). First use is covered by the authenticated CI build + that restores it, or by a one-off `--packages` warm. +- Does not resolve transitive closures (each warmed version is fetched on its own). diff --git a/packages/@rnw-scripts/warm-feed/bin.js b/packages/@rnw-scripts/warm-feed/bin.js new file mode 100644 index 00000000000..7a2dbfe745f --- /dev/null +++ b/packages/@rnw-scripts/warm-feed/bin.js @@ -0,0 +1,11 @@ +#!/usr/bin/env node + +/** + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + * + * @format + */ + +require('source-map-support').install(); +require('./lib-commonjs/warmFeed'); diff --git a/packages/@rnw-scripts/warm-feed/package.json b/packages/@rnw-scripts/warm-feed/package.json new file mode 100644 index 00000000000..875608f32de --- /dev/null +++ b/packages/@rnw-scripts/warm-feed/package.json @@ -0,0 +1,43 @@ +{ + "name": "@rnw-scripts/warm-feed", + "version": "0.0.1", + "private": true, + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/microsoft/react-native-windows", + "directory": "packages/@rnw-scripts/warm-feed" + }, + "scripts": { + "build": "rnw-scripts build", + "clean": "rnw-scripts clean", + "lint": "rnw-scripts lint", + "lint:fix": "rnw-scripts lint:fix", + "warm-feed": "rnw-scripts build && node bin.js", + "watch": "rnw-scripts watch" + }, + "main": "lib-commonjs/warmFeed.js", + "bin": "./bin.js", + "dependencies": { + "source-map-support": "^0.5.19" + }, + "devDependencies": { + "@rnw-scripts/eslint-config": "1.2.38", + "@rnw-scripts/just-task": "2.3.58", + "@rnw-scripts/ts-config": "2.0.6", + "@types/node": "^22.14.0", + "@typescript-eslint/eslint-plugin": "^8.36.0", + "@typescript-eslint/parser": "^8.36.0", + "eslint": "^8.19.0", + "prettier": "^3.6.2", + "typescript": "5.0.4" + }, + "files": [ + "bin.js", + "lib-commonjs", + "warm-feed.config.json" + ], + "engines": { + "node": ">= 22" + } +} diff --git a/packages/@rnw-scripts/warm-feed/src/auth.ts b/packages/@rnw-scripts/warm-feed/src/auth.ts new file mode 100644 index 00000000000..3c10b9d68c8 --- /dev/null +++ b/packages/@rnw-scripts/warm-feed/src/auth.ts @@ -0,0 +1,79 @@ +/** + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + * + * @format + */ + +import {spawnSync} from 'node:child_process'; +import type {Auth, Logger} from './types'; + +// Azure DevOps resource id for AAD access tokens. +const ADO_RESOURCE = '499b84ac-1321-427f-aa17-267ca6975798'; + +function envToken(): string | undefined { + return process.env.ADO_PAT || process.env.AZURE_DEVOPS_EXT_PAT || undefined; +} + +function azToken(log: Logger): string | undefined { + // All arguments are constant, so a single shell command string is safe and + // avoids the args-with-shell deprecation (DEP0190). `az` is a .cmd on Windows. + const res = spawnSync( + `az account get-access-token --resource ${ADO_RESOURCE} --query accessToken -o tsv`, + {encoding: 'utf8', shell: true}, + ); + if (res.status === 0 && res.stdout) return res.stdout.trim(); + log.debug(`az token acquisition failed: ${res.stderr.trim() || res.error}`); + return undefined; +} + +/** + * Resolve feed auth. Order: a pre-acquired AAD/MI access token via + * `WARM_FEED_TOKEN` (Bearer; the pipeline path), else an explicit/env PAT + * (Basic; single-org, local), else an AAD token from `az account get-access-token` + * (Bearer; local `az login`). + */ +export function resolveAuth(log: Logger, pat?: string): Auth { + const bearer = process.env.WARM_FEED_TOKEN; + if (bearer) { + log.debug('using pre-acquired access token (WARM_FEED_TOKEN)'); + return { + kind: 'aad', + async header() { + return {Authorization: `Bearer ${bearer}`}; + }, + }; + } + + const explicit = pat || envToken(); + if (explicit) { + log.debug('using PAT auth'); + const basic = Buffer.from(`:${explicit}`).toString('base64'); + return { + kind: 'pat', + async header() { + return {Authorization: `Basic ${basic}`}; + }, + }; + } + + let cached: string | undefined; + const get = (): string => { + if (!cached) { + cached = azToken(log); + if (!cached) { + throw new Error( + 'No Azure DevOps auth available. Run `az login`, or set $env:ADO_PAT / pass --pat.', + ); + } + log.debug('using AAD auth from az'); + } + return cached; + }; + return { + kind: 'aad', + async header() { + return {Authorization: `Bearer ${get()}`}; + }, + }; +} diff --git a/packages/@rnw-scripts/warm-feed/src/config.ts b/packages/@rnw-scripts/warm-feed/src/config.ts new file mode 100644 index 00000000000..e079c059214 --- /dev/null +++ b/packages/@rnw-scripts/warm-feed/src/config.ts @@ -0,0 +1,61 @@ +/** + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + * + * @format + */ + +import {readFileSync} from 'node:fs'; +import type { + EnumerateConfig, + ExpandConfig, + FeedConfig, + WarmerConfig, +} from './types'; + +interface RawConfig { + feeds?: FeedConfig; + enumerate?: Partial; + expand?: Partial; + concurrency?: number; + ignore?: string[]; +} + +const DEFAULT_EXPAND: ExpandConfig = { + scope: 'in-use-lines', + includeLatest: true, + includePrerelease: false, + maxMajorsBack: 0, +}; + +export function loadConfig(configPath: string): WarmerConfig { + let raw: RawConfig; + try { + raw = JSON.parse(readFileSync(configPath, 'utf8')) as RawConfig; + } catch (err) { + throw new Error( + `failed to read config '${configPath}': ${(err as Error).message}`, + ); + } + if (!raw.feeds || (!raw.feeds.npm && !raw.feeds.nuget)) { + throw new Error( + `config '${configPath}' must define at least one of feeds.npm / feeds.nuget`, + ); + } + if (!raw.enumerate?.feedManagementBase) { + throw new Error( + `config '${configPath}' must define enumerate.feedManagementBase`, + ); + } + return { + feeds: raw.feeds, + enumerate: { + feedManagementBase: raw.enumerate.feedManagementBase, + apiVersion: raw.enumerate.apiVersion ?? '7.1', + pageSize: raw.enumerate.pageSize ?? 1000, + }, + expand: {...DEFAULT_EXPAND, ...(raw.expand ?? {})}, + concurrency: raw.concurrency ?? 8, + ignore: raw.ignore ?? [], + }; +} diff --git a/packages/@rnw-scripts/warm-feed/src/expand.ts b/packages/@rnw-scripts/warm-feed/src/expand.ts new file mode 100644 index 00000000000..791db416cc3 --- /dev/null +++ b/packages/@rnw-scripts/warm-feed/src/expand.ts @@ -0,0 +1,147 @@ +/** + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + * + * @format + */ + +import type {Ecosystem, ExpandConfig, FeedPackage, WarmTarget} from './types'; +import type {NpmRegistry, NuGetRegistry} from './registries'; +import { + compareNuGet, + compareSemver, + isStable, + isStableNuGet, + parseNuGet, + parseSemver, + type NuGetVersion, + type SemVer, +} from './versions'; + +/** + * Reduce the in-use (major, minor) pairs to the set of lines to warm. For + * 'in-use-majors' the minor is collapsed to `undefined` (match any minor). + * `maxMajorsBack` keeps only the N most-recent majors. + */ +function inUseLines( + pairs: ReadonlyArray, + cfg: ExpandConfig, +): Array<[number, number | undefined]> { + const majors = [...new Set(pairs.map(([maj]) => maj))].sort((a, b) => b - a); + const allowed = + cfg.maxMajorsBack > 0 ? new Set(majors.slice(0, cfg.maxMajorsBack)) : null; + const seen = new Set(); + const lines: Array<[number, number | undefined]> = []; + for (const [maj, min] of pairs) { + if (allowed && !allowed.has(maj)) continue; + const key = cfg.scope === 'in-use-majors' ? `${maj}` : `${maj}.${min}`; + if (seen.has(key)) continue; + seen.add(key); + lines.push([maj, cfg.scope === 'in-use-majors' ? undefined : min]); + } + return lines; +} + +async function expandNpm( + pkg: FeedPackage, + npm: NpmRegistry, + cfg: ExpandConfig, +): Promise { + const saved = pkg.savedVersions + .map(parseSemver) + .filter((v): v is SemVer => v !== null) + .filter(v => cfg.includePrerelease || isStable(v)); + if (saved.length === 0) return []; + + const pool = (await npm.getVersions(pkg.id)) + .map(parseSemver) + .filter((v): v is SemVer => v !== null) + .filter(v => cfg.includePrerelease || isStable(v)); + if (pool.length === 0) return []; + + const out = new Map(); + const add = (v: SemVer | undefined, source: string) => { + if (v) + out.set(v.raw, {ecosystem: 'npm', id: pkg.id, version: v.raw, source}); + }; + + for (const [maj, min] of inUseLines( + saved.map(v => [v.major, v.minor] as const), + cfg, + )) { + let best: SemVer | undefined; + for (const v of pool) { + const match = + min === undefined + ? v.major === maj + : v.major === maj && v.minor === min; + if (match && (!best || compareSemver(v, best) > 0)) best = v; + } + add(best, 'expand:line'); + } + if (cfg.includeLatest) { + let latest: SemVer | undefined; + for (const v of pool) + if (!latest || compareSemver(v, latest) > 0) latest = v; + add(latest, 'expand:latest'); + } + return [...out.values()]; +} + +async function expandNuGet( + pkg: FeedPackage, + nuget: NuGetRegistry, + cfg: ExpandConfig, +): Promise { + const saved = pkg.savedVersions + .map(parseNuGet) + .filter((v): v is NuGetVersion => v !== null) + .filter(v => cfg.includePrerelease || isStableNuGet(v)); + if (saved.length === 0) return []; + + const pool = (await nuget.getVersions(pkg.id)) + .map(parseNuGet) + .filter((v): v is NuGetVersion => v !== null) + .filter(v => cfg.includePrerelease || isStableNuGet(v)); + if (pool.length === 0) return []; + + const out = new Map(); + const add = (v: NuGetVersion | undefined, source: string) => { + if (v) + out.set(v.raw, {ecosystem: 'nuget', id: pkg.id, version: v.raw, source}); + }; + + for (const [maj, min] of inUseLines( + saved.map(v => [v.parts[0], v.parts[1]] as const), + cfg, + )) { + let best: NuGetVersion | undefined; + for (const v of pool) { + const match = + min === undefined + ? v.parts[0] === maj + : v.parts[0] === maj && v.parts[1] === min; + if (match && (!best || compareNuGet(v, best) > 0)) best = v; + } + add(best, 'expand:line'); + } + if (cfg.includeLatest) { + let latest: NuGetVersion | undefined; + for (const v of pool) + if (!latest || compareNuGet(v, latest) > 0) latest = v; + add(latest, 'expand:latest'); + } + return [...out.values()]; +} + +/** Compute the warm targets (latest patch per in-use line) for one feed package. */ +export function expandPackage( + ecosystem: Ecosystem, + pkg: FeedPackage, + registry: NpmRegistry | NuGetRegistry, + cfg: ExpandConfig, +): Promise { + return ecosystem === 'npm' + ? expandNpm(pkg, registry as NpmRegistry, cfg) + : expandNuGet(pkg, registry as NuGetRegistry, cfg); +} diff --git a/packages/@rnw-scripts/warm-feed/src/feedPackages.ts b/packages/@rnw-scripts/warm-feed/src/feedPackages.ts new file mode 100644 index 00000000000..aed2cb83bcd --- /dev/null +++ b/packages/@rnw-scripts/warm-feed/src/feedPackages.ts @@ -0,0 +1,66 @@ +/** + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + * + * @format + */ + +import {fetchJson} from './http'; +import type {Auth, Ecosystem, FeedPackage, Logger, WarmerConfig} from './types'; + +interface AdoPackageVersion { + version?: string; + isDeleted?: boolean; +} + +interface AdoPackage { + name?: string; + versions?: AdoPackageVersion[]; +} + +interface AdoPackagesPage { + value?: AdoPackage[]; +} + +const PROTOCOL: Record = {npm: 'Npm', nuget: 'NuGet'}; + +/** + * List every package the feed already has (published + saved-from-upstream) via + * the ADO Get Packages API, with each package's saved versions. This is both the + * enumeration seed and the authoritative "already cached" set. + */ +export async function enumerateFeed( + config: WarmerConfig, + ecosystem: Ecosystem, + auth: Auth, + log: Logger, +): Promise { + const {feedManagementBase, apiVersion, pageSize} = config.enumerate; + const base = feedManagementBase.replace(/\/$/, ''); + const out: FeedPackage[] = []; + let skip = 0; + for (;;) { + const url = + `${base}/packages?api-version=${apiVersion}` + + `&protocolType=${PROTOCOL[ecosystem]}&includeAllVersions=true` + + `&$top=${pageSize}&$skip=${skip}`; + const headers = await auth.header(); + const {status, body} = await fetchJson(url, headers); + if (status === 401 || status === 403) { + throw new Error(`Get Packages auth failed (${status}) for ${ecosystem}`); + } + const page = body?.value ?? []; + if (page.length === 0) break; + for (const p of page) { + if (!p.name) continue; + const savedVersions = (p.versions ?? []) + .filter(v => v.version && !v.isDeleted) + .map(v => v.version as string); + out.push({ecosystem, id: p.name, savedVersions}); + } + if (page.length < pageSize) break; + skip += pageSize; + } + log.info(`enumerated ${out.length} ${ecosystem} package(s) from feed`); + return out; +} diff --git a/packages/@rnw-scripts/warm-feed/src/http.ts b/packages/@rnw-scripts/warm-feed/src/http.ts new file mode 100644 index 00000000000..3f01a2c7097 --- /dev/null +++ b/packages/@rnw-scripts/warm-feed/src/http.ts @@ -0,0 +1,146 @@ +/** + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + * + * Azure Artifacts blocks upstream ingestion for requests it thinks come from a + * browser. Node's global fetch (undici) auto-injects `Sec-Fetch-*` headers that + * trip that check, so all feed requests go through node:https, which sends only + * the headers we specify. + * + * @format + */ + +import https from 'node:https'; +import {URL} from 'node:url'; + +export function sleep(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +const CLIENT_UA = 'warm-feed/0.1'; +const DEFAULT_RETRY = [408, 429, 500, 502, 503, 504]; +const REDIRECTS = new Set([301, 302, 303, 307, 308]); + +export interface RawResponse { + status: number; + body: string | null; +} + +interface RawOptions { + method?: string; + headers?: Record; + /** false = drain and discard the body (for warm GETs). */ + buffer?: boolean; +} + +function rawOnce( + url: string, + opts: RawOptions, + redirectsLeft: number, +): Promise { + return new Promise((resolve, reject) => { + const headers: Record = { + 'User-Agent': CLIENT_UA, + ...(opts.headers ?? {}), + }; + const req = https.request( + new URL(url), + {method: opts.method ?? 'GET', headers}, + res => { + const status = res.statusCode ?? 0; + const location = res.headers.location; + if (REDIRECTS.has(status) && location && redirectsLeft > 0) { + res.resume(); + const nextUrl = new URL(location, url); + const cur = new URL(url); + // Drop auth on cross-origin redirects (e.g. feed -> blob storage with + // its own SAS); a stale bearer token would yield 403. + let nextHeaders = opts.headers; + if (nextUrl.host !== cur.host || nextUrl.protocol !== cur.protocol) { + nextHeaders = {...(opts.headers ?? {})}; + delete nextHeaders.Authorization; + delete nextHeaders.authorization; + } + rawOnce( + nextUrl.toString(), + {...opts, headers: nextHeaders}, + redirectsLeft - 1, + ).then(resolve, reject); + return; + } + if (opts.buffer === false) { + res.resume(); + res.on('end', () => resolve({status, body: null})); + res.on('error', reject); + return; + } + const chunks: Buffer[] = []; + res.on('data', (c: Buffer) => chunks.push(c)); + res.on('end', () => + resolve({status, body: Buffer.concat(chunks).toString('utf8')}), + ); + res.on('error', reject); + }, + ); + req.on('error', reject); + req.end(); + }); +} + +export async function httpRequest( + url: string, + opts: RawOptions & {attempts?: number; retryStatuses?: number[]} = {}, +): Promise { + const attempts = opts.attempts ?? 4; + const retry = new Set(opts.retryStatuses ?? DEFAULT_RETRY); + let lastError: unknown; + let lastResponse: RawResponse | undefined; + for (let i = 1; i <= attempts; i++) { + try { + const res = await rawOnce(url, opts, 5); + if (res.status < 400 || !retry.has(res.status)) return res; + lastResponse = res; + } catch (err) { + lastError = err; + } + if (i < attempts) { + await sleep(500 * 2 ** (i - 1) + Math.floor(Math.random() * 250)); + } + } + if (lastResponse) return lastResponse; + throw lastError ?? new Error(`request failed for ${url}`); +} + +export async function fetchJson( + url: string, + headers: Record, + attempts = 4, +): Promise<{status: number; body: T | null}> { + const res = await httpRequest(url, { + headers: {Accept: 'application/json', ...headers}, + attempts, + }); + if (res.status >= 400 || res.body === null) { + return {status: res.status, body: null}; + } + try { + return {status: res.status, body: JSON.parse(res.body) as T}; + } catch { + return {status: res.status, body: null}; + } +} + +/** GET a URL to trigger the feed's upstream save, draining and discarding the body. */ +export async function warmGet( + url: string, + headers: Record, +): Promise { + const res = await httpRequest(url, { + headers, + buffer: false, + attempts: 5, + // 404 while the feed is still pulling the version from upstream is retryable. + retryStatuses: [404, 408, 429, 500, 502, 503, 504], + }); + return res.status; +} diff --git a/packages/@rnw-scripts/warm-feed/src/logger.ts b/packages/@rnw-scripts/warm-feed/src/logger.ts new file mode 100644 index 00000000000..a47bd1d3311 --- /dev/null +++ b/packages/@rnw-scripts/warm-feed/src/logger.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + * + * @format + */ + +import type {Logger} from './types'; + +function stamp(): string { + return new Date().toISOString().replace('T', ' ').replace('Z', ''); +} + +export function createLogger(verbose: boolean): Logger { + return { + info(message) { + process.stdout.write(`[${stamp()}] ${message}\n`); + }, + warn(message) { + process.stdout.write(`[${stamp()}] WARN ${message}\n`); + }, + error(message) { + process.stderr.write(`[${stamp()}] ERROR ${message}\n`); + }, + debug(message) { + if (verbose) process.stdout.write(`[${stamp()}] debug ${message}\n`); + }, + }; +} diff --git a/packages/@rnw-scripts/warm-feed/src/pool.ts b/packages/@rnw-scripts/warm-feed/src/pool.ts new file mode 100644 index 00000000000..bbeb6aec48a --- /dev/null +++ b/packages/@rnw-scripts/warm-feed/src/pool.ts @@ -0,0 +1,26 @@ +/** + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + * + * @format + */ + +/** Run `worker` over `items` with bounded concurrency, preserving order. */ +export async function pool( + items: T[], + concurrency: number, + worker: (item: T, index: number) => Promise, +): Promise { + const results = new Array(items.length); + let next = 0; + const width = Math.max(1, Math.min(concurrency, items.length || 1)); + const runners = Array.from({length: width}, async () => { + for (;;) { + const i = next++; + if (i >= items.length) return; + results[i] = await worker(items[i], i); + } + }); + await Promise.all(runners); + return results; +} diff --git a/packages/@rnw-scripts/warm-feed/src/registries.ts b/packages/@rnw-scripts/warm-feed/src/registries.ts new file mode 100644 index 00000000000..339a462e3a0 --- /dev/null +++ b/packages/@rnw-scripts/warm-feed/src/registries.ts @@ -0,0 +1,146 @@ +/** + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + * + * @format + */ + +import {fetchJson} from './http'; +import type {Auth, Logger} from './types'; + +// --- npm --------------------------------------------------------------------- + +interface NpmPackument { + versions?: Record; +} + +interface NpmInfo { + versions: string[]; + tarballs: Record; +} + +export interface NpmRegistry { + getVersions(id: string): Promise; + getTarballUrl(id: string, version: string): Promise; +} + +/** Encode a package id for a registry path, preserving the scope `@`. */ +function encodeNpmId(id: string): string { + return id.startsWith('@') ? id.replace('/', '%2F') : id; +} + +export function createNpmRegistry( + registry: string, + auth: Auth, + log: Logger, +): NpmRegistry { + const base = registry.endsWith('/') ? registry : `${registry}/`; + // Cache a compact {versions, tarballs} instead of the full packument so a feed + // with thousands of packages does not hold every packument in memory. + const cache = new Map(); + + async function getInfo(id: string): Promise { + const cached = cache.get(id); + if (cached !== undefined) return cached; + const headers = await auth.header(); + const {status, body} = await fetchJson( + `${base}${encodeNpmId(id)}`, + {...headers, Accept: 'application/vnd.npm.install-v1+json'}, + ); + if (status === 401 || status === 403) { + throw new Error(`npm registry auth failed (${status}) for ${id}`); + } + if (!body || !body.versions) { + log.debug(`npm packument ${id} -> ${status}`); + cache.set(id, null); + return null; + } + const versions = Object.keys(body.versions); + const tarballs: Record = {}; + for (const v of versions) { + const url = body.versions[v].dist?.tarball; + if (url) tarballs[v] = url; + } + const info: NpmInfo = {versions, tarballs}; + cache.set(id, info); + return info; + } + + return { + async getVersions(id) { + return (await getInfo(id))?.versions ?? []; + }, + async getTarballUrl(id, version) { + return (await getInfo(id))?.tarballs[version] ?? null; + }, + }; +} + +// --- NuGet ------------------------------------------------------------------- + +interface ServiceIndex { + resources?: Array<{'@id': string; '@type': string}>; +} + +export interface NuGetRegistry { + getVersions(id: string): Promise; + nupkgUrl(id: string, version: string): Promise; +} + +function pickResource(index: ServiceIndex, type: string): string | null { + const r = index.resources?.find(x => x['@type'] === type); + return r ? r['@id'] : null; +} + +export function createNuGetRegistry( + indexUrl: string, + auth: Auth, + _log: Logger, +): NuGetRegistry { + let baseP: Promise | null = null; + const versionCache = new Map(); + + async function getBase(): Promise { + if (!baseP) { + baseP = (async () => { + const headers = await auth.header(); + const {status, body} = await fetchJson(indexUrl, headers); + if (!body) + throw new Error(`NuGet service index ${indexUrl} -> ${status}`); + const base = + pickResource(body, 'PackageBaseAddress/3.0.0') ?? + pickResource(body, 'PackageBaseAddress/3.0.0-beta'); + if (!base) + throw new Error('NuGet feed has no PackageBaseAddress resource'); + return base.endsWith('/') ? base : `${base}/`; + })(); + } + return baseP; + } + + return { + async getVersions(id) { + const key = id.toLowerCase(); + const cached = versionCache.get(key); + if (cached) return cached; + const base = await getBase(); + const headers = await auth.header(); + const {status, body} = await fetchJson<{versions?: string[]}>( + `${base}${key}/index.json`, + headers, + ); + if (status === 401 || status === 403) { + throw new Error(`NuGet feed auth failed (${status}) for ${id}`); + } + const versions = body?.versions ?? []; + versionCache.set(key, versions); + return versions; + }, + async nupkgUrl(id, version) { + const base = await getBase(); + const k = id.toLowerCase(); + const v = version.toLowerCase(); + return `${base}${k}/${v}/${k}.${v}.nupkg`; + }, + }; +} diff --git a/packages/@rnw-scripts/warm-feed/src/run.ts b/packages/@rnw-scripts/warm-feed/src/run.ts new file mode 100644 index 00000000000..f6dda21324a --- /dev/null +++ b/packages/@rnw-scripts/warm-feed/src/run.ts @@ -0,0 +1,266 @@ +/** + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + * + * @format + */ + +import type { + Ctx, + Ecosystem, + FeedPackage, + RunOptions, + WarmResult, + WarmTarget, +} from './types'; +import {loadConfig} from './config'; +import {resolveAuth} from './auth'; +import {createLogger} from './logger'; +import { + createNpmRegistry, + createNuGetRegistry, + type NpmRegistry, + type NuGetRegistry, +} from './registries'; +import {enumerateFeed} from './feedPackages'; +import {expandPackage} from './expand'; +import {createWarmers} from './warmers'; +import {pool} from './pool'; + +interface Registries { + npm?: NpmRegistry; + nuget?: NuGetRegistry; +} + +interface IgnoreRule { + ecosystem?: Ecosystem; + id: string; + version?: string; +} + +function targetKey(t: WarmTarget): string { + return `${t.ecosystem}|${t.id.toLowerCase()}|${t.version}`; +} + +function savedKey(ecosystem: Ecosystem, id: string): string { + return `${ecosystem}|${id.toLowerCase()}`; +} + +function dedupe(targets: WarmTarget[]): WarmTarget[] { + const map = new Map(); + for (const t of targets) { + if (!map.has(targetKey(t))) map.set(targetKey(t), t); + } + return [...map.values()]; +} + +function parseIgnore(entry: string): IgnoreRule | null { + let s = entry.trim(); + if (!s) return null; + let ecosystem: Ecosystem | undefined; + const eco = /^(npm|nuget):(.*)$/i.exec(s); + if (eco) { + ecosystem = eco[1].toLowerCase() as Ecosystem; + s = eco[2]; + } + const at = s.lastIndexOf('@'); + if (at > 0) return {ecosystem, id: s.slice(0, at), version: s.slice(at + 1)}; + return {ecosystem, id: s}; +} + +function matchesIgnore(t: WarmTarget, rules: IgnoreRule[]): boolean { + return rules.some( + r => + (!r.ecosystem || r.ecosystem === t.ecosystem) && + r.id.toLowerCase() === t.id.toLowerCase() && + (!r.version || r.version === '*' || r.version === t.version), + ); +} + +function parseSpec(spec: string, warn: (m: string) => void): WarmTarget | null { + const s = spec.trim(); + if (!s) return null; + const m = /^(npm|nuget):(.+)$/i.exec(s); + if (!m) { + warn(`ignoring '${spec}': expected 'npm:id@version' or 'nuget:id@version'`); + return null; + } + const rest = m[2]; + const at = rest.lastIndexOf('@'); + if (at <= 0) { + warn(`ignoring '${spec}': missing @version`); + return null; + } + return { + ecosystem: m[1].toLowerCase() as Ecosystem, + id: rest.slice(0, at), + version: rest.slice(at + 1), + source: 'cli-packages', + }; +} + +function summarize(results: WarmResult[]): Record { + const counts: Record = {warmed: 0, missing: 0, failed: 0}; + for (const r of results) counts[r.status] = (counts[r.status] ?? 0) + 1; + return counts; +} + +async function warmTargets( + ctx: Ctx, + registries: Registries, + targets: WarmTarget[], +): Promise { + const {log} = ctx; + if (targets.length === 0) { + log.info('nothing to warm'); + return 0; + } + // Fail fast if no credentials are available before issuing warm requests. + try { + await ctx.auth.header(); + } catch (err) { + log.error((err as Error).message); + return 2; + } + const warmers = createWarmers(ctx, registries); + const concurrency = ctx.options.concurrency ?? ctx.config.concurrency; + log.info(`warming ${targets.length} target(s) (concurrency ${concurrency})`); + const results = await pool( + targets, + concurrency, + async (t): Promise => { + const warmer = warmers[t.ecosystem]; + if (!warmer) { + return {target: t, status: 'failed', detail: 'no warmer for ecosystem'}; + } + try { + const r = await warmer.warm(t); + if (r.status === 'warmed') { + log.debug(`warmed ${t.ecosystem} ${t.id}@${t.version}`); + } else if (r.status === 'missing') { + log.warn(`missing ${t.ecosystem} ${t.id}@${t.version} (${r.detail})`); + } + return r; + } catch (err) { + return {target: t, status: 'failed', detail: (err as Error).message}; + } + }, + ); + const counts = summarize(results); + log.info( + `done: warmed=${counts.warmed} missing=${counts.missing} failed=${counts.failed}`, + ); + const failures = results.filter(r => r.status === 'failed'); + for (const f of failures.slice(0, 20)) { + log.error( + `FAILED ${f.target.ecosystem} ${f.target.id}@${f.target.version}: ${f.detail}`, + ); + } + return failures.length > 0 ? 1 : 0; +} + +export async function run(options: RunOptions, pat?: string): Promise { + const log = createLogger(options.verbose); + const config = loadConfig(options.configPath); + const auth = resolveAuth(log, pat); + const ctx: Ctx = {config, options, auth, log}; + + const registries: Registries = {}; + if (config.feeds.npm?.registry) { + registries.npm = createNpmRegistry(config.feeds.npm.registry, auth, log); + } + if (config.feeds.nuget?.index) { + registries.nuget = createNuGetRegistry(config.feeds.nuget.index, auth, log); + } + + let only: Ecosystem | undefined; + if (options.only === 'npm' || options.only === 'nuget') { + only = options.only; + } else if (options.only) { + log.warn(`ignoring invalid --only '${options.only}'`); + } + + const ignoreRules = config.ignore + .map(parseIgnore) + .filter((r): r is IgnoreRule => r !== null); + const keep = (t: WarmTarget) => + ignoreRules.length === 0 || !matchesIgnore(t, ignoreRules); + + // One-off mode: warm exactly the versions passed on the command line. + if (options.packages.length) { + const specs = options.packages.flatMap(p => p.split(/\s+/)).filter(Boolean); + const targets = dedupe( + specs + .map(s => parseSpec(s, m => log.warn(m))) + .filter((t): t is WarmTarget => t !== null) + .filter(t => !only || t.ecosystem === only) + .filter(keep), + ); + if (options.dryRun) { + log.info(`dry run (one-off): ${targets.length} target(s)`); + for (const t of targets) { + log.info(` ${t.ecosystem} ${t.id}@${t.version} [${t.source}]`); + } + return 0; + } + log.info(`one-off warm: ${targets.length} target(s)`); + return warmTargets(ctx, registries, targets); + } + + // Scheduled mode: enumerate the feed, expand to latest patch per in-use line, + // skip what is already cached, warm the rest. + const ecosystems = (['npm', 'nuget'] as Ecosystem[]) + .filter(e => (e === 'npm' ? registries.npm : registries.nuget)) + .filter(e => !only || e === only); + + const savedByPackage = new Map>(); + const targets: WarmTarget[] = []; + for (const eco of ecosystems) { + const packages = await enumerateFeed(config, eco, auth, log); + for (const p of packages) { + savedByPackage.set(savedKey(eco, p.id), new Set(p.savedVersions)); + } + const registry = eco === 'npm' ? registries.npm! : registries.nuget!; + const perPackage = await pool( + packages, + config.concurrency, + async (p: FeedPackage): Promise => { + try { + return await expandPackage(eco, p, registry, config.expand); + } catch (err) { + log.warn(`expand ${eco} ${p.id} failed: ${(err as Error).message}`); + return []; + } + }, + ); + for (const list of perPackage) targets.push(...list); + } + + const deduped = dedupe(targets.filter(keep)); + + if (options.dryRun) { + const byEco = deduped.reduce>((acc, t) => { + acc[t.ecosystem] = (acc[t.ecosystem] ?? 0) + 1; + return acc; + }, {}); + log.info(`dry run: ${deduped.length} target(s) ${JSON.stringify(byEco)}`); + for (const t of deduped.slice(0, 40)) { + log.info(` ${t.ecosystem} ${t.id}@${t.version} [${t.source}]`); + } + if (deduped.length > 40) log.info(` ... and ${deduped.length - 40} more`); + return 0; + } + + const toWarm = options.verify + ? deduped + : deduped.filter(t => { + const saved = savedByPackage.get(savedKey(t.ecosystem, t.id)); + return !saved || !saved.has(t.version); + }); + log.info( + `targets ${deduped.length}, already cached ${ + deduped.length - toWarm.length + }, to warm ${toWarm.length}`, + ); + return warmTargets(ctx, registries, toWarm); +} diff --git a/packages/@rnw-scripts/warm-feed/src/types.ts b/packages/@rnw-scripts/warm-feed/src/types.ts new file mode 100644 index 00000000000..69530b07943 --- /dev/null +++ b/packages/@rnw-scripts/warm-feed/src/types.ts @@ -0,0 +1,103 @@ +/** + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + * + * @format + */ + +export type Ecosystem = 'npm' | 'nuget'; + +export type WarmStatus = 'warmed' | 'missing' | 'failed'; + +/** A single concrete package version to warm into the feed. */ +export interface WarmTarget { + ecosystem: Ecosystem; + id: string; + version: string; + /** Stage/reason that produced this target, for diagnostics. */ + source: string; +} + +/** A package as the feed already knows it (from the Get Packages API). */ +export interface FeedPackage { + ecosystem: Ecosystem; + id: string; + /** Versions already saved in the feed. */ + savedVersions: string[]; +} + +export interface FeedConfig { + npm?: {registry: string}; + nuget?: {index: string}; +} + +export interface EnumerateConfig { + /** + * Feed Management base, e.g. + * https://feeds.dev.azure.com///_apis/packaging/Feeds/ + */ + feedManagementBase: string; + apiVersion: string; + pageSize: number; +} + +export interface ExpandConfig { + /** + * 'in-use-lines' warms the latest patch of each (major,minor) already in the + * feed; 'in-use-majors' warms the latest of each major only. + */ + scope: 'in-use-lines' | 'in-use-majors'; + /** Also warm the newest stable version overall (imminent bumps). */ + includeLatest: boolean; + includePrerelease: boolean; + /** Limit to the N most-recent majors already in use (0 = no limit). */ + maxMajorsBack: number; +} + +export interface WarmerConfig { + feeds: FeedConfig; + enumerate: EnumerateConfig; + expand: ExpandConfig; + concurrency: number; + /** Entries of the form 'id', 'id@version', or 'ecosystem:id@version'. */ + ignore: string[]; +} + +export interface RunOptions { + configPath: string; + /** Raw --only value; validated to an Ecosystem in run(). */ + only?: string; + dryRun: boolean; + /** Warm every target, ignoring the feed's already-cached skip. */ + verify: boolean; + concurrency?: number; + /** Ad-hoc 'ecosystem:id@version' entries (one-off warm). */ + packages: string[]; + verbose: boolean; +} + +export interface Logger { + info(message: string): void; + warn(message: string): void; + error(message: string): void; + debug(message: string): void; +} + +export interface Auth { + kind: 'pat' | 'aad'; + /** Authorization header for feed HTTP requests. */ + header(): Promise>; +} + +export interface Ctx { + config: WarmerConfig; + options: RunOptions; + auth: Auth; + log: Logger; +} + +export interface WarmResult { + target: WarmTarget; + status: WarmStatus; + detail?: string; +} diff --git a/packages/@rnw-scripts/warm-feed/src/versions.ts b/packages/@rnw-scripts/warm-feed/src/versions.ts new file mode 100644 index 00000000000..8d9f9cb1ed5 --- /dev/null +++ b/packages/@rnw-scripts/warm-feed/src/versions.ts @@ -0,0 +1,103 @@ +/** + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + * + * Minimal version parsing/comparison for npm and NuGet. We only need parse, + * compare, and stability checks (for "latest patch per line"), so we avoid the + * `semver` dependency. + * + * @format + */ + +// --- npm semver -------------------------------------------------------------- + +export interface SemVer { + major: number; + minor: number; + patch: number; + prerelease: string[]; + raw: string; +} + +const SEMVER_RE = + /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z-.]+))?(?:\+[0-9A-Za-z-.]+)?$/; + +export function parseSemver(v: string): SemVer | null { + const m = SEMVER_RE.exec(v.trim()); + if (!m) return null; + return { + major: Number(m[1]), + minor: Number(m[2]), + patch: Number(m[3]), + prerelease: m[4] ? m[4].split('.') : [], + raw: v, + }; +} + +export function isStable(v: SemVer): boolean { + return v.prerelease.length === 0; +} + +function comparePrerelease(a: string[], b: string[]): number { + if (a.length === 0 && b.length === 0) return 0; + if (a.length === 0) return 1; + if (b.length === 0) return -1; + const n = Math.min(a.length, b.length); + for (let i = 0; i < n; i++) { + const x = a[i]; + const y = b[i]; + const xn = /^\d+$/.test(x); + const yn = /^\d+$/.test(y); + if (xn && yn) { + const d = Number(x) - Number(y); + if (d !== 0) return d; + } else if (xn) { + return -1; + } else if (yn) { + return 1; + } else if (x !== y) { + return x < y ? -1 : 1; + } + } + // A larger set of prerelease fields has higher precedence when all shared + // identifiers are equal (semver spec). + return a.length - b.length; +} + +export function compareSemver(a: SemVer, b: SemVer): number { + if (a.major !== b.major) return a.major - b.major; + if (a.minor !== b.minor) return a.minor - b.minor; + if (a.patch !== b.patch) return a.patch - b.patch; + return comparePrerelease(a.prerelease, b.prerelease); +} + +// --- NuGet ------------------------------------------------------------------- + +export interface NuGetVersion { + parts: number[]; // always length 4 + prerelease: string[]; + raw: string; +} + +const NUGET_RE = + /^(\d+(?:\.\d+){0,3})(?:-([0-9A-Za-z-.]+))?(?:\+[0-9A-Za-z-.]+)?$/; + +export function parseNuGet(v: string): NuGetVersion | null { + const s = v.trim(); + const m = NUGET_RE.exec(s); + if (!m) return null; + const parts = m[1].split('.').map(Number); + while (parts.length < 4) parts.push(0); + return {parts, prerelease: m[2] ? m[2].toLowerCase().split('.') : [], raw: s}; +} + +export function isStableNuGet(v: NuGetVersion): boolean { + return v.prerelease.length === 0; +} + +export function compareNuGet(a: NuGetVersion, b: NuGetVersion): number { + for (let i = 0; i < 4; i++) { + if (a.parts[i] !== b.parts[i]) return a.parts[i] - b.parts[i]; + } + return comparePrerelease(a.prerelease, b.prerelease); +} diff --git a/packages/@rnw-scripts/warm-feed/src/warmFeed.ts b/packages/@rnw-scripts/warm-feed/src/warmFeed.ts new file mode 100644 index 00000000000..a6a9d419673 --- /dev/null +++ b/packages/@rnw-scripts/warm-feed/src/warmFeed.ts @@ -0,0 +1,76 @@ +/** + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + * + * Warm an Azure DevOps upstream feed with the latest patch of every npm/NuGet + * major.minor line already in the feed, so network-isolated PR/CI builds can + * restore them anonymously. Runs locally (az login / PAT) and in a pipeline (MI). + * + * @format + */ + +import {parseArgs} from 'node:util'; +import path from 'node:path'; +import type {RunOptions} from './types'; +import {run} from './run'; + +const HELP = `warm-feed — warm an Azure DevOps upstream feed by enumerating the +feed and re-pulling the latest patch of each major.minor line already in use. + +Usage: npx warm-feed [options] + +Options: + -c, --config Config file (default: ../warm-feed.config.json) + --only Restrict to one ecosystem + --packages One-off warm 'eco:id@version' (repeatable / space-list); + skips enumeration and warms exactly these versions + --dry-run Enumerate and plan only; do not warm + --verify Warm every target, ignoring the already-cached skip + --concurrency Parallel requests (default from config) + --pat ADO PAT (else $env:ADO_PAT / $env:WARM_FEED_TOKEN / az) + -v, --verbose Verbose logging + -h, --help Show this help +`; + +function defaultConfigPath(): string { + return path.resolve(__dirname, '..', 'warm-feed.config.json'); +} + +const {values} = parseArgs({ + options: { + config: {type: 'string', short: 'c'}, + only: {type: 'string'}, + packages: {type: 'string', multiple: true}, + 'dry-run': {type: 'boolean'}, + verify: {type: 'boolean'}, + concurrency: {type: 'string'}, + pat: {type: 'string'}, + verbose: {type: 'boolean', short: 'v'}, + help: {type: 'boolean', short: 'h'}, + }, + allowPositionals: false, +}); + +if (values.help) { + process.stdout.write(HELP); + process.exit(0); +} + +const options: RunOptions = { + configPath: values.config ?? defaultConfigPath(), + only: values.only, + dryRun: Boolean(values['dry-run']), + verify: Boolean(values.verify), + concurrency: values.concurrency ? Number(values.concurrency) : undefined, + packages: values.packages ?? [], + verbose: Boolean(values.verbose), +}; + +run(options, values.pat) + .then(code => process.exit(code)) + .catch(err => { + process.stderr.write( + `${err instanceof Error ? (err.stack ?? err.message) : String(err)}\n`, + ); + process.exit(1); + }); diff --git a/packages/@rnw-scripts/warm-feed/src/warmers.ts b/packages/@rnw-scripts/warm-feed/src/warmers.ts new file mode 100644 index 00000000000..58cd5f4ef48 --- /dev/null +++ b/packages/@rnw-scripts/warm-feed/src/warmers.ts @@ -0,0 +1,79 @@ +/** + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + * + * @format + */ + +import type {Ctx, Ecosystem, WarmResult, WarmTarget} from './types'; +import type {NpmRegistry, NuGetRegistry} from './registries'; +import {warmGet} from './http'; + +export interface Warmer { + warm(target: WarmTarget): Promise; +} + +interface Registries { + npm?: NpmRegistry; + nuget?: NuGetRegistry; +} + +/** + * Warm an npm version by fetching its tarball through the authenticated feed, + * which triggers the upstream save. Idempotent. + */ +function createNpmWarmer(ctx: Ctx, npm: NpmRegistry): Warmer { + return { + async warm(target: WarmTarget): Promise { + const tarball = await npm.getTarballUrl(target.id, target.version); + if (!tarball) { + return {target, status: 'missing', detail: 'version not in packument'}; + } + const headers = await ctx.auth.header(); + const status = await warmGet(tarball, headers); + if (status === 401 || status === 403) { + throw new Error( + `npm auth failed (${status}) fetching ${target.id} tarball`, + ); + } + if (status >= 200 && status < 300) return {target, status: 'warmed'}; + if (status === 404) + return {target, status: 'missing', detail: 'tarball 404'}; + return {target, status: 'failed', detail: `HTTP ${status}`}; + }, + }; +} + +/** + * Warm a NuGet version by fetching its `.nupkg` from the feed's flat2 endpoint, + * which triggers the upstream save. Idempotent. + */ +function createNuGetWarmer(ctx: Ctx, nuget: NuGetRegistry): Warmer { + return { + async warm(target: WarmTarget): Promise { + const url = await nuget.nupkgUrl(target.id, target.version); + const headers = await ctx.auth.header(); + const status = await warmGet(url, headers); + if (status === 401 || status === 403) { + throw new Error( + `nuget auth failed (${status}) fetching ${target.id}.${target.version}`, + ); + } + if (status >= 200 && status < 300) return {target, status: 'warmed'}; + if (status === 404) + return {target, status: 'missing', detail: 'nupkg 404'}; + return {target, status: 'failed', detail: `HTTP ${status}`}; + }, + }; +} + +export function createWarmers( + ctx: Ctx, + registries: Registries, +): Partial> { + const warmers: Partial> = {}; + if (registries.npm) warmers.npm = createNpmWarmer(ctx, registries.npm); + if (registries.nuget) + warmers.nuget = createNuGetWarmer(ctx, registries.nuget); + return warmers; +} diff --git a/packages/@rnw-scripts/warm-feed/tsconfig.json b/packages/@rnw-scripts/warm-feed/tsconfig.json new file mode 100644 index 00000000000..c62faa78baf --- /dev/null +++ b/packages/@rnw-scripts/warm-feed/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "@rnw-scripts/ts-config", + "include": ["src"], + "exclude": ["node_modules"] +} diff --git a/packages/@rnw-scripts/warm-feed/warm-feed.config.json b/packages/@rnw-scripts/warm-feed/warm-feed.config.json new file mode 100644 index 00000000000..25248a406a5 --- /dev/null +++ b/packages/@rnw-scripts/warm-feed/warm-feed.config.json @@ -0,0 +1,23 @@ +{ + "feeds": { + "npm": { + "registry": "https://pkgs.dev.azure.com/ms/react-native/_packaging/react-native-public/npm/registry/" + }, + "nuget": { + "index": "https://pkgs.dev.azure.com/ms/react-native/_packaging/react-native-public/nuget/v3/index.json" + } + }, + "enumerate": { + "feedManagementBase": "https://feeds.dev.azure.com/ms/react-native/_apis/packaging/Feeds/react-native-public", + "apiVersion": "7.1", + "pageSize": 1000 + }, + "expand": { + "scope": "in-use-lines", + "includeLatest": true, + "includePrerelease": false, + "maxMajorsBack": 0 + }, + "concurrency": 8, + "ignore": [] +} diff --git a/packages/e2e-test-app-fabric/test/__snapshots__/snapshotPages.test.js.snap b/packages/e2e-test-app-fabric/test/__snapshots__/snapshotPages.test.js.snap index bfed731ae54..521955b8181 100644 --- a/packages/e2e-test-app-fabric/test/__snapshots__/snapshotPages.test.js.snap +++ b/packages/e2e-test-app-fabric/test/__snapshots__/snapshotPages.test.js.snap @@ -29016,7 +29016,7 @@ exports[`snapshotAllPages Layout Events 1`] = ` onLayout={[Function]} source={ { - "uri": "https://www.facebook.com/favicon.ico", + "uri": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVR4nGNgAAIAAAUAAen63NgAAAAASUVORK5CYII=", } } style={ diff --git a/packages/e2e-test-app-fabric/windows/RNTesterApp-Fabric.Package/packages.lock.json b/packages/e2e-test-app-fabric/windows/RNTesterApp-Fabric.Package/packages.lock.json index 307deeeacd2..71d0fab9c8d 100644 --- a/packages/e2e-test-app-fabric/windows/RNTesterApp-Fabric.Package/packages.lock.json +++ b/packages/e2e-test-app-fabric/windows/RNTesterApp-Fabric.Package/packages.lock.json @@ -14,8 +14,8 @@ }, "Microsoft.JavaScript.Hermes": { "type": "Transitive", - "resolved": "0.0.0-2605.6002-2279da22", - "contentHash": "E59URq24UdJMpLMkMY92h3hs91EHelicbUsBRETn+FJ19uQbzU3gqkh2NPyBtMj4IVYK5K9FW+hIUTLg8SG25g==" + "resolved": "0.0.0-2608.12001-35d34796", + "contentHash": "6EirG0swfzCbfajtXtUUUFJJqi0yYzYOMRjkx0XcLB1a8qYjAhRcFdAfsNDvxPbs0ErI+6++EjBZquf3YFQZDA==" }, "Microsoft.SourceLink.Common": { "type": "Transitive", @@ -165,7 +165,7 @@ "type": "Project", "dependencies": { "Common": "[1.0.0, )", - "Microsoft.JavaScript.Hermes": "[0.0.0-2605.6002-2279da22, )", + "Microsoft.JavaScript.Hermes": "[0.0.0-2608.12001-35d34796, )", "Microsoft.SourceLink.GitHub": "[1.1.1, )", "Microsoft.WindowsAppSDK": "[1.8.260508005, )", "ReactCommon": "[1.0.0, )", @@ -182,7 +182,7 @@ "type": "Project", "dependencies": { "AutomationChannel": "[1.0.0, )", - "Microsoft.JavaScript.Hermes": "[0.0.0-2605.6002-2279da22, )", + "Microsoft.JavaScript.Hermes": "[0.0.0-2608.12001-35d34796, )", "Microsoft.ReactNative": "[1.0.0, )", "Microsoft.VCRTForwarders.140": "[1.0.2-rc, )", "Microsoft.WindowsAppSDK": "[1.8.260508005, )", diff --git a/packages/e2e-test-app-fabric/windows/RNTesterApp-Fabric/packages.lock.json b/packages/e2e-test-app-fabric/windows/RNTesterApp-Fabric/packages.lock.json index cbf7559f333..2dc1ef5c7de 100644 --- a/packages/e2e-test-app-fabric/windows/RNTesterApp-Fabric/packages.lock.json +++ b/packages/e2e-test-app-fabric/windows/RNTesterApp-Fabric/packages.lock.json @@ -10,9 +10,9 @@ }, "Microsoft.JavaScript.Hermes": { "type": "Direct", - "requested": "[0.0.0-2605.6002-2279da22, )", - "resolved": "0.0.0-2605.6002-2279da22", - "contentHash": "E59URq24UdJMpLMkMY92h3hs91EHelicbUsBRETn+FJ19uQbzU3gqkh2NPyBtMj4IVYK5K9FW+hIUTLg8SG25g==" + "requested": "[0.0.0-2608.12001-35d34796, )", + "resolved": "0.0.0-2608.12001-35d34796", + "contentHash": "6EirG0swfzCbfajtXtUUUFJJqi0yYzYOMRjkx0XcLB1a8qYjAhRcFdAfsNDvxPbs0ErI+6++EjBZquf3YFQZDA==" }, "Microsoft.VCRTForwarders.140": { "type": "Direct", @@ -175,7 +175,7 @@ "type": "Project", "dependencies": { "Common": "[1.0.0, )", - "Microsoft.JavaScript.Hermes": "[0.0.0-2605.6002-2279da22, )", + "Microsoft.JavaScript.Hermes": "[0.0.0-2608.12001-35d34796, )", "Microsoft.SourceLink.GitHub": "[1.1.1, )", "Microsoft.WindowsAppSDK": "[1.8.260508005, )", "ReactCommon": "[1.0.0, )", diff --git a/packages/playground/windows/playground-composition.Package/packages.experimentalwinui3.lock.json b/packages/playground/windows/playground-composition.Package/packages.experimentalwinui3.lock.json index b9675612935..7e8b72eaf40 100644 --- a/packages/playground/windows/playground-composition.Package/packages.experimentalwinui3.lock.json +++ b/packages/playground/windows/playground-composition.Package/packages.experimentalwinui3.lock.json @@ -14,8 +14,8 @@ }, "Microsoft.JavaScript.Hermes": { "type": "Transitive", - "resolved": "0.0.0-2605.6002-2279da22", - "contentHash": "E59URq24UdJMpLMkMY92h3hs91EHelicbUsBRETn+FJ19uQbzU3gqkh2NPyBtMj4IVYK5K9FW+hIUTLg8SG25g==" + "resolved": "0.0.0-2608.12001-35d34796", + "contentHash": "6EirG0swfzCbfajtXtUUUFJJqi0yYzYOMRjkx0XcLB1a8qYjAhRcFdAfsNDvxPbs0ErI+6++EjBZquf3YFQZDA==" }, "Microsoft.SourceLink.Common": { "type": "Transitive", @@ -156,7 +156,7 @@ "type": "Project", "dependencies": { "Common": "[1.0.0, )", - "Microsoft.JavaScript.Hermes": "[0.0.0-2605.6002-2279da22, )", + "Microsoft.JavaScript.Hermes": "[0.0.0-2608.12001-35d34796, )", "Microsoft.SourceLink.GitHub": "[1.1.1, )", "Microsoft.WindowsAppSDK": "[2.0.0-experimental3, )", "ReactCommon": "[1.0.0, )", @@ -166,7 +166,7 @@ "playground-composition": { "type": "Project", "dependencies": { - "Microsoft.JavaScript.Hermes": "[0.0.0-2605.6002-2279da22, )", + "Microsoft.JavaScript.Hermes": "[0.0.0-2608.12001-35d34796, )", "Microsoft.ReactNative": "[1.0.0, )", "Microsoft.VCRTForwarders.140": "[1.0.2-rc, )", "Microsoft.WindowsAppSDK": "[2.0.0-experimental3, )", diff --git a/packages/playground/windows/playground-composition.Package/packages.lock.json b/packages/playground/windows/playground-composition.Package/packages.lock.json index ee1fe9a4f9e..7ceb3348d29 100644 --- a/packages/playground/windows/playground-composition.Package/packages.lock.json +++ b/packages/playground/windows/playground-composition.Package/packages.lock.json @@ -14,8 +14,8 @@ }, "Microsoft.JavaScript.Hermes": { "type": "Transitive", - "resolved": "0.0.0-2605.6002-2279da22", - "contentHash": "E59URq24UdJMpLMkMY92h3hs91EHelicbUsBRETn+FJ19uQbzU3gqkh2NPyBtMj4IVYK5K9FW+hIUTLg8SG25g==" + "resolved": "0.0.0-2608.12001-35d34796", + "contentHash": "6EirG0swfzCbfajtXtUUUFJJqi0yYzYOMRjkx0XcLB1a8qYjAhRcFdAfsNDvxPbs0ErI+6++EjBZquf3YFQZDA==" }, "Microsoft.SourceLink.Common": { "type": "Transitive", @@ -156,7 +156,7 @@ "type": "Project", "dependencies": { "Common": "[1.0.0, )", - "Microsoft.JavaScript.Hermes": "[0.0.0-2605.6002-2279da22, )", + "Microsoft.JavaScript.Hermes": "[0.0.0-2608.12001-35d34796, )", "Microsoft.SourceLink.GitHub": "[1.1.1, )", "Microsoft.WindowsAppSDK": "[1.8.260508005, )", "ReactCommon": "[1.0.0, )", @@ -166,7 +166,7 @@ "playground-composition": { "type": "Project", "dependencies": { - "Microsoft.JavaScript.Hermes": "[0.0.0-2605.6002-2279da22, )", + "Microsoft.JavaScript.Hermes": "[0.0.0-2608.12001-35d34796, )", "Microsoft.ReactNative": "[1.0.0, )", "Microsoft.VCRTForwarders.140": "[1.0.2-rc, )", "Microsoft.WindowsAppSDK": "[1.8.260508005, )", diff --git a/packages/playground/windows/playground-composition/packages.experimentalwinui3.lock.json b/packages/playground/windows/playground-composition/packages.experimentalwinui3.lock.json index be550eb6783..3fdfba8616d 100644 --- a/packages/playground/windows/playground-composition/packages.experimentalwinui3.lock.json +++ b/packages/playground/windows/playground-composition/packages.experimentalwinui3.lock.json @@ -10,9 +10,9 @@ }, "Microsoft.JavaScript.Hermes": { "type": "Direct", - "requested": "[0.0.0-2605.6002-2279da22, )", - "resolved": "0.0.0-2605.6002-2279da22", - "contentHash": "E59URq24UdJMpLMkMY92h3hs91EHelicbUsBRETn+FJ19uQbzU3gqkh2NPyBtMj4IVYK5K9FW+hIUTLg8SG25g==" + "requested": "[0.0.0-2608.12001-35d34796, )", + "resolved": "0.0.0-2608.12001-35d34796", + "contentHash": "6EirG0swfzCbfajtXtUUUFJJqi0yYzYOMRjkx0XcLB1a8qYjAhRcFdAfsNDvxPbs0ErI+6++EjBZquf3YFQZDA==" }, "Microsoft.VCRTForwarders.140": { "type": "Direct", @@ -166,7 +166,7 @@ "type": "Project", "dependencies": { "Common": "[1.0.0, )", - "Microsoft.JavaScript.Hermes": "[0.0.0-2605.6002-2279da22, )", + "Microsoft.JavaScript.Hermes": "[0.0.0-2608.12001-35d34796, )", "Microsoft.SourceLink.GitHub": "[1.1.1, )", "Microsoft.WindowsAppSDK": "[2.0.0-experimental3, )", "ReactCommon": "[1.0.0, )", diff --git a/packages/playground/windows/playground-composition/packages.lock.json b/packages/playground/windows/playground-composition/packages.lock.json index 16d1e9ddc3c..7083a6040ad 100644 --- a/packages/playground/windows/playground-composition/packages.lock.json +++ b/packages/playground/windows/playground-composition/packages.lock.json @@ -10,9 +10,9 @@ }, "Microsoft.JavaScript.Hermes": { "type": "Direct", - "requested": "[0.0.0-2605.6002-2279da22, )", - "resolved": "0.0.0-2605.6002-2279da22", - "contentHash": "E59URq24UdJMpLMkMY92h3hs91EHelicbUsBRETn+FJ19uQbzU3gqkh2NPyBtMj4IVYK5K9FW+hIUTLg8SG25g==" + "requested": "[0.0.0-2608.12001-35d34796, )", + "resolved": "0.0.0-2608.12001-35d34796", + "contentHash": "6EirG0swfzCbfajtXtUUUFJJqi0yYzYOMRjkx0XcLB1a8qYjAhRcFdAfsNDvxPbs0ErI+6++EjBZquf3YFQZDA==" }, "Microsoft.VCRTForwarders.140": { "type": "Direct", @@ -166,7 +166,7 @@ "type": "Project", "dependencies": { "Common": "[1.0.0, )", - "Microsoft.JavaScript.Hermes": "[0.0.0-2605.6002-2279da22, )", + "Microsoft.JavaScript.Hermes": "[0.0.0-2608.12001-35d34796, )", "Microsoft.SourceLink.GitHub": "[1.1.1, )", "Microsoft.WindowsAppSDK": "[1.8.260508005, )", "ReactCommon": "[1.0.0, )", diff --git a/packages/sample-app-fabric/windows/SampleAppFabric.Package/packages.lock.json b/packages/sample-app-fabric/windows/SampleAppFabric.Package/packages.lock.json index 339906f7d97..a030c8475de 100644 --- a/packages/sample-app-fabric/windows/SampleAppFabric.Package/packages.lock.json +++ b/packages/sample-app-fabric/windows/SampleAppFabric.Package/packages.lock.json @@ -14,8 +14,8 @@ }, "Microsoft.JavaScript.Hermes": { "type": "Transitive", - "resolved": "0.0.0-2605.6002-2279da22", - "contentHash": "E59URq24UdJMpLMkMY92h3hs91EHelicbUsBRETn+FJ19uQbzU3gqkh2NPyBtMj4IVYK5K9FW+hIUTLg8SG25g==" + "resolved": "0.0.0-2608.12001-35d34796", + "contentHash": "6EirG0swfzCbfajtXtUUUFJJqi0yYzYOMRjkx0XcLB1a8qYjAhRcFdAfsNDvxPbs0ErI+6++EjBZquf3YFQZDA==" }, "Microsoft.SourceLink.Common": { "type": "Transitive", @@ -156,7 +156,7 @@ "type": "Project", "dependencies": { "Common": "[1.0.0, )", - "Microsoft.JavaScript.Hermes": "[0.0.0-2605.6002-2279da22, )", + "Microsoft.JavaScript.Hermes": "[0.0.0-2608.12001-35d34796, )", "Microsoft.SourceLink.GitHub": "[1.1.1, )", "Microsoft.WindowsAppSDK": "[1.8.260508005, )", "ReactCommon": "[1.0.0, )", @@ -172,7 +172,7 @@ "sampleappfabric": { "type": "Project", "dependencies": { - "Microsoft.JavaScript.Hermes": "[0.0.0-2605.6002-2279da22, )", + "Microsoft.JavaScript.Hermes": "[0.0.0-2608.12001-35d34796, )", "Microsoft.ReactNative": "[1.0.0, )", "Microsoft.VCRTForwarders.140": "[1.0.2-rc, )", "Microsoft.WindowsAppSDK": "[1.8.260508005, )", diff --git a/packages/sample-app-fabric/windows/SampleAppFabric/packages.lock.json b/packages/sample-app-fabric/windows/SampleAppFabric/packages.lock.json index 7f92bc676e4..9c51d2b20ae 100644 --- a/packages/sample-app-fabric/windows/SampleAppFabric/packages.lock.json +++ b/packages/sample-app-fabric/windows/SampleAppFabric/packages.lock.json @@ -10,9 +10,9 @@ }, "Microsoft.JavaScript.Hermes": { "type": "Direct", - "requested": "[0.0.0-2605.6002-2279da22, )", - "resolved": "0.0.0-2605.6002-2279da22", - "contentHash": "E59URq24UdJMpLMkMY92h3hs91EHelicbUsBRETn+FJ19uQbzU3gqkh2NPyBtMj4IVYK5K9FW+hIUTLg8SG25g==" + "requested": "[0.0.0-2608.12001-35d34796, )", + "resolved": "0.0.0-2608.12001-35d34796", + "contentHash": "6EirG0swfzCbfajtXtUUUFJJqi0yYzYOMRjkx0XcLB1a8qYjAhRcFdAfsNDvxPbs0ErI+6++EjBZquf3YFQZDA==" }, "Microsoft.VCRTForwarders.140": { "type": "Direct", @@ -166,7 +166,7 @@ "type": "Project", "dependencies": { "Common": "[1.0.0, )", - "Microsoft.JavaScript.Hermes": "[0.0.0-2605.6002-2279da22, )", + "Microsoft.JavaScript.Hermes": "[0.0.0-2608.12001-35d34796, )", "Microsoft.SourceLink.GitHub": "[1.1.1, )", "Microsoft.WindowsAppSDK": "[1.8.260508005, )", "ReactCommon": "[1.0.0, )", diff --git a/packages/sample-custom-component/windows/SampleCustomComponent/packages.experimentalwinui3.lock.json b/packages/sample-custom-component/windows/SampleCustomComponent/packages.experimentalwinui3.lock.json index c67c71ce177..3b957aef2d1 100644 --- a/packages/sample-custom-component/windows/SampleCustomComponent/packages.experimentalwinui3.lock.json +++ b/packages/sample-custom-component/windows/SampleCustomComponent/packages.experimentalwinui3.lock.json @@ -44,8 +44,8 @@ }, "Microsoft.JavaScript.Hermes": { "type": "Transitive", - "resolved": "0.0.0-2605.6002-2279da22", - "contentHash": "E59URq24UdJMpLMkMY92h3hs91EHelicbUsBRETn+FJ19uQbzU3gqkh2NPyBtMj4IVYK5K9FW+hIUTLg8SG25g==" + "resolved": "0.0.0-2608.12001-35d34796", + "contentHash": "6EirG0swfzCbfajtXtUUUFJJqi0yYzYOMRjkx0XcLB1a8qYjAhRcFdAfsNDvxPbs0ErI+6++EjBZquf3YFQZDA==" }, "Microsoft.SourceLink.Common": { "type": "Transitive", @@ -165,7 +165,7 @@ "type": "Project", "dependencies": { "Common": "[1.0.0, )", - "Microsoft.JavaScript.Hermes": "[0.0.0-2605.6002-2279da22, )", + "Microsoft.JavaScript.Hermes": "[0.0.0-2608.12001-35d34796, )", "Microsoft.SourceLink.GitHub": "[1.1.1, )", "Microsoft.WindowsAppSDK": "[2.0.0-experimental3, )", "ReactCommon": "[1.0.0, )", diff --git a/packages/sample-custom-component/windows/SampleCustomComponent/packages.lock.json b/packages/sample-custom-component/windows/SampleCustomComponent/packages.lock.json index 7016158f833..e6540da64bc 100644 --- a/packages/sample-custom-component/windows/SampleCustomComponent/packages.lock.json +++ b/packages/sample-custom-component/windows/SampleCustomComponent/packages.lock.json @@ -44,8 +44,8 @@ }, "Microsoft.JavaScript.Hermes": { "type": "Transitive", - "resolved": "0.0.0-2605.6002-2279da22", - "contentHash": "E59URq24UdJMpLMkMY92h3hs91EHelicbUsBRETn+FJ19uQbzU3gqkh2NPyBtMj4IVYK5K9FW+hIUTLg8SG25g==" + "resolved": "0.0.0-2608.12001-35d34796", + "contentHash": "6EirG0swfzCbfajtXtUUUFJJqi0yYzYOMRjkx0XcLB1a8qYjAhRcFdAfsNDvxPbs0ErI+6++EjBZquf3YFQZDA==" }, "Microsoft.SourceLink.Common": { "type": "Transitive", @@ -165,7 +165,7 @@ "type": "Project", "dependencies": { "Common": "[1.0.0, )", - "Microsoft.JavaScript.Hermes": "[0.0.0-2605.6002-2279da22, )", + "Microsoft.JavaScript.Hermes": "[0.0.0-2608.12001-35d34796, )", "Microsoft.SourceLink.GitHub": "[1.1.1, )", "Microsoft.WindowsAppSDK": "[1.8.260508005, )", "ReactCommon": "[1.0.0, )", diff --git a/vnext/Desktop.DLL/packages.experimentalwinui3.lock.json b/vnext/Desktop.DLL/packages.experimentalwinui3.lock.json index 1efe844bc14..0b8c8d50c36 100644 --- a/vnext/Desktop.DLL/packages.experimentalwinui3.lock.json +++ b/vnext/Desktop.DLL/packages.experimentalwinui3.lock.json @@ -10,9 +10,9 @@ }, "Microsoft.JavaScript.Hermes": { "type": "Direct", - "requested": "[0.0.0-2605.6002-2279da22, )", - "resolved": "0.0.0-2605.6002-2279da22", - "contentHash": "E59URq24UdJMpLMkMY92h3hs91EHelicbUsBRETn+FJ19uQbzU3gqkh2NPyBtMj4IVYK5K9FW+hIUTLg8SG25g==" + "requested": "[0.0.0-2608.12001-35d34796, )", + "resolved": "0.0.0-2608.12001-35d34796", + "contentHash": "6EirG0swfzCbfajtXtUUUFJJqi0yYzYOMRjkx0XcLB1a8qYjAhRcFdAfsNDvxPbs0ErI+6++EjBZquf3YFQZDA==" }, "Microsoft.SourceLink.GitHub": { "type": "Direct", @@ -160,7 +160,7 @@ "type": "Project", "dependencies": { "Common": "[1.0.0, )", - "Microsoft.JavaScript.Hermes": "[0.0.0-2605.6002-2279da22, )", + "Microsoft.JavaScript.Hermes": "[0.0.0-2608.12001-35d34796, )", "Microsoft.SourceLink.GitHub": "[1.1.1, )", "Microsoft.WindowsAppSDK": "[2.0.0-experimental3, )", "ReactCommon": "[1.0.0, )", diff --git a/vnext/Desktop.DLL/packages.lock.json b/vnext/Desktop.DLL/packages.lock.json index 97fe04599e4..fa9f0706be5 100644 --- a/vnext/Desktop.DLL/packages.lock.json +++ b/vnext/Desktop.DLL/packages.lock.json @@ -10,9 +10,9 @@ }, "Microsoft.JavaScript.Hermes": { "type": "Direct", - "requested": "[0.0.0-2605.6002-2279da22, )", - "resolved": "0.0.0-2605.6002-2279da22", - "contentHash": "E59URq24UdJMpLMkMY92h3hs91EHelicbUsBRETn+FJ19uQbzU3gqkh2NPyBtMj4IVYK5K9FW+hIUTLg8SG25g==" + "requested": "[0.0.0-2608.12001-35d34796, )", + "resolved": "0.0.0-2608.12001-35d34796", + "contentHash": "6EirG0swfzCbfajtXtUUUFJJqi0yYzYOMRjkx0XcLB1a8qYjAhRcFdAfsNDvxPbs0ErI+6++EjBZquf3YFQZDA==" }, "Microsoft.SourceLink.GitHub": { "type": "Direct", @@ -160,7 +160,7 @@ "type": "Project", "dependencies": { "Common": "[1.0.0, )", - "Microsoft.JavaScript.Hermes": "[0.0.0-2605.6002-2279da22, )", + "Microsoft.JavaScript.Hermes": "[0.0.0-2608.12001-35d34796, )", "Microsoft.SourceLink.GitHub": "[1.1.1, )", "Microsoft.WindowsAppSDK": "[1.8.260508005, )", "ReactCommon": "[1.0.0, )", diff --git a/vnext/Desktop.IntegrationTests/packages.experimentalwinui3.lock.json b/vnext/Desktop.IntegrationTests/packages.experimentalwinui3.lock.json index 536876fcbc3..7c4da9f1c39 100644 --- a/vnext/Desktop.IntegrationTests/packages.experimentalwinui3.lock.json +++ b/vnext/Desktop.IntegrationTests/packages.experimentalwinui3.lock.json @@ -10,9 +10,9 @@ }, "Microsoft.JavaScript.Hermes": { "type": "Direct", - "requested": "[0.0.0-2605.6002-2279da22, )", - "resolved": "0.0.0-2605.6002-2279da22", - "contentHash": "E59URq24UdJMpLMkMY92h3hs91EHelicbUsBRETn+FJ19uQbzU3gqkh2NPyBtMj4IVYK5K9FW+hIUTLg8SG25g==" + "requested": "[0.0.0-2608.12001-35d34796, )", + "resolved": "0.0.0-2608.12001-35d34796", + "contentHash": "6EirG0swfzCbfajtXtUUUFJJqi0yYzYOMRjkx0XcLB1a8qYjAhRcFdAfsNDvxPbs0ErI+6++EjBZquf3YFQZDA==" }, "Microsoft.Windows.CppWinRT": { "type": "Direct", @@ -165,7 +165,7 @@ "type": "Project", "dependencies": { "Common": "[1.0.0, )", - "Microsoft.JavaScript.Hermes": "[0.0.0-2605.6002-2279da22, )", + "Microsoft.JavaScript.Hermes": "[0.0.0-2608.12001-35d34796, )", "Microsoft.SourceLink.GitHub": "[1.1.1, )", "Microsoft.WindowsAppSDK": "[2.0.0-experimental3, )", "ReactCommon": "[1.0.0, )", @@ -176,7 +176,7 @@ "react.windows.desktop.dll": { "type": "Project", "dependencies": { - "Microsoft.JavaScript.Hermes": "[0.0.0-2605.6002-2279da22, )", + "Microsoft.JavaScript.Hermes": "[0.0.0-2608.12001-35d34796, )", "Microsoft.SourceLink.GitHub": "[1.1.1, )", "React.Windows.Desktop": "[1.0.0, )", "ReactNative.V8Jsi.Windows": "[0.71.8, )", diff --git a/vnext/Desktop.IntegrationTests/packages.lock.json b/vnext/Desktop.IntegrationTests/packages.lock.json index 6cf0f12e044..66393a8ac12 100644 --- a/vnext/Desktop.IntegrationTests/packages.lock.json +++ b/vnext/Desktop.IntegrationTests/packages.lock.json @@ -10,9 +10,9 @@ }, "Microsoft.JavaScript.Hermes": { "type": "Direct", - "requested": "[0.0.0-2605.6002-2279da22, )", - "resolved": "0.0.0-2605.6002-2279da22", - "contentHash": "E59URq24UdJMpLMkMY92h3hs91EHelicbUsBRETn+FJ19uQbzU3gqkh2NPyBtMj4IVYK5K9FW+hIUTLg8SG25g==" + "requested": "[0.0.0-2608.12001-35d34796, )", + "resolved": "0.0.0-2608.12001-35d34796", + "contentHash": "6EirG0swfzCbfajtXtUUUFJJqi0yYzYOMRjkx0XcLB1a8qYjAhRcFdAfsNDvxPbs0ErI+6++EjBZquf3YFQZDA==" }, "Microsoft.Windows.CppWinRT": { "type": "Direct", @@ -165,7 +165,7 @@ "type": "Project", "dependencies": { "Common": "[1.0.0, )", - "Microsoft.JavaScript.Hermes": "[0.0.0-2605.6002-2279da22, )", + "Microsoft.JavaScript.Hermes": "[0.0.0-2608.12001-35d34796, )", "Microsoft.SourceLink.GitHub": "[1.1.1, )", "Microsoft.WindowsAppSDK": "[1.8.260508005, )", "ReactCommon": "[1.0.0, )", @@ -176,7 +176,7 @@ "react.windows.desktop.dll": { "type": "Project", "dependencies": { - "Microsoft.JavaScript.Hermes": "[0.0.0-2605.6002-2279da22, )", + "Microsoft.JavaScript.Hermes": "[0.0.0-2608.12001-35d34796, )", "Microsoft.SourceLink.GitHub": "[1.1.1, )", "React.Windows.Desktop": "[1.0.0, )", "ReactNative.V8Jsi.Windows": "[0.71.8, )", diff --git a/vnext/Desktop.UnitTests/packages.experimentalwinui3.lock.json b/vnext/Desktop.UnitTests/packages.experimentalwinui3.lock.json index 2aa2e26260c..e145a629fdc 100644 --- a/vnext/Desktop.UnitTests/packages.experimentalwinui3.lock.json +++ b/vnext/Desktop.UnitTests/packages.experimentalwinui3.lock.json @@ -21,8 +21,8 @@ }, "Microsoft.JavaScript.Hermes": { "type": "Transitive", - "resolved": "0.0.0-2605.6002-2279da22", - "contentHash": "E59URq24UdJMpLMkMY92h3hs91EHelicbUsBRETn+FJ19uQbzU3gqkh2NPyBtMj4IVYK5K9FW+hIUTLg8SG25g==" + "resolved": "0.0.0-2608.12001-35d34796", + "contentHash": "6EirG0swfzCbfajtXtUUUFJJqi0yYzYOMRjkx0XcLB1a8qYjAhRcFdAfsNDvxPbs0ErI+6++EjBZquf3YFQZDA==" }, "Microsoft.SourceLink.Common": { "type": "Transitive", @@ -163,7 +163,7 @@ "type": "Project", "dependencies": { "Common": "[1.0.0, )", - "Microsoft.JavaScript.Hermes": "[0.0.0-2605.6002-2279da22, )", + "Microsoft.JavaScript.Hermes": "[0.0.0-2608.12001-35d34796, )", "Microsoft.SourceLink.GitHub": "[1.1.1, )", "Microsoft.WindowsAppSDK": "[2.0.0-experimental3, )", "ReactCommon": "[1.0.0, )", diff --git a/vnext/Desktop.UnitTests/packages.lock.json b/vnext/Desktop.UnitTests/packages.lock.json index e117daf4c8d..096e5ff52a4 100644 --- a/vnext/Desktop.UnitTests/packages.lock.json +++ b/vnext/Desktop.UnitTests/packages.lock.json @@ -21,8 +21,8 @@ }, "Microsoft.JavaScript.Hermes": { "type": "Transitive", - "resolved": "0.0.0-2605.6002-2279da22", - "contentHash": "E59URq24UdJMpLMkMY92h3hs91EHelicbUsBRETn+FJ19uQbzU3gqkh2NPyBtMj4IVYK5K9FW+hIUTLg8SG25g==" + "resolved": "0.0.0-2608.12001-35d34796", + "contentHash": "6EirG0swfzCbfajtXtUUUFJJqi0yYzYOMRjkx0XcLB1a8qYjAhRcFdAfsNDvxPbs0ErI+6++EjBZquf3YFQZDA==" }, "Microsoft.SourceLink.Common": { "type": "Transitive", @@ -163,7 +163,7 @@ "type": "Project", "dependencies": { "Common": "[1.0.0, )", - "Microsoft.JavaScript.Hermes": "[0.0.0-2605.6002-2279da22, )", + "Microsoft.JavaScript.Hermes": "[0.0.0-2608.12001-35d34796, )", "Microsoft.SourceLink.GitHub": "[1.1.1, )", "Microsoft.WindowsAppSDK": "[1.8.260508005, )", "ReactCommon": "[1.0.0, )", diff --git a/vnext/Desktop/packages.experimentalwinui3.lock.json b/vnext/Desktop/packages.experimentalwinui3.lock.json index d14ebcaabdf..056d55ecb31 100644 --- a/vnext/Desktop/packages.experimentalwinui3.lock.json +++ b/vnext/Desktop/packages.experimentalwinui3.lock.json @@ -10,9 +10,9 @@ }, "Microsoft.JavaScript.Hermes": { "type": "Direct", - "requested": "[0.0.0-2605.6002-2279da22, )", - "resolved": "0.0.0-2605.6002-2279da22", - "contentHash": "E59URq24UdJMpLMkMY92h3hs91EHelicbUsBRETn+FJ19uQbzU3gqkh2NPyBtMj4IVYK5K9FW+hIUTLg8SG25g==" + "requested": "[0.0.0-2608.12001-35d34796, )", + "resolved": "0.0.0-2608.12001-35d34796", + "contentHash": "6EirG0swfzCbfajtXtUUUFJJqi0yYzYOMRjkx0XcLB1a8qYjAhRcFdAfsNDvxPbs0ErI+6++EjBZquf3YFQZDA==" }, "Microsoft.SourceLink.GitHub": { "type": "Direct", diff --git a/vnext/Desktop/packages.lock.json b/vnext/Desktop/packages.lock.json index ab24797bfed..497e069fd80 100644 --- a/vnext/Desktop/packages.lock.json +++ b/vnext/Desktop/packages.lock.json @@ -10,9 +10,9 @@ }, "Microsoft.JavaScript.Hermes": { "type": "Direct", - "requested": "[0.0.0-2605.6002-2279da22, )", - "resolved": "0.0.0-2605.6002-2279da22", - "contentHash": "E59URq24UdJMpLMkMY92h3hs91EHelicbUsBRETn+FJ19uQbzU3gqkh2NPyBtMj4IVYK5K9FW+hIUTLg8SG25g==" + "requested": "[0.0.0-2608.12001-35d34796, )", + "resolved": "0.0.0-2608.12001-35d34796", + "contentHash": "6EirG0swfzCbfajtXtUUUFJJqi0yYzYOMRjkx0XcLB1a8qYjAhRcFdAfsNDvxPbs0ErI+6++EjBZquf3YFQZDA==" }, "Microsoft.SourceLink.GitHub": { "type": "Direct", diff --git a/vnext/Microsoft.ReactNative.CsWinRT/packages.experimentalwinui3.lock.json b/vnext/Microsoft.ReactNative.CsWinRT/packages.experimentalwinui3.lock.json index c512e8e1a2d..61726f0cc21 100644 --- a/vnext/Microsoft.ReactNative.CsWinRT/packages.experimentalwinui3.lock.json +++ b/vnext/Microsoft.ReactNative.CsWinRT/packages.experimentalwinui3.lock.json @@ -37,8 +37,8 @@ }, "Microsoft.JavaScript.Hermes": { "type": "Transitive", - "resolved": "0.0.0-2605.6002-2279da22", - "contentHash": "E59URq24UdJMpLMkMY92h3hs91EHelicbUsBRETn+FJ19uQbzU3gqkh2NPyBtMj4IVYK5K9FW+hIUTLg8SG25g==" + "resolved": "0.0.0-2608.12001-35d34796", + "contentHash": "6EirG0swfzCbfajtXtUUUFJJqi0yYzYOMRjkx0XcLB1a8qYjAhRcFdAfsNDvxPbs0ErI+6++EjBZquf3YFQZDA==" }, "Microsoft.SourceLink.Common": { "type": "Transitive", @@ -164,7 +164,7 @@ "type": "Project", "dependencies": { "Common": "[1.0.0, )", - "Microsoft.JavaScript.Hermes": "[0.0.0-2605.6002-2279da22, )", + "Microsoft.JavaScript.Hermes": "[0.0.0-2608.12001-35d34796, )", "Microsoft.SourceLink.GitHub": "[1.1.1, )", "Microsoft.WindowsAppSDK": "[2.0.0-experimental3, )", "ReactCommon": "[1.0.0, )", diff --git a/vnext/Microsoft.ReactNative.CsWinRT/packages.lock.json b/vnext/Microsoft.ReactNative.CsWinRT/packages.lock.json index 5c77e7b6b78..a76fb8e7251 100644 --- a/vnext/Microsoft.ReactNative.CsWinRT/packages.lock.json +++ b/vnext/Microsoft.ReactNative.CsWinRT/packages.lock.json @@ -37,8 +37,8 @@ }, "Microsoft.JavaScript.Hermes": { "type": "Transitive", - "resolved": "0.0.0-2605.6002-2279da22", - "contentHash": "E59URq24UdJMpLMkMY92h3hs91EHelicbUsBRETn+FJ19uQbzU3gqkh2NPyBtMj4IVYK5K9FW+hIUTLg8SG25g==" + "resolved": "0.0.0-2608.12001-35d34796", + "contentHash": "6EirG0swfzCbfajtXtUUUFJJqi0yYzYOMRjkx0XcLB1a8qYjAhRcFdAfsNDvxPbs0ErI+6++EjBZquf3YFQZDA==" }, "Microsoft.SourceLink.Common": { "type": "Transitive", @@ -164,7 +164,7 @@ "type": "Project", "dependencies": { "Common": "[1.0.0, )", - "Microsoft.JavaScript.Hermes": "[0.0.0-2605.6002-2279da22, )", + "Microsoft.JavaScript.Hermes": "[0.0.0-2608.12001-35d34796, )", "Microsoft.SourceLink.GitHub": "[1.1.1, )", "Microsoft.WindowsAppSDK": "[1.8.260508005, )", "ReactCommon": "[1.0.0, )", diff --git a/vnext/Microsoft.ReactNative.IntegrationTests/packages.experimentalwinui3.lock.json b/vnext/Microsoft.ReactNative.IntegrationTests/packages.experimentalwinui3.lock.json index 37a59d5dff2..3f200f514ff 100644 --- a/vnext/Microsoft.ReactNative.IntegrationTests/packages.experimentalwinui3.lock.json +++ b/vnext/Microsoft.ReactNative.IntegrationTests/packages.experimentalwinui3.lock.json @@ -50,8 +50,8 @@ }, "Microsoft.JavaScript.Hermes": { "type": "Transitive", - "resolved": "0.0.0-2605.6002-2279da22", - "contentHash": "E59URq24UdJMpLMkMY92h3hs91EHelicbUsBRETn+FJ19uQbzU3gqkh2NPyBtMj4IVYK5K9FW+hIUTLg8SG25g==" + "resolved": "0.0.0-2608.12001-35d34796", + "contentHash": "6EirG0swfzCbfajtXtUUUFJJqi0yYzYOMRjkx0XcLB1a8qYjAhRcFdAfsNDvxPbs0ErI+6++EjBZquf3YFQZDA==" }, "Microsoft.SourceLink.Common": { "type": "Transitive", @@ -171,7 +171,7 @@ "type": "Project", "dependencies": { "Common": "[1.0.0, )", - "Microsoft.JavaScript.Hermes": "[0.0.0-2605.6002-2279da22, )", + "Microsoft.JavaScript.Hermes": "[0.0.0-2608.12001-35d34796, )", "Microsoft.SourceLink.GitHub": "[1.1.1, )", "Microsoft.WindowsAppSDK": "[2.0.0-experimental3, )", "ReactCommon": "[1.0.0, )", diff --git a/vnext/Microsoft.ReactNative.IntegrationTests/packages.lock.json b/vnext/Microsoft.ReactNative.IntegrationTests/packages.lock.json index e1f80ae8a1f..697db5adbae 100644 --- a/vnext/Microsoft.ReactNative.IntegrationTests/packages.lock.json +++ b/vnext/Microsoft.ReactNative.IntegrationTests/packages.lock.json @@ -50,8 +50,8 @@ }, "Microsoft.JavaScript.Hermes": { "type": "Transitive", - "resolved": "0.0.0-2605.6002-2279da22", - "contentHash": "E59URq24UdJMpLMkMY92h3hs91EHelicbUsBRETn+FJ19uQbzU3gqkh2NPyBtMj4IVYK5K9FW+hIUTLg8SG25g==" + "resolved": "0.0.0-2608.12001-35d34796", + "contentHash": "6EirG0swfzCbfajtXtUUUFJJqi0yYzYOMRjkx0XcLB1a8qYjAhRcFdAfsNDvxPbs0ErI+6++EjBZquf3YFQZDA==" }, "Microsoft.SourceLink.Common": { "type": "Transitive", @@ -171,7 +171,7 @@ "type": "Project", "dependencies": { "Common": "[1.0.0, )", - "Microsoft.JavaScript.Hermes": "[0.0.0-2605.6002-2279da22, )", + "Microsoft.JavaScript.Hermes": "[0.0.0-2608.12001-35d34796, )", "Microsoft.SourceLink.GitHub": "[1.1.1, )", "Microsoft.WindowsAppSDK": "[1.8.260508005, )", "ReactCommon": "[1.0.0, )", diff --git a/vnext/Microsoft.ReactNative/packages.experimentalwinui3.lock.json b/vnext/Microsoft.ReactNative/packages.experimentalwinui3.lock.json index 49892dc5a3a..1f93ba09b28 100644 --- a/vnext/Microsoft.ReactNative/packages.experimentalwinui3.lock.json +++ b/vnext/Microsoft.ReactNative/packages.experimentalwinui3.lock.json @@ -10,9 +10,9 @@ }, "Microsoft.JavaScript.Hermes": { "type": "Direct", - "requested": "[0.0.0-2605.6002-2279da22, )", - "resolved": "0.0.0-2605.6002-2279da22", - "contentHash": "E59URq24UdJMpLMkMY92h3hs91EHelicbUsBRETn+FJ19uQbzU3gqkh2NPyBtMj4IVYK5K9FW+hIUTLg8SG25g==" + "requested": "[0.0.0-2608.12001-35d34796, )", + "resolved": "0.0.0-2608.12001-35d34796", + "contentHash": "6EirG0swfzCbfajtXtUUUFJJqi0yYzYOMRjkx0XcLB1a8qYjAhRcFdAfsNDvxPbs0ErI+6++EjBZquf3YFQZDA==" }, "Microsoft.SourceLink.GitHub": { "type": "Direct", diff --git a/vnext/Microsoft.ReactNative/packages.lock.json b/vnext/Microsoft.ReactNative/packages.lock.json index c0293c337e8..9e2f346b8b2 100644 --- a/vnext/Microsoft.ReactNative/packages.lock.json +++ b/vnext/Microsoft.ReactNative/packages.lock.json @@ -10,9 +10,9 @@ }, "Microsoft.JavaScript.Hermes": { "type": "Direct", - "requested": "[0.0.0-2605.6002-2279da22, )", - "resolved": "0.0.0-2605.6002-2279da22", - "contentHash": "E59URq24UdJMpLMkMY92h3hs91EHelicbUsBRETn+FJ19uQbzU3gqkh2NPyBtMj4IVYK5K9FW+hIUTLg8SG25g==" + "requested": "[0.0.0-2608.12001-35d34796, )", + "resolved": "0.0.0-2608.12001-35d34796", + "contentHash": "6EirG0swfzCbfajtXtUUUFJJqi0yYzYOMRjkx0XcLB1a8qYjAhRcFdAfsNDvxPbs0ErI+6++EjBZquf3YFQZDA==" }, "Microsoft.SourceLink.GitHub": { "type": "Direct", diff --git a/vnext/PropertySheets/JSEngine.props b/vnext/PropertySheets/JSEngine.props index c7074b16b22..30a71ecd1e5 100644 --- a/vnext/PropertySheets/JSEngine.props +++ b/vnext/PropertySheets/JSEngine.props @@ -6,7 +6,7 @@ true - 0.0.0-2605.6002-2279da22 + 0.0.0-2608.12001-35d34796 Microsoft.JavaScript.Hermes $(PkgMicrosoft_JavaScript_Hermes) $(NuGetPackageRoot)\$(HermesPackageName)\$(HermesVersion) diff --git a/vnext/PropertySheets/NuGet.LockFile.props b/vnext/PropertySheets/NuGet.LockFile.props index 842fc28a3d7..31670108aef 100644 --- a/vnext/PropertySheets/NuGet.LockFile.props +++ b/vnext/PropertySheets/NuGet.LockFile.props @@ -8,15 +8,8 @@ --> - true - - false + + false packages $(NuGetLockFileName).experimentalwinui3 diff --git a/vnext/ReactCommon.UnitTests/packages.experimentalwinui3.lock.json b/vnext/ReactCommon.UnitTests/packages.experimentalwinui3.lock.json index d4687ff9f47..77d8a9ef1a1 100644 --- a/vnext/ReactCommon.UnitTests/packages.experimentalwinui3.lock.json +++ b/vnext/ReactCommon.UnitTests/packages.experimentalwinui3.lock.json @@ -27,8 +27,8 @@ }, "Microsoft.JavaScript.Hermes": { "type": "Transitive", - "resolved": "0.0.0-2605.6002-2279da22", - "contentHash": "E59URq24UdJMpLMkMY92h3hs91EHelicbUsBRETn+FJ19uQbzU3gqkh2NPyBtMj4IVYK5K9FW+hIUTLg8SG25g==" + "resolved": "0.0.0-2608.12001-35d34796", + "contentHash": "6EirG0swfzCbfajtXtUUUFJJqi0yYzYOMRjkx0XcLB1a8qYjAhRcFdAfsNDvxPbs0ErI+6++EjBZquf3YFQZDA==" }, "Microsoft.SourceLink.Common": { "type": "Transitive", @@ -164,7 +164,7 @@ "type": "Project", "dependencies": { "Common": "[1.0.0, )", - "Microsoft.JavaScript.Hermes": "[0.0.0-2605.6002-2279da22, )", + "Microsoft.JavaScript.Hermes": "[0.0.0-2608.12001-35d34796, )", "Microsoft.SourceLink.GitHub": "[1.1.1, )", "Microsoft.WindowsAppSDK": "[2.0.0-experimental3, )", "ReactCommon": "[1.0.0, )", diff --git a/vnext/ReactCommon.UnitTests/packages.lock.json b/vnext/ReactCommon.UnitTests/packages.lock.json index efff6d922e4..b5444e7b1ff 100644 --- a/vnext/ReactCommon.UnitTests/packages.lock.json +++ b/vnext/ReactCommon.UnitTests/packages.lock.json @@ -27,8 +27,8 @@ }, "Microsoft.JavaScript.Hermes": { "type": "Transitive", - "resolved": "0.0.0-2605.6002-2279da22", - "contentHash": "E59URq24UdJMpLMkMY92h3hs91EHelicbUsBRETn+FJ19uQbzU3gqkh2NPyBtMj4IVYK5K9FW+hIUTLg8SG25g==" + "resolved": "0.0.0-2608.12001-35d34796", + "contentHash": "6EirG0swfzCbfajtXtUUUFJJqi0yYzYOMRjkx0XcLB1a8qYjAhRcFdAfsNDvxPbs0ErI+6++EjBZquf3YFQZDA==" }, "Microsoft.SourceLink.Common": { "type": "Transitive", @@ -164,7 +164,7 @@ "type": "Project", "dependencies": { "Common": "[1.0.0, )", - "Microsoft.JavaScript.Hermes": "[0.0.0-2605.6002-2279da22, )", + "Microsoft.JavaScript.Hermes": "[0.0.0-2608.12001-35d34796, )", "Microsoft.SourceLink.GitHub": "[1.1.1, )", "Microsoft.WindowsAppSDK": "[1.8.260508005, )", "ReactCommon": "[1.0.0, )", diff --git a/vnext/Scripts/NuGetRestoreForceEvaluateAllSolutions.ps1 b/vnext/Scripts/NuGetRestoreForceEvaluateAllSolutions.ps1 index cf4f26487c6..b66e7008a14 100644 --- a/vnext/Scripts/NuGetRestoreForceEvaluateAllSolutions.ps1 +++ b/vnext/Scripts/NuGetRestoreForceEvaluateAllSolutions.ps1 @@ -7,7 +7,33 @@ param( $StartingLocation = Get-Location Set-Location -Path $RepoRoot +$failedRestores = [System.Collections.Generic.List[string]]::new() + +function Restore-Solution([string] $solution, [string[]] $extraArgs) { + Write-Host "Restoring $solution $($extraArgs -join ' ')" + & msbuild /t:Restore /p:RestoreForceEvaluate=true @extraArgs $solution + if ($LASTEXITCODE -ne 0) { + $failedRestores.Add(("$solution $($extraArgs -join ' ')").Trim()) + } +} + try { + # Some node_modules packages ship a NuGet.Config that re-adds the public nuget.org feed. That + # breaks the repo's single-ADO-feed compliance and trips corporate network blocks on api.nuget.org. + # Strip any nuget.org source so restore only uses the repo's ADO feed. + Get-ChildItem -File -Recurse -Path $RepoRoot -Filter NuGet.Config -ErrorAction SilentlyContinue | + Where-Object { $_.FullName -match '\\node_modules\\' } | ForEach-Object { + $configPath = $_.FullName + try { [xml]$doc = Get-Content -LiteralPath $configPath -Raw } catch { return } + $sources = $doc.SelectSingleNode('/configuration/packageSources') + if (-not $sources) { return } + $badSources = @($sources.SelectNodes('add') | Where-Object { $_.GetAttribute('value') -match 'nuget\.org' }) + if ($badSources.Count -eq 0) { return } + $badSources | ForEach-Object { [void]$sources.RemoveChild($_) } + $doc.Save($configPath) + Write-Host "Removed nuget.org feed from $configPath" + } + if (-not $SkipLockDeletion) { # Delete existing lock files $existingLockFiles = (Get-ChildItem -File -Recurse -Path $RepoRoot -Filter *.lock.json) @@ -22,17 +48,40 @@ try { # Run all solutions with their defaults $($packagesSolutions; $vnextSolutions) | Foreach-Object { - Write-Host Restoring $_.FullName with defaults - & msbuild /t:Restore /p:RestoreForceEvaluate=true $_.FullName + Restore-Solution $_.FullName @() } # Re-run solutions that build with UseExperimentalWinUI3 $experimentalSolutions = @("playground-composition.sln", "Microsoft.ReactNative.NewArch.sln", "ReactWindows-Desktop.sln"); $($packagesSolutions; $vnextSolutions) | Where-Object { $experimentalSolutions -contains $_.Name } | Foreach-Object { - Write-Host Restoring $_.FullName with UseExperimentalWinUI3=true - & msbuild /t:Restore /p:RestoreForceEvaluate=true /p:UseExperimentalWinUI3=true $_.FullName + Restore-Solution $_.FullName @('/p:UseExperimentalWinUI3=true') } } finally { Set-Location -Path "$StartingLocation" -} \ No newline at end of file +} + +# Restore failures used to be ignored (msbuild's exit code was never checked), so a broken run +# looked successful while leaving lock files deleted. Fail loudly on both failure modes. +if ($failedRestores.Count -gt 0) { + Write-Host '' + Write-Host "ERROR: NuGet restore returned a non-zero exit code for $($failedRestores.Count) solution pass(es):" -ForegroundColor Red + $failedRestores | ForEach-Object { Write-Host " $_" -ForegroundColor Red } +} + +# A committed lock that was deleted but never came back means a project failed or was skipped during restore. +$missingTrackedLocks = @(git -C $RepoRoot ls-files -- '*.lock.json' | Where-Object { -not (Test-Path (Join-Path $RepoRoot $_)) }) +if ($missingTrackedLocks.Count -gt 0) { + Write-Host '' + Write-Host "ERROR: $($missingTrackedLocks.Count) committed lock file(s) were deleted but not regenerated:" -ForegroundColor Red + $missingTrackedLocks | ForEach-Object { Write-Host " $_" -ForegroundColor Red } +} + +if ($failedRestores.Count -gt 0 -or $missingTrackedLocks.Count -gt 0) { + Write-Host '' + Write-Host 'Lock file regeneration is INCOMPLETE - do not commit. Fix the errors above and re-run.' -ForegroundColor Red + exit 1 +} + +Write-Host '' +Write-Host 'All solutions restored and every committed lock file was regenerated.' -ForegroundColor Green \ No newline at end of file diff --git a/vnext/Scripts/Warm-RnwFeedCache.ps1 b/vnext/Scripts/Warm-RnwFeedCache.ps1 index febf59fb5f2..54f82875097 100644 --- a/vnext/Scripts/Warm-RnwFeedCache.ps1 +++ b/vnext/Scripts/Warm-RnwFeedCache.ps1 @@ -10,7 +10,11 @@ request). Any not-yet-saved transitive dependency therefore 404s on a PR build, e.g. the CLI lib job failing on 'is-unc-path'. - The script warms two ways, both with your credentials: + The script warms several ways, all with your credentials: + - repo: it runs an authenticated `yarn install` in this repo, pulling the repo's + own dev-dependency closure (including package.json resolutions, so freshly bumped + versions are saved) through the feed. This is the closure an anonymous PR build of + the repo restores. - npm: it reproduces the base project generations the CLI-init tests run (a create-react-native-library lib and a community-CLI app) and installs them, which pulls the whole toolchain closure into the feed. @@ -21,10 +25,13 @@ Auth (in order): -Pat / $env:ADO_PAT / $env:AZURE_DEVOPS_EXT_PAT, else an AAD token from `az account get-access-token` (requires `az login` locally, or an AzureCLI@2 - task with a managed identity in a pipeline). + task with a managed identity in a pipeline). The token reaches npm and Yarn only + through environment variables and a throwaway work-dir npmrc; your ~/.npmrc and + ~/.yarnrc.yml are never modified. - Run it manually from a clone, or on a schedule from the ADO warm-up pipeline. It - does not touch your local checkout: all work happens in a throwaway work dir. + Run it manually from a clone, or on a schedule from the ADO warm-up pipeline. Only + the repo pass touches your checkout (it refreshes node_modules and yarn.lock in + place); every other pass works in a throwaway work dir. .PARAMETER NpmRegistry The feed npm registry to warm. Defaults to ms/react-native-public. @@ -44,6 +51,10 @@ .PARAMETER CliVersion @react-native-community/cli version. Defaults to vnext/package.json. +.PARAMETER SkipRepo + Skip the repo warm pass (an authenticated `yarn install` in this repo). Unlike the + other passes it refreshes node_modules and yarn.lock in your checkout. + .PARAMETER SkipLib Skip the create-react-native-library warm pass. @@ -53,6 +64,9 @@ .PARAMETER SkipNuGet Skip the NuGet warm pass. +.PARAMETER SkipRnwPackages + Skip warming the repo's own already-published workspace packages into the feed. + .PARAMETER KeepWorkDir Keep the work dir instead of deleting it (for debugging). @@ -75,9 +89,11 @@ param( [string]$CreateLibraryVersion = '0.48.9', [string]$TemplateVersion = '@react-native-community/template@0.84.1', [string]$NuGetIndex = 'https://pkgs.dev.azure.com/ms/react-native/_packaging/react-native-public/nuget/v3/index.json', + [switch]$SkipRepo, [switch]$SkipLib, [switch]$SkipApp, [switch]$SkipNuGet, + [switch]$SkipRnwPackages, [switch]$KeepWorkDir ) @@ -174,6 +190,16 @@ function Update-NightlyPackageJson { # --- warm passes -------------------------------------------------------------- +function Warm-Repo { + # Authenticated install of the repo warms its whole dev closure (incl. resolutions) + # and refreshes yarn.lock; --mode=skip-build avoids the repo's postinstall build. + Push-Location $RepoRoot + try { + Invoke-Checked -What 'yarn install (repo)' -Script { & yarn install --mode=skip-build } + } + finally { Pop-Location } +} + function Warm-Lib { Push-Location $WorkDir try { @@ -296,16 +322,69 @@ function Warm-NuGet { Write-Host "Saved $($refs.Count) NuGet package(s)." -ForegroundColor Green } +# The CLI-init tests install react-native-windows, whose closure pulls the repo's own already-published +# workspace packages (e.g. @react-native-windows/package-utils). Verdaccio publishes only the *changed* +# packages locally and proxies the rest to the feed, so anonymous PR reads 404/500 unless those +# published versions are cached. CODESYNC: enumerates the same workspaces npmPack.js packs; the subset +# it strips with --check-npm (already on npmjs) is exactly what a PR build must read from the feed. +function Warm-RnwPackages { + $dir = Join-Path $WorkDir 'rnwpkgs' + New-Item -ItemType Directory -Path $dir | Out-Null + + $rootPkg = Get-Content (Join-Path $RepoRoot 'package.json') -Raw | ConvertFrom-Json + $seen = [System.Collections.Generic.HashSet[string]]::new() + $specs = [System.Collections.Generic.List[string]]::new() + foreach ($pattern in $rootPkg.workspaces.packages) { + $pkgDirs = @() + if ($pattern.EndsWith('/*')) { + $base = Join-Path $RepoRoot ($pattern.Substring(0, $pattern.Length - 2)) + if (Test-Path -LiteralPath $base) { $pkgDirs = @((Get-ChildItem -LiteralPath $base -Directory).FullName) } + } + else { $pkgDirs = @(Join-Path $RepoRoot $pattern) } + foreach ($d in $pkgDirs) { + $pj = Join-Path $d 'package.json' + if (-not (Test-Path -LiteralPath $pj)) { continue } + $p = Get-Content -LiteralPath $pj -Raw | ConvertFrom-Json + $props = $p.PSObject.Properties + if (($props['private'] -and $p.private -eq $true) -or -not $props['name'] -or -not $props['version']) { continue } + $spec = "$($p.name)@$($p.version)" + if ($seen.Add($spec)) { $specs.Add($spec) } + } + } + + Push-Location $dir + try { + # Skip versions not yet on the feed's upstream: the build's freshly bumped packages aren't published + # and the CLI test gets those from verdaccio locally, so a miss here is expected (not a failure). + $published = foreach ($spec in $specs) { + try { & npm view $spec version *> $null; if ($LASTEXITCODE -eq 0) { $spec } } catch { } + } + $published = @($published) + if ($published.Count -eq 0) { + Write-Host 'No already-published workspace packages to warm.' -ForegroundColor Yellow + return + } + Write-Host "Warming $($published.Count)/$($specs.Count) workspace package(s) into the feed." -ForegroundColor Cyan + Invoke-Checked -What 'warm RNW packages' -Script { + & npm install --ignore-scripts @published + } + } + finally { Pop-Location } +} + $passes = [ordered]@{} +if (-not $SkipRepo) { $passes['repo'] = ${function:Warm-Repo} } if (-not $SkipLib) { $passes['lib'] = ${function:Warm-Lib} } if (-not $SkipApp) { $passes['app'] = ${function:Warm-App} } if (-not $SkipNuGet) { $passes['nuget'] = ${function:Warm-NuGet} } +if (-not $SkipRnwPackages) { $passes['rnwpackages'] = ${function:Warm-RnwPackages} } -$results = foreach ($name in $passes.Keys) { +# Run each pass "bare" (no pipe/capture) so npm/yarn inherits the console: real TTY -> live progress, UTF-8 -> no mojibake. +$results = [System.Collections.Generic.List[object]]::new() +foreach ($name in $passes.Keys) { Write-Host "`n=== Warming '$name' ===" -ForegroundColor Green - # Route each pass's native stdout to the host so only the status object lands in $results. - try { & $passes[$name] | Out-Host; [pscustomobject]@{ Pass = $name; Status = 'OK' } } - catch { Write-Host "##[error]$($_.Exception.Message)" -ForegroundColor Red; [pscustomobject]@{ Pass = $name; Status = "FAILED: $($_.Exception.Message)" } } + try { & $passes[$name]; $results.Add([pscustomobject]@{ Pass = $name; Status = 'OK' }) } + catch { Write-Host "##[error]$($_.Exception.Message)" -ForegroundColor Red; $results.Add([pscustomobject]@{ Pass = $name; Status = "FAILED: $($_.Exception.Message)" }) } } # --- cleanup + summary -------------------------------------------------------- diff --git a/yarn.lock b/yarn.lock index 02522424024..ffaa347776e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5406,6 +5406,25 @@ __metadata: languageName: unknown linkType: soft +"@rnw-scripts/warm-feed@workspace:packages/@rnw-scripts/warm-feed": + version: 0.0.0-use.local + resolution: "@rnw-scripts/warm-feed@workspace:packages/@rnw-scripts/warm-feed" + dependencies: + "@rnw-scripts/eslint-config": "npm:1.2.38" + "@rnw-scripts/just-task": "npm:2.3.58" + "@rnw-scripts/ts-config": "npm:2.0.6" + "@types/node": "npm:^22.14.0" + "@typescript-eslint/eslint-plugin": "npm:^8.36.0" + "@typescript-eslint/parser": "npm:^8.36.0" + eslint: "npm:^8.19.0" + prettier: "npm:^3.6.2" + source-map-support: "npm:^0.5.19" + typescript: "npm:5.0.4" + bin: + warm-feed: ./bin.js + languageName: unknown + linkType: soft + "@rnx-kit/align-deps@npm:^3.4.7": version: 3.4.7 resolution: "@rnx-kit/align-deps@npm:3.4.7" From c00db002958a3cf45e04ef9f0f2f52e4de318751 Mon Sep 17 00:00:00 2001 From: Vladimir Morozov Date: Wed, 19 Aug 2026 20:34:05 -0700 Subject: [PATCH 2/3] Change files --- ...ation-channel-1d28e060-e543-46bb-8751-8c27c0c3c0bd.json | 7 +++++++ ...e-windows-cli-d3a95276-0d6a-4882-9ec7-705bee190db3.json | 7 +++++++ ...ative-windows-067c0855-154d-4ca7-91e8-3a87f41740d8.json | 7 +++++++ 3 files changed, 21 insertions(+) create mode 100644 change/@react-native-windows-automation-channel-1d28e060-e543-46bb-8751-8c27c0c3c0bd.json create mode 100644 change/@react-native-windows-cli-d3a95276-0d6a-4882-9ec7-705bee190db3.json create mode 100644 change/react-native-windows-067c0855-154d-4ca7-91e8-3a87f41740d8.json diff --git a/change/@react-native-windows-automation-channel-1d28e060-e543-46bb-8751-8c27c0c3c0bd.json b/change/@react-native-windows-automation-channel-1d28e060-e543-46bb-8751-8c27c0c3c0bd.json new file mode 100644 index 00000000000..8246e63d0fc --- /dev/null +++ b/change/@react-native-windows-automation-channel-1d28e060-e543-46bb-8751-8c27c0c3c0bd.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "Fix CI pipeline and add warm-feed", + "packageName": "@react-native-windows/automation-channel", + "email": "vmorozov@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@react-native-windows-cli-d3a95276-0d6a-4882-9ec7-705bee190db3.json b/change/@react-native-windows-cli-d3a95276-0d6a-4882-9ec7-705bee190db3.json new file mode 100644 index 00000000000..04420d3a2da --- /dev/null +++ b/change/@react-native-windows-cli-d3a95276-0d6a-4882-9ec7-705bee190db3.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "Fix CI pipeline and add warm-feed", + "packageName": "@react-native-windows/cli", + "email": "vmorozov@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/react-native-windows-067c0855-154d-4ca7-91e8-3a87f41740d8.json b/change/react-native-windows-067c0855-154d-4ca7-91e8-3a87f41740d8.json new file mode 100644 index 00000000000..927e4b68eb0 --- /dev/null +++ b/change/react-native-windows-067c0855-154d-4ca7-91e8-3a87f41740d8.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "Fix CI pipeline and add warm-feed", + "packageName": "react-native-windows", + "email": "vmorozov@microsoft.com", + "dependentChangeType": "patch" +} From 817fd92de7471bda7bdb89aa548efe00f109bbee Mon Sep 17 00:00:00 2001 From: Vladimir Morozov Date: Wed, 19 Aug 2026 21:15:56 -0700 Subject: [PATCH 3/3] Disable VS BG updater --- .ado/image/rnw-img-vs2026-node24.json | 48 +++++++++++++++++++++++++-- .ado/templates/prepare-build-env.yml | 13 ++++++++ 2 files changed, 58 insertions(+), 3 deletions(-) diff --git a/.ado/image/rnw-img-vs2026-node24.json b/.ado/image/rnw-img-vs2026-node24.json index 60fdfbd19ea..b68dff7d3f7 100755 --- a/.ado/image/rnw-img-vs2026-node24.json +++ b/.ado/image/rnw-img-vs2026-node24.json @@ -21,6 +21,9 @@ { "name": "windows-gitinstall" }, + { + "name": "windows-git-lfs" + }, { "name": "windows-AzPipeline-ImageHelpers" }, @@ -33,10 +36,16 @@ { "name": "windows-AzPipeline-7zip" }, + { + "name": "windows-chocolatey", + "parameters": { + "packages": "nasm" + } + }, { "name": "windows-visualstudio-bootstrapper", "parameters": { - "Workloads": "--add Microsoft.VisualStudio.Workload.ManagedDesktop --add Microsoft.VisualStudio.Workload.NativeDesktop --add Microsoft.VisualStudio.Workload.Universal --add Microsoft.VisualStudio.ComponentGroup.NativeDesktop.Core --add Microsoft.VisualStudio.ComponentGroup.UWP.Support --add Microsoft.VisualStudio.ComponentGroup.UWP.VC --add Microsoft.Component.MSBuild --add Microsoft.VisualStudio.Component.VC.Tools.x86.x64 --add Microsoft.VisualStudio.Component.Windows11SDK.22621 --includeRecommended --includeOptional", + "Workloads": "--add Microsoft.VisualStudio.Workload.ManagedDesktop --add Microsoft.VisualStudio.Workload.NativeDesktop --add Microsoft.VisualStudio.Workload.Universal --add Microsoft.VisualStudio.ComponentGroup.NativeDesktop.Core --add Microsoft.VisualStudio.ComponentGroup.UWP.Support --add Microsoft.VisualStudio.ComponentGroup.UWP.VC --add Microsoft.Component.MSBuild --add Microsoft.VisualStudio.Component.VC.CoreBuildTools --add Microsoft.VisualStudio.Component.VC.CoreIde --add Microsoft.VisualStudio.Component.VC.Tools.x86.x64 --add Microsoft.VisualStudio.Component.VC.Tools.ARM64 --add Microsoft.VisualStudio.Component.VC.Llvm.Clang --add Microsoft.VisualStudio.Component.VC.Llvm.ClangToolset --add Microsoft.VisualStudio.Component.VC.CMake.Project --add Microsoft.VisualStudio.Component.Windows11SDK.26100 --add Microsoft.VisualStudio.Component.Windows11Sdk.WindowsPerformanceToolkit --add Microsoft.VisualStudio.Component.Windows11SDK.22621 --add Microsoft.VisualStudio.Component.VC.ATL --add Microsoft.VisualStudio.Component.VC.ATL.ARM64 --add Microsoft.VisualStudio.Component.VC.ATLMFC --add Microsoft.VisualStudio.Component.VC.MFC.ARM64 --add Microsoft.VisualStudio.Component.UWP.VC.ARM64 --add Microsoft.VisualStudio.Component.VC.Runtimes.x86.x64.Spectre --add Microsoft.VisualStudio.Component.VC.Runtimes.ARM64.Spectre --add Microsoft.VisualStudio.Component.VC.ATL.Spectre --add Microsoft.VisualStudio.Component.VC.ATL.ARM64.Spectre --includeRecommended --includeOptional", "SKU": "Enterprise", "VSBootstrapperURL": "https://aka.ms/vs/18/stable/vs_Enterprise.exe" } @@ -44,7 +53,15 @@ { "name": "Windows-NodeJS", "parameters": { - "Version": "24.16.0" + "Version": "24.x", + "UseARM": "false" + } + }, + { + "name": "windows-install-python", + "parameters": { + "Version": "latest", + "Architecture": "x64" } }, { @@ -56,11 +73,36 @@ { "name": "windows-dotnetcore-sdk", "parameters": { - "DotNetCoreVersion": "10.0.300" + "DotNetCoreVersion": "latest", + "Channel": "10.0" + } + }, + { + "name": "windows-1es-pt-prerequisites-v2", + "parameters": { + "KVSecret_AppSecret": "https://pipelinesidentity.vault.azure.net/secrets/1es-gpt-read-only-app-secret" } }, { "name": "Windows-AzureCLI" + }, + { + "name": "windows-updateregistry", + "parameters": { + "RegistryPath": "HKEY_LOCAL_MACHINE\\SOFTWARE\\Policies\\Microsoft\\VisualStudio\\Setup", + "RegistryKey": "BackgroundDownloadDisabled", + "DataType": "REG_DWORD", + "Value": "1" + } + }, + { + "name": "windows-updateregistry", + "parameters": { + "RegistryPath": "HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\VisualStudio\\Setup", + "RegistryKey": "BackgroundDownloadDisabled", + "DataType": "REG_DWORD", + "Value": "1" + } } ] } diff --git a/.ado/templates/prepare-build-env.yml b/.ado/templates/prepare-build-env.yml index 8e6ea5449c4..9b8ed04796e 100644 --- a/.ado/templates/prepare-build-env.yml +++ b/.ado/templates/prepare-build-env.yml @@ -31,6 +31,19 @@ parameters: # invoked. Example: ['RNTesterApp-Fabric', 'Playground']. steps: + # VS Installer's background auto-update (BackgroundDownload.exe) fetches VS updates from the MS + # CDN mid-build and trips network isolation. Interim belt; the durable fix is BackgroundDownloadDisabled=1 + # baked into the agent image JSON (.ado/image/rnw-img-vs2026-node24.json) — remove once that image ships. + - pwsh: | + foreach ($key in @( + 'HKLM:\SOFTWARE\Microsoft\VisualStudio\Setup', + 'HKLM:\SOFTWARE\Policies\Microsoft\VisualStudio\Setup')) { + New-Item -Path $key -Force | Out-Null + New-ItemProperty -Path $key -Name BackgroundDownloadDisabled -PropertyType DWord -Value 1 -Force | Out-Null + } + Get-Process -Name BackgroundDownload -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue + displayName: Disable VS Installer background download + # The commit tag in the nuspec requires that we use at least nuget 5.8 (because things break with nuget versions before and Vs 16.8 or later) - task: NuGetToolInstaller@1 displayName: Set NuGet version