Skip to content
Merged
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
125 changes: 125 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
name: Release

on:
push:
tags: ['v*']

permissions:
contents: write

env:
ZIG_VERSION: '0.15.2'

jobs:
release:
name: Build, Sign & Release
runs-on: macos-15
timeout-minutes: 60

steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: recursive
fetch-depth: 0

- name: Set up Xcode
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: '16'

- name: Install Zig
uses: mlugg/setup-zig@v2
with:
version: ${{ env.ZIG_VERSION }}

- name: Extract version from tag
run: echo "CROW_VERSION=${GITHUB_REF_NAME#v}" >> "$GITHUB_ENV"

- name: Import signing certificate
env:
CERTIFICATE_BASE64: ${{ secrets.DEVELOPER_CERTIFICATE_BASE64 }}
CERTIFICATE_PASSWORD: ${{ secrets.DEVELOPER_CERTIFICATE_PASSWORD }}
run: |
CERTIFICATE_PATH="$RUNNER_TEMP/certificate.p12"
KEYCHAIN_PATH="$RUNNER_TEMP/app-signing.keychain-db"
KEYCHAIN_PASSWORD="$(openssl rand -hex 16)"

# Decode certificate
echo "$CERTIFICATE_BASE64" | base64 --decode > "$CERTIFICATE_PATH"

# Create temporary keychain
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"

# Import certificate
security import "$CERTIFICATE_PATH" \
-P "$CERTIFICATE_PASSWORD" \
-A \
-t cert \
-f pkcs12 \
-k "$KEYCHAIN_PATH"

# Allow codesign to access the keychain without UI prompt
security set-key-partition-list \
-S apple-tool:,apple: \
-k "$KEYCHAIN_PASSWORD" \
"$KEYCHAIN_PATH"

# Add to keychain search list
security list-keychains -d user -s "$KEYCHAIN_PATH" $(security list-keychains -d user | tr -d '"')

# Clean up certificate file
rm -f "$CERTIFICATE_PATH"

- name: Get Ghostty submodule SHA
id: ghostty-sha
run: echo "sha=$(git -C vendor/ghostty rev-parse HEAD)" >> "$GITHUB_OUTPUT"

- name: Cache Ghostty framework
id: ghostty-cache
uses: actions/cache@v4
with:
path: Frameworks
key: ghostty-${{ runner.os }}-${{ runner.arch }}-${{ steps.ghostty-sha.outputs.sha }}

- name: Build GhosttyKit
if: steps.ghostty-cache.outputs.cache-hit != 'true'
run: |
unset ZIG_LOCAL_CACHE_DIR ZIG_GLOBAL_CACHE_DIR
make ghostty

- name: Bundle Crow.app
env:
CROW_VERSION: ${{ env.CROW_VERSION }}
run: |
bash scripts/generate-build-info.sh
bash scripts/bundle.sh

- name: Sign, create DMG, and notarize
env:
DEVELOPER_ID_APPLICATION: ${{ secrets.DEVELOPER_ID_APPLICATION }}
CROW_VERSION: ${{ env.CROW_VERSION }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
run: bash scripts/sign-and-notarize.sh

- name: Build CLI
run: |
swift build -c release --product crow
cp .build/release/crow crow-cli-macos-arm64

- name: Cleanup keychain
if: always()
run: security delete-keychain "$RUNNER_TEMP/app-signing.keychain-db" 2>/dev/null || true

- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
files: |
Crow-${{ env.CROW_VERSION }}.dmg
crow-cli-macos-arm64
generate_release_notes: true
prerelease: ${{ contains(github.ref_name, '-') }}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Sources/CrowCLI/Generated/
# App bundle
*.app
*.dmg
*.p12

# Frameworks (built locally)
Frameworks/GhosttyKit.xcframework/
Expand Down
2 changes: 2 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ cd crow
make build
```

**Note:** Code signing is not required for development. Unsigned builds work normally for local testing. Official releases are signed and notarized automatically via GitHub Actions.

### Running Tests

```bash
Expand Down
17 changes: 17 additions & 0 deletions Crow.entitlements
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?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>
<!-- Hardened Runtime: allow unsigned executable memory (Zig-compiled GhosttyKit) -->
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>

<!-- Hardened Runtime: disable library validation (GhosttyKit objects aren't team-signed) -->
<key>com.apple.security.cs.disable-library-validation</key>
<true/>

<!-- Hardened Runtime: allow JIT (Metal shader compilation via Ghostty) -->
<key>com.apple.security.cs.allow-jit</key>
<true/>
</dict>
</plist>
6 changes: 5 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: build setup ghostty app release clean clean-all check help
.PHONY: build setup ghostty app release sign clean clean-all check help

FRAMEWORKS_DIR := Frameworks
XCFW := $(FRAMEWORKS_DIR)/GhosttyKit.xcframework
Expand All @@ -17,6 +17,7 @@ help:
@echo " ghostty Build GhosttyKit framework"
@echo " app Swift build only (debug)"
@echo " release Release build + .app bundle"
@echo " sign Sign, create DMG, and notarize (requires DEVELOPER_ID_APPLICATION)"
@echo " clean Remove .build/ (keeps ghostty framework)"
@echo " clean-all Remove .build/ and Frameworks/ (full rebuild)"
@echo ""
Expand Down Expand Up @@ -53,6 +54,9 @@ release: $(XCFW)
bash scripts/generate-build-info.sh
bash scripts/bundle.sh

sign: release
bash scripts/sign-and-notarize.sh

# --- Clean ---

clean:
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,16 @@ Run with log filtering:

We welcome contributions! See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines on reporting bugs, suggesting features, and submitting pull requests.

## Releases

Official releases are signed and notarized via GitHub Actions. Download the latest DMG from the [Releases](https://github.com/radiusmethod/crow/releases) page — it will install without Gatekeeper warnings.

**Building from source:** Code signing is only required for distribution. Developers building from source do not need a signing certificate — `make build` and `make release` produce unsigned but fully functional builds. If macOS quarantines an unsigned .app, remove it with:

```bash
xattr -cr Crow.app
```

## License

Apache 2.0 — see [LICENSE](LICENSE) for details.
5 changes: 5 additions & 0 deletions mise.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ run = "swift test"
description = "Create .app bundle"
run = "bash scripts/bundle.sh"

[tasks.sign]
description = "Sign, create DMG, and notarize (requires DEVELOPER_ID_APPLICATION)"
depends = ["bundle"]
run = "bash scripts/sign-and-notarize.sh"

[tasks.clean]
description = "Clean build artifacts"
run = "rm -rf .build .derived-data Crow.app"
Expand Down
105 changes: 105 additions & 0 deletions scripts/sign-and-notarize.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
#!/usr/bin/env bash
# Sign, create DMG, notarize, and staple the Crow app bundle.
#
# Required:
# DEVELOPER_ID_APPLICATION — signing identity (e.g. "Developer ID Application: Radius Method (TEAMID)")
#
# Optional:
# CROW_VERSION — version string for DMG filename (default: "dev")
# APPLE_ID — Apple ID for notarization
# APPLE_APP_SPECIFIC_PASSWORD — app-specific password for notarization
# APPLE_TEAM_ID — Apple Developer Team ID for notarization
#
# If all three notarization vars are set, the DMG is submitted for notarization and stapled.
# Otherwise, signing and DMG creation proceed without notarization.
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(dirname "$SCRIPT_DIR")"
ENTITLEMENTS="$ROOT_DIR/Crow.entitlements"
VERSION="${CROW_VERSION:-dev}"

# --- Validate inputs ---

APP_PATH="${1:-$ROOT_DIR/Crow.app}"

if [ ! -d "$APP_PATH" ]; then
echo "ERROR: App bundle not found at $APP_PATH"
echo "Run 'make release' first to build the app bundle."
exit 1
fi

if [ -z "${DEVELOPER_ID_APPLICATION:-}" ]; then
echo "ERROR: DEVELOPER_ID_APPLICATION is not set."
echo "Set it to your signing identity, e.g.:"
echo " export DEVELOPER_ID_APPLICATION=\"Developer ID Application: Your Name (TEAMID)\""
exit 1
fi

if [ ! -f "$ENTITLEMENTS" ]; then
echo "ERROR: Entitlements file not found at $ENTITLEMENTS"
exit 1
fi

IDENTITY="$DEVELOPER_ID_APPLICATION"

# --- Step 1: Code sign the .app bundle ---

echo "==> Signing $APP_PATH..."
codesign --force --deep \
--sign "$IDENTITY" \
--entitlements "$ENTITLEMENTS" \
--options runtime \
--timestamp \
"$APP_PATH"

echo "==> Verifying signature..."
codesign --verify --deep --strict --verbose=2 "$APP_PATH"
echo " Signature OK"

# --- Step 2: Create DMG ---

DMG_NAME="Crow-${VERSION}.dmg"
DMG_PATH="$ROOT_DIR/$DMG_NAME"
STAGING_DIR="$ROOT_DIR/.build/dmg-staging"

echo "==> Creating DMG: $DMG_NAME..."
rm -rf "$STAGING_DIR"
mkdir -p "$STAGING_DIR"
cp -R "$APP_PATH" "$STAGING_DIR/"
ln -s /Applications "$STAGING_DIR/Applications"

rm -f "$DMG_PATH"
hdiutil create \
-volname "Crow" \
-srcfolder "$STAGING_DIR" \
-ov \
-format UDZO \
"$DMG_PATH"

rm -rf "$STAGING_DIR"

# Sign the DMG
codesign --force --sign "$IDENTITY" --timestamp "$DMG_PATH"
echo " DMG created and signed: $DMG_PATH"

# --- Step 3: Notarize (conditional) ---

if [ -n "${APPLE_ID:-}" ] && [ -n "${APPLE_APP_SPECIFIC_PASSWORD:-}" ] && [ -n "${APPLE_TEAM_ID:-}" ]; then
echo "==> Submitting for notarization..."
xcrun notarytool submit "$DMG_PATH" \
--apple-id "$APPLE_ID" \
--password "$APPLE_APP_SPECIFIC_PASSWORD" \
--team-id "$APPLE_TEAM_ID" \
--wait

# --- Step 4: Staple ---

echo "==> Stapling notarization ticket..."
xcrun stapler staple "$DMG_PATH"
echo " Notarization complete and stapled"
else
echo "==> Skipping notarization (APPLE_ID, APPLE_APP_SPECIFIC_PASSWORD, or APPLE_TEAM_ID not set)"
fi

echo "==> Done! Output: $DMG_PATH"
Loading