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
76 changes: 51 additions & 25 deletions .github/workflows/ci-cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,9 @@ permissions:

jobs:
build:
runs-on: macos-latest
runs-on: xcode-27
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4

- name: Setup Swift
uses: swift-actions/setup-swift@7ca6abe6b3b0e8b5421b88be48feee39cbf52c6a # v2
with:
swift-version: "6.1"
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Install Task
run: brew install go-task/tap/go-task
Expand All @@ -29,18 +24,18 @@ 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

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 }}

Expand Down Expand Up @@ -69,16 +64,55 @@ jobs:
echo "should_release=false" >> "$GITHUB_OUTPUT"
fi

- name: Download DMG
- name: Install Task
if: steps.check.outputs.should_release == 'true'
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: BrowserSchedule.dmg
path: .build/pkg
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'
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
Expand All @@ -95,8 +129,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.
Expand Down Expand Up @@ -128,13 +161,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

Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,13 @@

## 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

- **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.

Expand Down
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,9 @@ 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)
- TOMLKit API: use direct assignment to TOMLTable subscripts (`table["key"] = "value"`), `TOMLArray(["a","b"])` for arrays, `table.convert()` for serialization

Expand Down
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// swift-tools-version: 6.0
// swift-tools-version: 6.4
import PackageDescription

let package = Package(
Expand Down
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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

Expand All @@ -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:
Expand Down
30 changes: 29 additions & 1 deletion Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)"
Expand Down
Loading