Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

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