# Hex Release Pipeline - GitHub Actions Plan ## Context Need to set up automated release pipeline for macOS app distribution via: - **Sparkle** (in-app updates via S3) - **Homebrew Cask** (requires ZIP artifact) - **GitHub Releases** (visibility/download page) ## Current State ### What Works ✅ - `scripts/tool.py` - Python script, works locally - `tools/release.ts` - TypeScript/Effect version, works locally - Both use local notarytool keychain profile (`AC_PASSWORD`) - Both upload to S3, generate Sparkle appcast ### What's Broken ❌ - `.github/workflows/release.yml` - Incomplete, has TODO stubs - No ZIP creation (Homebrew needs this) - No GitHub release integration ## Standard Approaches (2025) ### Option A: Pure GitHub Actions YAML **How most projects do it:** ```yaml - uses: apple-actions/import-codesign-certs@v2 - run: xcodebuild ... - uses: lando/notarize-action@v2 - run: create-dmg ... - uses: actions/create-release@v1 ``` **Pros:** Standard, lots of examples, community actions **Cons:** Verbose YAML, hard to test locally, logic spread across steps ### Option B: Call Local Script (What You Have) **Use existing `release.ts` from workflow:** ```yaml - uses: oven-sh/setup-bun@v2 - run: bun run tools/release.ts --bucket hex-updates ``` **Pros:** Test locally, type-safe, reusable, Effect composability **Cons:** Need env var auth fallback for CI ### Option C: Hybrid Use actions for signing/notarization, script for build/upload ## Recommended: Option B (Enhanced) Why? You already built a complete, working TypeScript tool. Just need CI auth. ### Architecture ``` GitHub Actions Workflow ├─ Setup (checkout, certs, Bun) ├─ Call: bun run tools/release.ts │ ├─ Detects CI via env │ ├─ Uses APPLE_ID/PASSWORD instead of keychain │ ├─ Builds, signs, notarizes │ ├─ Creates DMG + ZIP │ ├─ Uploads to S3 (Sparkle) │ └─ Returns artifacts paths └─ Create GitHub Release (upload DMG + ZIP) ``` ### Required Changes #### 1. Modify `release.ts` **Add CI detection:** ```typescript const getNotarizeArgs = () => { if (process.env.GITHUB_ACTIONS) { return [ "--apple-id", process.env.APPLE_ID!, "--password", process.env.APPLE_ID_PASSWORD!, "--team-id", process.env.TEAM_ID! ] } return ["--keychain-profile", "AC_PASSWORD"] } ``` **Add ZIP creation:** ```typescript // After DMG creation yield* runCommandCheck("ditto", "-c", "-k", "--keepParent", appBundle, join(updatesDir, `Hex-${newVersion}.zip`)) ``` #### 2. Update Workflow Replace `.github/workflows/release.yml` with: ```yaml name: Release on: workflow_dispatch: inputs: version_bump: type: choice options: [patch, minor, major] default: patch jobs: release: runs-on: macos-15 steps: - uses: actions/checkout@v4 - name: Setup Xcode run: sudo xcode-select -s /Applications/Xcode_16.2.app - name: Setup Bun uses: oven-sh/setup-bun@v2 - name: Install dependencies run: cd tools && bun install - name: Import signing certificate env: MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }} MACOS_CERTIFICATE_PWD: ${{ secrets.MACOS_CERTIFICATE_PWD }} run: | # Decode and import cert to default keychain echo "$MACOS_CERTIFICATE" | base64 --decode > /tmp/cert.p12 security import /tmp/cert.p12 -P "$MACOS_CERTIFICATE_PWD" -A security set-key-partition-list -S apple-tool:,apple: -s -k "" login.keychain-db rm /tmp/cert.p12 - name: Build and release run: bun run tools/release.ts --bucket hex-updates env: APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} TEAM_ID: ${{ secrets.TEAM_ID }} AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - name: Create GitHub Release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | VERSION=$(cat Hex/Info.plist | grep -A1 CFBundleShortVersionString | tail -1 | sed 's/.*\(.*\)<\/string>.*/\1/') # release.ts already tags + creates the release with the aggregated changelog. # Keep this step only if we ever skip that behavior in CI. gh release create "v$VERSION" \ --title "Hex v$VERSION" \ --notes-file build/release-notes-$VERSION.md \ updates/Hex-$VERSION.dmg \ updates/Hex-$VERSION.zip ``` ### Required Secrets (9) Set via: `gh secret set SECRET_NAME` ```bash # Apple Developer APPLE_ID # your@email.com APPLE_ID_PASSWORD # xxxx-xxxx-xxxx-xxxx (app-specific) TEAM_ID # QC99C9JE59 DEVELOPMENT_TEAM # QC99C9JE59 (same as TEAM_ID) # Code Signing MACOS_CERTIFICATE # base64 encoded .p12 MACOS_CERTIFICATE_PWD # .p12 password # S3 / Sparkle AWS_ACCESS_KEY_ID # S3 access AWS_SECRET_ACCESS_KEY # S3 secret # Auto-provided GITHUB_TOKEN # Auto-available in actions ``` #### How to Generate **App-specific password:** ```bash # 1. Go to appleid.apple.com # 2. Sign in > Security > App-Specific Passwords # 3. Generate for "Hex GitHub Actions" # 4. Copy xxxx-xxxx-xxxx-xxxx ``` **Certificate base64:** ```bash # Export from Keychain Access as .p12 base64 -i DeveloperID.p12 | pbcopy # Paste into secret ``` ## Comparison: What Others Do ### Electron Apps (Sparkle, DMG) - **VSCode**: Custom Azure Pipelines + scripts - **Obsidian**: Electron-builder in Actions - **Raycast**: Similar to our Option B (script from workflow) ### Native macOS Apps - **Bartender**: Manual releases - **Alfred**: Private infrastructure - **Rectangle**: GitHub Actions with notarize action **Trend:** Moving toward script-based (easier to test/debug locally) ## Homebrew Cask Setup After first release: ```bash # 1. Create cask brew create --cask hex # 2. Test brew install --cask hex # 3. Submit PR to homebrew/cask # OR create personal tap: homebrew-hex ``` **Cask needs:** - ZIP artifact (not DMG) - universal convention - GitHub release URL - SHA256 (auto-calculated by brew) ## Testing Strategy ### Local Testing ```bash # Full release (dry-run flag needed?) bun run tools/release.ts --bucket hex-updates-test # Test specific steps bun run tools/release.ts --skip-upload ``` ### CI Testing ```bash # Trigger workflow gh workflow run release.yml # Monitor gh run list gh run view ``` ## Migration Path 1. ✅ Modify `release.ts` - add CI auth 2. ✅ Add ZIP creation to `release.ts` 3. ✅ Replace workflow YAML 4. ✅ Set all 9 secrets 5. ✅ Test release to staging S3 bucket 6. ✅ First real release 7. ✅ Submit Homebrew cask ## ✅ IMPLEMENTED (Tag-Based Approach) ### Version Management **Solved:** Git tags determine version - Push `v0.2.12` → builds version `0.2.12` - Auto-increments build number - Updates all version files ### Configuration **Solved:** Effect Config system - Auto-detects CI vs local environment - Uses keychain locally, env vars in CI - Type-safe configuration ### Artifacts **Solved:** Creates both DMG + ZIP - DMG for direct download / Sparkle - ZIP for Homebrew cask - Both notarized and signed ### Changelog Automation **Solved:** Changesets CLI drives SemVer + release notes - Contributors run `bunx changeset` to capture a patch/minor/major summary - Release tool enforces pending changesets, runs `changeset version`, and copies the aggregated `CHANGELOG.md` into `Hex/Resources/changelog.md` - GitHub releases reuse the generated changelog section via `--notes-file`, so Sparkle, the in-app sheet, and GitHub stay identical ## Remaining Questions 1. **S3 bucket:** Staging bucket for pre-release testing? 2. **Rollback:** Failed notarization handling? 3. **Certificate expiry:** Track and rotate Developer ID cert ## Completed Tasks - [x] Update `release.ts` with Effect Config - [x] Add CI detection (Apple creds optional) - [x] Add ZIP creation for Homebrew - [x] Rewrite workflow for tag-based releases - [x] Document release process - [ ] Configure secrets (manual step) - [ ] First release test - [ ] Submit Homebrew cask