Skip to content

Commit 86dbe7d

Browse files
committed
Fix release build by removing pre-wired Apple signing vars
The build failed at bundling with: security: SecKeychainItemImport: One or more parameters passed to a function were not valid. failed to bundle project: failed codesign application: failed to run command security import: failed to import keychain certificate A referenced secret that does not exist still *defines* the environment variable, as an empty string. The bundler checks whether APPLE_CERTIFICATE is present, not whether it is non-empty, so it saw "" and tried to import a certificate from it. Wiring the Apple block up ahead of the secrets does not work, so it is removed rather than made conditional. Enabling notarization later means adding the secrets and the env block together. Worth noting the asymmetry, since it is easy to draw the wrong lesson: present-and-empty is exactly right for TAURI_SIGNING_PRIVATE_KEY_PASSWORD (it means "no password", and an unset variable would instead make Tauri prompt and hang), but fatal for APPLE_CERTIFICATE. Adds docs/RELEASING.md covering the release flow, the one required secret, this failure and the others worth recognising, and how to recover a failed run from its draft release.
1 parent d4df967 commit 86dbe7d

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)