Releasing OpenFlow (macOS)
Pushing a v* tag starts the release workflow. Publishing its draft GitHub Release ships the build through Releases and the in-app updater. Merging to main alone releases nothing.
Release train (multiple PRs, one release)
Merge feature PRs into main, then cut one desktop release for everything accumulated.
PR A (engine) ──┐
PR B (ui) ──┼──> main (no desktop version bump) ──> cut-release ──> tag vX.Y.Z ──> GitHub Release
PR C (desktop) ──┘- In feature PRs, bump only the library crates you touch (
engine,orchestration,providers,ui). Do not bump the desktop app version. - Merge after CI passes. No tag or GitHub Release needed.
- On
main, run./scripts/cut-release.sh(or open a small release PR with its output). This bumps the desktop app version once. - Tag it; the version must match
tauri.conf.json:git tag vX.Y.Z && git push origin vX.Y.Z. - Publish the draft GitHub Release once the Release workflow finishes.
check-version-bump.sh requires a library version bump for each touched crate. The desktop version is bumped once, at release cut.
Version files
CI enforces that these stay in sync:
| File | Role |
|---|---|
crates/desktop/tauri.conf.json | Canonical app version: updater, GitHub tag, user-facing version |
crates/desktop/Cargo.toml | Rust crate version (match tauri.conf.json) |
crates/desktop/package.json | npm wrapper version (match tauri.conf.json) |
crates/ui/package.json is separate: bump it when UI code changes (check-version-bump.sh).
PR checklist
- Classify the change (
development-lanes.md). - Run
./scripts/verify.shfor code PRs and release cuts. Docs/instruction-only PRs use link, path, syntax, and diff checks. - Bump the library crate version of every substantive crate you changed.
- If
crates/ui/**changed, bumpcrates/ui/package.json. - Skip the desktop app version unless this PR is explicitly the release cut (otherwise use
./scripts/cut-release.shonmain). - Open the PR. CI runs the version bump and release sync checks.
- Merge.
After merge (maintainer)
Only when cutting a release (desktop app version bumped):
git checkout main
git pull origin main
./scripts/cut-release.sh # if not already bumped in a release PR
git tag v0.1.5 # must match tauri.conf.json version
git push origin v0.1.5- The Release workflow runs
release-verifyon Ubuntu first:test-fast --execution --desktopandcargo nextest run -p orchestration --test workflow_e2e. Only if it passes does the macOS build run, producing code-signed and updater-signed artifacts, verifying their signatures and architectures, and opening a draft GitHub Release. - Review the assets (
latest.json,.tar.gz,.dmg). - Publish the release.
Installed apps check https://github.com/philbotar/OpenFlow/releases/latest/download/latest.json and show a blue Settings badge when the published version is newer.
macOS release targets
One native bundle per Mac architecture:
| Mac | GitHub Actions runner | Rust target |
|---|---|---|
| Apple Silicon (ARM64) | macos-26 (Tahoe) | aarch64-apple-darwin |
| Intel | macos-15-intel (Sequoia) | x86_64-apple-darwin |
Explicit runner labels keep each build on its native architecture. The runner OS sets the build environment, not the app's minimum supported macOS. Rust caches use the workspace root target/, keyed per runner and Rust target. Both bundles pass signature and architecture checks before release review.
Updater signing setup
| Secret / config | Purpose |
|---|---|
TAURI_SIGNING_PRIVATE_KEY | Signs updater bundles in CI |
TAURI_SIGNING_PRIVATE_KEY_PASSWORD | Only if the key has a real password: leave unset for --ci keys (empty password) |
plugins.updater.pubkey in tauri.conf.json | Public half of signing key (in repo) |
Generate keys and set the GitHub secret in one step, on your machine (needs gh auth):
./scripts/setup-tauri-signing.shManual equivalent, from crates/desktop:
CI=1 ../ui/node_modules/.bin/tauri signer generate --write-keys ~/.tauri/openflow.key --force --ci
gh secret set TAURI_SIGNING_PRIVATE_KEY --repo philbotar/OpenFlow < ~/.tauri/openflow.keyThen paste openflow.key.pub into plugins.updater.pubkey in tauri.conf.json. (The script does both.)
Validate before tagging:
./scripts/validate-tauri-signing.shOn a pubkey mismatch with a secret already on GitHub, run ./scripts/sync-tauri-pubkey.sh, then commit tauri.conf.json.
Updater signatures protect update integrity. They don't satisfy macOS Gatekeeper.
macOS code signing and notarization
tauri.conf.json sets bundle.macOS.signingIdentity to -, which makes a complete ad-hoc signature when Developer ID secrets are absent. Apple Silicon then won't call the downloaded app damaged, but users must still approve it in System Settings → Privacy & Security.
For public distribution, use a paid Apple Developer Program account. Create and export a Developer ID Application certificate, then add all of these repository secrets:
| Secret | Purpose |
|---|---|
APPLE_CERTIFICATE | Base64-encoded exported .p12 certificate |
APPLE_CERTIFICATE_PASSWORD | Password used when exporting the .p12 |
APPLE_SIGNING_IDENTITY | Full Developer ID Application: Name (TEAMID) identity |
APPLE_ID | Apple account email used for notarization |
APPLE_PASSWORD | App-specific Apple account password |
APPLE_TEAM_ID | Apple Developer team ID |
KEYCHAIN_PASSWORD | Ephemeral CI keychain password |
With all secrets present, the Release workflow imports the certificate, overrides the ad-hoc identity, has Tauri notarize and staple the app, and requires Gatekeeper and stapler validation to pass. A partial secret setup fails before the build.
Before publishing a draft, download both DMGs and confirm:
./scripts/verify-macos-app.sh /path/to/OpenFlow.app arm64 developer-id
./scripts/verify-macos-app.sh /path/to/OpenFlow.app x86_64 developer-idCommon cases
| Change | Library crate bump? | Desktop bump in PR? | Tag after merge? |
|---|---|---|---|
| Bug fix users should get | Yes (touched crates) | No: use release train | Only after cut-release |
| Engine/orchestration only | Yes | No | Only after cut-release |
| UI change | Yes (ui) | No | Only after cut-release |
| Desktop IPC / Tauri adapter | No (unless other crates touched) | No | Only after cut-release |
| Docs, CI, internal refactor | No | No | No |
Ready to ship accumulated main | - | Yes (./scripts/cut-release.sh) | Yes |
Never retag a published version; bump the patch version and push a new tag. Drafts are invisible to the updater until published. Users on builds without an updater need one manual install first.
