Skip to content

Commit b57bd13

Browse files
authored
Merge pull request #10 from code4mk/dev
Add one-click auto-update via the Tauri updater plugin
2 parents e803d1b + d4df967 commit b57bd13

18 files changed

Lines changed: 868 additions & 48 deletions

File tree

‎.github/workflows/release.yml‎

Lines changed: 100 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,13 @@ jobs:
5555
cat > "$RUNNER_TEMP/notes.md" <<'NOTES'
5656
## FigyTerm v__VERSION__
5757
58+
### Updating
59+
60+
FigyTerm updates itself from here on — it checks for new versions and
61+
installs them in-app, with no Gatekeeper prompt and no `xattr` step.
62+
If you're coming from **v0.0.6 or earlier**, this one install is manual;
63+
after it, updates are automatic.
64+
5865
### Installation (macOS)
5966
6067
1. Download `FigyTerm___VERSION___aarch64.dmg` (Apple Silicon) or `FigyTerm___VERSION___x64.dmg` (Intel)
@@ -186,16 +193,108 @@ jobs:
186193
uses: tauri-apps/tauri-action@v1
187194
env:
188195
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
196+
# Minisign keypair for the updater. Unrelated to Apple code signing —
197+
# it only lets installed clients verify an update came from us.
198+
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
199+
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
200+
# Apple signing is optional and currently unset; tauri-action builds
201+
# unsigned when these are absent. Present so enabling notarization
202+
# later is a secret upload rather than a workflow change.
203+
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
204+
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
205+
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
206+
APPLE_ID: ${{ secrets.APPLE_ID }}
207+
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
208+
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
189209
with:
190210
releaseId: ${{ needs.create-release.outputs.release_id }}
191211
args: ${{ matrix.args }}
212+
# The manifest is written by the updater-manifest job instead. With a
213+
# two-target matrix, letting both builds upload latest.json races:
214+
# whichever finishes last overwrites the other's entry, producing an
215+
# updater that silently 404s for one architecture.
192216
uploadUpdaterJson: false
193217

194-
publish-release:
218+
# Composes latest.json once, after both architectures have uploaded their
219+
# updater artifacts, so it always describes both.
220+
updater-manifest:
195221
needs: [create-release, build-macos]
196222
permissions:
197223
contents: write
198224
runs-on: ubuntu-latest
225+
steps:
226+
- name: Compose and upload latest.json
227+
env:
228+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
229+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
230+
GH_REPO: ${{ github.repository }}
231+
TAG: ${{ needs.create-release.outputs.tag }}
232+
VERSION: ${{ needs.create-release.outputs.version }}
233+
RELEASE_ID: ${{ needs.create-release.outputs.release_id }}
234+
run: |
235+
set -euo pipefail
236+
237+
# The release is still a draft here, so it must be addressed by id.
238+
gh api --paginate "repos/$GH_REPO/releases/$RELEASE_ID/assets?per_page=100" \
239+
> assets.json
240+
241+
PLATFORMS='{}'
242+
243+
for ARCH in aarch64 x64; do
244+
case "$ARCH" in
245+
aarch64) PLATFORM=darwin-aarch64 ;;
246+
x64) PLATFORM=darwin-x86_64 ;;
247+
esac
248+
249+
TARBALL="FigyTerm_${VERSION}_${ARCH}.app.tar.gz"
250+
SIG_ID=$(jq -r --arg n "${TARBALL}.sig" \
251+
'.[] | select(.name == $n) | .id' assets.json | head -n1)
252+
253+
if [ -z "$SIG_ID" ]; then
254+
echo "::error::${TARBALL}.sig is missing from the release. Is TAURI_SIGNING_PRIVATE_KEY set?"
255+
exit 1
256+
fi
257+
258+
SIG=$(gh api -H "Accept: application/octet-stream" \
259+
"repos/$GH_REPO/releases/assets/$SIG_ID")
260+
261+
# Public download URL, valid once the release is undrafted.
262+
URL="https://github.com/$GH_REPO/releases/download/$TAG/$TARBALL"
263+
264+
PLATFORMS=$(jq \
265+
--arg k "$PLATFORM" --arg s "$SIG" --arg u "$URL" \
266+
'. + {($k): {signature: $s, url: $u}}' <<<"$PLATFORMS")
267+
done
268+
269+
jq -n \
270+
--arg v "$VERSION" \
271+
--arg d "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
272+
--argjson p "$PLATFORMS" \
273+
'{version: $v, notes: "See the release notes on GitHub.", pub_date: $d, platforms: $p}' \
274+
> latest.json
275+
276+
cat latest.json
277+
278+
# Re-runs: drop the previous manifest so the upload doesn't collide.
279+
OLD_ID=$(jq -r '.[] | select(.name == "latest.json") | .id' assets.json | head -n1)
280+
if [ -n "$OLD_ID" ]; then
281+
gh api --method DELETE "repos/$GH_REPO/releases/assets/$OLD_ID" >/dev/null
282+
fi
283+
284+
curl -sSf -X POST \
285+
-H "Authorization: Bearer $GITHUB_TOKEN" \
286+
-H "Content-Type: application/json" \
287+
--data-binary @latest.json \
288+
"https://uploads.github.com/repos/$GH_REPO/releases/$RELEASE_ID/assets?name=latest.json" \
289+
>/dev/null
290+
291+
echo "Uploaded latest.json for $TAG"
292+
293+
publish-release:
294+
needs: [create-release, build-macos, updater-manifest]
295+
permissions:
296+
contents: write
297+
runs-on: ubuntu-latest
199298
steps:
200299
- name: Undraft release
201300
env:

‎docs/UPDATE-SYSTEM.md‎

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# FigyTerm Update System — Design & Implementation Plan
22

3-
**Status:** Phase 1 implemented. Phases 2–3 planned.
3+
**Status:** Phases 1–2 implemented. Phase 3 planned.
44
**Scope:** macOS only (Apple Silicon + Intel), distributed via GitHub Releases
55
**Related:** `.github/workflows/release.yml`, `src-tauri/src/menu.rs`, `src/components/Settings/Settings.tsx`
66

@@ -568,17 +568,44 @@ waiver for nonprofits/educational institutions (unlikely to apply here, but it e
568568
- Release notes render to React elements with no HTML passthrough at all, so markup
569569
injection from a remote release body is structurally impossible.
570570

571-
### Phase 2 — Auto-update
572-
- [ ] Generate + back up the minisign keypair
573-
- [ ] Add repo secrets
574-
- [ ] Add `tauri-plugin-updater`, `createUpdaterArtifacts`, `plugins.updater`, capability
575-
- [ ] Pass signing env to `tauri-action`
576-
- [ ] Add the `updater-manifest` job; keep `uploadUpdaterJson: false`
577-
- [ ] Download → progress → install → relaunch flow in the modal
578-
- [ ] Running-process guard before relaunch
579-
- [ ] Fallback to the Phase 1 manual flow on every failure path
580-
- [ ] **Verify on a real Mac:** no quarantine on updater-installed builds; detect
581-
translocation / read-only bundle; test both architectures
571+
### Phase 2 — Auto-update ✅ implemented
572+
- [x] Generate the minisign keypair (`~/.tauri/figyterm.key`, mode 600, outside the repo)
573+
- [x] Add `tauri-plugin-updater`, `createUpdaterArtifacts`, `plugins.updater`, capability
574+
- [x] Pass signing env to `tauri-action`
575+
- [x] Add the `updater-manifest` job; keep `uploadUpdaterJson: false`
576+
- [x] Download → progress → install → relaunch flow in the modal
577+
- [x] Running-process guard before relaunch
578+
- [x] Fallback to the manual flow on every failure path
579+
- [ ] **Add the repo secrets** — `TAURI_SIGNING_PRIVATE_KEY` and
580+
`TAURI_SIGNING_PRIVATE_KEY_PASSWORD` (empty). Until these exist the
581+
`updater-manifest` job fails the release by design, rather than shipping a
582+
release whose updater silently does nothing.
583+
- [ ] **Back up the private key** outside the machine. If it's lost, every
584+
installed client stops accepting updates permanently.
585+
- [ ] **Verify on a real Mac after the first updater-enabled release:** an
586+
updater-installed build carries no quarantine flag; both architectures
587+
update; a translocated / read-only bundle falls back cleanly.
588+
589+
**Files added:** `src/services/installer.ts`
590+
591+
**Architecture:** discovery and install are deliberately separate. Discovery stays
592+
on the GitHub Releases API (Phase 1) because it yields rich release notes, dates,
593+
asset sizes and a prerelease flag — none of which `latest.json` carries. The
594+
updater plugin is used purely as the mechanism that swaps the bundle. That split
595+
is also what makes the fallback natural: if the plugin can't install (no manifest
596+
on old releases, `tauri dev`, a read-only bundle), the UI reveals the manual DMG
597+
path it already had.
598+
599+
**Running-process guard:** `PtyInstance::foreground_pid()` compares the tty's
600+
foreground process group leader against the shell's own pid; when they differ the
601+
user is running something. `running_foreground_commands` resolves those pids to
602+
names via `sysinfo` so the warning can say *what* would be killed. Undeterminable
603+
is treated as idle — a false "busy" would block updating forever, while a false
604+
"idle" costs only a confirmation the user would have clicked through.
605+
606+
**Bootstrapping:** only builds that already contain the updater plugin can
607+
auto-update, so v0.0.6 and earlier must install the first updater-enabled release
608+
by hand. The release-notes template says so.
582609

583610
### Phase 3 — First install (unsigned-permanent)
584611
- [ ] `install.sh` — arch detect, curl download, mount, copy, `xattr -cr`, detach, launch

‎package-lock.json‎

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"@headlessui/react": "^2.2.10",
1515
"@tauri-apps/api": "^2.0.0",
1616
"@tauri-apps/plugin-shell": "^2.0.0",
17+
"@tauri-apps/plugin-updater": "^2.11.0",
1718
"@xterm/addon-fit": "^0.10.0",
1819
"@xterm/addon-search": "^0.16.0",
1920
"@xterm/addon-unicode11": "^0.9.0",

0 commit comments

Comments
 (0)