Skip to content

Commit e54c136

Browse files
authored
Merge pull request #11 from code4mk/dev
Fix release build by removing pre-wired Apple signing vars
2 parents b57bd13 + 86dbe7d commit e54c136

3 files changed

Lines changed: 213 additions & 12 deletions

File tree

‎.github/workflows/release.yml‎

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -197,15 +197,11 @@ jobs:
197197
# it only lets installed clients verify an update came from us.
198198
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
199199
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 }}
200+
# No Apple signing vars here on purpose. A missing secret still sets
201+
# the variable to an empty string, and the bundler treats a *present*
202+
# APPLE_CERTIFICATE as "sign this", then fails on `security import`.
203+
# To enable notarization later, add the block documented in
204+
# docs/RELEASING.md — don't pre-wire it. See that doc before editing.
209205
with:
210206
releaseId: ${{ needs.create-release.outputs.release_id }}
211207
args: ${{ matrix.args }}

‎docs/RELEASING.md‎

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
# Releasing FigyTerm
2+
3+
How to cut a release, what the pipeline does, and how to fix it when it breaks.
4+
5+
Related: [`UPDATE-SYSTEM.md`](./UPDATE-SYSTEM.md) covers the in-app update system this
6+
pipeline feeds.
7+
8+
---
9+
10+
## Cutting a release
11+
12+
```bash
13+
git tag v0.0.7
14+
git push origin v0.0.7
15+
```
16+
17+
That's it. The tag drives everything — the version in `package.json`,
18+
`tauri.conf.json` and `Cargo.toml` is rewritten from it at build time, so those
19+
committed values don't need to be correct.
20+
21+
Tags must be `vMAJOR.MINOR.PATCH` with an optional prerelease suffix
22+
(`v1.0.0-beta.1`). Anything with a suffix ships as a GitHub prerelease and is
23+
excluded from the updater's "latest" endpoint.
24+
25+
To rebuild an existing tag, use **Actions → Release → Run workflow** and enter the
26+
tag. The workflow is re-run safe: it PATCHes the existing release rather than
27+
creating a duplicate, and replaces `latest.json` instead of colliding with it.
28+
29+
---
30+
31+
## What the pipeline does
32+
33+
```
34+
create-release → build-macos (aarch64 + x64) → updater-manifest → publish-release
35+
```
36+
37+
| Job | Does |
38+
|-----|------|
39+
| `create-release` | Validates the tag, renders release notes, creates a **draft** release. Runs once so the body isn't raced by the build matrix. |
40+
| `build-macos` | Builds and bundles both architectures, uploads `.dmg` + `.app.tar.gz` + `.sig` to the draft. |
41+
| `updater-manifest` | Composes `latest.json` from both architectures' signatures and uploads it. |
42+
| `publish-release` | Undrafts. This is what makes the release visible and updatable. |
43+
44+
The release stays a **draft** until every job succeeds. A failure part-way leaves a
45+
draft release with partial assets — see [Recovering](#recovering-from-a-failed-run).
46+
47+
---
48+
49+
## Required secrets
50+
51+
Only one:
52+
53+
| Secret | Value |
54+
|--------|-------|
55+
| `TAURI_SIGNING_PRIVATE_KEY` | Full contents of `~/.tauri/figyterm.key`, both lines including the `untrusted comment:` line. |
56+
57+
Set it with `gh secret set TAURI_SIGNING_PRIVATE_KEY < ~/.tauri/figyterm.key`, or paste
58+
it at **Settings → Secrets and variables → Actions → New repository secret**.
59+
60+
### Why there is no `TAURI_SIGNING_PRIVATE_KEY_PASSWORD` secret
61+
62+
The key was generated without a password, and **GitHub's web UI won't save a secret
63+
with an empty value**. You don't need to: `${{ secrets.MISSING }}` evaluates to an
64+
empty string, so the workflow sets the variable to `""` either way. Tauri reads that
65+
as "no password" and proceeds.
66+
67+
What matters is that the variable is *set*, not that the secret exists — an unset
68+
variable can make Tauri prompt for a password and hang the job. The workflow always
69+
sets it.
70+
71+
If you later switch to a password-protected key, just add the secret. No workflow
72+
change needed.
73+
74+
### Backing up the signing key
75+
76+
`~/.tauri/figyterm.key` has no recovery path. GitHub secrets are write-only, so you
77+
cannot read it back out of Actions. If you lose the local copy and the machine, every
78+
installed client stops accepting updates permanently and every user has to reinstall
79+
by hand. Put it in a password manager.
80+
81+
---
82+
83+
## Troubleshooting
84+
85+
### `failed to run command security import: failed to import keychain certificate`
86+
87+
```
88+
security: SecKeychainItemImport: One or more parameters passed to a function were not valid.
89+
failed to bundle project: failed codesign application: failed to run command
90+
security import: failed to import keychain certificate
91+
```
92+
93+
**Cause:** an `APPLE_CERTIFICATE` env var is set to an empty string. A referenced
94+
secret that doesn't exist still *defines* the variable, and the Tauri bundler checks
95+
whether `APPLE_CERTIFICATE` is **present**, not whether it's non-empty. Seeing it
96+
present, it tries to import a certificate from an empty value and fails.
97+
98+
**Fix:** don't reference Apple signing secrets at all until you actually have them.
99+
The `Build Tauri app` step must have no `APPLE_*` variables in its `env:` block.
100+
101+
This is the one asymmetry worth remembering: for `TAURI_SIGNING_PRIVATE_KEY_PASSWORD`,
102+
present-and-empty is exactly right. For `APPLE_CERTIFICATE`, present-and-empty is
103+
fatal. "Wire it up now, add the secret later" works for the first and breaks the
104+
second.
105+
106+
### `FigyTerm_<version>_<arch>.app.tar.gz.sig is missing from the release`
107+
108+
**Cause:** `TAURI_SIGNING_PRIVATE_KEY` isn't set, so the bundler produced no updater
109+
signature.
110+
111+
**Fix:** add the secret, then re-run the workflow for that tag.
112+
113+
This check is deliberate. Without it the release would publish successfully with a
114+
`latest.json` that never matches anything, and the auto-updater would silently do
115+
nothing for every user — a failure you'd only discover much later.
116+
117+
### DMG bundling fails locally with `error running bundle_dmg.sh`
118+
119+
**Cause:** a volume named `FigyTerm` is already mounted — usually a previously
120+
downloaded FigyTerm `.dmg` still open in Finder. The bundler can't create its own
121+
volume under that name.
122+
123+
**Fix:** eject it (`ls /Volumes`, then `hdiutil detach /Volumes/FigyTerm`), or skip
124+
DMG bundling while iterating:
125+
126+
```bash
127+
npm run tauri build -- --bundles app
128+
```
129+
130+
CI is unaffected — runners have no stray mounts.
131+
132+
### Verifying a signature locally
133+
134+
To confirm a build's signature matches the public key in `tauri.conf.json` — i.e.
135+
that installed clients will actually accept it:
136+
137+
```bash
138+
python3 -c "
139+
import base64, json
140+
sig_text = base64.b64decode(open('src-tauri/target/release/bundle/macos/FigyTerm.app.tar.gz.sig').read()).decode()
141+
sig = base64.b64decode(sig_text.splitlines()[1])
142+
pub_text = base64.b64decode(json.load(open('src-tauri/tauri.conf.json'))['plugins']['updater']['pubkey']).decode()
143+
pub = base64.b64decode(pub_text.splitlines()[1])
144+
print('MATCH' if sig[2:10] == pub[2:10] else 'MISMATCH — clients would reject this update')"
145+
```
146+
147+
Build with the key in the environment first:
148+
149+
```bash
150+
TAURI_SIGNING_PRIVATE_KEY="$(cat ~/.tauri/figyterm.key)" \
151+
TAURI_SIGNING_PRIVATE_KEY_PASSWORD="" \
152+
npm run tauri build -- --bundles app
153+
```
154+
155+
---
156+
157+
## Recovering from a failed run
158+
159+
A failed run leaves a **draft** release holding whatever assets did upload. Nothing is
160+
public yet, so there's no rush.
161+
162+
**Same tag, after fixing the workflow:** push the fix, then **Actions → Release → Run
163+
workflow** with the tag. It reuses the draft, refreshes the notes, and re-uploads.
164+
Stale assets from the failed attempt are replaced.
165+
166+
Note the re-run builds from the **tag's** commit, not your branch — the checkout uses
167+
`ref: <tag>`. A workflow fix on `dev` won't be picked up until the tag points at it.
168+
Either move the tag:
169+
170+
```bash
171+
git tag -f v0.0.7 && git push -f origin v0.0.7
172+
```
173+
174+
or, cleaner, abandon the tag and cut the next one.
175+
176+
**Starting over:** delete the draft release in the GitHub UI, then
177+
`git push --delete origin v0.0.7 && git tag -d v0.0.7`.
178+
179+
---
180+
181+
## Enabling notarization later
182+
183+
Currently out of scope — an Apple Developer ID is $99/yr and there's no budget. See
184+
[`UPDATE-SYSTEM.md` §7.4](./UPDATE-SYSTEM.md). Recorded here so the door stays open.
185+
186+
Nothing else depends on it: the update system works unsigned, and notarization only
187+
improves *first* install (no `xattr`, no "unidentified developer", no "damaged and
188+
can't be opened").
189+
190+
When there is a Developer ID, add these secrets **and** the matching `env:` block to
191+
the `Build Tauri app` step — both together, never the block alone:
192+
193+
```yaml
194+
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
195+
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
196+
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
197+
APPLE_ID: ${{ secrets.APPLE_ID }}
198+
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
199+
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
200+
```
201+
202+
`APPLE_PASSWORD` is an app-specific password, not the Apple ID password.

‎docs/UPDATE-SYSTEM.md‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -485,9 +485,12 @@ secrets (`APPLE_CERTIFICATE`, `APPLE_CERTIFICATE_PASSWORD`, `APPLE_SIGNING_IDENT
485485
`APPLE_ID`, `APPLE_PASSWORD`, `APPLE_TEAM_ID`) and `tauri-action` signs and notarizes
486486
automatically. The update system keeps working unchanged; only first install improves.
487487

488-
**Design implication for now:** don't hardcode "unsigned" assumptions into the workflow.
489-
Keep the signing env block present but driven by optional secrets, so the build succeeds
490-
unsigned when they're absent and the eventual switch is a secret upload with no code change.
488+
**Design implication for now:** do *not* pre-wire the Apple signing env block "ready for
489+
later". This was tried and broke the release build: a referenced secret that doesn't
490+
exist still defines the variable as an empty string, and the bundler treats a *present*
491+
`APPLE_CERTIFICATE` as "sign this", then fails on `security import`. The env block and
492+
the secrets have to arrive together. See [`RELEASING.md`](./RELEASING.md) for the exact
493+
block to add and the failure signature.
491494

492495
Possible funding routes if it becomes worth revisiting: GitHub Sponsors, or Apple's fee
493496
waiver for nonprofits/educational institutions (unlikely to apply here, but it exists).

0 commit comments

Comments
 (0)