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.

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