Skip to content
303 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 - name: What Tauri needs on Linux
79 # Tried three times: the mirror has answered with a mismatched package before.
80 run: |
81 ok=0
82 for try in 1 2 3; do
83 if sudo apt-get update && sudo apt-get install -y libwebkit2gtk-4.1-dev build-essential curl wget file libxdo-dev libssl-dev libayatana-appindicator3-dev librsvg2-dev; then ok=1; break; fi
84 sudo apt-get clean; sleep 15
85 done
86 [ "$ok" = 1 ]
87 - uses: actions/cache@v4
88 with:
89 path: |
90 ~/.cargo/registry
91 apps/desktop/src-tauri/target
92 key: desktop-release-linux-${{ hashFiles('apps/desktop/src-tauri/Cargo.lock') }}
93 # npm's downloads, kept between runs: an install reads them first
94 # and asks the registry only for what changed, so a dropped
95 # connection to it rarely stops the job.
96 - name: Cache npm's downloads
97 uses: actions/cache@v4
98 with:
99 path: ~/.npm
100 key: npm-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
101 restore-keys: npm-${{ runner.os }}-
102 - name: Install
103 run: npm ci --prefer-offline --no-audit --no-fund
104 - name: Build and sign
105 env:
106 TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.DESKTOP_SIGNING_KEY }}
107 TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ""
108 run: node scripts/desktop-release.mjs build
109 - name: Collect
110 run: node scripts/desktop-release.mjs collect
111 - uses: actions/upload-artifact@v4
112 with:
113 name: desktop-linux
114 overwrite: true
115 path: target/desktop-release/${{ needs.version.outputs.version }}/
116
117 windows:
118 name: Windows
119 needs: version
120 if: vars.DESKTOP_WINDOWS_RUNNER == 'true'
121 runs-on: [self-hosted, windows]
122 # Production's secrets, without a deployment: a release is not a deploy of
123 # g1t.sh, and would otherwise show on its card as one in progress.
124 environment:
125 name: production
126 deployment: false
127 timeout-minutes: 90
128 steps:
129 - uses: actions/checkout@v5
130 # A runner that is a Windows service runs as the system account, whose
131 # PATH and profile hold no Rust even when the machine's user has one;
132 # the job brings its own, once, into that account's profile.
133 - name: Rust, if this account lacks it
134 run: |
135 if (-not (Get-Command cargo -ErrorAction SilentlyContinue) -and -not (Test-Path "$env:USERPROFILE\.cargo\bin\cargo.exe")) {
136 Invoke-WebRequest https://win.rustup.rs/x86_64 -OutFile "$env:TEMP\rustup-init.exe" -UseBasicParsing
137 & "$env:TEMP\rustup-init.exe" -y --profile minimal --default-toolchain stable | Out-Null
138 }
139 "$env:USERPROFILE\.cargo\bin" | Out-File -Append -Encoding utf8 $env:GITHUB_PATH
140 - name: Install
141 run: npm ci --no-audit --no-fund
142 - name: Build and sign
143 env:
144 TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.DESKTOP_SIGNING_KEY }}
145 TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ""
146 run: node scripts/desktop-release.mjs build
147 - name: Collect
148 run: node scripts/desktop-release.mjs collect
149 - uses: actions/upload-artifact@v4
150 with:
151 name: desktop-windows
152 overwrite: true
153 path: target/desktop-release/${{ needs.version.outputs.version }}/
154
155 windows_cross:
156 name: Windows, built on Linux
157 needs: version
158 if: vars.DESKTOP_WINDOWS_FROM_LINUX == 'true' && vars.DESKTOP_WINDOWS_RUNNER != 'true'
159 runs-on: g1t-4core
160 # Production's secrets, without a deployment: a release is not a deploy of
161 # g1t.sh, and would otherwise show on its card as one in progress.
162 environment:
163 name: production
164 deployment: false
165 timeout-minutes: 60 # the most g1t's own machines run a job
166 steps:
167 - uses: actions/checkout@v5
168 - name: What the cross build needs
169 # The tray's Linux library too: Tauri's bundler looks for it on the
170 # machine it runs on, whatever the target, and stops without it.
171 run: |
172 ok=0
173 for try in 1 2 3; do
174 if sudo apt-get update && sudo apt-get install -y build-essential curl wget file libssl-dev libayatana-appindicator3-dev nsis clang lld llvm; then ok=1; break; fi
175 sudo apt-get clean; sleep 15
176 done
177 [ "$ok" = 1 ]
178 # cargo-xwin compiles C (ring's, among others) with clang-cl and
179 # links with lld-link and llvm-lib. Each is a mode of a tool
180 # Debian does install, chosen by the name it is run as: clang-cl
181 # is clang, lld-link is lld, and llvm-lib is llvm-ar. Debian ships
182 # none of the three names, so each is linked to its tool.
183 for pair in clang-cl:clang lld-link:lld llvm-lib:llvm-ar; do
184 name="${pair%%:*}" tool="${pair##*:}"
185 command -v "$name" >/dev/null && continue
186 found="$(command -v "$tool" || ls /usr/lib/llvm-*/bin/"$tool" 2>/dev/null | sort -V | tail -1)"
187 [ -n "$found" ] || { echo "::error::$tool is not installed, so there is no $name"; exit 1; }
188 sudo ln -sf "$(readlink -f "$found")" "/usr/local/bin/$name"
189 echo "$name: $(readlink -f "$found")"
190 done
191 # Tauri's installer script includes Win\RestartManager.nsh, which
192 # NSIS ships from 3.10 on; Debian's is 3.08. The header stands
193 # alone (defines and System calls), so it is added from NSIS 3.11's
194 # source, checked against its SHA-256.
195 rm_nsh=/usr/share/nsis/Include/Win/RestartManager.nsh
196 if [ ! -f "$rm_nsh" ]; then
197 curl -fsSL --retry 3 -o /tmp/RestartManager.nsh https://raw.githubusercontent.com/kichik/nsis/v311/Include/Win/RestartManager.nsh
198 echo "67149978412da9b283e2e1abe48a360093df9caf6363373e2cabe463978b7c0e /tmp/RestartManager.nsh" | sha256sum -c -
199 sudo install -m 0644 /tmp/RestartManager.nsh "$rm_nsh"
200 fi
201 cargo install --locked cargo-xwin
202 - uses: actions/cache@v4
203 with:
204 path: |
205 ~/.cargo/registry
206 apps/desktop/src-tauri/target
207 key: desktop-release-windows-${{ hashFiles('apps/desktop/src-tauri/Cargo.lock') }}
208 # npm's downloads, kept between runs: an install reads them first
209 # and asks the registry only for what changed, so a dropped
210 # connection to it rarely stops the job.
211 - name: Cache npm's downloads
212 uses: actions/cache@v4
213 with:
214 path: ~/.npm
215 key: npm-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
216 restore-keys: npm-${{ runner.os }}-
217 - name: Install
218 run: npm ci --prefer-offline --no-audit --no-fund
219 - name: Build and sign
220 env:
221 TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.DESKTOP_SIGNING_KEY }}
222 TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ""
223 run: node scripts/desktop-release.mjs build --windows-from-linux --only-windows
224 - name: Collect
225 run: node scripts/desktop-release.mjs collect
226 - uses: actions/upload-artifact@v4
227 with:
228 name: desktop-windows
229 overwrite: true
230 path: target/desktop-release/${{ needs.version.outputs.version }}/
231
232 macos:
233 name: macOS
234 needs: version
235 if: vars.DESKTOP_MACOS_RUNNER == 'true'
236 runs-on: [self-hosted, macos]
237 # Production's secrets, without a deployment: a release is not a deploy of
238 # g1t.sh, and would otherwise show on its card as one in progress.
239 environment:
240 name: production
241 deployment: false
242 timeout-minutes: 90
243 steps:
244 - uses: actions/checkout@v5
245 - name: Install
246 run: npm ci --no-audit --no-fund
247 - name: Build and sign, for Apple silicon and Intel
248 env:
249 TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.DESKTOP_SIGNING_KEY }}
250 TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ""
251 run: |
252 rustup target add aarch64-apple-darwin x86_64-apple-darwin
253 npx tauri build --ci --target aarch64-apple-darwin
254 npx tauri build --ci --target x86_64-apple-darwin
255 working-directory: apps/desktop
256 - name: Collect
257 run: node scripts/desktop-release.mjs collect
258 - uses: actions/upload-artifact@v4
259 with:
260 name: desktop-macos
261 overwrite: true
262 path: target/desktop-release/${{ needs.version.outputs.version }}/
263
264 publish:
265 name: Publish to g1t.sh/downloads/desktop
266 needs: [version, linux, windows, windows_cross, macos]
267 # A platform with no machine registered is skipped, and the release still goes out with the rest.
268 if: >-
269 always() && needs.linux.result == 'success'
270 && contains(fromJSON('["success","skipped"]'), needs.windows.result)
271 && contains(fromJSON('["success","skipped"]'), needs.windows_cross.result)
272 && contains(fromJSON('["success","skipped"]'), needs.macos.result)
273 runs-on: ubuntu-latest
274 # Production's secrets, without a deployment: a release is not a deploy of
275 # g1t.sh, and would otherwise show on its card as one in progress.
276 environment:
277 name: production
278 deployment: false
279 timeout-minutes: 20
280 steps:
281 - uses: actions/checkout@v5
282 - uses: actions/download-artifact@v4
283 with:
284 pattern: desktop-*
285 merge-multiple: true
286 path: target/desktop-release/${{ needs.version.outputs.version }}/
287 # npm's downloads, kept between runs: an install reads them first
288 # and asks the registry only for what changed, so a dropped
289 # connection to it rarely stops the job.
290 - name: Cache npm's downloads
291 uses: actions/cache@v4
292 with:
293 path: ~/.npm
294 key: npm-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
295 restore-keys: npm-${{ runner.os }}-
296 - name: Install Wrangler
297 run: npm ci --prefer-offline --workspaces=false --no-audit --no-fund
298 - name: The manifest
299 run: node scripts/desktop-release.mjs manifest --notes "${{ inputs.notes }}"
300 - name: Publish
301 env:
302 CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
303 run: node scripts/desktop-release.mjs publish