diff --git a/.ado/publish.yml b/.ado/publish.yml index ae944533621..0d00cfa6274 100644 --- a/.ado/publish.yml +++ b/.ado/publish.yml @@ -65,7 +65,7 @@ variables: - name: FailCGOnAlert value: false - name: EnableCodesign - value: false + value: true trigger: none pr: none @@ -145,6 +145,9 @@ extends: - script: echo NpmDistTag is $(NpmDistTag) displayName: Show NPM dist tag + - script: copy ".ado\scripts\npmPack.js" "$(Build.StagingDirectory)\versionEnvVars\npmPack.js" + displayName: Include npmPack.js in VersionEnvVars artifact + - task: AzureArtifacts.manifest-generator-task.manifest-generator-task.ManifestGeneratorTask@0 displayName: 📒 Generate Manifest Npm inputs: @@ -351,9 +354,12 @@ extends: configuration: Debug # Symbol Publishing for Work Item 59264834 - MSRC Compliance + # continueOnError: Duplicate symbols are expected when the pipeline + # is re-run for the same version. The symbols already exist on the + # server, so it is safe to continue. - task: PublishSymbols@2 displayName: 'Publish Symbols to Microsoft Symbol Server' - enabled: true + continueOnError: true inputs: UseNetCoreClientTool: true ConnectedServiceName: Office-React-Native-Windows-Bot @@ -362,7 +368,7 @@ extends: SymbolServerType: 'TeamServices' SymbolsProduct: 'ReactNativeWindows' SymbolsVersion: '$(Build.BuildNumber)' - SymbolsArtifactName: 'ReactNativeWindows-Symbols' + SymbolsArtifactName: 'ReactNativeWindows-Symbols-$(Build.BuildId)' DetailedLog: true TreatNotIndexedAsWarning: false diff --git a/.ado/release.yml b/.ado/release.yml index 98b0dd104ab..d8a20a99e4c 100644 --- a/.ado/release.yml +++ b/.ado/release.yml @@ -1,21 +1,28 @@ +# +# The Release pipeline entry point. +# It releases npm packages to npmjs.com and NuGet packages to the public +# ms/react-native and ms/react-native-public ADO feeds and to nuget.org. +# +# The triggers are overridden by the ADO pipeline UI definition. +# + name: RNW NuGet Release $(Date:yyyyMMdd).$(Rev:r) trigger: none +pr: none resources: pipelines: - pipeline: 'Publish' project: 'ReactNative' source: 'Publish' - trigger: - branches: - include: - - -1espublish + trigger: none repositories: - repository: 1ESPipelineTemplates type: git name: 1ESPipelineTemplates/1ESPipelineTemplates ref: refs/tags/release + extends: template: v1/1ES.Official.PipelineTemplate.yml@1ESPipelineTemplates parameters: @@ -28,13 +35,18 @@ extends: stages: - stage: Release displayName: Publish artifacts + # Allow manual runs unconditionally; for build-completion triggers, + # only proceed if the commit message starts with 'RELEASE:'. + condition: or(eq(variables['Build.Reason'], 'Manual'), startsWith(variables['Build.SourceVersionMessage'], 'RELEASE:')) jobs: - job: PushNpm displayName: npmjs.com - Publish npm packages variables: - group: RNW Secrets - timeoutInMinutes: 0 + timeoutInMinutes: 30 templateContext: + type: releaseJob + isProduction: true inputs: - input: pipelineArtifact pipeline: 'Publish' @@ -45,15 +57,13 @@ extends: artifactName: 'VersionEnvVars' targetPath: '$(Pipeline.Workspace)/VersionEnvVars' steps: - - checkout: self - clean: false - task: CmdLine@2 displayName: Apply version variables inputs: script: node $(Pipeline.Workspace)/VersionEnvVars/versionEnvVars.js - script: dir /s "$(Pipeline.Workspace)\published-packages" displayName: Show npm packages before cleanup - - script: node .ado/scripts/npmPack.js --no-pack --check-npm --no-color "$(Pipeline.Workspace)\published-packages" + - script: node "$(Pipeline.Workspace)\VersionEnvVars\npmPack.js" --no-pack --check-npm --no-color "$(Pipeline.Workspace)\published-packages" displayName: Remove already published packages - script: dir /s "$(Pipeline.Workspace)\published-packages" displayName: Show npm packages after cleanup @@ -82,112 +92,69 @@ extends: - job: PushPrivateAdo displayName: ADO - nuget - react-native - + timeoutInMinutes: 30 templateContext: + type: releaseJob + isProduction: true inputs: - input: pipelineArtifact pipeline: 'Publish' artifactName: 'ReactWindows-final-nuget' targetPath: '$(Pipeline.Workspace)/ReactWindows-final-nuget' - steps: - - checkout: none - - - script: dir /S $(Pipeline.Workspace)\ReactWindows-final-nuget - displayName: Show directory contents - - - task: AzureCLI@2 - displayName: Override NuGet credentials with Managed Identity - inputs: - azureSubscription: 'Office-React-Native-Windows-Bot' - visibleAzLogin: false - scriptType: 'pscore' - scriptLocation: 'inlineScript' - inlineScript: | - $accessToken = az account get-access-token --query accessToken --resource 499b84ac-1321-427f-aa17-267ca6975798 -o tsv - # Set the access token as a secret, so it doesn't get leaked in the logs - Write-Host "##vso[task.setsecret]$accessToken" - # Override the apitoken of the nuget service connection, for the duration of this stage - Write-Host "##vso[task.setendpoint id=a7e33797-4804-4a1d-911d-5bd325e50a85;field=authParameter;key=apitoken]$accessToken" - - - task: 1ES.PublishNuGet@1 - displayName: NuGet push to ms/react-native-public - inputs: - useDotNetTask: true + - template: .ado/templates/publish-nuget-to-ado-feed.yml@self + parameters: + endpointId: 'a7e33797-4804-4a1d-911d-5bd325e50a85' + nugetFeedUrl: 'https://pkgs.dev.azure.com/ms/_packaging/react-native/nuget/v3/index.json' packageParentPath: '$(Pipeline.Workspace)/ReactWindows-final-nuget' packagesToPush: '$(Pipeline.Workspace)/ReactWindows-final-nuget/*.nupkg' - nuGetFeedType: external publishFeedCredentials: 'ms/react-native ADO Feed' - externalEndpoint: 'ms/react-native ADO Feed' - publishPackageMetadata: true + feedDisplayName: 'ms/react-native' - job: PushPublicAdo displayName: ADO - nuget - react-native-public - + timeoutInMinutes: 30 templateContext: + type: releaseJob + isProduction: true inputs: - input: pipelineArtifact pipeline: 'Publish' artifactName: 'ReactWindows-final-nuget' targetPath: '$(Pipeline.Workspace)/ReactWindows-final-nuget' - steps: - - checkout: none - - - script: dir /S $(Pipeline.Workspace)\ReactWindows-final-nuget - displayName: Show directory contents - - - task: AzureCLI@2 - displayName: Override NuGet credentials with Managed Identity - inputs: - azureSubscription: 'Office-React-Native-Windows-Bot' - visibleAzLogin: false - scriptType: 'pscore' - scriptLocation: 'inlineScript' - inlineScript: | - $accessToken = az account get-access-token --query accessToken --resource 499b84ac-1321-427f-aa17-267ca6975798 -o tsv - # Set the access token as a secret, so it doesn't get leaked in the logs - Write-Host "##vso[task.setsecret]$accessToken" - # Override the apitoken of the nuget service connection, for the duration of this stage - Write-Host "##vso[task.setendpoint id=9a2456d0-c163-405b-be24-c03fd74b155a;field=authParameter;key=apitoken]$accessToken" - - - task: 1ES.PublishNuGet@1 - displayName: NuGet push to ms/react-native-public - inputs: - useDotNetTask: true + - template: .ado/templates/publish-nuget-to-ado-feed.yml@self + parameters: + endpointId: '9a2456d0-c163-405b-be24-c03fd74b155a' + nugetFeedUrl: 'https://pkgs.dev.azure.com/ms/react-native/_packaging/react-native-public/nuget/v3/index.json' packageParentPath: '$(Pipeline.Workspace)/ReactWindows-final-nuget' packagesToPush: '$(Pipeline.Workspace)/ReactWindows-final-nuget/*.nupkg' - nuGetFeedType: external publishFeedCredentials: 'ms/react-native-public ADO Feed' - externalEndpoint: 'ms/react-native-public ADO Feed' - publishPackageMetadata: true + feedDisplayName: 'ms/react-native-public' - job: PushNuGetOrg displayName: nuget.org - Push nuget packages variables: - group: RNW Secrets - timeoutInMinutes: 0 + timeoutInMinutes: 30 templateContext: + type: releaseJob + isProduction: true inputs: - input: pipelineArtifact pipeline: 'Publish' artifactName: 'ReactWindows-final-nuget' targetPath: '$(Pipeline.Workspace)/ReactWindows-final-nuget' steps: - - checkout: none - task: NuGetToolInstaller@1 - displayName: 'Use NuGet ' + displayName: 'Use NuGet' - task: CmdLine@2 displayName: NuGet SetApiKey (nuget.org) inputs: script: nuget.exe SetApiKey $(nugetorg-apiKey-push) workingDirectory: $(Pipeline.Workspace)/ReactWindows-final-nuget - - task: PowerShell@2 + - script: dir /S "$(Pipeline.Workspace)\ReactWindows-final-nuget" + displayName: Show directory contents + - script: nuget.exe push .\Microsoft.ReactNative.*.nupkg -Source https://api.nuget.org/v3/index.json -SkipDuplicate -NoSymbol -NonInteractive -Verbosity Detailed displayName: NuGet push (nuget.org) - inputs: - targetType: inline - errorActionPreference: silentlyContinue - script: | - if (Get-ChildItem -Path .\ -Filter '*0.0.0-canary*' -ErrorAction SilentlyContinue) { Write-Output "Canary builds found, exiting."; return 0; } - nuget.exe push .\Microsoft.ReactNative.*.nupkg -Source https://api.nuget.org/v3/index.json -SkipDuplicate -NoSymbol -NonInteractive -Verbosity Detailed - workingDirectory: $(Pipeline.Workspace)/ReactWindows-final-nuget + workingDirectory: $(Pipeline.Workspace)/ReactWindows-final-nuget diff --git a/.ado/scripts/npmPack.js b/.ado/scripts/npmPack.js index a89519b6f04..5a3c141ac39 100644 --- a/.ado/scripts/npmPack.js +++ b/.ado/scripts/npmPack.js @@ -391,14 +391,19 @@ function main() { const targetDirArg = args.positionals[0]; try { - // Find repo root - const repoRoot = findEnlistmentRoot(); - console.log(`${colorize('Repository root:', colors.bright)} ${repoRoot}`); + // Find repo root (not needed when --no-pack is used with an absolute path) + const repoRoot = (noPackFlag && targetDirArg && path.isAbsolute(targetDirArg)) + ? null + : findEnlistmentRoot(); + + if (repoRoot) { + console.log(`${colorize('Repository root:', colors.bright)} ${repoRoot}`); + } // Determine target directory const targetDir = targetDirArg - ? path.resolve(repoRoot, targetDirArg) - : path.join(repoRoot, 'npm-pkgs'); + ? (repoRoot ? path.resolve(repoRoot, targetDirArg) : path.resolve(targetDirArg)) + : path.join(/** @type {string} */ (repoRoot), 'npm-pkgs'); console.log(`${colorize('Target directory:', colors.bright)} ${targetDir}`); diff --git a/.ado/templates/authenticate-office-react-native-windows-bot.yml b/.ado/templates/authenticate-office-react-native-windows-bot.yml deleted file mode 100644 index ab43399a15d..00000000000 --- a/.ado/templates/authenticate-office-react-native-windows-bot.yml +++ /dev/null @@ -1,11 +0,0 @@ -steps: - - task: AzureCLI@2 - inputs: - azureSubscription: 'Office-React-Native-Windows-Bot' - scriptType: 'bash' - scriptLocation: 'inlineScript' - inlineScript: | - # Note that the resource is specified to limit the token to Azure DevOps - aadToken=$(az account get-access-token --query accessToken --resource 499b84ac-1321-427f-aa17-267ca6975798 -o tsv) - echo "##vso[task.setvariable variable=oficeReactnativeWindowsBotAadAuthToken;issecret=true]$aadToken" - displayName: 'Generate oficeReactnativeWindowsBotAadAuthToken AAD token using Azure CLI' \ No newline at end of file diff --git a/.ado/templates/publish-nuget-to-ado-feed.yml b/.ado/templates/publish-nuget-to-ado-feed.yml new file mode 100644 index 00000000000..48c38c92364 --- /dev/null +++ b/.ado/templates/publish-nuget-to-ado-feed.yml @@ -0,0 +1,106 @@ +parameters: +- name: azureSubscription + type: string + default: 'Office-React-Native-Windows-Bot' +- name: endpointId + type: string +- name: nugetFeedUrl + type: string +- name: packageParentPath + type: string +- name: packagesToPush + type: string +- name: publishFeedCredentials + type: string +- name: feedDisplayName + type: string + +steps: +- script: dir /S "${{ parameters.packageParentPath }}" + displayName: Show NuGet packages before cleanup + +- task: AzureCLI@2 + displayName: Override NuGet credentials with Managed Identity + inputs: + azureSubscription: ${{ parameters.azureSubscription }} + visibleAzLogin: false + scriptType: 'pscore' + scriptLocation: 'inlineScript' + inlineScript: | + $accessToken = az account get-access-token --query accessToken --resource 499b84ac-1321-427f-aa17-267ca6975798 -o tsv + # Set the access token as a secret, so it doesn't get leaked in the logs + Write-Host "##vso[task.setsecret]$accessToken" + # Override the apitoken of the nuget service connection, for the duration of this stage + Write-Host "##vso[task.setendpoint id=${{ parameters.endpointId }};field=authParameter;key=apitoken]$accessToken" + # Also expose the token for the pre-push duplicate check + Write-Host "##vso[task.setvariable variable=NuGetAccessToken;issecret=true]$accessToken" + +- powershell: | + Add-Type -AssemblyName System.IO.Compression.FileSystem + + $feedUrl = "${{ parameters.nugetFeedUrl }}" + $token = "$(NuGetAccessToken)" + $headers = @{ Authorization = "Bearer $token" } + + # Discover the flat container (PackageBaseAddress) URL from the V3 index + $index = Invoke-RestMethod -Uri $feedUrl -Headers $headers + $baseAddress = ($index.resources | + Where-Object { $_.'@type' -like 'PackageBaseAddress*' } | + Select-Object -First 1).'@id' + if (-not $baseAddress) { throw "Could not find PackageBaseAddress in NuGet V3 index at $feedUrl" } + if (-not $baseAddress.EndsWith('/')) { $baseAddress += '/' } + Write-Host "PackageBaseAddress: $baseAddress" + + $nupkgs = Get-ChildItem -Path "${{ parameters.packageParentPath }}" -Filter "*.nupkg" -Recurse + $removedCount = 0 + + foreach ($file in $nupkgs) { + # Read the .nuspec from inside the nupkg (zip) to get the exact id and version + $zip = [System.IO.Compression.ZipFile]::OpenRead($file.FullName) + try { + $nuspecEntry = $zip.Entries | Where-Object { $_.FullName -like "*.nuspec" } | Select-Object -First 1 + $reader = New-Object System.IO.StreamReader($nuspecEntry.Open()) + [xml]$nuspec = $reader.ReadToEnd() + $reader.Close() + } finally { $zip.Dispose() } + + $id = $nuspec.package.metadata.id + $version = $nuspec.package.metadata.version + + # Query the flat container for all published versions of this package + $versionsUrl = "${baseAddress}$($id.ToLower())/index.json" + try { + $result = Invoke-RestMethod -Uri $versionsUrl -Headers $headers -ErrorAction Stop + if ($version.ToLower() -in $result.versions) { + Write-Host " SKIP $id $version — already on feed" + Remove-Item $file.FullName + $removedCount++ + continue + } + } catch { + if ($_.Exception.Response.StatusCode -eq [System.Net.HttpStatusCode]::NotFound) { + # Package has never been published — keep it + } else { throw } + } + Write-Host " PUSH $id $version — new" + } + + $remaining = (Get-ChildItem -Path "${{ parameters.packageParentPath }}" -Filter "*.nupkg" -Recurse).Count + Write-Host "Removed $removedCount already-published package(s). $remaining package(s) to push." + Write-Host "##vso[task.setvariable variable=HasNewPackages]$($remaining -gt 0)" + displayName: Remove already-published packages + +- script: dir /S "${{ parameters.packageParentPath }}" + displayName: Show NuGet packages after cleanup + +- task: 1ES.PublishNuGet@1 + displayName: 'NuGet push to ${{ parameters.feedDisplayName }}' + condition: and(succeeded(), eq(variables['HasNewPackages'], 'True')) + inputs: + useDotNetTask: true + packageParentPath: '${{ parameters.packageParentPath }}' + packagesToPush: '${{ parameters.packagesToPush }}' + nuGetFeedType: external + publishFeedCredentials: '${{ parameters.publishFeedCredentials }}' + externalEndpoint: '${{ parameters.publishFeedCredentials }}' + publishPackageMetadata: true