Repository navigation
342 lines (306 loc) · 14.4 KB
/
Copy pathrelease-cli.yml
File metadata and controls
342 lines (306 loc) · 14.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
# Releases the `chopin` CLI. Pushing a cli-v<version> tag builds the five binaries, scans the
# Windows one with Microsoft Defender, checks that the chopin-cli-release environment is
# protected, and then waits for a person to approve that environment before attesting and
# publishing them. Running it by hand builds and scans a version without publishing anything.
# See docs/releasing-cli.md.
name: release-cli
on:
push:
tags: ["cli-v*"]
workflow_dispatch:
inputs:
version:
description: Version to build and scan without publishing; it must match apps/connector/package.json (for example 0.2.0)
required: true
type: string
# Never cancel a release half-way; a second run for the same tag waits instead.
concurrency:
group: release-cli-${{ github.ref }}
cancel-in-progress: false
permissions: {}
env:
BUN_VERSION: 1.4.2
jobs:
build:
name: build
runs-on: ubuntu-latest
permissions:
contents: read
outputs:
version: ${{ steps.version.outputs.version }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Check the version against apps/connector/package.json
id: version
env:
INPUT_VERSION: ${{ inputs.version }}
run: |
if [ "$GITHUB_EVENT_NAME" = push ]; then
version="${GITHUB_REF_NAME#cli-v}"
else
version="${INPUT_VERSION#cli-}"
version="${version#v}"
fi
package="$(jq -r .version apps/connector/package.json)"
if [ "$version" != "$package" ]; then
echo "::error::The release is $version, but apps/connector/package.json says $package."
exit 1
fi
echo "version=$version" >> "$GITHUB_OUTPUT"
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version: ${{ env.BUN_VERSION }}
- run: bun install --frozen-lockfile
- run: bun run --cwd apps/connector build:binaries
- name: Check the linux-x64 binary reports the release version
env:
VERSION: ${{ steps.version.outputs.version }}
run: |
reported="$(apps/connector/dist/chopin-linux-x64 --version)"
echo "$reported"
case "$reported" in
"chopin $VERSION" | "chopin $VERSION "*) ;;
*) echo "::error::Expected chopin $VERSION."; exit 1 ;;
esac
- name: Collect the release assets and write checksums.txt
run: |
mkdir release
cp apps/connector/dist/chopin-* apps/connector/install/install.sh apps/connector/install/install.ps1 release/
cd release
sha256sum -- * > checksums.txt
cat checksums.txt
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: chopin-cli-release
path: release/
if-no-files-found: error
retention-days: 7
defender:
name: Microsoft Defender scan
needs: build
runs-on: windows-latest
permissions: {}
steps:
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: chopin-cli-release
path: release
- name: Verify and scan the Windows binaries
shell: pwsh
run: |
$binaries = @(Get-ChildItem -Path release -Filter '*.exe' -File)
if ($binaries.Count -eq 0) { Write-Host '::error::No Windows binaries were built.'; exit 1 }
$expected = @{}
foreach ($line in Get-Content -LiteralPath release/checksums.txt) {
if ($line -match '^([0-9a-fA-F]{64})\s+\*?(\S+)\s*$') { $expected[$Matches[2]] = $Matches[1].ToLowerInvariant() }
}
foreach ($binary in $binaries) {
$actual = (Get-FileHash -LiteralPath $binary.FullName -Algorithm SHA256).Hash.ToLowerInvariant()
if ($expected[$binary.Name] -ne $actual) {
Write-Host "::error::$($binary.Name) is $actual, but checksums.txt says '$($expected[$binary.Name])'."
exit 1
}
Write-Host "Verified $($binary.Name): $actual"
}
$mpCmdRun = Join-Path $env:ProgramFiles 'Windows Defender\MpCmdRun.exe'
if (-not (Test-Path -LiteralPath $mpCmdRun)) { Write-Host "::error::Microsoft Defender is not installed ($mpCmdRun)."; exit 1 }
# The runner image's signatures can be weeks old, and the update endpoint is sometimes slow.
$updated = $false
for ($attempt = 1; $attempt -le 3 -and -not $updated; $attempt++) {
Write-Host "Updating Microsoft Defender signatures (attempt $attempt of 3)"
& $mpCmdRun -SignatureUpdate
$updated = $LASTEXITCODE -eq 0
if (-not $updated -and $attempt -lt 3) { Start-Sleep -Seconds 15 }
}
if (-not $updated) { Write-Host '::error::Could not update Microsoft Defender signatures.'; exit 1 }
$status = Get-MpComputerStatus -ErrorAction SilentlyContinue
if ($status) { Write-Host "Signatures $($status.AntivirusSignatureVersion), updated $($status.AntivirusSignatureLastUpdated); engine $($status.AMEngineVersion)" }
# Scan a copy outside the workspace, so a workspace exclusion cannot skip it.
$scanDirectory = Join-Path $env:RUNNER_TEMP 'defender-scan'
New-Item -ItemType Directory -Path $scanDirectory -Force | Out-Null
$failed = $false
foreach ($binary in $binaries) {
$copy = Join-Path $scanDirectory $binary.Name
Copy-Item -LiteralPath $binary.FullName -Destination $copy -Force
if ((Get-FileHash -LiteralPath $copy -Algorithm SHA256).Hash.ToLowerInvariant() -ne $expected[$binary.Name]) {
Write-Host "::error::The copy of $($binary.Name) does not match checksums.txt."
$failed = $true
continue
}
# The service sometimes reports 0x800106ba while it restarts; that is retried.
for ($attempt = 1; $attempt -le 3; $attempt++) {
$output = @(& $mpCmdRun -Scan -ScanType 3 -File $copy -DisableRemediation 2>&1 | ForEach-Object { "$_" })
$code = $LASTEXITCODE
if ($code -eq 0 -or ($output -join "`n") -notmatch '0x800106ba' -or $attempt -eq 3) { break }
Start-Sleep -Seconds 15
}
$output | ForEach-Object { Write-Host $_ }
$text = $output -join "`n"
if ($code -ne 0) {
Write-Host "::error::Microsoft Defender flagged or failed to scan $($binary.Name) (exit code $code)."
$failed = $true
} elseif ($text -match '(?i)was skipped|cannot be scanned|not performed|excluded') {
Write-Host "::error::Microsoft Defender did not scan $($binary.Name)."
$failed = $true
} elseif ($text -match '\bThreat\b') {
Write-Host "::error::Microsoft Defender reported a threat in $($binary.Name)."
$failed = $true
} elseif ($text -notmatch 'Scan starting' -or $text -notmatch 'Scan finished') {
Write-Host "::error::Could not confirm Microsoft Defender finished scanning $($binary.Name)."
$failed = $true
} else {
Write-Host "Microsoft Defender found nothing in $($binary.Name)."
}
}
if ($failed) { exit 1 }
# SPDX and CycloneDX SBOMs of the tagged source's lockfile and the built binaries, kept as
# workflow artifacts. The third-party SBOM action runs here, where the token can only read.
sbom:
name: SBOMs
needs: build
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: chopin-cli-release
path: release
- name: Verify the assets against checksums.txt
working-directory: release
run: sha256sum --check --strict checksums.txt
- uses: anchore/sbom-action@66cbf4bc1f1c0d2edc94016e65bc221b6bb0ad6c # v0.24.3
with:
path: .
format: spdx-json
output-file: sbom.spdx.json
upload-artifact: false
upload-release-assets: false
- uses: anchore/sbom-action@66cbf4bc1f1c0d2edc94016e65bc221b6bb0ad6c # v0.24.3
with:
path: .
format: cyclonedx-json
output-file: sbom.cdx.json
upload-artifact: false
upload-release-assets: false
- name: Audit the SBOMs for secrets
run: |
pattern='GITHUB_TOKEN|SECRET|PASSWORD|API_KEY|PRIVATE KEY|PRIVATE_KEY|gh[pousr]_[A-Za-z0-9]{36}|github_pat_[A-Za-z0-9_]{20,}|AKIA[0-9A-Z]{16}'
if grep -nE "$pattern" sbom.spdx.json sbom.cdx.json; then
echo "::error::An SBOM contains what looks like a secret."
exit 1
fi
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: chopin-cli-sbom
path: |-
sbom.spdx.json
sbom.cdx.json
if-no-files-found: error
# GitHub creates an environment that a job names but that does not exist, without any
# protection, so publish waits for this check that chopin-cli-release requires a reviewer
# and only lets cli-v* tags deploy. It fails closed on anything else.
preflight:
name: Check the chopin-cli-release environment
if: github.event_name == 'push'
runs-on: ubuntu-latest
# Reading an environment and its deployment branch policies needs actions: read.
permissions:
actions: read
steps:
- name: Require a reviewer and only cli-v* tags on chopin-cli-release
env:
GH_TOKEN: ${{ github.token }}
ENVIRONMENT: chopin-cli-release
run: |
settings="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/settings/environments"
fail() {
echo "::error::$1 A repository administrator must set it up in $settings as docs/releasing-cli.md describes. Nothing was published."
exit 1
}
scratch="$(mktemp -d)"
get() {
curl -sS --retry 3 -o "$scratch/$2" -w '%{http_code}' \
-H "Authorization: Bearer $GH_TOKEN" \
-H 'Accept: application/vnd.github+json' \
"$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/environments/$ENVIRONMENT$1"
}
status="$(get '' environment.json)" || status=unreachable
case "$status" in
200) ;;
404) fail "The $ENVIRONMENT environment does not exist, and GitHub would create it without required reviewers or a cli-v* tag policy." ;;
*) fail "Could not read the $ENVIRONMENT environment (HTTP $status)." ;;
esac
jq -e '[.protection_rules[]? | select(.type == "required_reviewers") | .reviewers[]?] | length > 0' \
"$scratch/environment.json" >/dev/null \
|| fail "The $ENVIRONMENT environment has no required reviewers, so nobody would approve the release."
jq -e '.deployment_branch_policy.custom_branch_policies == true' "$scratch/environment.json" >/dev/null \
|| fail "The $ENVIRONMENT environment does not limit deployments to selected tags; only the cli-v* tag may deploy."
status="$(get '/deployment-branch-policies?per_page=100' policies.json)" || status=unreachable
[ "$status" = 200 ] || fail "Could not read the deployment policies of the $ENVIRONMENT environment (HTTP $status); only the cli-v* tag may deploy."
jq -e '(.branch_policies | length) == .total_count and (.branch_policies | length > 0 and all(.type == "tag" and .name == "cli-v*"))' \
"$scratch/policies.json" >/dev/null \
|| fail "Only the cli-v* tag may deploy to the $ENVIRONMENT environment, but its deployment policies are $(jq -c '[.branch_policies[]? | {name, type}]' "$scratch/policies.json")."
echo "$ENVIRONMENT requires a reviewer, and only cli-v* tags may deploy to it."
publish:
name: publish
needs: [build, defender, sbom, preflight]
# A manual run is a dry run: it builds and scans, and publishes nothing.
if: github.event_name == 'push'
runs-on: ubuntu-latest
# Required reviewers on this environment are the release's human approval.
environment: chopin-cli-release
permissions:
contents: write
id-token: write
attestations: write
env:
VERSION: ${{ needs.build.outputs.version }}
steps:
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: chopin-cli-release
path: release
- name: Verify the assets against checksums.txt
working-directory: release
run: sha256sum --check --strict checksums.txt
- uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
with:
subject-path: |-
release/chopin-linux-x64
release/chopin-linux-arm64
release/chopin-darwin-x64
release/chopin-darwin-arm64
release/chopin-windows-x64.exe
- name: Create the GitHub release
env:
GH_TOKEN: ${{ github.token }}
run: |
notes="Install this version of the chopin CLI on Linux or macOS:
\`\`\`sh
curl -fsSL https://github.com/$GITHUB_REPOSITORY/releases/download/cli-v$VERSION/install.sh | sh -s -- $VERSION
\`\`\`
or on Windows:
\`\`\`powershell
\$env:CHOPIN_VERSION = '$VERSION'; irm https://github.com/$GITHUB_REPOSITORY/releases/download/cli-v$VERSION/install.ps1 | iex
\`\`\`
Every binary has a build provenance attestation: \`gh attestation verify <file> -R $GITHUB_REPOSITORY\`."
gh release create "cli-v$VERSION" \
--repo "$GITHUB_REPOSITORY" \
--verify-tag \
--latest \
--title "chopin CLI $VERSION" \
--notes "$notes" \
release/chopin-linux-x64 \
release/chopin-linux-arm64 \
release/chopin-darwin-x64 \
release/chopin-darwin-arm64 \
release/chopin-windows-x64.exe \
release/checksums.txt \
release/install.sh \
release/install.ps1