Skip to content
319 linesCodeBlameRaw
1# Releases the desktop app, g1t (apps/desktop): builds it for Linux on
2# g1t's machines, for Windows and macOS on machines registered for them,
3# signs every updater file, publishes the release to
4# g1t.sh/downloads/desktop/ (the g1t-downloads R2 bucket), and points
5# latest.json at it. Apps already installed update themselves to it;
6# g1t.sh/download offers it, and says which platforms a release lacks.
7#
8# A release is a tag `desktop-v<version>`, where the version is the one in
9# apps/desktop/src-tauri/Cargo.toml; or run it by hand. scripts/
10# desktop-release.mjs does the work, and its header says how to make the
11# signing key the first time; CONTRIBUTING.md ("Deploying") says how a
12# release is made; the guide is docs.g1t.sh/guides/desktop/.
13#
14# Needs the repository secret DESKTOP_SIGNING_KEY (the updater's private
15# key; its public half is in tauri.conf.json), and CLOUDFLARE_API_TOKEN and
16# CLOUDFLARE_ACCOUNT_ID as the deploy has them. The platform jobs beyond
17# Linux each wait on a repository variable, so a release goes out with what
18# can be built:
19#
20# DESKTOP_WINDOWS_RUNNER=true a self-hosted runner on Windows, labels `self-hosted` and `windows`,
21# with Rust, Node and the Visual Studio build tools (the NSIS installer is fetched)
22# DESKTOP_WINDOWS_FROM_LINUX=true no Windows machine: cross-build on g1t's Linux machine with cargo-xwin and NSIS.
23# Needs aka.ms and download.visualstudio.microsoft.com among the project's
24# workflow-only domains for desktop-release.yml in production (Settings, Guardrails),
25# where cargo-xwin fetches the MSVC CRT from.
26# DESKTOP_MACOS_RUNNER=true a self-hosted runner on a Mac, labels `self-hosted` and `macos`.
27name: Desktop release
28
29on:
30 push:
31 tags: ["desktop-v*"]
32 workflow_dispatch:
33 inputs:
34 notes:
35 description: "A line about the release, shown in the manifest"
36 type: string
37
38permissions:
39 contents: read
40
41concurrency:
42 group: desktop-release
43 cancel-in-progress: false
44
45env:
46 CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID }}
47 WRANGLER_SEND_METRICS: "false"
48
49jobs:
50 version:
51 name: The tag names this version
52 runs-on: ubuntu-latest
53 outputs:
54 version: ${{ steps.read.outputs.version }}
55 steps:
56 - uses: actions/checkout@v5
57 - id: read
58 run: |
59 version="$(sed -n 's/^version = "\(.*\)"/\1/p' apps/desktop/src-tauri/Cargo.toml | head -1)"
60 echo "version=$version" >> "$GITHUB_OUTPUT"
61 if [ "${GITHUB_REF_TYPE}" = tag ] && [ "desktop-v$version" != "${GITHUB_REF_NAME}" ]; then
62 echo "::error::The tag is ${GITHUB_REF_NAME}, but apps/desktop is $version"; exit 1
63 fi
64
65 linux:
66 name: Linux
67 needs: version
68 # The larger machine: linking Tauri on the small one dies with a bus error.
69 runs-on: g1t-4core
70 # Production's secrets, without a deployment: a release is not a deploy of
71 # g1t.sh, and would otherwise show on its card as one in progress.
72 environment:
73 name: production
74 deployment: false
75 timeout-minutes: 60 # the most g1t's own machines run a job
76 steps:
77 - uses: actions/checkout@v5
78 # rust: the toolchain; desktop: the app's Cargo.lock's crates.io and
79 # git packages, not its own version (scripts/deploy/ci-cache.mjs).
80 - name: Cache keys
81 id: keys
82 run: node scripts/deploy/ci-cache.mjs keys
83 # apt's lists and downloads, as CI's desktop job saves them on main.
84 - name: Cache apt's downloads
85 uses: actions/cache/restore@v4
86 with:
87 path: ~/.cache/apt/tauri
88 key: apt-tauri-${{ runner.os }}-${{ hashFiles('scripts/apt-install.sh') }}
89 - name: What Tauri needs on Linux
90 run: bash scripts/apt-install.sh tauri
91 # The app's release build. A tag's run restores only from its own tag
92 # and from main, never from the last release's, so the Caches
93 # workflow (caches.yml) builds and saves this on main whenever the
94 # key is new. Only restored here.
95 - name: Cache the release build
96 uses: actions/cache/restore@v4
97 with:
98 path: |
99 ~/.cargo/registry/index
100 ~/.cargo/registry/cache
101 apps/desktop/src-tauri/target/release
102 !apps/desktop/src-tauri/target/release/bundle
103 !apps/desktop/src-tauri/target/**/incremental
104 key: desktop-release-linux-${{ runner.os }}-${{ steps.keys.outputs.rust }}-${{ steps.keys.outputs.desktop }}
105 restore-keys: desktop-release-linux-${{ runner.os }}-${{ steps.keys.outputs.rust }}-
106 # npm's downloads for the whole lockfile, which the Caches workflow
107 # keeps on main: an install reads them first and asks the registry
108 # only for what changed. Only restored: a tag's entries are read by
109 # nothing else.
110 - name: Cache npm's downloads
111 uses: actions/cache/restore@v4
112 with:
113 path: ~/.npm
114 key: npm-full-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
115 restore-keys: npm-full-${{ runner.os }}-
116 - name: Install
117 run: node scripts/deploy/npm-ci.mjs --prefer-offline --no-audit --no-fund
118 - name: Build and sign
119 env:
120 TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.DESKTOP_SIGNING_KEY }}
121 TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ""
122 run: node scripts/desktop-release.mjs build
123 - name: Collect
124 run: node scripts/desktop-release.mjs collect
125 - uses: actions/upload-artifact@v4
126 with:
127 name: desktop-linux
128 overwrite: true
129 path: target/desktop-release/${{ needs.version.outputs.version }}/
130
131 windows:
132 name: Windows
133 needs: version
134 if: vars.DESKTOP_WINDOWS_RUNNER == 'true'
135 runs-on: [self-hosted, windows]
136 # Production's secrets, without a deployment: a release is not a deploy of
137 # g1t.sh, and would otherwise show on its card as one in progress.
138 environment:
139 name: production
140 deployment: false
141 timeout-minutes: 90
142 steps:
143 - uses: actions/checkout@v5
144 # A runner that is a Windows service runs as the system account, whose
145 # PATH and profile hold no Rust even when the machine's user has one;
146 # the job brings its own, once, into that account's profile.
147 - name: Rust, if this account lacks it
148 run: |
149 if (-not (Get-Command cargo -ErrorAction SilentlyContinue) -and -not (Test-Path "$env:USERPROFILE\.cargo\bin\cargo.exe")) {
150 Invoke-WebRequest https://win.rustup.rs/x86_64 -OutFile "$env:TEMP\rustup-init.exe" -UseBasicParsing
151 & "$env:TEMP\rustup-init.exe" -y --profile minimal --default-toolchain stable | Out-Null
152 }
153 "$env:USERPROFILE\.cargo\bin" | Out-File -Append -Encoding utf8 $env:GITHUB_PATH
154 - name: Install
155 run: node scripts/deploy/npm-ci.mjs --no-audit --no-fund
156 - name: Build and sign
157 env:
158 TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.DESKTOP_SIGNING_KEY }}
159 TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ""
160 run: node scripts/desktop-release.mjs build
161 - name: Collect
162 run: node scripts/desktop-release.mjs collect
163 - uses: actions/upload-artifact@v4
164 with:
165 name: desktop-windows
166 overwrite: true
167 path: target/desktop-release/${{ needs.version.outputs.version }}/
168
169 windows_cross:
170 name: Windows, built on Linux
171 needs: version
172 if: vars.DESKTOP_WINDOWS_FROM_LINUX == 'true' && vars.DESKTOP_WINDOWS_RUNNER != 'true'
173 runs-on: g1t-4core
174 # Production's secrets, without a deployment: a release is not a deploy of
175 # g1t.sh, and would otherwise show on its card as one in progress.
176 environment:
177 name: production
178 deployment: false
179 timeout-minutes: 60 # the most g1t's own machines run a job
180 steps:
181 - uses: actions/checkout@v5
182 - name: Cache keys
183 id: keys
184 run: node scripts/deploy/ci-cache.mjs keys
185 # apt's lists and downloads, which the Caches workflow keeps on main.
186 - name: Cache apt's downloads
187 uses: actions/cache/restore@v4
188 with:
189 path: ~/.cache/apt/tauri-windows
190 key: apt-tauri-windows-${{ runner.os }}-${{ hashFiles('scripts/apt-install.sh') }}
191 - name: What the cross build needs
192 # The tray's Linux library too: Tauri's bundler looks for it on the
193 # machine it runs on, whatever the target, and stops without it.
194 run: |
195 bash scripts/desktop-cross-tools.sh
196 # cargo-xwin, pinned, and the MSVC CRT and Windows SDK it downloads on
197 # first use: 88 s compiling it and 59 s downloading them in every
198 # release before. The Caches workflow keeps them on main.
199 - name: Cache cargo-xwin and the MSVC CRT
200 id: xwin
201 uses: actions/cache/restore@v4
202 with:
203 path: |
204 ~/.cargo/bin/cargo-xwin
205 ~/.cache/cargo-xwin
206 key: xwin-${{ runner.os }}-0.23.1
207 - name: cargo-xwin
208 if: ${{ steps.xwin.outputs.cache-hit != 'true' }}
209 run: cargo install --locked cargo-xwin@0.23.1
210 # The app's Windows build, which the Caches workflow keeps on main.
211 - name: Cache the release build
212 uses: actions/cache/restore@v4
213 with:
214 path: |
215 ~/.cargo/registry/index
216 ~/.cargo/registry/cache
217 apps/desktop/src-tauri/target/release
218 apps/desktop/src-tauri/target/x86_64-pc-windows-msvc/release
219 !apps/desktop/src-tauri/target/**/bundle
220 !apps/desktop/src-tauri/target/**/incremental
221 key: desktop-release-windows-${{ runner.os }}-${{ steps.keys.outputs.rust }}-${{ steps.keys.outputs.desktop }}
222 restore-keys: desktop-release-windows-${{ runner.os }}-${{ steps.keys.outputs.rust }}-
223 # npm's downloads for the whole lockfile, which the Caches workflow
224 # keeps on main: an install reads them first and asks the registry
225 # only for what changed. Only restored: a tag's entries are read by
226 # nothing else.
227 - name: Cache npm's downloads
228 uses: actions/cache/restore@v4
229 with:
230 path: ~/.npm
231 key: npm-full-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
232 restore-keys: npm-full-${{ runner.os }}-
233 - name: Install
234 run: node scripts/deploy/npm-ci.mjs --prefer-offline --no-audit --no-fund
235 - name: Build and sign
236 env:
237 TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.DESKTOP_SIGNING_KEY }}
238 TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ""
239 run: node scripts/desktop-release.mjs build --windows-from-linux --only-windows
240 - name: Collect
241 run: node scripts/desktop-release.mjs collect
242 - uses: actions/upload-artifact@v4
243 with:
244 name: desktop-windows
245 overwrite: true
246 path: target/desktop-release/${{ needs.version.outputs.version }}/
247
248 macos:
249 name: macOS
250 needs: version
251 if: vars.DESKTOP_MACOS_RUNNER == 'true'
252 runs-on: [self-hosted, macos]
253 # Production's secrets, without a deployment: a release is not a deploy of
254 # g1t.sh, and would otherwise show on its card as one in progress.
255 environment:
256 name: production
257 deployment: false
258 timeout-minutes: 90
259 steps:
260 - uses: actions/checkout@v5
261 - name: Install
262 run: node scripts/deploy/npm-ci.mjs --no-audit --no-fund
263 - name: Build and sign, for Apple silicon and Intel
264 env:
265 TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.DESKTOP_SIGNING_KEY }}
266 TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ""
267 run: |
268 rustup target add aarch64-apple-darwin x86_64-apple-darwin
269 npx tauri build --ci --target aarch64-apple-darwin
270 npx tauri build --ci --target x86_64-apple-darwin
271 working-directory: apps/desktop
272 - name: Collect
273 run: node scripts/desktop-release.mjs collect
274 - uses: actions/upload-artifact@v4
275 with:
276 name: desktop-macos
277 overwrite: true
278 path: target/desktop-release/${{ needs.version.outputs.version }}/
279
280 publish:
281 name: Publish to g1t.sh/downloads/desktop
282 needs: [version, linux, windows, windows_cross, macos]
283 # A platform with no machine registered is skipped, and the release still goes out with the rest.
284 if: >-
285 always() && needs.linux.result == 'success'
286 && contains(fromJSON('["success","skipped"]'), needs.windows.result)
287 && contains(fromJSON('["success","skipped"]'), needs.windows_cross.result)
288 && contains(fromJSON('["success","skipped"]'), needs.macos.result)
289 runs-on: ubuntu-latest
290 # Production's secrets, without a deployment: a release is not a deploy of
291 # g1t.sh, and would otherwise show on its card as one in progress.
292 environment:
293 name: production
294 deployment: false
295 timeout-minutes: 20
296 steps:
297 - uses: actions/checkout@v5
298 - uses: actions/download-artifact@v4
299 with:
300 pattern: desktop-*
301 merge-multiple: true
302 path: target/desktop-release/${{ needs.version.outputs.version }}/
303 # npm's downloads, kept between runs: an install reads them first
304 # and asks the registry only for what changed, so a dropped
305 # connection to it rarely stops the job.
306 - name: Cache npm's downloads
307 uses: actions/cache/restore@v4
308 with:
309 path: ~/.npm
310 key: npm-full-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
311 restore-keys: npm-full-${{ runner.os }}-
312 - name: Install Wrangler
313 run: node scripts/deploy/npm-ci.mjs --prefer-offline --workspaces=false --no-audit --no-fund
314 - name: The manifest
315 run: node scripts/desktop-release.mjs manifest --notes "${{ inputs.notes }}"
316 - name: Publish
317 env:
318 CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
319 run: node scripts/desktop-release.mjs publish