| name | release |
| description | Use when the user says "release", "cut a release", "ship a new version", "release new version", "do the desktop release", or "tag a release". Computes the next version, generates the changelog, pushes a `v*-macos` tag, and monitors the Codemagic CI build that produces and publishes the macOS desktop release. NEVER builds locally. |
| allowed-tools | Bash, Read, Edit, Grep |
Fazm Desktop Release Skill (Codemagic-only)
Cut a new Fazm Desktop release. All builds run in Codemagic CI triggered by a v*-macos git tag. There is no local build path.
CRITICAL RULES
- NEVER build the .app locally for a release. No
xcodebuild, no xcrun swift build, no shell wrapper that produces release artifacts. The local build path (release.sh) was deleted on purpose 2026-05-02. It silently stripped overlay assets and shipped two broken versions (v2.7.0 and v2.7.1) without browser-overlay-init.js because it lacked the codemagic.yaml hard-check. Codemagic has stricter checks; use it.
- NEVER push the same
v*-macos tag twice. Each Codemagic build generates a fresh EdDSA signature for the Sparkle ZIP. Re-pushing the tag (or re-running the build) produces a new ZIP with a different signature while the OLD ZIP can still be cached on the appcast โ Sparkle then refuses the update with "improperly signed" for every user. If you need to retry, bump to the next patch version (e.g. v2.7.2+2007002-macos).
- NEVER edit
codemagic.yaml mid-release. If a step fails, root-cause first; do not patch CI to silence it. Once you know the fix, land it on main, bump version, push a fresh tag.
- NEVER skip the changelog step. Codemagic's "Prepare changelog" step commits the consolidated CHANGELOG.json back to
main; if unreleased is empty, the GitHub release notes will be empty too.
- Do not promote to beta or stable without explicit user approval. Each promotion (staging โ beta โ stable) is a separate user decision.
- NEVER add a write to
gs://fazm-prod-releases/desktop/latest.json to codemagic.yaml. That manifest is what new website signups read via the stub installer. The CI workflow is shared by all -macos tags, and every -macos tag registers as the staging channel in Firestore; if CI also writes the global manifest, every staging build silently ships to every new signup, bypassing the entire promotion gate. The ONLY writer is scripts/promote_release.sh, fired on explicit promotion to beta or stable. This regression has been introduced twice already (404836cc 2026-03-20, undoing e7741d9f 2026-03-19) and bled 35 staging builds (2.9.13 through 2.9.47) to ~250 new users between March and May 2026. verify-release.sh step 2 now hard-fails on it. If a CI step seems to "need" to update the global manifest, you are misreading the channel model; read Desktop/Sources/UpdaterViewModel.swift:269 and scripts/promote_release.sh:120-135 before touching anything.
How a release works (architecture)
git tag v$VERSION+$BUILD-macos โ git push origin <tag> โ Codemagic triggers
โ
build ยท sign ยท notarize ยท staple ยท DMG ยท Sparkle ZIP
โ
gh release create + Firestore register
โ
appcast.xml deploy โ users update
- Codemagic project:
fazm-desktop-release (workflow defined in codemagic.yaml)
- App ID:
69a8b2c779d9075efc609b8d
- API token:
$CODEMAGIC_API_TOKEN (set in ~/.zshrc)
- Triggering tag patterns:
v*-macos (production) and v*-macos-staging (pre-release)
- Final artifacts:
Fazm.zip (Sparkle auto-update), Fazm.dmg (manual install), arch-specific zips on GCS, stub installer DMG
The full pipeline (in order, defined in codemagic.yaml):
- Extract version + build number from tag
- Set up keychain + Developer ID
- Build acp-bridge (TypeScript โ JS, runs
npm install which triggers patch-playwright-overlay.cjs postinstall)
- Prepare universal ffmpeg / Node / cloudflared (arm64 + x86_64)
- Resolve SPM packages
- Build mcp-server-macos-use, whatsapp-mcp (both arm64 + x86_64)
- Clone Google Workspace MCP
- Build Swift app (universal binary)
- Create universal app bundle โ includes the overlay file copy AND the
_fazmOverlayScript hard-check
- Sign app (Developer ID, all native binaries in node_modules)
- Notarize, staple
- Create arch-specific zips for stub installer
- Upload dSYMs to Sentry
- Create + sign + notarize + staple DMG
- Create Sparkle ZIP + EdDSA sign
- Prepare changelog (consolidates
unreleased โ versioned, commits back to main)
- Create GitHub release (uploads
Fazm.zip and Fazm.dmg)
- Register release in Firestore (channel =
staging initially)
- Deploy appcast.xml
- Upload arch-specific zips to GCS
- Build + upload stub installer
Pre-release checklist (run locally BEFORE pushing the tag)
xcrun swift scripts/check_settings_search.swift
python3 -c "
import json
data = json.load(open('CHANGELOG.json'))
entries = data.get('unreleased', [])
if not entries:
print('FATAL: unreleased is empty โ add entries before tagging'); exit(1)
print('\n'.join(entries))
"
git status --porcelain
git rev-parse --abbrev-ref HEAD
cd acp-bridge && PATH=/opt/homebrew/bin:$PATH npm install --no-audit --no-fund && npm run build && cd ..
grep -q "_fazmOverlayScript" acp-bridge/node_modules/playwright-core/lib/coreBundle.js \
|| { echo "FATAL: overlay patch did not apply locally โ Codemagic will fail too"; exit 1; }
if grep -vE '^\s*#' codemagic.yaml | grep -qE 'gs://[^"]*desktop/latest\.json'; then
echo "FATAL: codemagic.yaml has a write to gs://.../desktop/latest.json outside scripts/promote_release.sh."
echo " This is the exact March 2026 regression (commit 404836cc)."
echo " Read CRITICAL RULE #6 in this skill before continuing."
exit 1
fi
If any check fails, fix it on main first. Tagging a broken commit wastes ~20 min of CI time.
Cutting the release
Step 1: Compute next version
Versions are MAJOR.MINOR.PATCH+BUILD. Build number is monotonically increasing; the convention has been MMmmppp + 2000000 (e.g. v2.7.1 โ build 2007001).
LAST_TAG=$(git tag -l 'v*-macos' | grep -v staging | sort -V | tail -1)
echo "Last release tag: $LAST_TAG"
Step 2: Push the tag
TAG="v${VERSION}+${BUILD}-macos"
git tag "$TAG"
git push origin "$TAG"
That single push triggers Codemagic. Do not push to main separately for the release; Codemagic itself commits the consolidated changelog back during the build.
Step 3: Monitor the build
TOKEN=$CODEMAGIC_API_TOKEN
APP_ID=69a8b2c779d9075efc609b8d
TAG="v${VERSION}+${BUILD}-macos"
sleep 30
BUILD_ID=$(curl -s -H "x-auth-token: $TOKEN" \
"https://api.codemagic.io/builds?appId=$APP_ID&limit=10" | \
python3 -c "
import json, sys
for b in json.load(sys.stdin).get('builds', []):
if b.get('tag') == '$TAG':
print(b['_id']); break
")
echo "Build: $BUILD_ID"
while true; do
STATUS=$(curl -s -H "x-auth-token: $TOKEN" \
"https://api.codemagic.io/builds/$BUILD_ID" | \
python3 -c "import json, sys; print(json.load(sys.stdin)['build']['status'])")
echo "$(date +%T) status=$STATUS"
case "$STATUS" in
finished) echo "โ Build succeeded"; break ;;
failed|canceled) echo "โ Build $STATUS โ fetch logs via the codemagic skill"; break ;;
esac
sleep 30
done
If a step fails, fetch the per-step log via the codemagic skill and root-cause it. Do not retry the same tag; bump to the next patch version after the fix lands.
Step 4: Verify the release
./verify-release.sh "$VERSION"
This script automatically:
- Checks the appcast serves the correct version
- Downloads
Fazm.zip (what auto-updaters get) and verifies signature, notarization, Gatekeeper acceptance
- Launches the app and confirms it starts
- Checks the DMG download endpoint
If verification fails, immediately roll back with the rollback skill (.claude/skills/rollback/SKILL.md) โ do not try to "fix the live release" by pushing another tag.
Step 5: Smoke-test on staging
A fresh release lands on the staging channel. Run /test-release on the remote MacStadium machine (staging channel only). See .claude/skills/test-release/SKILL.md.
Report results. Wait for explicit user approval before promoting.
Step 6: Promote (only when user explicitly says so)
./scripts/promote_release.sh "$TAG"
./scripts/promote_release.sh "$TAG" --stable
Each promotion is a separate user decision. Re-test on the appropriate machine after each.
Failure handling
Codemagic step fails
- Identify the failing step from the build log (use the
codemagic skill)
- Land the fix on
main
- Bump to the next patch version (do not re-push the same tag)
- Push the new tag
Notarization fails (unsigned binary)
- Check the notarytool log for the unsigned path
- Add a
codesign --force --options runtime --timestamp $CS_PAGESIZE --sign "$SIGN_IDENTITY" "$path" line to the "Sign app" step in codemagic.yaml
- Land it, bump version, retag
Stapling fails (Apple CDN propagation)
- Common transient. Bump to the next patch version and retry. The build is fast on cache hit; notarization is usually quick the second time.
Hard-check fails (overlay asset โฆ missing or coreBundle.js NOT patched)
- The patch script
acp-bridge/scripts/patch-playwright-overlay.cjs did not match the current Playwright MCP shape (see commit 4584c94f for the most recent successful update)
- Fix the OLD_BLOCK / NEW_BLOCK in the patch script to match the new shape
- Land the fix, bump version, retag
- Do not set
FAZM_ALLOW_MISSING_OVERLAY=1 in CI just to ship โ that ships a broken indicator
"Improperly signed" reported by users after release
- Means the same tag was built twice with different EdDSA signatures, or the Sparkle ZIP on GitHub does not match the signature in the appcast
- Do not retag. Bump to the next patch version and push a fresh release. The new appcast entry will carry the new ZIP and matching signature.
After a release ships
- Append a changelog entry to
unreleased in CHANGELOG.json for any subsequent user-visible change. The next release rolls those into its versioned entry automatically.
- Use
./scripts/sentry-release.sh to monitor crash health on the new version (see the sentry-release skill).
What was deleted and why
release.sh (root) โ local build pipeline. Deleted 2026-05-02. It had no overlay-asset copy step and no hard check, so it shipped v2.7.0 and v2.7.1 missing browser-overlay-init.js, silently breaking the "Browser controlled by Fazm" indicator. Codemagic has full feature parity (and stricter checks). Local builds are forbidden for release artifacts; use ./run.sh only for dev iteration on Fazm Dev.app.
Bash(./release.sh:*) in .claude/settings.local.json โ removed at the same time so agents can't accidentally invoke a path that no longer exists.
Key files
- CI workflow:
codemagic.yaml
- Verification:
verify-release.sh
- Promotion:
scripts/promote_release.sh
- Changelog:
CHANGELOG.json
- Overlay patch:
acp-bridge/scripts/patch-playwright-overlay.cjs (postinstall, must match current @playwright/mcp shape)
- Rollback skill:
.claude/skills/rollback/SKILL.md
- Codemagic skill:
.claude/skills/codemagic/SKILL.md
- Test skill:
.claude/skills/test-release/SKILL.md