Skip to content

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.

text
PR A (engine)  ──┐
PR B (ui)      ──┼──> main (no desktop version bump) ──> cut-release ──> tag vX.Y.Z ──> GitHub Release
PR C (desktop) ──┘
  1. In feature PRs, bump only the library crates you touch (engine, orchestration, providers, ui). Do not bump the desktop app version.
  2. Merge after CI passes. No tag or GitHub Release needed.
  3. On main, run ./scripts/cut-release.sh (or open a small release PR with its output). This bumps the desktop app version once.
  4. Tag it; the version must match tauri.conf.json: git tag vX.Y.Z && git push origin vX.Y.Z.
  5. 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:

FileRole
crates/desktop/tauri.conf.jsonCanonical app version: updater, GitHub tag, user-facing version
crates/desktop/Cargo.tomlRust crate version (match tauri.conf.json)
crates/desktop/package.jsonnpm wrapper version (match tauri.conf.json)

crates/ui/package.json is separate: bump it when UI code changes (check-version-bump.sh).

PR checklist ​

  1. Classify the change (development-lanes.md).
  2. Run ./scripts/verify.sh for code PRs and release cuts. Docs/instruction-only PRs use link, path, syntax, and diff checks.
  3. Bump the library crate version of every substantive crate you changed.
  4. If crates/ui/** changed, bump crates/ui/package.json.
  5. Skip the desktop app version unless this PR is explicitly the release cut (otherwise use ./scripts/cut-release.sh on main).
  6. Open the PR. CI runs the version bump and release sync checks.
  7. Merge.

After merge (maintainer) ​

Only when cutting a release (desktop app version bumped):

bash
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
  1. The Release workflow runs release-verify on Ubuntu first: test-fast --execution --desktop and cargo 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.
  2. Review the assets (latest.json, .tar.gz, .dmg).
  3. 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:

MacGitHub Actions runnerRust target
Apple Silicon (ARM64)macos-26 (Tahoe)aarch64-apple-darwin
Intelmacos-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 / configPurpose
TAURI_SIGNING_PRIVATE_KEYSigns updater bundles in CI
TAURI_SIGNING_PRIVATE_KEY_PASSWORDOnly if the key has a real password: leave unset for --ci keys (empty password)
plugins.updater.pubkey in tauri.conf.jsonPublic half of signing key (in repo)

Generate keys and set the GitHub secret in one step, on your machine (needs gh auth):

bash
./scripts/setup-tauri-signing.sh

Manual equivalent, from crates/desktop:

bash
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.key

Then paste openflow.key.pub into plugins.updater.pubkey in tauri.conf.json. (The script does both.)

Validate before tagging:

bash
./scripts/validate-tauri-signing.sh

On 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:

SecretPurpose
APPLE_CERTIFICATEBase64-encoded exported .p12 certificate
APPLE_CERTIFICATE_PASSWORDPassword used when exporting the .p12
APPLE_SIGNING_IDENTITYFull Developer ID Application: Name (TEAMID) identity
APPLE_IDApple account email used for notarization
APPLE_PASSWORDApp-specific Apple account password
APPLE_TEAM_IDApple Developer team ID
KEYCHAIN_PASSWORDEphemeral 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:

bash
./scripts/verify-macos-app.sh /path/to/OpenFlow.app arm64 developer-id
./scripts/verify-macos-app.sh /path/to/OpenFlow.app x86_64 developer-id

Common cases ​

ChangeLibrary crate bump?Desktop bump in PR?Tag after merge?
Bug fix users should getYes (touched crates)No: use release trainOnly after cut-release
Engine/orchestration onlyYesNoOnly after cut-release
UI changeYes (ui)NoOnly after cut-release
Desktop IPC / Tauri adapterNo (unless other crates touched)NoOnly after cut-release
Docs, CI, internal refactorNoNoNo
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.

Released under the MIT License.