diff --git a/.ado/build-template.yml b/.ado/build-template.yml index 6fe27be873b..b8bceb4d871 100644 --- a/.ado/build-template.yml +++ b/.ado/build-template.yml @@ -206,6 +206,9 @@ extends: condition: and(succeeded(), eq('${{ parameters.buildEnvironment }}', 'Continuous')) templateContext: + sdl: + codeql: + enabled: false outputs: - output: pipelineArtifact displayName: 'Publish version variables' diff --git a/.ado/release-pipeline.yml b/.ado/release-pipeline.yml new file mode 100644 index 00000000000..9739ec36050 --- /dev/null +++ b/.ado/release-pipeline.yml @@ -0,0 +1,348 @@ +# +# The Release pipeline entry point. +# It publishes npm packages to npmjs.com, NuGet packages to the public +# ms/react-native and ms/react-native-public ADO feeds and to nuget.org, +# and PDB symbols to the Microsoft Symbol Server. +# +# This file replaces release.yml and references the renamed "CI" pipeline +# (formerly "Publish"). Once all branches use this file, release.yml can +# be deleted. +# +# The pipeline completion trigger is defined below in the pipeline resource. +# Do NOT add a build completion trigger in the ADO UI — UI triggers override +# YAML triggers and cause the pipeline to always run against the default branch +# with incorrect metadata (wrong commit message and branch). +# + +name: RNW Release $(Date:yyyyMMdd).$(Rev:r) + +trigger: none +pr: none + +resources: + pipelines: + - pipeline: 'CI' + project: 'ReactNative' + source: 'CI' + trigger: + branches: + include: + - main + - '0.74-stable' + - '0.81-stable' + - '0.82-stable' + - '0.83-stable' + - '0.84-stable' + repositories: + - repository: 1ESPipelineTemplates + type: git + name: 1ESPipelineTemplates/1ESPipelineTemplates + ref: refs/tags/release + +extends: + template: v1/1ES.Official.PipelineTemplate.yml@1ESPipelineTemplates + parameters: + pool: + name: Azure-Pipelines-1ESPT-ExDShared + image: windows-latest + os: windows + stages: + # + # Gate stage — runs unconditionally for every trigger. + # It determines whether the Release stage should proceed and sets a + # descriptive build number so the pipeline history is easy to scan. + # + - stage: Gate + displayName: Evaluate release + jobs: + - job: Evaluate + displayName: Check if release should proceed + steps: + - checkout: none + + - pwsh: | + Write-Host "== Build Variables ==" + Write-Host "Build.Reason: $env:BUILD_REASON" + Write-Host "Build.SourceBranch: $env:BUILD_SOURCEBRANCH" + Write-Host "Build.SourceVersion: $env:BUILD_SOURCEVERSION" + Write-Host "Build.SourceVersionMessage: $env:BUILD_SOURCEVERSIONMESSAGE" + Write-Host "Build.BuildNumber: $env:BUILD_BUILDNUMBER" + Write-Host "Build.BuildId: $env:BUILD_BUILDID" + Write-Host "Build.DefinitionName: $env:BUILD_DEFINITIONNAME" + Write-Host "Build.Repository.Name: $env:BUILD_REPOSITORY_NAME" + Write-Host "System.TeamProject: $env:SYSTEM_TEAMPROJECT" + Write-Host "" + Write-Host "== Pipeline Resource: CI ==" + Write-Host "CI.runName: $env:CI_RUNNAME" + Write-Host "CI.runID: $env:CI_RUNID" + Write-Host "CI.sourceBranch: $env:CI_SOURCEBRANCH" + Write-Host "CI.sourceCommit: $env:CI_SOURCECOMMIT" + Write-Host "CI.pipelineID: $env:CI_PIPELINEID" + Write-Host "CI.requestedFor: $env:CI_REQUESTEDFOR" + Write-Host "CI.requestedForID: $env:CI_REQUESTEDFORID" + displayName: Log all pipeline variables + env: + BUILD_REASON: $(Build.Reason) + BUILD_SOURCEBRANCH: $(Build.SourceBranch) + BUILD_SOURCEVERSION: $(Build.SourceVersion) + BUILD_SOURCEVERSIONMESSAGE: $(Build.SourceVersionMessage) + BUILD_BUILDNUMBER: $(Build.BuildNumber) + BUILD_BUILDID: $(Build.BuildId) + BUILD_DEFINITIONNAME: $(Build.DefinitionName) + BUILD_REPOSITORY_NAME: $(Build.Repository.Name) + SYSTEM_TEAMPROJECT: $(System.TeamProject) + CI_RUNNAME: $(resources.pipeline.CI.runName) + CI_RUNID: $(resources.pipeline.CI.runID) + CI_SOURCEBRANCH: $(resources.pipeline.CI.sourceBranch) + CI_SOURCECOMMIT: $(resources.pipeline.CI.sourceCommit) + CI_PIPELINEID: $(resources.pipeline.CI.pipelineID) + CI_REQUESTEDFOR: $(resources.pipeline.CI.requestedFor) + CI_REQUESTEDFORID: $(resources.pipeline.CI.requestedForID) + + - pwsh: | + $buildReason = $env:BUILD_REASON + # Use only the first line of the commit message + $sourceMessage = ($env:SOURCE_MESSAGE -split "`n")[0].Trim() + $ciRunName = $env:CI_RUN_NAME + $sourceBranch = $env:SOURCE_BRANCH -replace '^refs/heads/', '' + + # Extract the datestamp (e.g. "20260319.4") from the original build number + # which has the format "RNW Release 20260319.4" + $originalBuildNumber = $env:BUILD_BUILDNUMBER + $dateStamp = if ($originalBuildNumber -match '(\d{8}\.\d+)$') { $Matches[1] } else { "" } + + $shouldRelease = $false + $buildNumber = "" + + if ($buildReason -eq "Manual") { + $shouldRelease = $true + if ($ciRunName) { + $buildNumber = "$ciRunName ($sourceBranch) - $dateStamp" + } else { + $buildNumber = "Release ($sourceBranch) - $dateStamp" + } + } + elseif ($sourceMessage.StartsWith("RELEASE:")) { + $shouldRelease = $true + $buildNumber = "$ciRunName ($sourceBranch) - $dateStamp" + } + else { + $shouldRelease = $false + # Truncate commit message for readability + $shortMsg = $sourceMessage + if ($shortMsg.Length -gt 60) { + $shortMsg = $shortMsg.Substring(0, 57) + "..." + } + $buildNumber = "Skipped - $shortMsg ($sourceBranch) - $dateStamp" + } + + # Sanitize: ADO build numbers cannot contain " / : < > \ | ? @ * + # and cannot end with '.' + $buildNumber = $buildNumber -replace '["/:<>\\|?@*]', '_' + $buildNumber = $buildNumber.TrimEnd('.') + + Write-Host "shouldRelease: $shouldRelease" + Write-Host "buildNumber: $buildNumber" + + Write-Host "##vso[build.updatebuildnumber]$buildNumber" + Write-Host "##vso[task.setvariable variable=shouldRelease;isOutput=true]$shouldRelease" + name: gate + displayName: Determine release eligibility and set build number + env: + BUILD_REASON: $(Build.Reason) + BUILD_BUILDNUMBER: $(Build.BuildNumber) + SOURCE_MESSAGE: $(Build.SourceVersionMessage) + CI_RUN_NAME: $(resources.pipeline.CI.runName) + SOURCE_BRANCH: $(resources.pipeline.CI.sourceBranch) + + - script: echo Proceeding with release + displayName: RELEASING - proceeding to publish + condition: eq(variables['gate.shouldRelease'], 'True') + + - script: echo Skipping release + displayName: SKIPPED - not a RELEASE commit + condition: eq(variables['gate.shouldRelease'], 'False') + + - stage: Release + displayName: Publish artifacts + dependsOn: Gate + condition: eq(dependencies.Gate.outputs['Evaluate.gate.shouldRelease'], 'True') + jobs: + - job: PushNpm + displayName: npmjs.com - Publish npm packages + variables: + - group: RNW Secrets + timeoutInMinutes: 30 + templateContext: + type: releaseJob + isProduction: true + inputs: + - input: pipelineArtifact + pipeline: 'CI' + artifactName: 'NpmPackedTarballs' + targetPath: '$(Pipeline.Workspace)/published-packages' + - input: pipelineArtifact + pipeline: 'CI' + artifactName: 'VersionEnvVars' + targetPath: '$(Pipeline.Workspace)/VersionEnvVars' + steps: + - 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 "$(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 + - pwsh: | + $tgzFiles = Get-ChildItem -Path "$(Pipeline.Workspace)\published-packages" -Filter "*.tgz" -Recurse + $tgzCount = $tgzFiles.Count + Write-Host "Found $tgzCount .tgz files" + Write-Host "##vso[task.setvariable variable=HasPackagesToPublish]$($tgzCount -gt 0)" + displayName: Check if there are packages to publish + - task: 'EsrpRelease@11' + displayName: 'ESRP Release to npmjs.com' + condition: and(succeeded(), ne(variables['NpmDistTag'], ''), eq(variables['HasPackagesToPublish'], 'true')) + inputs: + connectedservicename: 'ESRP-CodeSigning-OGX-JSHost-RNW' + usemanagedidentity: false + keyvaultname: 'OGX-JSHost-KV' + authcertname: 'OGX-JSHost-Auth4' + signcertname: 'OGX-JSHost-Sign3' + clientid: '0a35e01f-eadf-420a-a2bf-def002ba898d' + domaintenantid: 'cdc5aeea-15c5-4db6-b079-fcadd2505dc2' + contenttype: npm + folderlocation: '$(Pipeline.Workspace)\published-packages' + productstate: '$(NpmDistTag)' + owners: 'vmorozov@microsoft.com' + approvers: 'khosany@microsoft.com' + + - job: PushPrivateAdo + displayName: ADO - nuget - react-native + timeoutInMinutes: 30 + templateContext: + type: releaseJob + isProduction: true + inputs: + - input: pipelineArtifact + pipeline: 'CI' + artifactName: 'ReactWindows-final-nuget' + targetPath: '$(Pipeline.Workspace)/ReactWindows-final-nuget' + steps: + - 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' + publishFeedCredentials: 'ms/react-native ADO Feed' + feedDisplayName: 'ms/react-native' + + - job: PushPublicAdo + displayName: ADO - nuget - react-native-public + timeoutInMinutes: 30 + templateContext: + type: releaseJob + isProduction: true + inputs: + - input: pipelineArtifact + pipeline: 'CI' + artifactName: 'ReactWindows-final-nuget' + targetPath: '$(Pipeline.Workspace)/ReactWindows-final-nuget' + steps: + - 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' + publishFeedCredentials: 'ms/react-native-public ADO Feed' + feedDisplayName: 'ms/react-native-public' + + - job: PushNuGetOrg + displayName: nuget.org - Push nuget packages + variables: + - group: RNW Secrets + timeoutInMinutes: 30 + templateContext: + type: releaseJob + isProduction: true + inputs: + - input: pipelineArtifact + pipeline: 'CI' + artifactName: 'ReactWindows-final-nuget' + targetPath: '$(Pipeline.Workspace)/ReactWindows-final-nuget' + steps: + - task: NuGetToolInstaller@1 + displayName: 'Use NuGet' + - pwsh: nuget.exe SetApiKey "$env:NUGET_API_KEY" + displayName: NuGet SetApiKey (nuget.org) + workingDirectory: $(Pipeline.Workspace)/ReactWindows-final-nuget + env: + NUGET_API_KEY: $(nugetorg-apiKey-push) + - 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) + workingDirectory: $(Pipeline.Workspace)/ReactWindows-final-nuget + + - job: PublishSymbols + displayName: Publish PDB Symbols to Symbol Server + timeoutInMinutes: 30 + templateContext: + type: releaseJob + isProduction: true + inputs: + - input: pipelineArtifact + pipeline: 'CI' + artifactName: 'ReactWindows-final-nuget' + targetPath: '$(Pipeline.Workspace)/ReactWindows-final-nuget' + steps: + - pwsh: | + # Extract PDB files from all NuGet packages (.nupkg are ZIP archives) + $nugetDir = "$(Pipeline.Workspace)/ReactWindows-final-nuget" + $symbolsDir = "$(Pipeline.Workspace)/symbols" + New-Item -ItemType Directory -Path $symbolsDir -Force | Out-Null + + $nupkgs = Get-ChildItem "$nugetDir/*.nupkg" + Write-Host "Found $($nupkgs.Count) NuGet packages" + + foreach ($nupkg in $nupkgs) { + Write-Host "Extracting PDBs from: $($nupkg.Name)" + $extractDir = "$symbolsDir/$($nupkg.BaseName)" + # Rename to .zip for Expand-Archive compatibility + $zipPath = "$nugetDir/$($nupkg.BaseName).zip" + Copy-Item $nupkg.FullName $zipPath + Expand-Archive -Path $zipPath -DestinationPath $extractDir -Force + Remove-Item $zipPath + } + + # Show extracted PDBs + $pdbs = Get-ChildItem "$symbolsDir" -Recurse -Filter "*.pdb" + Write-Host "`nFound $($pdbs.Count) PDB files:" + foreach ($pdb in $pdbs) { + Write-Host " $($pdb.FullName) ($([math]::Round($pdb.Length / 1MB, 2)) MB)" + } + + if ($pdbs.Count -eq 0) { + Write-Host "##vso[task.logissue type=warning]No PDB files found in NuGet packages" + } + displayName: Extract PDBs from NuGet packages + + - task: PublishSymbols@2 + displayName: 'Publish Symbols to Microsoft Symbol Server' + continueOnError: true + inputs: + UseNetCoreClientTool: true + ConnectedServiceName: Office-React-Native-Windows-Bot + SymbolsFolder: '$(Pipeline.Workspace)/symbols' + SearchPattern: '**/*.pdb' + SymbolServerType: 'TeamServices' + IndexSources: false # SourceLink is already embedded in PDBs at compile time + SymbolsProduct: 'ReactNativeWindows' + SymbolsVersion: '$(Build.BuildNumber)' + SymbolsArtifactName: 'ReactNativeWindows-Symbols-$(Build.BuildId)' + DetailedLog: true + TreatNotIndexedAsWarning: false