Skip to content

Commit a73dd42

Browse files
authored
Improve release pipeline reliability and 1ES compliance (#15751) (#15764)
1 parent 0fb2029 commit a73dd42

5 files changed

Lines changed: 166 additions & 93 deletions

File tree

.ado/publish.yml

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ variables:
6565
- name: FailCGOnAlert
6666
value: false
6767
- name: EnableCodesign
68-
value: false
68+
value: true
6969

7070
trigger: none
7171
pr: none
@@ -145,6 +145,9 @@ extends:
145145
- script: echo NpmDistTag is $(NpmDistTag)
146146
displayName: Show NPM dist tag
147147

148+
- script: copy ".ado\scripts\npmPack.js" "$(Build.StagingDirectory)\versionEnvVars\npmPack.js"
149+
displayName: Include npmPack.js in VersionEnvVars artifact
150+
148151
- task: AzureArtifacts.manifest-generator-task.manifest-generator-task.ManifestGeneratorTask@0
149152
displayName: 📒 Generate Manifest Npm
150153
inputs:
@@ -351,9 +354,12 @@ extends:
351354
configuration: Debug
352355

353356
# Symbol Publishing for Work Item 59264834 - MSRC Compliance
357+
# continueOnError: Duplicate symbols are expected when the pipeline
358+
# is re-run for the same version. The symbols already exist on the
359+
# server, so it is safe to continue.
354360
- task: PublishSymbols@2
355361
displayName: 'Publish Symbols to Microsoft Symbol Server'
356-
enabled: true
362+
continueOnError: true
357363
inputs:
358364
UseNetCoreClientTool: true
359365
ConnectedServiceName: Office-React-Native-Windows-Bot

.ado/release.yml

Lines changed: 42 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,28 @@
1+
#
2+
# The Release pipeline entry point.
3+
# It releases npm packages to npmjs.com and NuGet packages to the public
4+
# ms/react-native and ms/react-native-public ADO feeds and to nuget.org.
5+
#
6+
# The triggers are overridden by the ADO pipeline UI definition.
7+
#
8+
19
name: RNW NuGet Release $(Date:yyyyMMdd).$(Rev:r)
210

311
trigger: none
12+
pr: none
413

514
resources:
615
pipelines:
716
- pipeline: 'Publish'
817
project: 'ReactNative'
918
source: 'Publish'
10-
trigger:
11-
branches:
12-
include:
13-
- -1espublish
19+
trigger: none
1420
repositories:
1521
- repository: 1ESPipelineTemplates
1622
type: git
1723
name: 1ESPipelineTemplates/1ESPipelineTemplates
1824
ref: refs/tags/release
25+
1926
extends:
2027
template: v1/1ES.Official.PipelineTemplate.yml@1ESPipelineTemplates
2128
parameters:
@@ -28,13 +35,18 @@ extends:
2835
stages:
2936
- stage: Release
3037
displayName: Publish artifacts
38+
# Allow manual runs unconditionally; for build-completion triggers,
39+
# only proceed if the commit message starts with 'RELEASE:'.
40+
condition: or(eq(variables['Build.Reason'], 'Manual'), startsWith(variables['Build.SourceVersionMessage'], 'RELEASE:'))
3141
jobs:
3242
- job: PushNpm
3343
displayName: npmjs.com - Publish npm packages
3444
variables:
3545
- group: RNW Secrets
36-
timeoutInMinutes: 0
46+
timeoutInMinutes: 30
3747
templateContext:
48+
type: releaseJob
49+
isProduction: true
3850
inputs:
3951
- input: pipelineArtifact
4052
pipeline: 'Publish'
@@ -45,15 +57,13 @@ extends:
4557
artifactName: 'VersionEnvVars'
4658
targetPath: '$(Pipeline.Workspace)/VersionEnvVars'
4759
steps:
48-
- checkout: self
49-
clean: false
5060
- task: CmdLine@2
5161
displayName: Apply version variables
5262
inputs:
5363
script: node $(Pipeline.Workspace)/VersionEnvVars/versionEnvVars.js
5464
- script: dir /s "$(Pipeline.Workspace)\published-packages"
5565
displayName: Show npm packages before cleanup
56-
- script: node .ado/scripts/npmPack.js --no-pack --check-npm --no-color "$(Pipeline.Workspace)\published-packages"
66+
- script: node "$(Pipeline.Workspace)\VersionEnvVars\npmPack.js" --no-pack --check-npm --no-color "$(Pipeline.Workspace)\published-packages"
5767
displayName: Remove already published packages
5868
- script: dir /s "$(Pipeline.Workspace)\published-packages"
5969
displayName: Show npm packages after cleanup
@@ -82,112 +92,69 @@ extends:
8292

8393
- job: PushPrivateAdo
8494
displayName: ADO - nuget - react-native
85-
95+
timeoutInMinutes: 30
8696
templateContext:
97+
type: releaseJob
98+
isProduction: true
8799
inputs:
88100
- input: pipelineArtifact
89101
pipeline: 'Publish'
90102
artifactName: 'ReactWindows-final-nuget'
91103
targetPath: '$(Pipeline.Workspace)/ReactWindows-final-nuget'
92-
93104
steps:
94-
- checkout: none
95-
96-
- script: dir /S $(Pipeline.Workspace)\ReactWindows-final-nuget
97-
displayName: Show directory contents
98-
99-
- task: AzureCLI@2
100-
displayName: Override NuGet credentials with Managed Identity
101-
inputs:
102-
azureSubscription: 'Office-React-Native-Windows-Bot'
103-
visibleAzLogin: false
104-
scriptType: 'pscore'
105-
scriptLocation: 'inlineScript'
106-
inlineScript: |
107-
$accessToken = az account get-access-token --query accessToken --resource 499b84ac-1321-427f-aa17-267ca6975798 -o tsv
108-
# Set the access token as a secret, so it doesn't get leaked in the logs
109-
Write-Host "##vso[task.setsecret]$accessToken"
110-
# Override the apitoken of the nuget service connection, for the duration of this stage
111-
Write-Host "##vso[task.setendpoint id=a7e33797-4804-4a1d-911d-5bd325e50a85;field=authParameter;key=apitoken]$accessToken"
112-
113-
- task: 1ES.PublishNuGet@1
114-
displayName: NuGet push to ms/react-native-public
115-
inputs:
116-
useDotNetTask: true
105+
- template: .ado/templates/publish-nuget-to-ado-feed.yml@self
106+
parameters:
107+
endpointId: 'a7e33797-4804-4a1d-911d-5bd325e50a85'
108+
nugetFeedUrl: 'https://pkgs.dev.azure.com/ms/_packaging/react-native/nuget/v3/index.json'
117109
packageParentPath: '$(Pipeline.Workspace)/ReactWindows-final-nuget'
118110
packagesToPush: '$(Pipeline.Workspace)/ReactWindows-final-nuget/*.nupkg'
119-
nuGetFeedType: external
120111
publishFeedCredentials: 'ms/react-native ADO Feed'
121-
externalEndpoint: 'ms/react-native ADO Feed'
122-
publishPackageMetadata: true
112+
feedDisplayName: 'ms/react-native'
123113

124114
- job: PushPublicAdo
125115
displayName: ADO - nuget - react-native-public
126-
116+
timeoutInMinutes: 30
127117
templateContext:
118+
type: releaseJob
119+
isProduction: true
128120
inputs:
129121
- input: pipelineArtifact
130122
pipeline: 'Publish'
131123
artifactName: 'ReactWindows-final-nuget'
132124
targetPath: '$(Pipeline.Workspace)/ReactWindows-final-nuget'
133-
134125
steps:
135-
- checkout: none
136-
137-
- script: dir /S $(Pipeline.Workspace)\ReactWindows-final-nuget
138-
displayName: Show directory contents
139-
140-
- task: AzureCLI@2
141-
displayName: Override NuGet credentials with Managed Identity
142-
inputs:
143-
azureSubscription: 'Office-React-Native-Windows-Bot'
144-
visibleAzLogin: false
145-
scriptType: 'pscore'
146-
scriptLocation: 'inlineScript'
147-
inlineScript: |
148-
$accessToken = az account get-access-token --query accessToken --resource 499b84ac-1321-427f-aa17-267ca6975798 -o tsv
149-
# Set the access token as a secret, so it doesn't get leaked in the logs
150-
Write-Host "##vso[task.setsecret]$accessToken"
151-
# Override the apitoken of the nuget service connection, for the duration of this stage
152-
Write-Host "##vso[task.setendpoint id=9a2456d0-c163-405b-be24-c03fd74b155a;field=authParameter;key=apitoken]$accessToken"
153-
154-
- task: 1ES.PublishNuGet@1
155-
displayName: NuGet push to ms/react-native-public
156-
inputs:
157-
useDotNetTask: true
126+
- template: .ado/templates/publish-nuget-to-ado-feed.yml@self
127+
parameters:
128+
endpointId: '9a2456d0-c163-405b-be24-c03fd74b155a'
129+
nugetFeedUrl: 'https://pkgs.dev.azure.com/ms/react-native/_packaging/react-native-public/nuget/v3/index.json'
158130
packageParentPath: '$(Pipeline.Workspace)/ReactWindows-final-nuget'
159131
packagesToPush: '$(Pipeline.Workspace)/ReactWindows-final-nuget/*.nupkg'
160-
nuGetFeedType: external
161132
publishFeedCredentials: 'ms/react-native-public ADO Feed'
162-
externalEndpoint: 'ms/react-native-public ADO Feed'
163-
publishPackageMetadata: true
133+
feedDisplayName: 'ms/react-native-public'
164134

165135
- job: PushNuGetOrg
166136
displayName: nuget.org - Push nuget packages
167137
variables:
168138
- group: RNW Secrets
169-
timeoutInMinutes: 0
139+
timeoutInMinutes: 30
170140
templateContext:
141+
type: releaseJob
142+
isProduction: true
171143
inputs:
172144
- input: pipelineArtifact
173145
pipeline: 'Publish'
174146
artifactName: 'ReactWindows-final-nuget'
175147
targetPath: '$(Pipeline.Workspace)/ReactWindows-final-nuget'
176148
steps:
177-
- checkout: none
178149
- task: NuGetToolInstaller@1
179-
displayName: 'Use NuGet '
150+
displayName: 'Use NuGet'
180151
- task: CmdLine@2
181152
displayName: NuGet SetApiKey (nuget.org)
182153
inputs:
183154
script: nuget.exe SetApiKey $(nugetorg-apiKey-push)
184155
workingDirectory: $(Pipeline.Workspace)/ReactWindows-final-nuget
185-
- task: PowerShell@2
156+
- script: dir /S "$(Pipeline.Workspace)\ReactWindows-final-nuget"
157+
displayName: Show directory contents
158+
- script: nuget.exe push .\Microsoft.ReactNative.*.nupkg -Source https://api.nuget.org/v3/index.json -SkipDuplicate -NoSymbol -NonInteractive -Verbosity Detailed
186159
displayName: NuGet push (nuget.org)
187-
inputs:
188-
targetType: inline
189-
errorActionPreference: silentlyContinue
190-
script: |
191-
if (Get-ChildItem -Path .\ -Filter '*0.0.0-canary*' -ErrorAction SilentlyContinue) { Write-Output "Canary builds found, exiting."; return 0; }
192-
nuget.exe push .\Microsoft.ReactNative.*.nupkg -Source https://api.nuget.org/v3/index.json -SkipDuplicate -NoSymbol -NonInteractive -Verbosity Detailed
193-
workingDirectory: $(Pipeline.Workspace)/ReactWindows-final-nuget
160+
workingDirectory: $(Pipeline.Workspace)/ReactWindows-final-nuget

.ado/scripts/npmPack.js

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -391,14 +391,19 @@ function main() {
391391
const targetDirArg = args.positionals[0];
392392

393393
try {
394-
// Find repo root
395-
const repoRoot = findEnlistmentRoot();
396-
console.log(`${colorize('Repository root:', colors.bright)} ${repoRoot}`);
394+
// Find repo root (not needed when --no-pack is used with an absolute path)
395+
const repoRoot = (noPackFlag && targetDirArg && path.isAbsolute(targetDirArg))
396+
? null
397+
: findEnlistmentRoot();
398+
399+
if (repoRoot) {
400+
console.log(`${colorize('Repository root:', colors.bright)} ${repoRoot}`);
401+
}
397402

398403
// Determine target directory
399404
const targetDir = targetDirArg
400-
? path.resolve(repoRoot, targetDirArg)
401-
: path.join(repoRoot, 'npm-pkgs');
405+
? (repoRoot ? path.resolve(repoRoot, targetDirArg) : path.resolve(targetDirArg))
406+
: path.join(/** @type {string} */ (repoRoot), 'npm-pkgs');
402407

403408
console.log(`${colorize('Target directory:', colors.bright)} ${targetDir}`);
404409

.ado/templates/authenticate-office-react-native-windows-bot.yml

Lines changed: 0 additions & 11 deletions
This file was deleted.
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
parameters:
2+
- name: azureSubscription
3+
type: string
4+
default: 'Office-React-Native-Windows-Bot'
5+
- name: endpointId
6+
type: string
7+
- name: nugetFeedUrl
8+
type: string
9+
- name: packageParentPath
10+
type: string
11+
- name: packagesToPush
12+
type: string
13+
- name: publishFeedCredentials
14+
type: string
15+
- name: feedDisplayName
16+
type: string
17+
18+
steps:
19+
- script: dir /S "${{ parameters.packageParentPath }}"
20+
displayName: Show NuGet packages before cleanup
21+
22+
- task: AzureCLI@2
23+
displayName: Override NuGet credentials with Managed Identity
24+
inputs:
25+
azureSubscription: ${{ parameters.azureSubscription }}
26+
visibleAzLogin: false
27+
scriptType: 'pscore'
28+
scriptLocation: 'inlineScript'
29+
inlineScript: |
30+
$accessToken = az account get-access-token --query accessToken --resource 499b84ac-1321-427f-aa17-267ca6975798 -o tsv
31+
# Set the access token as a secret, so it doesn't get leaked in the logs
32+
Write-Host "##vso[task.setsecret]$accessToken"
33+
# Override the apitoken of the nuget service connection, for the duration of this stage
34+
Write-Host "##vso[task.setendpoint id=${{ parameters.endpointId }};field=authParameter;key=apitoken]$accessToken"
35+
# Also expose the token for the pre-push duplicate check
36+
Write-Host "##vso[task.setvariable variable=NuGetAccessToken;issecret=true]$accessToken"
37+
38+
- powershell: |
39+
Add-Type -AssemblyName System.IO.Compression.FileSystem
40+
41+
$feedUrl = "${{ parameters.nugetFeedUrl }}"
42+
$token = "$(NuGetAccessToken)"
43+
$headers = @{ Authorization = "Bearer $token" }
44+
45+
# Discover the flat container (PackageBaseAddress) URL from the V3 index
46+
$index = Invoke-RestMethod -Uri $feedUrl -Headers $headers
47+
$baseAddress = ($index.resources |
48+
Where-Object { $_.'@type' -like 'PackageBaseAddress*' } |
49+
Select-Object -First 1).'@id'
50+
if (-not $baseAddress) { throw "Could not find PackageBaseAddress in NuGet V3 index at $feedUrl" }
51+
if (-not $baseAddress.EndsWith('/')) { $baseAddress += '/' }
52+
Write-Host "PackageBaseAddress: $baseAddress"
53+
54+
$nupkgs = Get-ChildItem -Path "${{ parameters.packageParentPath }}" -Filter "*.nupkg" -Recurse
55+
$removedCount = 0
56+
57+
foreach ($file in $nupkgs) {
58+
# Read the .nuspec from inside the nupkg (zip) to get the exact id and version
59+
$zip = [System.IO.Compression.ZipFile]::OpenRead($file.FullName)
60+
try {
61+
$nuspecEntry = $zip.Entries | Where-Object { $_.FullName -like "*.nuspec" } | Select-Object -First 1
62+
$reader = New-Object System.IO.StreamReader($nuspecEntry.Open())
63+
[xml]$nuspec = $reader.ReadToEnd()
64+
$reader.Close()
65+
} finally { $zip.Dispose() }
66+
67+
$id = $nuspec.package.metadata.id
68+
$version = $nuspec.package.metadata.version
69+
70+
# Query the flat container for all published versions of this package
71+
$versionsUrl = "${baseAddress}$($id.ToLower())/index.json"
72+
try {
73+
$result = Invoke-RestMethod -Uri $versionsUrl -Headers $headers -ErrorAction Stop
74+
if ($version.ToLower() -in $result.versions) {
75+
Write-Host " SKIP $id $version — already on feed"
76+
Remove-Item $file.FullName
77+
$removedCount++
78+
continue
79+
}
80+
} catch {
81+
if ($_.Exception.Response.StatusCode -eq [System.Net.HttpStatusCode]::NotFound) {
82+
# Package has never been published — keep it
83+
} else { throw }
84+
}
85+
Write-Host " PUSH $id $version — new"
86+
}
87+
88+
$remaining = (Get-ChildItem -Path "${{ parameters.packageParentPath }}" -Filter "*.nupkg" -Recurse).Count
89+
Write-Host "Removed $removedCount already-published package(s). $remaining package(s) to push."
90+
Write-Host "##vso[task.setvariable variable=HasNewPackages]$($remaining -gt 0)"
91+
displayName: Remove already-published packages
92+
93+
- script: dir /S "${{ parameters.packageParentPath }}"
94+
displayName: Show NuGet packages after cleanup
95+
96+
- task: 1ES.PublishNuGet@1
97+
displayName: 'NuGet push to ${{ parameters.feedDisplayName }}'
98+
condition: and(succeeded(), eq(variables['HasNewPackages'], 'True'))
99+
inputs:
100+
useDotNetTask: true
101+
packageParentPath: '${{ parameters.packageParentPath }}'
102+
packagesToPush: '${{ parameters.packagesToPush }}'
103+
nuGetFeedType: external
104+
publishFeedCredentials: '${{ parameters.publishFeedCredentials }}'
105+
externalEndpoint: '${{ parameters.publishFeedCredentials }}'
106+
publishPackageMetadata: true

0 commit comments

Comments
 (0)