- name
- wordbrain-release
- description
- Publish a new release of the WordBrain desktop app and monitor CI on github.com/dickwu/wordbrain. Use whenever the user asks to "publish a release", "cut a release", "ship v0.x.y", "bump the version", "release WordBrain", "check ci" after a tag push, "write release notes", or "publish release notes". Also covers verifying the auto-updater endpoint, monitoring the Homebrew cask workflow, and recovering from common Tauri v2 release-pipeline failures (rustup `universal-apple-darwin`, frozen lockfile drift, missing `libpipewire`, signing-key format, Windows PowerShell expansion, `--no-default-features` rejected by tauri CLI, bash-3.2 globstar on macOS runners, Node 20 action deprecation, `release: published` not firing from `GITHUB_TOKEN`-created releases). Also covers the post-release symptoms: `brew install` failing with "undefined method 'license' for Cask" (cask DSL has no `license` stanza), the auto-updater failing with `None of the fallback platforms […] were found` (Tauri queries `darwin-aarch64`, not `darwin-universal`), and the Gatekeeper "WordBrain is damaged" prompt on un-notarized installs (xattr quarantine workaround). Always prefer this skill over running publish.sh from memory — the pipeline has subtle traps documented below.
# WordBrain Release Workflow
The project ships via `scripts/publish.sh` plus two GitHub Actions workflows.
A release goes from a clean main to a signed cross-platform GitHub release
plus an auto-updated Homebrew cask in ~10–15 minutes when nothing is broken.
## Project facts (memorise these)
| Fact | Value |
| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Repo | `dickwu/wordbrain` (the `lifefarmer/*` namespace is **only** the macOS bundle id) |
| Bundle id | `com.lifefarmer.wordbrain` (kept for compatibility with the user's other apps) |
| Publish script | `scripts/publish.sh` (NOT `./publish.sh` — it lives under `scripts/`) |
| Release workflow | `.github/workflows/release.yml` — fires on tag `v*` |
| Homebrew workflow | `.github/workflows/homebrew.yml` — fires on `workflow_run` watching `release.yml` (**not** `release: published`, which is blocked by GitHub for `GITHUB_TOKEN`-created releases — see trap #11) |
| Tap repo | `dickwu/homebrew-tap` (shared with the r2 project; cask file is `Casks/wordbrain.rb`) |
| Brew install | `brew install dickwu/tap/wordbrain` |
| Updater endpoint | `https://github.com/dickwu/wordbrain/releases/latest/download/latest.json` (configured in `src-tauri/tauri.conf.json` — verify this any time the GH owner is touched) |
| Updater pubkey | committed at `src-tauri/tauri.conf.json` `plugins.updater.pubkey` |
| Updater private key | `.omc/updater/wordbrain.key` (gitignored, never commit) |
## Required GitHub secrets
| Secret | Purpose | Format |
| ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `TAURI_SIGNING_PRIVATE_KEY` | Signs `latest.json` artifacts so the updater verifies them | **Raw text content of `.omc/updater/wordbrain.key`** — including the leading `untrusted comment: …` line. **NOT** base64-encoded. The publish.sh header used to say "store base64" but that's wrong; Tauri's signer parses the raw minisign format and rejects base64 with `Missing comment in secret key`. |
| `TAURI_SIGNING_PRIVATE_KEY_PASSWORD` | Password for the encrypted key (or empty string) | If you generated with `bunx tauri signer generate -w …` and accepted the empty password, set this secret to an empty string explicitly — leaving it unset behaves differently than setting it to `""`. |
| `HOMEBREW_TAP_TOKEN` | PAT used by `homebrew.yml` to push to the tap repo | Fine-grained PAT scoped to `dickwu/homebrew-tap` only, **Contents: Read and write**. |
| `APPLE_*` (optional) | macOS notarization (`APPLE_CERTIFICATE`, `APPLE_ID`, `APPLE_PASSWORD`, `APPLE_TEAM_ID`, `APPLE_SIGNING_IDENTITY`, `APPLE_CERTIFICATE_PASSWORD`) | Skips the Gatekeeper warning. Without these the build still succeeds but users see a "downloaded from Internet" prompt. **Do NOT** wire empty `APPLE_*` env vars into `release.yml` — Tauri's signer treats empty strings as "try to use the keychain" and fails with `failed to import keychain certificate`. Either set the secrets to real values or omit the env block entirely (current state). |
### Quick path: bootstrapping `HOMEBREW_TAP_TOKEN`
If the maintainer's `gh auth status` shows admin/push on `dickwu/homebrew-tap`
already (it does for `dickwu`), the fastest setup is to reuse the gh CLI's
own session token:
```bash
gh auth token | gh secret set HOMEBREW_TAP_TOKEN --repo dickwu/wordbrain
```
Caveat: this stores the user's gh CLI session token. If they `gh auth refresh`
or migrate machines the token rotates and the secret needs re-setting. Swap to
a dedicated fine-grained PAT scoped to `dickwu/homebrew-tap` only when hygiene
matters more than speed.
## Pre-flight
`scripts/publish.sh` requires a clean working tree. Always check first:
```bash
git status --short
```
If anything is dirty, commit or stash. Then sanity-check locally:
```bash
bun run test --run # vitest
bun run build # Next/Turbopack production build (catches missing deps — see trap #12)
( cd src-tauri && cargo check ) # base build
( cd src-tauri && cargo check --features dev-connector ) # dev path
```
Fix anything red before tagging — CI will surface the same issues 10 minutes
later, which wastes a build slot.
`bun run build` is the most important step: vitest + cargo + lockfile checks
all pass on incomplete `package.json` (a stray local `bun add` puts a package
in `node_modules` but never records it). Only Turbopack's module resolver,
running on a clean install, surfaces it — which is exactly what CI does.
## Running the release
`scripts/publish.sh` accepts an **explicit semver** (no `patch`/`minor`/`major`
shortcuts) plus an optional `--ci` flag:
```bash
scripts/publish.sh <x.y.z> --ci # bump version, commit, tag, push — CI does the build
scripts/publish.sh <x.y.z> # full local build + signed gh release create
scripts/publish.sh --keygen # one-time updater keypair generation
```
Default to `--ci` mode. The local mode requires Docker (Linux), `cargo-xwin`
(Windows cross-compile), and the Apple notarization stack — not worth the
local pain unless CI is actively broken.
After `--ci` exits, the tag and the bump commit are already on `origin/main`.
The release run starts immediately.
## Monitoring CI
```bash
gh run list --limit 4 --workflow release.yml
```
Find the new run (`pending` or `in_progress`), then:
```bash
gh run watch <run-id> --exit-status # blocks; exits non-zero on failure
```
The watch tool is sometimes flaky on its own exit code — always re-confirm
with `gh run list` after the watch returns.
Tauri triple-matrix builds usually run **10–15 minutes**. The macOS leg is
the longest because it builds both arm64 and x64 then `lipo`s them.
When green, two further runs typically appear:
- `homebrew.yml` (release-published trigger) — usually <30 s
- `release.yml`'s `publish` job — collects artifacts and creates the GH release
Verify the release after CI finishes:
```bash
gh release view v<version> --json assets --jq '.assets[].name'
```
You should see (rough names):
- `WordBrain_<version>_universal.dmg`
- `WordBrain_<version>_universal.app.tar.gz` + `.sig`
- `WordBrain_<version>_x64_en-US.msi` + `.msi.zip` + `.msi.zip.sig`
- `wordbrain_<version>_amd64.deb`
- `WordBrain_<version>_amd64.AppImage` + `.AppImage.tar.gz` + `.sig`
- `latest.json`
## Known traps (how to recognise + fix)
These have all bitten this pipeline once. If the symptom matches, jump to
the fix — don't go down the same investigative tree again.
### 1. `error: component 'rust-std' for target 'universal-apple-darwin' is unavailable`
`universal-apple-darwin` is a Tauri-CLI virtual target, not a real rustup
target. The `dtolnay/rust-toolchain@stable` step must install both real
targets on macOS:
```yaml
targets: ${{ matrix.os == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || matrix.target }}
```
Already wired in `release.yml` (Apr 2026) — don't revert.
### 2. `error: failed to run custom build command for libspa-sys` on Linux
PipeWire dev headers missing. Fix is two-pronged:
- `release.yml` apt-installs `libpipewire-0.3-dev` (belt).
- `tauri-plugin-connector` is now an optional `dev-connector` feature so
release builds don't compile xcap → libspa at all (suspenders).
If this re-appears, check that `Cargo.toml`'s `[features] default` is `[]`
and that the `Build & bundle` step in `release.yml` doesn't sneak the
feature back in.
### 3. `error: lockfile had changes, but lockfile is frozen`
`bun install --frozen-lockfile` (in CI) refuses to update `bun.lock`.
Caused by `package.json` drifting without re-running `bun install` locally.
Fix:
```bash
bun install # regenerates bun.lock
git add bun.lock
git commit -m "chore: refresh bun.lock"
```
Then re-cut the release at the next patch version (don't reuse a tagged
version).
### 4. `error: unexpected argument '--no-default-features' found` (tauri build)
Tauri CLI's `build` subcommand only accepts: `--runner`, `--debug`,
`--target`, `--features`, `--bundles`, `--no-bundle`, `--config`, `ARGS`,
`--ci`, `--skip-stapling`, `--ignore-version-mismatches`, `--no-sign`.
It does NOT pass unknown flags through to cargo.
WordBrain's strategy is to make the dev-only feature **opt-in**, not
default-on-then-stripped. So `Cargo.toml` has `default = []`, dev workflow
is `bun run tauri:dev` (which adds `--features dev-connector`), and CI
release just runs `tauri build` with no features.
If you ever need to disable a default feature in CI, you must flip the
default in `Cargo.toml`, NOT pass `--no-default-features`.
### 5. Windows build completes but produces no `.msi`
Windows runners default to PowerShell, where `$VAR` is PowerShell syntax
and does NOT expand env vars. Bash-style `$TAURI_ARGS --bundles $TAURI_BUNDLES`
becomes literal strings, tauri ignores `--bundles`, and only the bare
Auf GitHub ansehen