diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..d568425
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,85 @@
+name: release
+
+# Build, Developer ID-sign, notarize, and publish a quill .dmg whenever a
+# v* tag is pushed. Mirrors scripts/build-release.sh. Signs a menu-bar .app
+# with a single Developer ID Application cert — no Installer cert needed.
+#
+# Required repository secrets:
+# DEVELOPER_ID_APP_P12_BASE64 base64 of the Developer ID Application .p12
+# DEV_ID_P12_PASSWORD password for the .p12
+# APP_IDENTITY "Developer ID Application: NAME (TEAMID)"
+# NOTARY_KEY_P8_BASE64 base64 of the App Store Connect API key (.p8)
+# NOTARY_KEY_ID App Store Connect key id
+# NOTARY_ISSUER_ID App Store Connect issuer id
+
+on:
+ push:
+ tags: ["v*"]
+ workflow_dispatch:
+ inputs:
+ version:
+ description: "Version to build (without leading v)"
+ required: true
+
+permissions:
+ contents: write
+
+jobs:
+ release:
+ runs-on: macos-15
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Resolve version
+ id: v
+ run: |
+ if [ -n "${{ github.event.inputs.version }}" ]; then
+ echo "version=${{ github.event.inputs.version }}" >> "$GITHUB_OUTPUT"
+ else
+ echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
+ fi
+
+ - name: Import signing certificate
+ env:
+ APP_P12: ${{ secrets.DEVELOPER_ID_APP_P12_BASE64 }}
+ P12_PASSWORD: ${{ secrets.DEV_ID_P12_PASSWORD }}
+ run: |
+ KEYCHAIN="$RUNNER_TEMP/build.keychain"
+ KEYCHAIN_PW="$(uuidgen)"
+ security create-keychain -p "$KEYCHAIN_PW" "$KEYCHAIN"
+ security set-keychain-settings -lut 21600 "$KEYCHAIN"
+ security unlock-keychain -p "$KEYCHAIN_PW" "$KEYCHAIN"
+
+ echo "$APP_P12" | base64 --decode > "$RUNNER_TEMP/app.p12"
+ security import "$RUNNER_TEMP/app.p12" -k "$KEYCHAIN" \
+ -P "$P12_PASSWORD" -T /usr/bin/codesign
+ rm -f "$RUNNER_TEMP/app.p12"
+ security set-key-partition-list -S apple-tool:,apple:,codesign: \
+ -s -k "$KEYCHAIN_PW" "$KEYCHAIN"
+ security list-keychains -d user -s "$KEYCHAIN" login.keychain
+
+ - name: Store notary credentials
+ env:
+ NOTARY_KEY_P8: ${{ secrets.NOTARY_KEY_P8_BASE64 }}
+ NOTARY_KEY_ID: ${{ secrets.NOTARY_KEY_ID }}
+ NOTARY_ISSUER_ID: ${{ secrets.NOTARY_ISSUER_ID }}
+ run: |
+ echo "$NOTARY_KEY_P8" | base64 --decode > "$RUNNER_TEMP/notary.p8"
+ xcrun notarytool store-credentials quill-notary \
+ --key "$RUNNER_TEMP/notary.p8" \
+ --key-id "$NOTARY_KEY_ID" \
+ --issuer "$NOTARY_ISSUER_ID"
+ rm -f "$RUNNER_TEMP/notary.p8"
+
+ - name: Build, sign, notarize
+ env:
+ VERSION: ${{ steps.v.outputs.version }}
+ APP_IDENTITY: ${{ secrets.APP_IDENTITY }}
+ NOTARY_PROFILE: quill-notary
+ run: ./scripts/build-release.sh
+
+ - name: Publish release
+ uses: softprops/action-gh-release@v2
+ with:
+ files: dist/quill-${{ steps.v.outputs.version }}.dmg
+ generate_release_notes: true
diff --git a/.gitignore b/.gitignore
index e94f7ce..15039b9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,3 @@
.build/
.DS_Store
+dist/
diff --git a/README.md b/README.md
index 13488d4..d6e6635 100644
--- a/README.md
+++ b/README.md
@@ -10,6 +10,25 @@ Swift binary, menu-bar tray, no app bundle.
## Install
+**Download (recommended).** Grab the latest `.dmg` from the
+[Releases](https://github.com/digimata/quill/releases) page, open it, and drag
+**quill.app** to Applications. Launch it and the feather appears in the menu
+bar.
+
+The app is signed with a Developer ID and notarized by Apple, so it opens
+without Gatekeeper warnings.
+
+To also use the CLI (`quill doctor`, `quill run`, …), symlink the bundled
+binary onto your `PATH`:
+
+```sh
+sudo ln -sf /Applications/quill.app/Contents/MacOS/quill /usr/local/bin/quill
+quill doctor # check permissions & models
+quill install --launch-at-login # optional — run in the background on login
+```
+
+**Build from source.**
+
```sh
cd quill
swift build -c release
@@ -17,9 +36,13 @@ sudo cp .build/release/quill /usr/local/bin/quill
quill install --launch-at-login # optional — runs in the background on login
```
+To produce a signed, notarized `.dmg` yourself, see
+[`scripts/build-release.sh`](scripts/build-release.sh) and
+[Releasing](#releasing).
+
**Requires:** macOS 15+ (Core Audio process taps for system audio — no
-virtual device, no kernel extension). Apple Silicon recommended for
-transcription speed.
+virtual device, no kernel extension). Apple Silicon (the release binary is
+`arm64`; build from source for Intel).
## How to use
@@ -114,6 +137,35 @@ quill install --uninstall
- **FluidAudio / Parakeet** — on-device Core ML transcription
- **NSStatusItem** — the whole UI
+## Releasing
+
+Distribution artifacts are Developer ID-signed and Apple-notarized so they run
+without Gatekeeper prompts. quill ships as a menu-bar `quill.app` inside a
+`.dmg`; everything signs with a single **Developer ID Application**
+certificate — no Developer ID Installer cert required.
+
+- **Locally:** `scripts/build-release.sh` builds the release binary, wraps it
+ in `quill.app` (Info.plist from `packaging/Info.plist`), codesigns it
+ (hardened runtime + `packaging/quill.entitlements`), notarizes and staples
+ the app, then packages and notarizes a `.dmg`. It reads its config from the
+ environment:
+
+ ```sh
+ APP_IDENTITY="Developer ID Application: NAME (TEAMID)" \
+ NOTARY_PROFILE=quill-notary \
+ VERSION=0.1.0 \
+ ./scripts/build-release.sh
+ ```
+
+ `NOTARY_PROFILE` is a `notarytool` keychain profile
+ (`xcrun notarytool store-credentials`). Set `SKIP_NOTARIZE=1` to sign without
+ notarizing (local testing only).
+
+- **CI:** push a `v*` tag and `.github/workflows/release.yml` builds, signs,
+ notarizes, and attaches the `.dmg` to the GitHub release. It expects the
+ certificate and App Store Connect notary secrets documented at the top of
+ that workflow.
+
## Gotchas
- A global tap records *everything* the Mac plays — notification dings,
diff --git a/packaging/Info.plist b/packaging/Info.plist
new file mode 100644
index 0000000..2648f5a
--- /dev/null
+++ b/packaging/Info.plist
@@ -0,0 +1,30 @@
+
+
+
+
+ CFBundleIdentifier
+ com.digimata.quill
+ CFBundleName
+ quill
+ CFBundleDisplayName
+ quill
+ CFBundleExecutable
+ quill
+ CFBundleIconFile
+ quill
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ @VERSION@
+ CFBundleVersion
+ @VERSION@
+ LSMinimumSystemVersion
+ 15.0
+ LSUIElement
+
+ NSMicrophoneUsageDescription
+ quill records your microphone during meetings so you can transcribe them later. Audio never leaves this Mac.
+ NSAudioCaptureUsageDescription
+ quill records system audio (the other side of your meetings) so you can transcribe them later. Audio never leaves this Mac.
+
+
diff --git a/packaging/icon.svg b/packaging/icon.svg
new file mode 100644
index 0000000..13368ad
--- /dev/null
+++ b/packaging/icon.svg
@@ -0,0 +1,21 @@
+
diff --git a/packaging/quill.entitlements b/packaging/quill.entitlements
new file mode 100644
index 0000000..d459cb2
--- /dev/null
+++ b/packaging/quill.entitlements
@@ -0,0 +1,8 @@
+
+
+
+
+ com.apple.security.device.audio-input
+
+
+
diff --git a/packaging/quill.icns b/packaging/quill.icns
new file mode 100644
index 0000000..d469b03
Binary files /dev/null and b/packaging/quill.icns differ
diff --git a/scripts/build-release.sh b/scripts/build-release.sh
new file mode 100755
index 0000000..da9a938
--- /dev/null
+++ b/scripts/build-release.sh
@@ -0,0 +1,91 @@
+#!/usr/bin/env bash
+#
+# Build, sign, notarize, and package quill for distribution as a menu-bar .app
+# shipped inside a .dmg. Everything signs with a single Developer ID
+# Application certificate — no Developer ID Installer cert required.
+#
+# The same script runs locally and in CI.
+#
+# Configuration (all via environment, with local-dev defaults):
+# VERSION release version, e.g. 0.1.0 (default: 0.1.0)
+# APP_IDENTITY "Developer ID Application: ..." name / hash
+# NOTARY_PROFILE notarytool keychain profile name (enables notarize+staple)
+# SKIP_NOTARIZE=1 build & sign but don't notarize
+#
+# Requirements: Xcode toolchain and a Developer ID Application cert. Notarization
+# needs stored credentials (`xcrun notarytool store-credentials`).
+set -euo pipefail
+
+ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+cd "$ROOT"
+
+VERSION="${VERSION:-0.1.0}"
+APP_IDENTITY="${APP_IDENTITY:-Developer ID Application: Omer Karisman (XH64JAYUW5)}"
+NOTARY_PROFILE="${NOTARY_PROFILE:-}"
+BUNDLE_ID="com.digimata.quill"
+
+BUILD_BIN="$ROOT/.build/release/quill"
+ENTITLEMENTS="$ROOT/packaging/quill.entitlements"
+PLIST_TEMPLATE="$ROOT/packaging/Info.plist"
+DIST="$ROOT/dist"
+APP="$DIST/quill.app"
+DMG="$DIST/quill-$VERSION.dmg"
+DMG_STAGE="$DIST/dmg"
+
+step() { printf '\n\033[1;34m==>\033[0m %s\n' "$1"; }
+
+step "Building release binary (arm64)"
+swift build -c release
+
+step "Assembling quill.app"
+rm -rf "$APP" "$DMG" "$DMG_STAGE"
+mkdir -p "$APP/Contents/MacOS" "$APP/Contents/Resources"
+sed "s/@VERSION@/$VERSION/g" "$PLIST_TEMPLATE" > "$APP/Contents/Info.plist"
+cp "$BUILD_BIN" "$APP/Contents/MacOS/quill"
+cp "$ROOT/packaging/quill.icns" "$APP/Contents/Resources/quill.icns"
+xattr -cr "$APP"
+
+step "Codesigning app (hardened runtime + entitlements)"
+codesign --force --options runtime --timestamp \
+ --entitlements "$ENTITLEMENTS" \
+ --sign "$APP_IDENTITY" \
+ "$APP"
+codesign --verify --strict --verbose=2 "$APP"
+
+if [[ "${SKIP_NOTARIZE:-}" != "1" && -n "$NOTARY_PROFILE" ]]; then
+ step "Notarizing app"
+ ditto -c -k --keepParent "$APP" "$DIST/quill-app.zip"
+ xcrun notarytool submit "$DIST/quill-app.zip" --keychain-profile "$NOTARY_PROFILE" --wait
+ rm -f "$DIST/quill-app.zip"
+ step "Stapling app"
+ xcrun stapler staple "$APP"
+else
+ step "Skipping app notarization (SKIP_NOTARIZE set or NOTARY_PROFILE empty)"
+fi
+
+step "Building .dmg"
+mkdir -p "$DMG_STAGE"
+cp -R "$APP" "$DMG_STAGE/"
+ln -s /Applications "$DMG_STAGE/Applications"
+hdiutil create -volname "quill $VERSION" -srcfolder "$DMG_STAGE" \
+ -ov -format UDZO "$DMG" >/dev/null
+rm -rf "$DMG_STAGE"
+
+step "Codesigning .dmg"
+codesign --force --timestamp --sign "$APP_IDENTITY" "$DMG"
+
+if [[ "${SKIP_NOTARIZE:-}" != "1" && -n "$NOTARY_PROFILE" ]]; then
+ step "Notarizing .dmg"
+ xcrun notarytool submit "$DMG" --keychain-profile "$NOTARY_PROFILE" --wait
+ step "Stapling .dmg"
+ xcrun stapler staple "$DMG"
+ xcrun stapler validate "$DMG"
+ spctl --assess --type open --context context:primary-signature --verbose=4 "$DMG" || true
+else
+ step "Skipping .dmg notarization"
+ echo "WARNING: unsigned-of-notarization build — for local testing only."
+fi
+
+step "Done"
+echo "App: $APP"
+echo "Artifact: $DMG"