Repository navigation
619 lines (577 loc) · 29.2 KB
/
Copy pathrelease.yml
File metadata and controls
619 lines (577 loc) · 29.2 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
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
name: release
# Build, sign, and publish a Redrob Code release.
#
# Versions are ours and start at 0.1.0. They are NOT derived from the upstream version
# this tree is built on: that provenance lives in `UPSTREAM_VERSION` at the repository
# root, which the release notes read. See docs/VERSIONING.md for why the old
# `1.18.31-redrob.N` shape was retired.
#
# `workflow_dispatch` ONLY. This job reaches real signing credentials, so it must never be
# reachable from a pull request, a comment, or a schedule. Dispatch requires write access
# to this repository, which is the access control.
#
# It does not push to `main`. The last step opens a pull request from `develop` into
# `main`, because `main` is what people believe is released and advancing it deserves the
# same review as anything else -- it is also the one branch where an unreviewed automated
# push would be hard to notice. See docs/BRANCHING.md.
on:
workflow_dispatch:
inputs:
bump:
description: "Which part of the version to advance."
required: true
default: "patch"
type: choice
options:
- patch
- minor
- major
dry_run:
description: "Build and sign, but publish nothing."
required: false
default: false
type: boolean
permissions:
contents: write
# For actions/attest-build-provenance. Provenance is free to produce and is the one
# thing that lets someone verify an artifact came from this repository and this workflow
# rather than from a machine that had the signing keys.
id-token: write
attestations: write
concurrency:
group: release-${{ inputs.bump }}
cancel-in-progress: false
jobs:
version:
runs-on: ubuntu-latest
# Pinned to this repository's immutable id, not its name. A fork must never run this,
# and a rename must not silently switch the check off -- a false `if:` SKIPS a job
# rather than failing it, which is invisible behind a green checkmark.
if: github.repository_id == '1371675259'
outputs:
version: ${{ steps.compute.outputs.version }}
tag: ${{ steps.compute.outputs.tag }}
upstream: ${{ steps.compute.outputs.upstream }}
# The one commit this release is. Every later job checks THIS out, not `develop`:
# a merge that lands while the build runs must not make the tag point at code the
# binaries were not built from (it did, once, with a merge two minutes after dispatch).
sha: ${{ steps.compute.outputs.sha }}
steps:
# Tags are the version state, so this job needs them. `fetch-tags` alone is not
# enough with a shallow checkout: the tag objects arrive without the commits they
# point at and `--sort=-v:refname` then has nothing to order.
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
ref: develop
fetch-depth: 0
- id: compute
env:
BUMP: ${{ inputs.bump }}
run: |
set -euo pipefail
# Only plain `vMAJOR.MINOR.PATCH` counts. Anything carrying a `-` is a prerelease
# or a legacy upstream-derived tag, and letting one of those win here would make
# the next version a continuation of a line we deliberately left behind.
latest="$(git tag --list 'v[0-9]*.[0-9]*.[0-9]*' --sort=-v:refname \
| grep -v -- '-' | head -n1 || true)"
if [ -z "$latest" ]; then
# No release yet. A `minor` bump from here is 0.1.0, which is the first release.
current="0.0.0"
echo "::notice::no existing release tag; bumping from $current"
else
current="${latest#v}"
echo "::notice::newest release tag is $latest"
fi
major="${current%%.*}"
rest="${current#*.}"
minor="${rest%%.*}"
patch="${rest#*.}"
case "$BUMP" in
major) major=$((major + 1)); minor=0; patch=0 ;;
minor) minor=$((minor + 1)); patch=0 ;;
patch) patch=$((patch + 1)) ;;
*) echo "::error::unknown bump '$BUMP'"; exit 1 ;;
esac
version="${major}.${minor}.${patch}"
if git rev-parse --verify --quiet "refs/tags/v$version" >/dev/null; then
echo "::error::v$version already exists; a release cannot be recomputed onto an existing tag"
exit 1
fi
# The upstream version the tag no longer carries. Absent is a defect rather than
# a blank field: the release notes state what this is built on, and a release
# that cannot say so should not be published.
if [ ! -s UPSTREAM_VERSION ]; then
echo "::error::UPSTREAM_VERSION is missing or empty; see docs/VERSIONING.md"
exit 1
fi
upstream="$(tr -d '[:space:]' < UPSTREAM_VERSION)"
echo "version=$version" >> "$GITHUB_OUTPUT"
echo "tag=v$version" >> "$GITHUB_OUTPUT"
echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
echo "upstream=$upstream" >> "$GITHUB_OUTPUT"
echo "::notice::releasing v$version (built on opencode v$upstream)"
build:
needs: version
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
ref: ${{ needs.version.outputs.sha }}
fetch-depth: 1
- uses: ./.github/actions/setup-bun
# Bun cross-compiles every target from one host, so there is no build matrix and
# every artifact in a release comes from a single commit and a single toolchain.
- name: Build
run: |
./packages/redrob/script/build.ts
env:
REDROB_VERSION: ${{ needs.version.outputs.version }}
GH_TOKEN: ${{ github.token }}
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: cli-unsigned
path: |
packages/redrob/dist/redrob-darwin*
packages/redrob/dist/redrob-linux*
packages/redrob/dist/redrob-windows*
sign-macos:
needs: [version, build]
runs-on: macos-14
env:
APPLE_CODESIGN_CERT_P12_BASE64: ${{ secrets.APPLE_CODESIGN_CERT_P12_BASE64 }}
APPLE_CODESIGN_CERT_PASSWORD: ${{ secrets.APPLE_CODESIGN_CERT_PASSWORD }}
APPLE_NOTARY_API_KEY_P8_BASE64: ${{ secrets.APPLE_NOTARY_API_KEY_P8_BASE64 }}
APPLE_NOTARY_API_KEY_ID: ${{ secrets.APPLE_NOTARY_API_KEY_ID }}
APPLE_NOTARY_API_ISSUER_ID: ${{ secrets.APPLE_NOTARY_API_ISSUER_ID }}
steps:
# Only the installer scripts are needed from the repository (sparse: one directory).
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
sparse-checkout: |
installer
sparse-checkout-cone-mode: true
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: cli-unsigned
path: dist
# Fail here rather than three steps later inside codesign, where the error is about
# a keychain and says nothing about the missing secret that caused it.
- name: Require the Apple credentials
run: |
set -euo pipefail
missing=()
for name in \
APPLE_CODESIGN_CERT_P12_BASE64 \
APPLE_CODESIGN_CERT_PASSWORD \
APPLE_NOTARY_API_KEY_P8_BASE64 \
APPLE_NOTARY_API_KEY_ID \
APPLE_NOTARY_API_ISSUER_ID; do
if [ -z "${!name:-}" ]; then missing+=("$name"); fi
done
if [ "${#missing[@]}" -gt 0 ]; then
echo "::error::missing macOS signing secrets: ${missing[*]}"
exit 1
fi
- name: Import the Developer ID certificate
run: |
set -euo pipefail
p12="$RUNNER_TEMP/signing.p12"
keychain="$RUNNER_TEMP/signing.keychain-db"
keychain_password="$(openssl rand -hex 24)"
printf '%s' "$APPLE_CODESIGN_CERT_P12_BASE64" | base64 --decode > "$p12"
security create-keychain -p "$keychain_password" "$keychain"
security set-keychain-settings -lut 21600 "$keychain"
security unlock-keychain -p "$keychain_password" "$keychain"
security import "$p12" -k "$keychain" -P "$APPLE_CODESIGN_CERT_PASSWORD" -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple: -s -k "$keychain_password" "$keychain"
security list-keychains -d user -s "$keychain"
identity="$(security find-identity -v -p codesigning "$keychain" | awk '/Developer ID Application/ { print $2; exit }')"
if [ -z "$identity" ]; then
echo "::error::no Developer ID Application identity in the imported certificate"
exit 1
fi
printf '%s' "$APPLE_NOTARY_API_KEY_P8_BASE64" | base64 --decode > "$RUNNER_TEMP/AuthKey.p8"
chmod 600 "$RUNNER_TEMP/AuthKey.p8"
{
echo "APPLE_SIGNING_IDENTITY=$identity"
echo "APPLE_SIGNING_KEYCHAIN=$keychain"
} >> "$GITHUB_ENV"
- name: Sign and notarize
run: |
set -euo pipefail
shopt -s nullglob
binaries=(dist/redrob-darwin-*/bin/redrob)
if [ "${#binaries[@]}" -lt 2 ]; then
echo "::error::expected darwin arm64 and x64 binaries, found ${#binaries[@]}"
exit 1
fi
for binary in "${binaries[@]}"; do
chmod 755 "$binary"
# --options runtime is the hardened runtime, without which notarization is
# rejected. --timestamp is required for the signature to outlive the cert.
codesign --force --options runtime --timestamp \
--keychain "$APPLE_SIGNING_KEYCHAIN" --sign "$APPLE_SIGNING_IDENTITY" "$binary"
codesign --verify --strict --verbose=2 "$binary"
archive="$RUNNER_TEMP/$(basename "$(dirname "$(dirname "$binary")")").zip"
ditto -c -k --keepParent "$binary" "$archive"
xcrun notarytool submit "$archive" \
--key "$RUNNER_TEMP/AuthKey.p8" \
--key-id "$APPLE_NOTARY_API_KEY_ID" \
--issuer "$APPLE_NOTARY_API_ISSUER_ID" \
--wait
done
- name: Repack
working-directory: dist
run: |
set -euo pipefail
for directory in redrob-darwin-*; do
[ -d "$directory" ] || continue
(cd "$directory/bin" && zip -qr "../../${directory}.zip" .)
done
# ── The disk images ───────────────────────────────────────────────────────────────────
#
# Redrob Launcher installs macOS apps only from a DMG holding a signed `.app`, so the signed
# binary of each architecture is wrapped in `Redrob Code.app` (installer/macos/make-dmg.sh),
# the bundle signed, the image notarized and stapled. The launcher then links
# ~/.redrob/bin/redrob to the binary inside, where every app looks for the engine.
- name: Build, notarize and staple the disk images
env:
VERSION: ${{ needs.version.outputs.version }}
run: |
set -euo pipefail
for pair in "redrob-darwin-arm64:macos-aarch64" "redrob-darwin-x64:macos-x86_64"; do
directory="${pair%%:*}"; token="${pair##*:}"
dmg="$(bash installer/macos/make-dmg.sh "dist/$directory/bin/redrob" "$VERSION" "$token" dist)"
xcrun notarytool submit "$dmg" \
--key "$RUNNER_TEMP/AuthKey.p8" \
--key-id "$APPLE_NOTARY_API_KEY_ID" \
--issuer "$APPLE_NOTARY_API_ISSUER_ID" \
--wait
xcrun stapler staple "$dmg"
xcrun stapler validate "$dmg"
echo "$dmg notarized and stapled"
done
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: release-macos
path: |
dist/redrob-darwin-*.zip
dist/redrob-code-*-macos-*.dmg
if-no-files-found: error
# `if: always()` so a failed notarization still takes the private key off the runner.
- name: Delete the signing keychain
if: always()
run: |
if [ -n "${APPLE_SIGNING_KEYCHAIN:-}" ]; then
security delete-keychain "$APPLE_SIGNING_KEYCHAIN" || true
fi
sign-windows:
needs: [version, build]
runs-on: windows-latest
# `release` is the environment the organization's federated credential names
# (repo:redrob-labs@<org id>/<repo>@<id>:environment:release); any other name is refused.
environment: release
permissions:
contents: read
# The OIDC token `azure/login` exchanges for the Artifact Signing credential.
id-token: write
env:
REDROB_WINDOWS_BINARIES: >-
dist/redrob-windows-arm64/bin/redrob.exe
dist/redrob-windows-x64/bin/redrob.exe
dist/redrob-windows-x64-baseline/bin/redrob.exe
REDROB_WINDOWS_INSTALLER: >-
dist/redrob-code-x64-setup.exe
dist/redrob-code-${{ needs.version.outputs.version }}-windows-x64.msi
dist/redrob-code-${{ needs.version.outputs.version }}-windows-arm64.msi
steps:
# Only the installer definition is needed from the repository; the signing recipe is the
# organization's (redrob-labs/.github/actions/sign-windows), not a copy kept here. Sparse,
# because cloning this whole repository to read one directory is a minute of clone for nothing.
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
sparse-checkout: |
installer
sparse-checkout-cone-mode: true
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: cli-unsigned
path: dist
# Azure Artifact Signing, authenticated with OpenID Connect: the job's GitHub token is
# exchanged for an Azure token against the organization's app registration, whose federated
# credential accepts `repo:redrob-labs@<org id>/*:environment:release`. No certificate file
# and no client secret exist anywhere. The login comes AFTER the download and right BEFORE
# signing: GitHub's OIDC token lives minutes, and a login done earlier left signtool without
# a usable credential.
- name: Log in to Azure (OIDC)
uses: azure/login@a641126d1b8aa4d1fa005f4f92df94a3a4c4c906 # v3
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
# The shared action signs, then reads every file back (`signtool verify /pa`, Status Valid,
# signer CN `McKinley Rice, Inc.`) and prints `<file> signed by CN=...`. With the three
# ARTIFACT_SIGNING_* secrets all unset it warns and leaves the files unsigned; half-set is an
# error. A failed signature fails the job: a release never ships an unsigned Windows binary
# behind a green check.
- name: Sign the binaries
uses: redrob-labs/.github/actions/sign-windows@main
with:
files: ${{ env.REDROB_WINDOWS_BINARIES }}
description: Redrob Code
url: https://github.com/${{ github.repository }}
endpoint: ${{ secrets.ARTIFACT_SIGNING_ENDPOINT }}
account: ${{ secrets.ARTIFACT_SIGNING_ACCOUNT }}
cert-profile: ${{ secrets.ARTIFACT_SIGNING_CERT_PROFILE }}
# ── The installer ─────────────────────────────────────────────────────────────────────
#
# Built HERE rather than in `build`, and specifically after the signing step above, because
# the installer embeds the binary: building it earlier would wrap an unsigned redrob.exe in a
# signed installer, so Windows would accept the download and then ship an unsigned program.
# Signing is therefore two-stage -- payload first, wrapper second.
- name: Build the Windows installer
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
# Inno Setup ships on the windows-latest image via Chocolatey. Pinned rather than latest:
# an installer definition is not something to have silently recompiled by a new major.
choco install innosetup --version=6.2.2 --no-progress -y
$iscc = "${env:ProgramFiles(x86)}\Inno Setup 6\ISCC.exe"
if (-not (Test-Path $iscc)) { throw "ISCC.exe not found at $iscc" }
& $iscc `
"/DAppVersion=${{ needs.version.outputs.version }}" `
"/DSourceBin=${{ github.workspace }}\dist\redrob-windows-x64\bin" `
"/O${{ github.workspace }}\dist" `
"${{ github.workspace }}\installer\windows\redrob-code.iss"
if ($LASTEXITCODE -ne 0) { throw "ISCC failed with $LASTEXITCODE" }
$setup = "${{ github.workspace }}\dist\redrob-code-x64-setup.exe"
if (-not (Test-Path $setup)) { throw "the installer was not produced at $setup" }
Write-Host "built $((Get-Item $setup).Length) bytes"
# The MSIs: the one package format Redrob Launcher installs on Windows (per user, into
# %LOCALAPPDATA%\Redrob\bin, PATH appended; installer/windows/redrob-code.wxs). One per
# architecture, each wrapping the binary signed above; signed themselves in the next step.
- name: Build the Windows MSIs
shell: pwsh
env:
VERSION: ${{ needs.version.outputs.version }}
run: |
$ErrorActionPreference = "Stop"
dotnet tool install --global wix --version 5.0.2
# MSI versions are numeric: a prerelease suffix is dropped.
$msiVersion = ($env:VERSION -split '-')[0]
foreach ($pair in @(@('x64', 'redrob-windows-x64'), @('arm64', 'redrob-windows-arm64'))) {
$arch = $pair[0]; $dir = $pair[1]
$out = "${{ github.workspace }}\dist\redrob-code-$env:VERSION-windows-$arch.msi"
wix build "${{ github.workspace }}\installer\windows\redrob-code.wxs" -arch $arch `
-d "Version=$msiVersion" -d "SourceBin=${{ github.workspace }}\dist\$dir\bin" -o $out
if ($LASTEXITCODE -ne 0) { throw "wix build ($arch) failed with $LASTEXITCODE" }
Remove-Item -Force -ErrorAction SilentlyContinue ([IO.Path]::ChangeExtension($out, '.wixpdb'))
if (-not (Test-Path $out)) { throw "the MSI was not produced at $out" }
Write-Host "built $out ($((Get-Item $out).Length) bytes)"
}
# A fresh login right before the second signing point, for the same reason as the first:
# the installer build above takes minutes, and the credential must not be assumed to have
# outlived it.
- name: Log in to Azure again (OIDC)
uses: azure/login@a641126d1b8aa4d1fa005f4f92df94a3a4c4c906 # v3
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Sign the installers
uses: redrob-labs/.github/actions/sign-windows@main
with:
files: ${{ env.REDROB_WINDOWS_INSTALLER }}
description: Redrob Code
url: https://github.com/${{ github.repository }}
endpoint: ${{ secrets.ARTIFACT_SIGNING_ENDPOINT }}
account: ${{ secrets.ARTIFACT_SIGNING_ACCOUNT }}
cert-profile: ${{ secrets.ARTIFACT_SIGNING_CERT_PROFILE }}
- name: Repack
shell: bash
working-directory: dist
run: |
set -euo pipefail
for directory in redrob-windows-*; do
[ -d "$directory" ] || continue
(cd "$directory/bin" && 7z a -tzip "../../${directory}.zip" . >/dev/null)
done
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: release-windows
path: |
dist/redrob-windows-*.zip
dist/redrob-code-x64-setup.exe
dist/redrob-code-*-windows-*.msi
if-no-files-found: error
# The assets only this repository knows how to shape: the Linux archives, the Redrob Launcher
# copies, the `VERSION` marker and the build provenance. Checksums, the launcher manifest and the
# release itself are the organization's job (`publish` below).
assets:
needs: [version, build, sign-macos, sign-windows]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
ref: ${{ needs.version.outputs.sha }}
fetch-depth: 1
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: cli-unsigned
path: unsigned
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
pattern: release-*
path: signed
merge-multiple: true
- name: Pack the Linux binaries
run: |
set -euo pipefail
mkdir -p assets
cd unsigned
for directory in redrob-linux-*; do
[ -d "$directory" ] || continue
(cd "$directory/bin" && zip -qr "../../../assets/${directory}.zip" .)
# Both formats for Linux, and only for Linux. A minimal container routinely has
# `tar` and no `unzip`, and install.sh running on a machine that cannot open the
# archive it just downloaded is the failure this avoids. macOS ships `unzip` and
# the Windows installer uses `Expand-Archive`, so those stay zip-only rather
# than doubling every release's asset count for nothing.
#
# One `..`, not two: this runs from `unsigned/` while the zip above runs from
# `$directory/bin`, so the same destination is a different number of levels up.
tar -czf "../assets/${directory}.tar.gz" -C "$directory/bin" .
done
# Redrob Launcher installs only `<stem>-<version>-<os>-<arch>[-portable].<ext>` with its own
# os-arch spellings (`linux-x86_64`, `macos-aarch64`), which none of the names above are. The
# names above stay -- install.sh, `redrob upgrade`, Homebrew, AUR and the Dockerfile download
# them verbatim -- and the launcher's are added beside them. See the script's header for the
# layout inside; `test/installation/launcher-assets.test.ts` guards both.
#
# Bun alone, without the repository's dependencies: the script imports nothing but Bun and
# Node built-ins, and `bun install` here would cost minutes for nothing.
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version-file: package.json
- name: Add the Redrob Launcher assets
env:
VERSION: ${{ needs.version.outputs.version }}
run: |
set -euo pipefail
bun ./packages/redrob/script/launcher-assets.ts \
--version "$VERSION" --unsigned unsigned --signed signed --out assets
# The marker install.sh and `redrob upgrade` read to answer "what is the newest
# version". `releases/latest/download/VERSION` resolves to whichever release is newest,
# so no client has to hardcode a tag and nothing here consults the GitHub API, which is
# rate limited to 60 requests an hour for an anonymous caller.
- name: Write the latest-version marker
working-directory: assets
env:
VERSION: ${{ needs.version.outputs.version }}
run: printf '%s\n' "$VERSION" > VERSION
# Lets anyone verify an artifact was produced by this repository and this workflow,
# which a code signature alone does not establish -- a signature says the key was
# used, not where.
- name: Attest build provenance
uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
with:
subject-path: |
signed/redrob-*.zip
signed/redrob-code-*.msi
signed/redrob-code-*.dmg
assets/redrob-*.zip
assets/redrob-*.tar.gz
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: release-linux-and-launcher
path: assets/*
if-no-files-found: error
# The release is dispatch-driven, so the workflow creates the tag itself: `publish` below
# releases exactly this tag at exactly the commit `version` resolved.
tag:
needs: [version, build, sign-macos, sign-windows, assets]
if: ${{ !inputs.dry_run }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
ref: ${{ needs.version.outputs.sha }}
fetch-depth: 0
- name: Create and push the tag
env:
TAG: ${{ needs.version.outputs.tag }}
UPSTREAM: ${{ needs.version.outputs.upstream }}
run: |
set -euo pipefail
# An annotated tag records a tagger, and a bare runner has no git identity, so
# `git tag -a` fails with "empty ident name" AFTER the build, the signing and the
# notarization have all succeeded -- the most expensive possible place to
# discover it. The tag stays annotated rather than lightweight because it is what
# a release points at and it should say who made it and when, and its message
# carries the upstream version the release notes no longer state.
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git tag -a "$TAG" -m "Redrob Code $TAG (built on opencode v$UPSTREAM)" 2>/dev/null || \
echo "tag $TAG already exists locally"
# A release that fails after tagging must be retryable. Without this a single
# transient failure in the publish job leaves the version permanently unusable:
# the tag exists, so every retry dies on push before reaching the release.
#
# The tag is named explicitly. `--tags` and `--follow-tags` would push whatever
# else happens to be in this checkout's tag namespace, which is how a tag nobody
# decided to publish ends up published.
git push origin "refs/tags/$TAG" 2>/dev/null || echo "tag $TAG is already on the remote"
- name: Point at the promotion to main
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ needs.version.outputs.tag }}
run: |
set -euo pipefail
# Deliberately does NOT open the pull request itself.
#
# `gh pr create` from a workflow needs "Allow GitHub Actions to create and
# approve pull requests", and that one setting grants BOTH. In a public
# repository, letting the Actions token approve pull requests means a workflow
# can approve its own, which is worth more than the convenience of not clicking
# a link. So this prints the link and a person opens it.
#
# Compared against the remote rather than a local ref: this job checks out one
# commit, so `origin/main` does not exist here and a `git log origin/main..HEAD`
# would come back empty and read as "nothing to promote" -- passing for the wrong
# reason, which is how the previous version of this step silently did nothing.
main_sha="$(git ls-remote origin refs/heads/main | cut -f1)"
if [ -z "$main_sha" ]; then
echo "::notice::no main branch yet; create it from $TAG to start tracking released state"
exit 0
fi
if [ "$main_sha" = "$(git rev-parse HEAD)" ]; then
echo "::notice::main already matches develop; nothing to promote"
exit 0
fi
url="${{ github.server_url }}/${{ github.repository }}/compare/main...develop?expand=1"
{
echo "### Promote \`develop\` to \`main\` for $TAG"
echo
echo "The release is being published. \`main\` still points at \`${main_sha:0:9}\`, so what"
echo "the repository says is *released* has not moved yet."
echo
echo "[Open the promotion pull request]($url)"
} >> "$GITHUB_STEP_SUMMARY"
echo "::notice::promote develop to main at $url"
# Checksums (`SHA256SUMS.txt`), the launcher manifest (`redrob-launcher.json`) and the release
# itself, one way for every redrob-labs repository. Published as latest; a version carrying a `-`
# is a prerelease. Not on a dry run, and not when any build job failed, was cancelled or skipped.
# The tag is passed explicitly because this workflow runs from a dispatch, not from a tag push.
publish:
needs: [version, build, sign-macos, sign-windows, assets, tag]
if: always() && !inputs.dry_run && !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') && !contains(needs.*.result, 'skipped')
uses: redrob-labs/.github/.github/workflows/release-publish.yml@main
permissions:
contents: write
with:
version: ${{ needs.version.outputs.version }}
title: Redrob Code
launcher_manifest: true
publish: true
tag: ${{ needs.version.outputs.tag }}
target: ${{ needs.version.outputs.sha }}