-
-
Notifications
You must be signed in to change notification settings - Fork 1
425 lines (380 loc) · 16.7 KB
/
Copy pathrelease.yml
File metadata and controls
425 lines (380 loc) · 16.7 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
name: Release
# Dispatch with a bump (npm's names, or an exact version), or push a v* tag.
# A version with a "-" becomes a GitHub pre-release: invisible to releases/latest, so the updater
# feed never offers it.
# The release stays a draft until every bundle has been read back; `publish` lifts it.
# Nothing is pushed until then: the bump is applied to each checkout, and the bump commit and tag
# land on the branch only after every platform and the update feed have passed. A failed run
# leaves the branch and tags untouched, plus a draft release that a re-run reuses.
# TAURI_SIGNING_PRIVATE_KEY is required. Apple secrets are optional; without them macOS bundles are
# unsigned.
# The web app is deployed last, by a Cloudflare Pages deploy hook (CLOUDFLARE_DEPLOY_HOOK), and
# only for a mainline release: a pre-release deploys nothing.
on:
workflow_dispatch:
inputs:
bump:
description: 'Version bump'
type: choice
default: none
options:
- none
- patch
- minor
- major
- prerelease
- prepatch
- preminor
- premajor
version:
description: 'Exact version instead of a bump, e.g. 0.2.0-rc.1'
type: string
default: ''
preid:
description: 'Pre-release identifier for the pre* bumps'
type: string
default: 'rc'
push:
tags:
- 'v*'
# Queue, never cancel: a half-cancelled run leaves a tag with no bundles.
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false
permissions:
contents: write
jobs:
prepare:
name: Version & tag
runs-on: ubuntu-22.04
outputs:
release: ${{ steps.version.outputs.release }}
bumped: ${{ steps.version.outputs.bumped }}
version: ${{ steps.version.outputs.version }}
tag: ${{ steps.version.outputs.tag }}
prerelease: ${{ steps.version.outputs.prerelease }}
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
# Resolves the bump to a version string. The files it edits are thrown away with this
# checkout; `build` and `publish` re-apply the exact version.
- name: Work out the version
id: version
env:
EVENT: ${{ github.event_name }}
REF_NAME: ${{ github.ref_name }}
BUMP: ${{ inputs.bump }}
EXACT: ${{ inputs.version }}
PREID: ${{ inputs.preid }}
run: |
set -euo pipefail
release=true
bumped=false
if [ "$EVENT" = "push" ]; then
# Hand-pushed tag: the tree must already hold this version.
version="${REF_NAME#v}"
in_tree=$(node -p "require('./package.json').version")
if [ "$version" != "$in_tree" ]; then
echo "::error::tag $REF_NAME names version $version but the tree is at $in_tree. Cut releases with Run workflow, or bump and commit before tagging."
exit 1
fi
node scripts/sync-version.mjs --check
elif [ "$BUMP" = "none" ] && [ -z "$EXACT" ]; then
release=false
version=$(node -p "require('./package.json').version")
echo "No bump requested: building every platform, releasing nothing."
else
npm version "${EXACT:-$BUMP}" --preid "$PREID" --no-git-tag-version > /dev/null
version=$(node -p "require('./package.json').version")
bumped=true
fi
case "$version" in
*-*) prerelease=true ;;
*) prerelease=false ;;
esac
echo "version $version (release=$release, prerelease=$prerelease)"
{
echo "release=$release"
echo "bumped=$bumped"
echo "version=$version"
echo "tag=v$version"
echo "prerelease=$prerelease"
} >> "$GITHUB_OUTPUT"
# Early, so a doomed run does not spend three platforms of build minutes.
- name: The tag has to be new
if: steps.version.outputs.bumped == 'true'
env:
TAG: ${{ steps.version.outputs.tag }}
run: |
set -euo pipefail
if git ls-remote --exit-code --tags origin "refs/tags/$TAG" > /dev/null 2>&1; then
echo "::error::$TAG already exists. Pick a different bump, or delete the tag and its release first."
exit 1
fi
build:
needs: prepare
strategy:
fail-fast: false
matrix:
include:
# One universal macOS leg; tauri-action lists it under both darwin targets in latest.json.
- platform: macos-latest # universal .app + .dmg
args: '--target universal-apple-darwin'
bundle: src-tauri/target/universal-apple-darwin/release/bundle
- platform: ubuntu-22.04 # AppImage + .deb + .rpm
args: ''
bundle: src-tauri/target/release/bundle
- platform: windows-latest # NSIS .exe
args: ''
bundle: src-tauri/target/release/bundle
runs-on: ${{ matrix.platform }}
env:
# A step's `if:` cannot read secrets. An unset secret is "", and the bundler treats an empty
# APPLE_CERTIFICATE as a key.
SIGN_MACOS: ${{ secrets.APPLE_CERTIFICATE != '' }}
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: ./.github/actions/linux-deps
if: matrix.platform == 'ubuntu-22.04'
with:
bundling: 'true'
- uses: ./.github/actions/setup-node
with:
install: 'false'
# The bundles bake in the version, so the tree must carry it before the build. Same edit
# `publish` commits, so the tag's tree is what was built.
- name: Apply the version bump
if: needs.prepare.outputs.bumped == 'true'
shell: bash
env:
VERSION: ${{ needs.prepare.outputs.version }}
run: |
set -euo pipefail
npm version "$VERSION" --no-git-tag-version > /dev/null
node scripts/sync-version.mjs
- name: Install Rust
uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # master, 2026-08
with:
toolchain: stable
targets: ${{ matrix.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
- name: Rust cache
uses: swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
workspaces: './src-tauri -> target'
key: release${{ matrix.args }}
- name: Install frontend dependencies
run: pnpm install --frozen-lockfile
# `tr -d`: base64 on Linux wraps at 76 columns.
- name: Prepare Apple signing
if: startsWith(matrix.platform, 'macos') && env.SIGN_MACOS == 'true'
env:
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
run: |
for name in APPLE_CERTIFICATE APPLE_CERTIFICATE_PASSWORD APPLE_SIGNING_IDENTITY \
APPLE_ID APPLE_PASSWORD APPLE_TEAM_ID; do
value=$(printf '%s' "${!name}" | tr -d '\r\n')
if [ -z "$value" ]; then
echo "::error::$name is empty. Signing is all-or-nothing: set every Apple secret, or none."
exit 1
fi
echo "$name=$value" >> "$GITHUB_ENV"
done
- name: Build and release
uses: tauri-apps/tauri-action@1deb371b0cd8bd54025b384f1cd735e725c4060f # v1.0.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# The updater (minisign) key, not code signing. Password is empty when the key has none.
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
# Apple variables come from the step above, and only when set.
with:
args: ${{ matrix.args }}
# Empty tagName builds everything and releases nothing (the "none" bump).
tagName: ${{ needs.prepare.outputs.release == 'true' && needs.prepare.outputs.tag || '' }}
releaseName: ${{ needs.prepare.outputs.release == 'true' && 'Set v__VERSION__' || '' }}
releaseBody: 'See the assets below to download and install this version.'
# Draft until every check passes. A pre-release stays out of releases/latest.
releaseDraft: true
prerelease: ${{ needs.prepare.outputs.prerelease }}
# Runs after the assets are attached, which is why the release is a draft.
- name: Check the bundles
if: matrix.platform != 'windows-latest'
run: scripts/check-bundle.sh '${{ matrix.bundle }}'
- name: Check the installers
if: matrix.platform == 'windows-latest'
shell: pwsh
run: scripts/check-bundle.ps1 '${{ matrix.bundle }}'
# !cancelled(): a rejected bundle is the one worth downloading.
- name: Upload bundles as artifacts (no-release runs)
if: ${{ !cancelled() && needs.prepare.outputs.release != 'true' }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: set-${{ matrix.platform }}${{ matrix.args != '' && format('-{0}', matrix.args) || '' }}
path: |
src-tauri/target/**/release/bundle/**/*.dmg
src-tauri/target/**/release/bundle/**/*.app/**
src-tauri/target/**/release/bundle/**/*.AppImage
src-tauri/target/**/release/bundle/**/*.deb
src-tauri/target/**/release/bundle/**/*.rpm
src-tauri/target/**/release/bundle/**/*.exe
src-tauri/target/**/release/bundle/**/*.tar.gz
src-tauri/target/**/release/bundle/**/*.sig
if-no-files-found: warn
retention-days: 7
# Every build leg read-modify-writes latest.json with no lock, so an entry can be lost. Re-run to
# fix.
updater-feed:
name: Check the update feed
needs: [prepare, build]
if: needs.prepare.outputs.release == 'true'
runs-on: ubuntu-22.04
steps:
- name: Every platform is in latest.json
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ needs.prepare.outputs.tag }}
run: |
set -euo pipefail
gh release download "$TAG" \
--repo "$GITHUB_REPOSITORY" --pattern latest.json --dir .
missing=0
for platform in darwin-aarch64 darwin-x86_64 linux-x86_64 windows-x86_64; do
url=$(jq -r --arg p "$platform" '.platforms[$p].url // ""' latest.json)
signature=$(jq -r --arg p "$platform" '.platforms[$p].signature // ""' latest.json)
if [ -z "$url" ] || [ -z "$signature" ]; then
echo "::error::latest.json has no signed bundle for $platform"
missing=1
else
echo "$platform -> $url"
fi
done
exit "$missing"
# A plain version moves the updater feed. A pre-release is published but offered to nobody.
publish:
name: Publish the release
needs: [prepare, build, updater-feed]
if: needs.prepare.outputs.release == 'true'
runs-on: ubuntu-22.04
steps:
# Full history: a shallow clone cannot push.
- name: Checkout
if: needs.prepare.outputs.bumped == 'true'
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- uses: ./.github/actions/setup-node
if: needs.prepare.outputs.bumped == 'true'
with:
install: 'false'
# The tag has to exist before the draft is lifted, or GitHub would create it at github.sha,
# one commit short of the bump.
- name: Commit the bump and tag
if: needs.prepare.outputs.bumped == 'true'
env:
VERSION: ${{ needs.prepare.outputs.version }}
TAG: ${{ needs.prepare.outputs.tag }}
BRANCH: ${{ github.ref_name }}
run: |
set -euo pipefail
npm version "$VERSION" --no-git-tag-version > /dev/null
node scripts/sync-version.mjs
git config user.name 'github-actions[bot]'
git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
git add package.json src-tauri/Cargo.toml src-tauri/Cargo.lock src-tauri/tauri.conf.json
git commit -m "chore(release): $TAG"
git tag -a "$TAG" -m "$TAG"
# The bump sits on the dispatched commit, which is what was built. If the branch has
# moved since, the push is rejected rather than tagging a tree nobody built.
# --atomic: branch and tag together, so a rejected push leaves neither.
if ! git push --atomic origin "HEAD:refs/heads/$BRANCH" "refs/tags/$TAG"; then
echo "::error::could not push $TAG and the bump to $BRANCH. Either $BRANCH moved during the run, or $TAG appeared. The release is still a draft; re-run the workflow to rebuild from the branch tip."
exit 1
fi
# Version-free download links. Globs, since each bundler picks its own arch suffix; zero or
# many matches fails.
- name: Stable download names
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ needs.prepare.outputs.tag }}
run: |
set -euo pipefail
alias_asset() {
local pattern="$1" stable="$2"
local found=(dist/$pattern)
if [ ! -e "${found[0]}" ]; then
echo "::error::no release asset matches $pattern (wanted as $stable)"
return 1
fi
if [ "${#found[@]}" -gt 1 ]; then
echo "::error::$pattern matches ${#found[@]} assets, so which one $stable should be is ambiguous: ${found[*]}"
return 1
fi
cp "${found[0]}" "$stable"
echo "$stable <- $(basename "${found[0]}")"
}
# Not the .tar.gz updater bundles.
gh release download "$TAG" --repo "$GITHUB_REPOSITORY" --dir dist \
--pattern '*universal.dmg' \
--pattern '*-setup.exe' \
--pattern '*.AppImage' \
--pattern '*.deb' \
--pattern '*.rpm'
alias_asset '*universal.dmg' Set-macOS.dmg
alias_asset '*-setup.exe' Set-Windows-x64-setup.exe
alias_asset '*.AppImage' Set-linux-x86_64.AppImage
alias_asset '*.deb' Set-linux-amd64.deb
alias_asset '*.rpm' Set-linux-x86_64.rpm
# --clobber so a re-run over an existing draft replaces the aliases.
gh release upload "$TAG" --repo "$GITHUB_REPOSITORY" --clobber \
Set-macOS.dmg \
Set-Windows-x64-setup.exe \
Set-linux-x86_64.AppImage \
Set-linux-amd64.deb \
Set-linux-x86_64.rpm
- name: Publish
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ needs.prepare.outputs.tag }}
PRERELEASE: ${{ needs.prepare.outputs.prerelease }}
run: |
set -euo pipefail
# Best-effort: the first release has nothing to compare against.
notes=$(gh api "repos/$GITHUB_REPOSITORY/releases/generate-notes" \
-f tag_name="$TAG" --jq .body 2> /dev/null || true)
if [ -n "$notes" ]; then
gh release edit "$TAG" --repo "$GITHUB_REPOSITORY" --notes "$notes"
fi
if [ "$PRERELEASE" = 'true' ]; then
gh release edit "$TAG" --repo "$GITHUB_REPOSITORY" \
--draft=false --prerelease --latest=false
else
gh release edit "$TAG" --repo "$GITHUB_REPOSITORY" \
--draft=false --prerelease=false --latest
fi
echo "published $TAG (prerelease=$PRERELEASE)"
web:
name: Deploy the web app
needs: [prepare, publish]
if: needs.prepare.outputs.release == 'true' && needs.prepare.outputs.prerelease != 'true'
runs-on: ubuntu-22.04
steps:
# A Cloudflare Pages deploy hook: one POST builds the project's branch, which by now holds
# the release commit. The URL is the credential, so it lives in a secret.
- name: Ask Cloudflare Pages to build
env:
HOOK: ${{ secrets.CLOUDFLARE_DEPLOY_HOOK }}
TAG: ${{ needs.prepare.outputs.tag }}
run: |
set -euo pipefail
if [ -z "$HOOK" ]; then
echo "::error::CLOUDFLARE_DEPLOY_HOOK is not set; $TAG was released but the web app was not deployed."
exit 1
fi
curl --fail --silent --show-error --request POST "$HOOK" > /dev/null
echo "deploy requested for $TAG"