diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index f6ff858..17103ba 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -354,8 +354,12 @@ jobs: ditto -c -k --sequesterRsrc --keepParent VocaMac.app "$ZIP_NAME" echo "ZIP_NAME=${ZIP_NAME}" >> "$GITHUB_ENV" + # Create stable-named copies for Homebrew cask (no date/SHA) + cp "$DMG_NAME" "VocaMac-nightly-${ARCH}.dmg" + cp "$ZIP_NAME" "VocaMac-nightly-${ARCH}.zip" + echo "Artifacts created:" - ls -lh "$DMG_NAME" "$ZIP_NAME" + ls -lh "$DMG_NAME" "$ZIP_NAME" "VocaMac-nightly-${ARCH}.dmg" "VocaMac-nightly-${ARCH}.zip" - name: Generate checksums run: | diff --git a/.github/workflows/update-homebrew-cask.yml b/.github/workflows/update-homebrew-cask.yml new file mode 100644 index 0000000..fedc4cb --- /dev/null +++ b/.github/workflows/update-homebrew-cask.yml @@ -0,0 +1,67 @@ +# Updates the Homebrew tap cask (jatinkrmalik/homebrew-vocamac) when a GitHub Release is published. +# Downloads the release DMG, computes its sha256, and commits the updated version+checksum to the tap repo. +name: Update Homebrew Cask + +on: + release: + types: [published] + +jobs: + update-cask: + runs-on: ubuntu-latest + steps: + - name: Checkout main repo + uses: actions/checkout@v4 + + - name: Extract version from release tag + id: version + run: | + TAG="${{ github.event.release.tag_name }}" + VERSION="${TAG#v}" + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + echo "Extracted version: ${VERSION}" + + - name: Download DMG artifact + run: | + VERSION="${{ steps.version.outputs.version }}" + DMG_URL="https://github.com/jatinkrmalik/vocamac/releases/download/v${VERSION}/VocaMac-${VERSION}-arm64.dmg" + curl -sL -o vocamac.dmg "$DMG_URL" + echo "Downloaded DMG from ${DMG_URL}" + + - name: Compute DMG sha256 + id: checksum + run: | + SHA256=$(shasum -a 256 vocamac.dmg | cut -d' ' -f1) + echo "sha256=${SHA256}" >> "$GITHUB_OUTPUT" + echo "Computed sha256: ${SHA256}" + + - name: Checkout tap repo + uses: actions/checkout@v4 + with: + repository: jatinkrmalik/homebrew-vocamac + token: ${{ secrets.HOMEBREW_TAP_TOKEN }} + path: homebrew-vocamac + + - name: Update cask file + run: | + VERSION="${{ steps.version.outputs.version }}" + SHA256="${{ steps.checksum.outputs.sha256 }}" + CASK_FILE="homebrew-vocamac/Casks/vocamac.rb" + + # Update the version line + sed -i "s/^ version \".*\"/ version \"${VERSION}\"/" "$CASK_FILE" + # Update the sha256 line + sed -i "s/^ sha256 \".*\"/ sha256 \"${SHA256}\"/" "$CASK_FILE" + + echo "Updated ${CASK_FILE}:" + grep -E "^\s+(version|sha256)" "$CASK_FILE" + + - name: Commit and push to tap repo + run: | + VERSION="${{ steps.version.outputs.version }}" + cd homebrew-vocamac + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add Casks/vocamac.rb + git commit -m "chore: update vocamac to v${VERSION}" + git push diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 41b47d4..f98aad8 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -496,7 +496,7 @@ swift build -c release ### 7.3 Distribution Strategy 1. **GitHub Releases** — Developer ID signed & notarized DMG and ZIP, built by CI -2. **Homebrew Cask** - `brew install --cask vocamac` (planned) +2. **Homebrew Cask** - `brew install --cask vocamac` (see docs/HOMEBREW.md) 3. **Mac App Store** - Future consideration (requires sandbox compliance) --- diff --git a/docs/HOMEBREW.md b/docs/HOMEBREW.md new file mode 100644 index 0000000..24c5be0 --- /dev/null +++ b/docs/HOMEBREW.md @@ -0,0 +1,235 @@ +# VocaMac Homebrew Distribution Guide + +## Overview + +VocaMac is distributed via Homebrew as a **cask**, not a formula. This distinction matters: + +- **Formula** — for command-line tools and libraries built from source +- **Cask** — for pre-built macOS applications distributed as binaries (`.app`, `.dmg`) + +Since VocaMac is a native macOS `.app` distributed as a signed and notarized DMG via GitHub Releases, a cask is the correct packaging format. Users get the exact same binary as the manual download, installed with a single command. + +## Quick Start (For Users) + +```bash +# Install +brew tap jatinkrmalik/vocamac +brew install --cask vocamac + +# Upgrade to latest version +brew upgrade --cask vocamac + +# Uninstall +brew uninstall --cask vocamac +brew untap jatinkrmalik/vocamac +``` + +After installation, VocaMac appears in `/Applications/VocaMac.app`. Launch it from Spotlight or the Applications folder. + +## Nightly Builds + +A nightly cask is also available, built daily from the latest `main` branch: + +```bash +brew install --cask vocamac-nightly +``` + +The nightly cask uses `version :latest` and `sha256 :no_check` because the DMG content changes with every daily build. Homebrew will always fetch the newest artifact without needing a cask definition update. + +**Stable and nightly conflict.** Both casks install to `/Applications/VocaMac.app`, so you can only have one installed at a time. Uninstall the stable cask before installing nightly, or vice versa: + +```bash +brew uninstall --cask vocamac +brew install --cask vocamac-nightly +``` + +Nightly is a pre-release build intended for testing and early feedback. Use the stable release for daily use. + +No auto-update workflow is needed for the nightly cask. The cask definition itself never changes. Homebrew re-downloads the latest DMG each time `brew upgrade --cask vocamac-nightly` runs. + +## Custom Tap Setup + +The cask lives in a custom tap repository: `jatinkrmalik/homebrew-vocamac`. + +### Creating the Tap Repository + +1. Create a new public GitHub repository named `homebrew-vocamac` under the `jatinkrmalik` account +2. The repository must follow Homebrew tap naming: `homebrew-` +3. Clone it locally: + ```bash + git clone https://github.com/jatinkrmalik/homebrew-vocamac.git + cd homebrew-vocamac + ``` +4. Create the cask directory structure: + ```bash + mkdir -p Casks + ``` +5. Copy the cask file from the main repo: + ```bash + cp /path/to/vocamac/homebrew/Casks/vocamac.rb Casks/ + ``` +6. Commit and push: + ```bash + git add Casks/vocamac.rb + git commit -m "chore: add vocamac cask" + git push origin main + ``` + +Users can then install with `brew tap jatinkrmalik/vocamac && brew install --cask vocamac`. + +## Testing Locally + +Before pushing a cask update to the tap, test it locally against a real DMG: + +```bash +brew install --cask ./homebrew/Casks/vocamac.rb +``` + +This installs the cask directly from the file path, bypassing the tap. It requires a real DMG to exist at the URL specified in the cask (i.e., a published GitHub Release). + +To verify the installation: + +```bash +ls /Applications/VocaMac.app +brew info --cask vocamac +``` + +To uninstall after testing: + +```bash +brew uninstall --cask vocamac +``` + +## Manual Cask Update + +When a new VocaMac version ships, the cask needs two updates: the `version` string and the `sha256` checksum. + +1. Download the new DMG: + ```bash + curl -L -o VocaMac-X.Y.Z-arm64.dmg \ + https://github.com/jatinkrmalik/vocamac/releases/download/vX.Y.Z/VocaMac-X.Y.Z-arm64.dmg + ``` + +2. Compute the SHA-256: + ```bash + shasum -a 256 VocaMac-X.Y.Z-arm64.dmg + ``` + +3. Update `homebrew/Casks/vocamac.rb`: + - Change `version "X.Y.Z"` to the new version + - Replace `sha256 :no_check` with `sha256 ""` + +4. Test locally (see [Testing Locally](#testing-locally) above) + +5. Commit and push to the tap repository: + ```bash + cd /path/to/homebrew-vocamac + git add Casks/vocamac.rb + git commit -m "chore: update vocamac to vX.Y.Z" + git push origin main + ``` + +## Auto-Update Workflow + +The repository includes `.github/workflows/update-homebrew-cask.yml`, which automates cask updates on every release publish. + +### How It Works + +1. The workflow triggers on `release` event with `types: [published]` +2. It extracts the version tag (e.g., `v0.6.2` → `0.6.2`) +3. It downloads the DMG from the release assets +4. It computes the SHA-256 checksum +5. It updates `homebrew/Casks/vocamac.rb` with the new version and sha256 +6. It pushes the change to the `jatinkrmalik/homebrew-vocamac` tap repository + +### Required GitHub Secret + +The workflow needs a Personal Access Token with `repo` scope to push to the tap repository: + +- **Secret name:** `HOMEBREW_TAP_TOKEN` +- **Scope:** `repo` (full control of private and public repositories) +- **Set at:** Repository Settings → Secrets and variables → Actions + +Generate the token at [github.com/settings/tokens](https://github.com/settings/tokens) with the `repo` scope. The token owner must have write access to `jatinkrmalik/homebrew-vocamac`. + +## Submitting to homebrew-cask + +Once VocaMac meets the notability requirements, the cask can be submitted to the official [homebrew-cask](https://github.com/Homebrew/homebrew-cask) repository, eliminating the need for a custom tap. + +### Requirements + +- **75+ GitHub stars** on the repository +- **Signed and notarized** DMG (VocaMac already meets this) +- **Stable release** (not a pre-release or nightly) +- **Active maintenance** (recent commits, responsive maintainer) + +### Submission Process + +1. Fork [Homebrew/homebrew-cask](https://github.com/Homebrew/homebrew-cask) +2. Create a branch: `git checkout -b add-vocamac` +3. Run `brew create --cask ` to generate the cask file +4. Place it in `Casks/v/vocamac.rb` (note the subdirectory structure) +5. Test: `brew install --cask ./Casks/v/vocamac.rb` +6. Commit and open a PR against `Homebrew/homebrew-cask` +7. Respond to reviewer feedback + +Once merged, users install with just `brew install --cask vocamac` — no tap required. + +## Zap Behavior + +Running `brew uninstall --zap vocamac` removes the app **and** all associated user data: + +```ruby +zap trash: [ + "~/Library/Application Support/VocaMac", # Downloaded models, user config + "~/Library/Caches/com.vocamac.app", # Cached data + "~/Library/Preferences/com.vocamac.app.plist", # UserDefaults/preferences + "~/Library/Saved Application State/com.vocamac.app.savedState", # Window state +] +``` + +This is useful for a clean reinstall or when troubleshooting. A plain `brew uninstall --cask vocamac` only removes the `.app` bundle and leaves user data intact. + +## Troubleshooting + +### Cask install fails with "SHA256 mismatch" + +The checksum in the cask file doesn't match the downloaded DMG. This happens when the cask hasn't been updated for a new release. + +**Fix:** Update the cask manually (see [Manual Cask Update](#manual-cask-update)) or wait for the auto-update workflow to complete. + +### "It seems there is already an App at..." + +A previous installation exists at `/Applications/VocaMac.app`. + +**Fix:** Remove the existing app first: +```bash +rm -rf /Applications/VocaMac.app +brew install --cask vocamac +``` + +### Cask not found after `brew tap` + +The tap repository may not exist or the cask file is missing. + +**Fix:** Verify the tap: +```bash +brew tap --repair jatinkrmalik/vocamac +ls "$(brew --prefix)/Homebrew/Library/Taps/jatinkrmalik/homebrew-vocamac/Casks/" +``` + +### App won't launch after Homebrew install + +Homebrew installs the app to `/Applications/VocaMac.app` — it behaves identically to a manual DMG install. If the app won't launch: + +1. Check Gatekeeper: `spctl --assess /Applications/VocaMac.app` +2. If quarantined: `xattr -d com.apple.quarantine /Applications/VocaMac.app` +3. Grant permissions in System Settings → Privacy & Security + +### Auto-update workflow fails + +Check the workflow run logs in the main repository's Actions tab. Common causes: + +- `HOMEBREW_TAP_TOKEN` secret is missing or expired +- The tap repository doesn't exist or the token lacks write access +- The release DMG asset name doesn't match the expected pattern diff --git a/docs/RELEASE.md b/docs/RELEASE.md index 34e18b4..dace58e 100644 --- a/docs/RELEASE.md +++ b/docs/RELEASE.md @@ -28,6 +28,7 @@ Pre-release versions use suffixes: `v0.1.0-alpha`, `v0.1.0-beta.1` - `scripts/build.sh` — both `CFBundleVersion` and `CFBundleShortVersionString` in the Info.plist template - `web/layouts/index.html` — `softwareVersion` in JSON-LD schema and hero version badge (two occurrences) - _(No Swift change needed — the About tab reads the version from `Info.plist` via `appVersionDisplay` in `SettingsView.swift`.)_ + - `homebrew/Casks/vocamac.rb` — version and sha256 (if doing a manual tap update) - **Do NOT** create a `docs/RELEASE_NOTES_vX.Y.Z.md` file — release notes live out-of-tree (see [Release Notes (out-of-tree)](#release-notes-out-of-tree) below) 4. **Test locally**: ```bash @@ -66,7 +67,9 @@ Pre-release versions use suffixes: `v0.1.0-alpha`, `v0.1.0-beta.1` - Verify artifacts are attached - **Publish** the release when ready -4. **Website auto-deploys** when the release is published (via `deploy-website.yml`) +4. **Homebrew tap auto-updates** — The `update-homebrew-cask.yml` workflow runs automatically on release publish, updating the custom tap with the new version and SHA. If it fails, update manually (see `docs/HOMEBREW.md`). + +5. **Website auto-deploys** when the release is published (via `deploy-website.yml`) ## Release Notes (out-of-tree) diff --git a/homebrew/Casks/vocamac-nightly.rb b/homebrew/Casks/vocamac-nightly.rb new file mode 100644 index 0000000..7a06258 --- /dev/null +++ b/homebrew/Casks/vocamac-nightly.rb @@ -0,0 +1,24 @@ +cask "vocamac-nightly" do + version :latest + sha256 :no_check + + url "https://github.com/jatinkrmalik/vocamac/releases/download/nightly/VocaMac-nightly-arm64.dmg", + verified: "github.com/jatinkrmalik/vocamac/" + name "VocaMac Nightly" + desc "Nightly build of VocaMac — local voice-to-text dictation for macOS" + homepage "https://vocamac.com" + + conflicts_with cask: "vocamac" + + depends_on arch: :arm64 + depends_on macos: ">= :ventura" + + app "VocaMac.app" + + zap trash: [ + "~/Library/Application Support/VocaMac", + "~/Library/Caches/com.vocamac.app", + "~/Library/Preferences/com.vocamac.app.plist", + "~/Library/Saved Application State/com.vocamac.app.savedState", + ] +end diff --git a/homebrew/Casks/vocamac.rb b/homebrew/Casks/vocamac.rb new file mode 100644 index 0000000..8e45ab1 --- /dev/null +++ b/homebrew/Casks/vocamac.rb @@ -0,0 +1,34 @@ +cask "vocamac" do + version "0.6.2" + + url "https://github.com/jatinkrmalik/vocamac/releases/download/v#{version}/VocaMac-#{version}-arm64.dmg", + verified: "github.com/jatinkrmalik/vocamac" + name "VocaMac" + desc "Local voice-to-text dictation for macOS, powered by WhisperKit" + homepage "https://vocamac.com" + + # :no_check is used here because the sha256 will be injected by the + # automated cask update workflow when a new release is published. + sha256 :no_check + + livecheck do + url :url + strategy :github_latest + end + + license "AGPL-3.0-only" + + depends_on arch: :arm64 + depends_on macos: ">= :ventura" + + conflicts_with cask: "vocamac-nightly" + + app "VocaMac.app" + + zap trash: [ + "~/Library/Application Support/VocaMac", + "~/Library/Caches/com.vocamac.app", + "~/Library/Preferences/com.vocamac.app.plist", + "~/Library/Saved Application State/com.vocamac.app.savedState", + ] +end diff --git a/homebrew/README.md b/homebrew/README.md new file mode 100644 index 0000000..7d075ea --- /dev/null +++ b/homebrew/README.md @@ -0,0 +1,39 @@ +# VocaMac Homebrew Tap + +Official Homebrew tap for [VocaMac](https://github.com/jatinkrmalik/vocamac) — a native macOS menu bar application for local voice-to-text dictation powered by WhisperKit. + +## Installation + +```bash +brew tap jatinkrmalik/vocamac +brew install --cask vocamac +``` + +## Nightly Builds + +For early access to the latest features, install the nightly build: + +```bash +brew install --cask vocamac-nightly +``` + +> **Note:** Nightly builds may be unstable. Use the stable release for daily use. + +## Upgrade + +```bash +brew upgrade --cask vocamac +``` + +## Uninstall + +```bash +brew uninstall --cask vocamac +brew untap jatinkrmalik/vocamac +``` + +## Links + +- **Source:** [github.com/jatinkrmalik/vocamac](https://github.com/jatinkrmalik/vocamac) +- **Website:** [vocamac.com](https://vocamac.com) +- **Issues:** [github.com/jatinkrmalik/vocamac/issues](https://github.com/jatinkrmalik/vocamac/issues)