From 6eff713125755e7da092487807916b52ac5271f1 Mon Sep 17 00:00:00 2001 From: James Cleveland Date: Tue, 29 Sep 2026 12:54:13 +0100 Subject: [PATCH 1/3] Releases are signed with a Developer ID and notarised The release job builds the DMG itself with the Developer ID identity from repository secrets, notarises and staples it. The cask no longer strips the quarantine attribute, and manual installs no longer need xattr. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01YXLtjqcyvZ1VP9uK9jnMuR --- .github/workflows/ci-cd.yml | 63 +++++++++++++++++++++++++++++-------- CHANGELOG.md | 4 +++ CLAUDE.md | 1 + README.md | 7 +++-- Taskfile.yml | 30 +++++++++++++++++- 5 files changed, 88 insertions(+), 17 deletions(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 2415c58..c8bb06b 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -69,12 +69,57 @@ jobs: echo "should_release=false" >> "$GITHUB_OUTPUT" fi - - name: Download DMG + - name: Setup Swift if: steps.check.outputs.should_release == 'true' - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + uses: swift-actions/setup-swift@7ca6abe6b3b0e8b5421b88be48feee39cbf52c6a # v2 with: - name: BrowserSchedule.dmg - path: .build/pkg + swift-version: "6.1" + + - name: Install Task + if: steps.check.outputs.should_release == 'true' + run: brew install go-task/tap/go-task + + # The Developer ID certificate goes into a keychain made for this job + # alone, with a random password, and codesign may use its key without a + # prompt. The identity name is read back from the certificate so it can + # be renewed without editing the workflow. + - name: Import Developer ID certificate + if: steps.check.outputs.should_release == 'true' + env: + MACOS_CERTIFICATE_P12: ${{ secrets.MACOS_CERTIFICATE_P12 }} + MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }} + run: | + keychain="$RUNNER_TEMP/signing.keychain-db" + password=$(openssl rand -hex 24) + security create-keychain -p "$password" "$keychain" + security set-keychain-settings -lut 21600 "$keychain" + security unlock-keychain -p "$password" "$keychain" + printf '%s' "$MACOS_CERTIFICATE_P12" | base64 --decode > "$RUNNER_TEMP/certificate.p12" + security import "$RUNNER_TEMP/certificate.p12" -k "$keychain" \ + -P "$MACOS_CERTIFICATE_PASSWORD" -T /usr/bin/codesign + rm "$RUNNER_TEMP/certificate.p12" + security set-key-partition-list -S apple-tool:,apple: -s -k "$password" "$keychain" >/dev/null + security list-keychains -d user -s "$keychain" $(security list-keychains -d user | tr -d '"') + identity=$(security find-identity -v -p codesigning "$keychain" \ + | awk -F'"' '/Developer ID Application/ { print $2; exit }') + test -n "$identity" || { echo "::error::no Developer ID Application identity in MACOS_CERTIFICATE_P12"; exit 1; } + echo "SIGN_IDENTITY=$identity" >> "$GITHUB_ENV" + + - name: Build signed DMG + if: steps.check.outputs.should_release == 'true' + run: task build-dmg + + - name: Notarise + if: steps.check.outputs.should_release == 'true' + env: + APPLE_API_KEY_P8: ${{ secrets.APPLE_API_KEY_P8 }} + APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} + APPLE_API_ISSUER_ID: ${{ secrets.APPLE_API_ISSUER_ID }} + run: | + export APPLE_API_KEY_PATH="$RUNNER_TEMP/AuthKey.p8" + printf '%s' "$APPLE_API_KEY_P8" | base64 --decode > "$APPLE_API_KEY_PATH" + trap 'rm -f "$APPLE_API_KEY_PATH"' EXIT + task notarize - name: Create Release if: steps.check.outputs.should_release == 'true' @@ -95,8 +140,7 @@ jobs: ### Install Manually 1. Download `BrowserSchedule.dmg` below 2. Mount the DMG and drag BrowserSchedule.app to Applications - 3. Remove quarantine: `xattr -cr /Applications/BrowserSchedule.app` - 4. **Double-click the app** to open settings and set as default browser + 3. **Double-click the app** to open settings and set as default browser ### Changes See commit history for detailed changes. @@ -128,13 +172,6 @@ jobs: printf '\n' >> Casks/browser-schedule.rb printf ' app "BrowserSchedule.app"\n' >> Casks/browser-schedule.rb printf '\n' >> Casks/browser-schedule.rb - printf ' postflight_steps do\n' >> Casks/browser-schedule.rb - printf ' run "/usr/bin/xattr",\n' >> Casks/browser-schedule.rb - printf ' args: ["-cr", "{{appdir}}/BrowserSchedule.app"],\n' >> Casks/browser-schedule.rb - printf ' writable_paths: ["BrowserSchedule.app"],\n' >> Casks/browser-schedule.rb - printf ' writable_base: :appdir\n' >> Casks/browser-schedule.rb - printf ' end\n' >> Casks/browser-schedule.rb - printf '\n' >> Casks/browser-schedule.rb printf ' zap trash: "~/.config/browser-schedule"\n' >> Casks/browser-schedule.rb printf 'end\n' >> Casks/browser-schedule.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 1dcc51e..28e73c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Changed + +- **Releases are signed with a Developer ID and notarised.** Gatekeeper previously blocked the ad-hoc signed app, so the cask stripped the quarantine attribute and manual installs had to run `xattr -cr`. Neither is needed now, and the cask no longer has a postflight step. + ### Fixed - **The cask uses `postflight_steps`.** Homebrew deprecated the block form of `postflight` and warns about it on every `brew update`. diff --git a/CLAUDE.md b/CLAUDE.md index 65d64da..cf24b0a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -101,6 +101,7 @@ Configurable via the Settings UI or direct file editing. The app validates time - Requires macOS 14+ (Sonoma) for `@Observable` macro and modern SwiftUI - Uses Swift Package Manager with TOMLKit for TOML parsing +- Signing: `create-app-bundle` ad-hoc signs unless `SIGN_IDENTITY` is set, in which case it signs with hardened runtime + timestamp for notarisation (`task notarize`). CI release job imports the Developer ID cert into a throwaway keychain. - App bundle at `/Applications/BrowserSchedule.app` with `LSUIElement=true` in Info.plist (background by default, switches to foreground when showing UI) - TOMLKit API: use direct assignment to TOMLTable subscripts (`table["key"] = "value"`), `TOMLArray(["a","b"])` for arrays, `table.convert()` for serialization diff --git a/README.md b/README.md index 31e9c9b..0ed46bb 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ Automatically switches default browser based on time, day, and URL patterns. Bui brew install --cask radiosilence/browser-schedule/browser-schedule ``` -This installs via a [Homebrew Cask](https://github.com/radiosilence/homebrew-browser-schedule) which handles quarantine removal automatically. Double-click the app to open settings and set as default browser. +This installs via a [Homebrew Cask](https://github.com/radiosilence/homebrew-browser-schedule). Double-click the app to open settings and set as default browser. #### Brewfile @@ -29,8 +29,7 @@ cask "browser-schedule" 1. Download `BrowserSchedule.dmg` from the [latest release](https://github.com/radiosilence/browser-schedule/releases/latest) 2. Mount the DMG and drag BrowserSchedule.app to Applications -3. Remove quarantine: `xattr -cr /Applications/BrowserSchedule.app` -4. **Double-click the app** to open settings and set as default browser +3. **Double-click the app** to open settings and set as default browser ### Build from Source @@ -40,6 +39,8 @@ task install Builds, ad-hoc signs, creates the app bundle, and installs to `/Applications`. +Release builds are signed with a Developer ID and notarised in CI, so Gatekeeper opens them without a quarantine workaround. `task build-dmg notarize` does this locally given `SIGN_IDENTITY` (a Developer ID Application identity) and an App Store Connect API key (`APPLE_API_KEY_PATH`, `APPLE_API_KEY_ID`, `APPLE_API_ISSUER_ID`). The release job needs the repository secrets `MACOS_CERTIFICATE_P12` (base64 Developer ID Application `.p12`), `MACOS_CERTIFICATE_PASSWORD`, `APPLE_API_KEY_P8` (base64 `.p8`), `APPLE_API_KEY_ID` and `APPLE_API_ISSUER_ID`. + ## Settings UI Double-clicking the app opens a native macOS settings window with four tabs: diff --git a/Taskfile.yml b/Taskfile.yml index 555ec20..fa3134f 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -195,6 +195,23 @@ tasks: - rm -f ./.build/pkg/BrowserSchedule.dmg - hdiutil create -volname "BrowserSchedule" -srcfolder ./.build/pkg/BrowserSchedule.app -ov -format UDZO ./.build/pkg/BrowserSchedule.dmg + notarize: + desc: Sign, notarise and staple the DMG (needs SIGN_IDENTITY and an App Store Connect API key) + silent: true + requires: + vars: [SIGN_IDENTITY, APPLE_API_KEY_PATH, APPLE_API_KEY_ID, APPLE_API_ISSUER_ID] + cmds: + - | + set -euo pipefail + dmg=./.build/pkg/BrowserSchedule.dmg + codesign --force --timestamp --sign "$SIGN_IDENTITY" "$dmg" + xcrun notarytool submit "$dmg" \ + --key "$APPLE_API_KEY_PATH" --key-id "$APPLE_API_KEY_ID" --issuer "$APPLE_API_ISSUER_ID" \ + --wait --timeout 30m + xcrun stapler staple "$dmg" + xcrun stapler validate "$dmg" + spctl --assess --type open --context context:primary-signature --verbose=2 "$dmg" + create-app-bundle: desc: Create app bundle in build directory internal: true @@ -245,7 +262,18 @@ tasks: - cp .build/release/BrowserScheduleCLI ./.build/pkg/BrowserSchedule.app/Contents/MacOS/browser-schedule-cli - chmod +x ./.build/pkg/BrowserSchedule.app/Contents/MacOS/browser-schedule - chmod +x ./.build/pkg/BrowserSchedule.app/Contents/MacOS/browser-schedule-cli - - codesign -s - --deep --force ./.build/pkg/BrowserSchedule.app + # SIGN_IDENTITY set to a Developer ID signs for notarisation: hardened + # runtime, secure timestamp, and the CLI signed before the bundle that + # contains it. Unset, the bundle is ad-hoc signed for local use. + - | + app=./.build/pkg/BrowserSchedule.app + if [ -n "${SIGN_IDENTITY:-}" ]; then + codesign --force --options runtime --timestamp --sign "$SIGN_IDENTITY" "$app/Contents/MacOS/browser-schedule-cli" + codesign --force --options runtime --timestamp --sign "$SIGN_IDENTITY" "$app" + else + codesign --force --deep --sign - "$app" + fi + codesign --verify --strict --deep --verbose=2 "$app" release: desc: "Create and push a semantic version release (usage: task release -- major|minor|patch)" From 4fb73161c5b1aff3bc4d82936b2f64bde98e7097 Mon Sep 17 00:00:00 2001 From: James Cleveland Date: Tue, 29 Sep 2026 13:06:24 +0100 Subject: [PATCH 2/3] CI builds with the runner's Xcode toolchain The pinned Swift 6.1 toolchain cannot build against the macOS 26.5 SDK in the runner's Xcode, so every build failed at the first import. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01YXLtjqcyvZ1VP9uK9jnMuR --- .github/workflows/ci-cd.yml | 11 ----------- CHANGELOG.md | 1 + 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index c8bb06b..21b880a 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -14,11 +14,6 @@ jobs: steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - - name: Setup Swift - uses: swift-actions/setup-swift@7ca6abe6b3b0e8b5421b88be48feee39cbf52c6a # v2 - with: - swift-version: "6.1" - - name: Install Task run: brew install go-task/tap/go-task @@ -69,12 +64,6 @@ jobs: echo "should_release=false" >> "$GITHUB_OUTPUT" fi - - name: Setup Swift - if: steps.check.outputs.should_release == 'true' - uses: swift-actions/setup-swift@7ca6abe6b3b0e8b5421b88be48feee39cbf52c6a # v2 - with: - swift-version: "6.1" - - name: Install Task if: steps.check.outputs.should_release == 'true' run: brew install go-task/tap/go-task diff --git a/CHANGELOG.md b/CHANGELOG.md index 28e73c8..efa29a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ ### Fixed +- **CI builds with the runner's Xcode toolchain.** The pinned Swift 6.1 toolchain cannot build against the SDK in current Xcode, which failed every build. - **The cask uses `postflight_steps`.** Homebrew deprecated the block form of `postflight` and warns about it on every `brew update`. - **The generated cask declares `depends_on macos: :sonoma`.** The tap had been corrected by hand; releases were regenerating the deprecated `">= :sonoma"` string form. From 46da0bbdb983f9ae36052346f86035d594461b47 Mon Sep 17 00:00:00 2001 From: James Cleveland Date: Tue, 29 Sep 2026 13:08:06 +0100 Subject: [PATCH 3/3] Build with Xcode 27 and Swift 6.4 CI moves to the xcode-27 runner image, since macos-latest defaults to Xcode 26.6. The manifest requires Swift tools 6.4, and the checkout, upload-artifact and action-gh-release pins move to their current majors. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01YXLtjqcyvZ1VP9uK9jnMuR --- .github/workflows/ci-cd.yml | 12 ++++++------ CHANGELOG.md | 2 +- CLAUDE.md | 1 + Package.swift | 2 +- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 21b880a..0b2c8a4 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -10,9 +10,9 @@ permissions: jobs: build: - runs-on: macos-latest + runs-on: xcode-27 steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Install Task run: brew install go-task/tap/go-task @@ -24,7 +24,7 @@ jobs: run: task build-dmg - name: Upload DMG - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: BrowserSchedule.dmg path: .build/pkg/BrowserSchedule.dmg @@ -32,10 +32,10 @@ jobs: release: needs: build if: github.ref == 'refs/heads/main' - runs-on: macos-latest + runs-on: xcode-27 steps: - name: Checkout - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: token: ${{ secrets.RELEASE_TOKEN }} @@ -112,7 +112,7 @@ jobs: - name: Create Release if: steps.check.outputs.should_release == 'true' - uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2 + uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3.0.3 with: tag_name: v${{ steps.version.outputs.version }} files: ./.build/pkg/BrowserSchedule.dmg diff --git a/CHANGELOG.md b/CHANGELOG.md index efa29a5..66bd2c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ ### Fixed -- **CI builds with the runner's Xcode toolchain.** The pinned Swift 6.1 toolchain cannot build against the SDK in current Xcode, which failed every build. +- **CI builds with Xcode 27 and Swift 6.4.** The pinned Swift 6.1 toolchain could not build against the SDK in the runner's Xcode, which failed every build. CI now runs on the `xcode-27` runner image with Xcode's own toolchain, and the package requires Swift tools 6.4. - **The cask uses `postflight_steps`.** Homebrew deprecated the block form of `postflight` and warns about it on every `brew update`. - **The generated cask declares `depends_on macos: :sonoma`.** The tap had been corrected by hand; releases were regenerating the deprecated `">= :sonoma"` string form. diff --git a/CLAUDE.md b/CLAUDE.md index cf24b0a..fc14199 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -100,6 +100,7 @@ Configurable via the Settings UI or direct file editing. The app validates time ## Development Notes - Requires macOS 14+ (Sonoma) for `@Observable` macro and modern SwiftUI +- Builds with Xcode 27 / Swift 6.4 (`swift-tools-version: 6.4`). CI runs on the `xcode-27` runner label because `macos-latest` still defaults to Xcode 26 - Uses Swift Package Manager with TOMLKit for TOML parsing - Signing: `create-app-bundle` ad-hoc signs unless `SIGN_IDENTITY` is set, in which case it signs with hardened runtime + timestamp for notarisation (`task notarize`). CI release job imports the Developer ID cert into a throwaway keychain. - App bundle at `/Applications/BrowserSchedule.app` with `LSUIElement=true` in Info.plist (background by default, switches to foreground when showing UI) diff --git a/Package.swift b/Package.swift index b61ae9d..30b9432 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 6.0 +// swift-tools-version: 6.4 import PackageDescription let package = Package(