303 lines
8.1 KiB
Markdown
Executable File
303 lines
8.1 KiB
Markdown
Executable File
# 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>\(.*\)<\/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 <run-id>
|
|
```
|
|
|
|
## 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
|