|
| 1 | +# |
| 2 | +# The Release pipeline entry point. |
| 3 | +# It publishes npm packages to npmjs.com, NuGet packages to the public |
| 4 | +# ms/react-native and ms/react-native-public ADO feeds and to nuget.org, |
| 5 | +# and PDB symbols to the Microsoft Symbol Server. |
| 6 | +# |
| 7 | +# This file replaces release.yml and references the renamed "CI" pipeline |
| 8 | +# (formerly "Publish"). Once all branches use this file, release.yml can |
| 9 | +# be deleted. |
| 10 | +# |
| 11 | +# The pipeline completion trigger is defined below in the pipeline resource. |
| 12 | +# Do NOT add a build completion trigger in the ADO UI — UI triggers override |
| 13 | +# YAML triggers and cause the pipeline to always run against the default branch |
| 14 | +# with incorrect metadata (wrong commit message and branch). |
| 15 | +# |
| 16 | + |
| 17 | +name: RNW Release $(Date:yyyyMMdd).$(Rev:r) |
| 18 | + |
| 19 | +trigger: none |
| 20 | +pr: none |
| 21 | + |
| 22 | +resources: |
| 23 | + pipelines: |
| 24 | + - pipeline: 'CI' |
| 25 | + project: 'ReactNative' |
| 26 | + source: 'CI' |
| 27 | + trigger: |
| 28 | + branches: |
| 29 | + include: |
| 30 | + - main |
| 31 | + - '0.74-stable' |
| 32 | + - '0.81-stable' |
| 33 | + - '0.82-stable' |
| 34 | + - '0.83-stable' |
| 35 | + - '0.84-stable' |
| 36 | + repositories: |
| 37 | + - repository: 1ESPipelineTemplates |
| 38 | + type: git |
| 39 | + name: 1ESPipelineTemplates/1ESPipelineTemplates |
| 40 | + ref: refs/tags/release |
| 41 | + |
| 42 | +extends: |
| 43 | + template: v1/1ES.Official.PipelineTemplate.yml@1ESPipelineTemplates |
| 44 | + parameters: |
| 45 | + pool: |
| 46 | + name: Azure-Pipelines-1ESPT-ExDShared |
| 47 | + image: windows-latest |
| 48 | + os: windows |
| 49 | + stages: |
| 50 | + # |
| 51 | + # Gate stage — runs unconditionally for every trigger. |
| 52 | + # It determines whether the Release stage should proceed and sets a |
| 53 | + # descriptive build number so the pipeline history is easy to scan. |
| 54 | + # |
| 55 | + - stage: Gate |
| 56 | + displayName: Evaluate release |
| 57 | + jobs: |
| 58 | + - job: Evaluate |
| 59 | + displayName: Check if release should proceed |
| 60 | + steps: |
| 61 | + - checkout: none |
| 62 | + |
| 63 | + - pwsh: | |
| 64 | + Write-Host "== Build Variables ==" |
| 65 | + Write-Host "Build.Reason: $env:BUILD_REASON" |
| 66 | + Write-Host "Build.SourceBranch: $env:BUILD_SOURCEBRANCH" |
| 67 | + Write-Host "Build.SourceVersion: $env:BUILD_SOURCEVERSION" |
| 68 | + Write-Host "Build.SourceVersionMessage: $env:BUILD_SOURCEVERSIONMESSAGE" |
| 69 | + Write-Host "Build.BuildNumber: $env:BUILD_BUILDNUMBER" |
| 70 | + Write-Host "Build.BuildId: $env:BUILD_BUILDID" |
| 71 | + Write-Host "Build.DefinitionName: $env:BUILD_DEFINITIONNAME" |
| 72 | + Write-Host "Build.Repository.Name: $env:BUILD_REPOSITORY_NAME" |
| 73 | + Write-Host "System.TeamProject: $env:SYSTEM_TEAMPROJECT" |
| 74 | + Write-Host "" |
| 75 | + Write-Host "== Pipeline Resource: CI ==" |
| 76 | + Write-Host "CI.runName: $env:CI_RUNNAME" |
| 77 | + Write-Host "CI.runID: $env:CI_RUNID" |
| 78 | + Write-Host "CI.sourceBranch: $env:CI_SOURCEBRANCH" |
| 79 | + Write-Host "CI.sourceCommit: $env:CI_SOURCECOMMIT" |
| 80 | + Write-Host "CI.pipelineID: $env:CI_PIPELINEID" |
| 81 | + Write-Host "CI.requestedFor: $env:CI_REQUESTEDFOR" |
| 82 | + Write-Host "CI.requestedForID: $env:CI_REQUESTEDFORID" |
| 83 | + displayName: Log all pipeline variables |
| 84 | + env: |
| 85 | + BUILD_REASON: $(Build.Reason) |
| 86 | + BUILD_SOURCEBRANCH: $(Build.SourceBranch) |
| 87 | + BUILD_SOURCEVERSION: $(Build.SourceVersion) |
| 88 | + BUILD_SOURCEVERSIONMESSAGE: $(Build.SourceVersionMessage) |
| 89 | + BUILD_BUILDNUMBER: $(Build.BuildNumber) |
| 90 | + BUILD_BUILDID: $(Build.BuildId) |
| 91 | + BUILD_DEFINITIONNAME: $(Build.DefinitionName) |
| 92 | + BUILD_REPOSITORY_NAME: $(Build.Repository.Name) |
| 93 | + SYSTEM_TEAMPROJECT: $(System.TeamProject) |
| 94 | + CI_RUNNAME: $(resources.pipeline.CI.runName) |
| 95 | + CI_RUNID: $(resources.pipeline.CI.runID) |
| 96 | + CI_SOURCEBRANCH: $(resources.pipeline.CI.sourceBranch) |
| 97 | + CI_SOURCECOMMIT: $(resources.pipeline.CI.sourceCommit) |
| 98 | + CI_PIPELINEID: $(resources.pipeline.CI.pipelineID) |
| 99 | + CI_REQUESTEDFOR: $(resources.pipeline.CI.requestedFor) |
| 100 | + CI_REQUESTEDFORID: $(resources.pipeline.CI.requestedForID) |
| 101 | +
|
| 102 | + - pwsh: | |
| 103 | + $buildReason = $env:BUILD_REASON |
| 104 | + # Use only the first line of the commit message |
| 105 | + $sourceMessage = ($env:SOURCE_MESSAGE -split "`n")[0].Trim() |
| 106 | + $ciRunName = $env:CI_RUN_NAME |
| 107 | + $sourceBranch = $env:SOURCE_BRANCH -replace '^refs/heads/', '' |
| 108 | +
|
| 109 | + # Extract the datestamp (e.g. "20260319.4") from the original build number |
| 110 | + # which has the format "RNW Release 20260319.4" |
| 111 | + $originalBuildNumber = $env:BUILD_BUILDNUMBER |
| 112 | + $dateStamp = if ($originalBuildNumber -match '(\d{8}\.\d+)$') { $Matches[1] } else { "" } |
| 113 | +
|
| 114 | + $shouldRelease = $false |
| 115 | + $buildNumber = "" |
| 116 | +
|
| 117 | + if ($buildReason -eq "Manual") { |
| 118 | + $shouldRelease = $true |
| 119 | + if ($ciRunName) { |
| 120 | + $buildNumber = "$ciRunName ($sourceBranch) - $dateStamp" |
| 121 | + } else { |
| 122 | + $buildNumber = "Release ($sourceBranch) - $dateStamp" |
| 123 | + } |
| 124 | + } |
| 125 | + elseif ($sourceMessage.StartsWith("RELEASE:")) { |
| 126 | + $shouldRelease = $true |
| 127 | + $buildNumber = "$ciRunName ($sourceBranch) - $dateStamp" |
| 128 | + } |
| 129 | + else { |
| 130 | + $shouldRelease = $false |
| 131 | + # Truncate commit message for readability |
| 132 | + $shortMsg = $sourceMessage |
| 133 | + if ($shortMsg.Length -gt 60) { |
| 134 | + $shortMsg = $shortMsg.Substring(0, 57) + "..." |
| 135 | + } |
| 136 | + $buildNumber = "Skipped - $shortMsg ($sourceBranch) - $dateStamp" |
| 137 | + } |
| 138 | +
|
| 139 | + # Sanitize: ADO build numbers cannot contain " / : < > \ | ? @ * |
| 140 | + # and cannot end with '.' |
| 141 | + $buildNumber = $buildNumber -replace '["/:<>\\|?@*]', '_' |
| 142 | + $buildNumber = $buildNumber.TrimEnd('.') |
| 143 | +
|
| 144 | + Write-Host "shouldRelease: $shouldRelease" |
| 145 | + Write-Host "buildNumber: $buildNumber" |
| 146 | +
|
| 147 | + Write-Host "##vso[build.updatebuildnumber]$buildNumber" |
| 148 | + Write-Host "##vso[task.setvariable variable=shouldRelease;isOutput=true]$shouldRelease" |
| 149 | + name: gate |
| 150 | + displayName: Determine release eligibility and set build number |
| 151 | + env: |
| 152 | + BUILD_REASON: $(Build.Reason) |
| 153 | + BUILD_BUILDNUMBER: $(Build.BuildNumber) |
| 154 | + SOURCE_MESSAGE: $(Build.SourceVersionMessage) |
| 155 | + CI_RUN_NAME: $(resources.pipeline.CI.runName) |
| 156 | + SOURCE_BRANCH: $(resources.pipeline.CI.sourceBranch) |
| 157 | +
|
| 158 | + - script: echo Proceeding with release |
| 159 | + displayName: RELEASING - proceeding to publish |
| 160 | + condition: eq(variables['gate.shouldRelease'], 'True') |
| 161 | + |
| 162 | + - script: echo Skipping release |
| 163 | + displayName: SKIPPED - not a RELEASE commit |
| 164 | + condition: eq(variables['gate.shouldRelease'], 'False') |
| 165 | + |
| 166 | + - stage: Release |
| 167 | + displayName: Publish artifacts |
| 168 | + dependsOn: Gate |
| 169 | + condition: eq(dependencies.Gate.outputs['Evaluate.gate.shouldRelease'], 'True') |
| 170 | + jobs: |
| 171 | + - job: PushNpm |
| 172 | + displayName: npmjs.com - Publish npm packages |
| 173 | + variables: |
| 174 | + - group: RNW Secrets |
| 175 | + timeoutInMinutes: 30 |
| 176 | + templateContext: |
| 177 | + type: releaseJob |
| 178 | + isProduction: true |
| 179 | + inputs: |
| 180 | + - input: pipelineArtifact |
| 181 | + pipeline: 'CI' |
| 182 | + artifactName: 'NpmPackedTarballs' |
| 183 | + targetPath: '$(Pipeline.Workspace)/published-packages' |
| 184 | + - input: pipelineArtifact |
| 185 | + pipeline: 'CI' |
| 186 | + artifactName: 'VersionEnvVars' |
| 187 | + targetPath: '$(Pipeline.Workspace)/VersionEnvVars' |
| 188 | + steps: |
| 189 | + - task: CmdLine@2 |
| 190 | + displayName: Apply version variables |
| 191 | + inputs: |
| 192 | + script: node $(Pipeline.Workspace)/VersionEnvVars/versionEnvVars.js |
| 193 | + - script: dir /s "$(Pipeline.Workspace)\published-packages" |
| 194 | + displayName: Show npm packages before cleanup |
| 195 | + - script: node "$(Pipeline.Workspace)\VersionEnvVars\npmPack.js" --no-pack --check-npm --no-color "$(Pipeline.Workspace)\published-packages" |
| 196 | + displayName: Remove already published packages |
| 197 | + - script: dir /s "$(Pipeline.Workspace)\published-packages" |
| 198 | + displayName: Show npm packages after cleanup |
| 199 | + - pwsh: | |
| 200 | + $tgzFiles = Get-ChildItem -Path "$(Pipeline.Workspace)\published-packages" -Filter "*.tgz" -Recurse |
| 201 | + $tgzCount = $tgzFiles.Count |
| 202 | + Write-Host "Found $tgzCount .tgz files" |
| 203 | + Write-Host "##vso[task.setvariable variable=HasPackagesToPublish]$($tgzCount -gt 0)" |
| 204 | + displayName: Check if there are packages to publish |
| 205 | + - task: 'EsrpRelease@11' |
| 206 | + displayName: 'ESRP Release to npmjs.com' |
| 207 | + condition: and(succeeded(), ne(variables['NpmDistTag'], ''), eq(variables['HasPackagesToPublish'], 'true')) |
| 208 | + inputs: |
| 209 | + connectedservicename: 'ESRP-CodeSigning-OGX-JSHost-RNW' |
| 210 | + usemanagedidentity: false |
| 211 | + keyvaultname: 'OGX-JSHost-KV' |
| 212 | + authcertname: 'OGX-JSHost-Auth4' |
| 213 | + signcertname: 'OGX-JSHost-Sign3' |
| 214 | + clientid: '0a35e01f-eadf-420a-a2bf-def002ba898d' |
| 215 | + domaintenantid: 'cdc5aeea-15c5-4db6-b079-fcadd2505dc2' |
| 216 | + contenttype: npm |
| 217 | + folderlocation: '$(Pipeline.Workspace)\published-packages' |
| 218 | + productstate: '$(NpmDistTag)' |
| 219 | + owners: 'vmorozov@microsoft.com' |
| 220 | + approvers: 'khosany@microsoft.com' |
| 221 | + |
| 222 | + - job: PushPrivateAdo |
| 223 | + displayName: ADO - nuget - react-native |
| 224 | + timeoutInMinutes: 30 |
| 225 | + templateContext: |
| 226 | + type: releaseJob |
| 227 | + isProduction: true |
| 228 | + inputs: |
| 229 | + - input: pipelineArtifact |
| 230 | + pipeline: 'CI' |
| 231 | + artifactName: 'ReactWindows-final-nuget' |
| 232 | + targetPath: '$(Pipeline.Workspace)/ReactWindows-final-nuget' |
| 233 | + steps: |
| 234 | + - template: .ado/templates/publish-nuget-to-ado-feed.yml@self |
| 235 | + parameters: |
| 236 | + endpointId: 'a7e33797-4804-4a1d-911d-5bd325e50a85' |
| 237 | + nugetFeedUrl: 'https://pkgs.dev.azure.com/ms/_packaging/react-native/nuget/v3/index.json' |
| 238 | + packageParentPath: '$(Pipeline.Workspace)/ReactWindows-final-nuget' |
| 239 | + packagesToPush: '$(Pipeline.Workspace)/ReactWindows-final-nuget/*.nupkg' |
| 240 | + publishFeedCredentials: 'ms/react-native ADO Feed' |
| 241 | + feedDisplayName: 'ms/react-native' |
| 242 | + |
| 243 | + - job: PushPublicAdo |
| 244 | + displayName: ADO - nuget - react-native-public |
| 245 | + timeoutInMinutes: 30 |
| 246 | + templateContext: |
| 247 | + type: releaseJob |
| 248 | + isProduction: true |
| 249 | + inputs: |
| 250 | + - input: pipelineArtifact |
| 251 | + pipeline: 'CI' |
| 252 | + artifactName: 'ReactWindows-final-nuget' |
| 253 | + targetPath: '$(Pipeline.Workspace)/ReactWindows-final-nuget' |
| 254 | + steps: |
| 255 | + - template: .ado/templates/publish-nuget-to-ado-feed.yml@self |
| 256 | + parameters: |
| 257 | + endpointId: '9a2456d0-c163-405b-be24-c03fd74b155a' |
| 258 | + nugetFeedUrl: 'https://pkgs.dev.azure.com/ms/react-native/_packaging/react-native-public/nuget/v3/index.json' |
| 259 | + packageParentPath: '$(Pipeline.Workspace)/ReactWindows-final-nuget' |
| 260 | + packagesToPush: '$(Pipeline.Workspace)/ReactWindows-final-nuget/*.nupkg' |
| 261 | + publishFeedCredentials: 'ms/react-native-public ADO Feed' |
| 262 | + feedDisplayName: 'ms/react-native-public' |
| 263 | + |
| 264 | + - job: PushNuGetOrg |
| 265 | + displayName: nuget.org - Push nuget packages |
| 266 | + variables: |
| 267 | + - group: RNW Secrets |
| 268 | + timeoutInMinutes: 30 |
| 269 | + templateContext: |
| 270 | + type: releaseJob |
| 271 | + isProduction: true |
| 272 | + inputs: |
| 273 | + - input: pipelineArtifact |
| 274 | + pipeline: 'CI' |
| 275 | + artifactName: 'ReactWindows-final-nuget' |
| 276 | + targetPath: '$(Pipeline.Workspace)/ReactWindows-final-nuget' |
| 277 | + steps: |
| 278 | + - task: NuGetToolInstaller@1 |
| 279 | + displayName: 'Use NuGet' |
| 280 | + - pwsh: nuget.exe SetApiKey "$env:NUGET_API_KEY" |
| 281 | + displayName: NuGet SetApiKey (nuget.org) |
| 282 | + workingDirectory: $(Pipeline.Workspace)/ReactWindows-final-nuget |
| 283 | + env: |
| 284 | + NUGET_API_KEY: $(nugetorg-apiKey-push) |
| 285 | + - script: dir /S "$(Pipeline.Workspace)\ReactWindows-final-nuget" |
| 286 | + displayName: Show directory contents |
| 287 | + - script: nuget.exe push .\Microsoft.ReactNative.*.nupkg -Source https://api.nuget.org/v3/index.json -SkipDuplicate -NoSymbol -NonInteractive -Verbosity Detailed |
| 288 | + displayName: NuGet push (nuget.org) |
| 289 | + workingDirectory: $(Pipeline.Workspace)/ReactWindows-final-nuget |
| 290 | + |
| 291 | + - job: PublishSymbols |
| 292 | + displayName: Publish PDB Symbols to Symbol Server |
| 293 | + timeoutInMinutes: 30 |
| 294 | + templateContext: |
| 295 | + type: releaseJob |
| 296 | + isProduction: true |
| 297 | + inputs: |
| 298 | + - input: pipelineArtifact |
| 299 | + pipeline: 'CI' |
| 300 | + artifactName: 'ReactWindows-final-nuget' |
| 301 | + targetPath: '$(Pipeline.Workspace)/ReactWindows-final-nuget' |
| 302 | + steps: |
| 303 | + - pwsh: | |
| 304 | + # Extract PDB files from all NuGet packages (.nupkg are ZIP archives) |
| 305 | + $nugetDir = "$(Pipeline.Workspace)/ReactWindows-final-nuget" |
| 306 | + $symbolsDir = "$(Pipeline.Workspace)/symbols" |
| 307 | + New-Item -ItemType Directory -Path $symbolsDir -Force | Out-Null |
| 308 | +
|
| 309 | + $nupkgs = Get-ChildItem "$nugetDir/*.nupkg" |
| 310 | + Write-Host "Found $($nupkgs.Count) NuGet packages" |
| 311 | +
|
| 312 | + foreach ($nupkg in $nupkgs) { |
| 313 | + Write-Host "Extracting PDBs from: $($nupkg.Name)" |
| 314 | + $extractDir = "$symbolsDir/$($nupkg.BaseName)" |
| 315 | + # Rename to .zip for Expand-Archive compatibility |
| 316 | + $zipPath = "$nugetDir/$($nupkg.BaseName).zip" |
| 317 | + Copy-Item $nupkg.FullName $zipPath |
| 318 | + Expand-Archive -Path $zipPath -DestinationPath $extractDir -Force |
| 319 | + Remove-Item $zipPath |
| 320 | + } |
| 321 | +
|
| 322 | + # Show extracted PDBs |
| 323 | + $pdbs = Get-ChildItem "$symbolsDir" -Recurse -Filter "*.pdb" |
| 324 | + Write-Host "`nFound $($pdbs.Count) PDB files:" |
| 325 | + foreach ($pdb in $pdbs) { |
| 326 | + Write-Host " $($pdb.FullName) ($([math]::Round($pdb.Length / 1MB, 2)) MB)" |
| 327 | + } |
| 328 | +
|
| 329 | + if ($pdbs.Count -eq 0) { |
| 330 | + Write-Host "##vso[task.logissue type=warning]No PDB files found in NuGet packages" |
| 331 | + } |
| 332 | + displayName: Extract PDBs from NuGet packages |
| 333 | +
|
| 334 | + - task: PublishSymbols@2 |
| 335 | + displayName: 'Publish Symbols to Microsoft Symbol Server' |
| 336 | + continueOnError: true |
| 337 | + inputs: |
| 338 | + UseNetCoreClientTool: true |
| 339 | + ConnectedServiceName: Office-React-Native-Windows-Bot |
| 340 | + SymbolsFolder: '$(Pipeline.Workspace)/symbols' |
| 341 | + SearchPattern: '**/*.pdb' |
| 342 | + SymbolServerType: 'TeamServices' |
| 343 | + IndexSources: false # SourceLink is already embedded in PDBs at compile time |
| 344 | + SymbolsProduct: 'ReactNativeWindows' |
| 345 | + SymbolsVersion: '$(Build.BuildNumber)' |
| 346 | + SymbolsArtifactName: 'ReactNativeWindows-Symbols-$(Build.BuildId)' |
| 347 | + DetailedLog: true |
| 348 | + TreatNotIndexedAsWarning: false |
0 commit comments