Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
.build/
.DS_Store
dist/
56 changes: 54 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,39 @@ 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
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

Expand Down Expand Up @@ -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,
Expand Down
30 changes: 30 additions & 0 deletions packaging/Info.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleIdentifier</key>
<string>com.digimata.quill</string>
<key>CFBundleName</key>
<string>quill</string>
<key>CFBundleDisplayName</key>
<string>quill</string>
<key>CFBundleExecutable</key>
<string>quill</string>
<key>CFBundleIconFile</key>
<string>quill</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>@VERSION@</string>
<key>CFBundleVersion</key>
<string>@VERSION@</string>
<key>LSMinimumSystemVersion</key>
<string>15.0</string>
<key>LSUIElement</key>
<true/>
<key>NSMicrophoneUsageDescription</key>
<string>quill records your microphone during meetings so you can transcribe them later. Audio never leaves this Mac.</string>
<key>NSAudioCaptureUsageDescription</key>
<string>quill records system audio (the other side of your meetings) so you can transcribe them later. Audio never leaves this Mac.</string>
</dict>
</plist>
21 changes: 21 additions & 0 deletions packaging/icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions packaging/quill.entitlements
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.device.audio-input</key>
<true/>
</dict>
</plist>
Binary file added packaging/quill.icns
Binary file not shown.
91 changes: 91 additions & 0 deletions scripts/build-release.sh
Original file line number Diff line number Diff line change
@@ -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"