diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..8848062
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,10 @@
+version: 2
+
+# Workflow actions are pinned to full commit SHAs because the release job
+# imports the Developer ID signing key. Dependabot keeps those pins current so
+# pinning does not mean running stale action code.
+updates:
+ - package-ecosystem: github-actions
+ directory: /
+ schedule:
+ interval: weekly
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 91f7ae7..77e49f8 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -13,17 +13,17 @@ jobs:
build-test:
runs-on: macos-15
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- name: Select Xcode
- uses: maxim-lobanov/setup-xcode@v1
+ uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1.7.0
with:
# latest-stable on the pinned macos-15 image. Pin to an exact
# version (e.g. '16.4') once a known-good version is confirmed.
xcode-version: latest-stable
- name: Cache SwiftPM
- uses: actions/cache@v4
+ uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
with:
path: |
~/Library/Developer/Xcode/DerivedData/**/SourcePackages
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index d778677..a4bc50a 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -1,103 +1,158 @@
name: Release
+# Build, Developer ID-sign, notarize and publish TickerBar on every v* tag.
+#
+# Signing is done by xcodebuild (archive + export), not by a hand-rolled
+# `codesign --deep`. Sparkle's own docs are explicit that --deep is "a common
+# source of Sandboxing errors" and must not be used, because the bundled XPC
+# services have different signing requirements from the rest of the bundle.
+# Archive + export signs Sparkle.framework, Updater.app, Autoupdate and the
+# XPC services inside-out, preserves the hardened runtime and strips
+# get-task-allow, which is the workflow Sparkle recommends.
+#
+# TickerBar is deliberately NOT sandboxed. It ships via Developer ID only,
+# never the App Store, so the sandbox is optional. Enabling it would move
+# preferences into ~/Library/Containers and orphan the watchlist, holdings
+# and alerts of every existing user, who all run unsandboxed builds. Do not
+# add an entitlements file back without a data migration.
+#
+# Secrets in the "release" environment, which only v* tags can read:
+# DEVELOPER_ID_APP_P12_BASE64 base64 of the Developer ID Application .p12
+# DEV_ID_P12_PASSWORD password for that .p12
+# APP_IDENTITY "Developer ID Application: NAME (TEAMID)"
+# TEAM_ID Apple Developer team id
+# 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
+#
+# Repository secrets:
+# SPARKLE_PRIVATE_KEY Sparkle EdDSA private key
+# HOMEBREW_TAP_TOKEN token that can push to TerrifiedBug/homebrew-tap
+
on:
push:
tags:
- 'v*'
+ workflow_dispatch:
+ inputs:
+ version:
+ description: 'Version to build (without the leading v)'
+ required: true
permissions:
contents: write
+env:
+ ZIP: dist/tickerbar.zip
+
jobs:
- build:
- runs-on: macos-latest
+ release:
+ runs-on: macos-15
+ # Signing secrets live in this environment, which is restricted to v* tags.
+ # No other workflow or ref can read the Developer ID key.
+ environment: release
+ timeout-minutes: 45
steps:
- - uses: actions/checkout@v4
+ # This job imports the Developer ID private key, so every third-party
+ # action is pinned to a full commit SHA. A mutable tag could be moved to
+ # code that exfiltrates the key. Dependabot bumps these.
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- - name: Set version from tag
+ - name: Select Xcode
+ uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1.7.0
+ with:
+ xcode-version: latest-stable
+
+ - name: Resolve version
+ id: v
env:
- TAG: ${{ github.ref_name }}
+ INPUT_VERSION: ${{ github.event.inputs.version }}
run: |
- VERSION="${TAG#v}"
- /usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $VERSION" TickerBar/Info.plist
- /usr/libexec/PlistBuddy -c "Set :CFBundleVersion $VERSION" TickerBar/Info.plist
+ VERSION="${INPUT_VERSION:-${GITHUB_REF_NAME#v}}"
+ echo "version=$VERSION" >> "$GITHUB_OUTPUT"
+ echo "tag=v$VERSION" >> "$GITHUB_OUTPUT"
- - name: Build Release
+ - name: Import signing certificate
+ env:
+ APP_P12: ${{ secrets.DEVELOPER_ID_APP_P12_BASE64 }}
+ P12_PASSWORD: ${{ secrets.DEV_ID_P12_PASSWORD }}
run: |
- xcodebuild -project TickerBar.xcodeproj \
- -scheme TickerBar \
- -configuration Release \
- -derivedDataPath build \
- CODE_SIGN_IDENTITY="-"
-
- - name: Codesign and notarize
+ if [ -z "$APP_P12" ]; then
+ echo "::error::DEVELOPER_ID_APP_P12_BASE64 is not set. Releases must be signed and notarized."
+ exit 1
+ fi
+ 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" >/dev/null
+ security list-keychains -d user -s "$KEYCHAIN" login.keychain
+ security find-identity -v -p codesigning "$KEYCHAIN"
+
+ - name: Store notary credentials
env:
- DEVELOPER_ID_CERT_P12_BASE64: ${{ secrets.DEVELOPER_ID_CERT_P12_BASE64 }}
- DEVELOPER_ID_CERT_PASSWORD: ${{ secrets.DEVELOPER_ID_CERT_PASSWORD }}
- DEVELOPER_ID_IDENTITY: ${{ secrets.DEVELOPER_ID_IDENTITY }}
- NOTARY_APPLE_ID: ${{ secrets.NOTARY_APPLE_ID }}
- NOTARY_PASSWORD: ${{ secrets.NOTARY_PASSWORD }}
- NOTARY_TEAM_ID: ${{ secrets.NOTARY_TEAM_ID }}
+ NOTARY_KEY_P8: ${{ secrets.NOTARY_KEY_P8_BASE64 }}
+ NOTARY_KEY_ID: ${{ secrets.NOTARY_KEY_ID }}
+ NOTARY_ISSUER_ID: ${{ secrets.NOTARY_ISSUER_ID }}
run: |
- APP="build/Build/Products/Release/TickerBar.app"
- if [ -n "$DEVELOPER_ID_CERT_P12_BASE64" ] && [ -n "$DEVELOPER_ID_IDENTITY" ]; then
- echo "Developer ID secrets present — signing and notarizing."
- KEYCHAIN="$RUNNER_TEMP/build.keychain-db"
- KEYCHAIN_PASSWORD="$(uuidgen)"
- security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN"
- security set-keychain-settings -lut 21600 "$KEYCHAIN"
- security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN"
- echo "$DEVELOPER_ID_CERT_P12_BASE64" | base64 --decode > "$RUNNER_TEMP/cert.p12"
- security import "$RUNNER_TEMP/cert.p12" -k "$KEYCHAIN" -P "$DEVELOPER_ID_CERT_PASSWORD" -T /usr/bin/codesign
- security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN" >/dev/null
- security list-keychains -d user -s "$KEYCHAIN" login.keychain-db
- # Sign with hardened runtime (Sparkle nested code included via --deep).
- codesign --force --deep --options runtime --timestamp \
- --entitlements TickerBar/TickerBar.entitlements \
- --sign "$DEVELOPER_ID_IDENTITY" "$APP"
- codesign --verify --strict --verbose=2 "$APP"
- # Notarize a temporary zip, then staple the app itself.
- ditto -c -k --keepParent "$APP" "$RUNNER_TEMP/notarize.zip"
- xcrun notarytool submit "$RUNNER_TEMP/notarize.zip" \
- --apple-id "$NOTARY_APPLE_ID" --password "$NOTARY_PASSWORD" --team-id "$NOTARY_TEAM_ID" --wait
- xcrun stapler staple "$APP"
- else
- echo "No Developer ID secrets — falling back to ad-hoc signing (Gatekeeper warning on first launch)."
- codesign --force --deep --sign - "$APP"
+ if [ -z "$NOTARY_KEY_P8" ]; then
+ echo "::error::NOTARY_KEY_P8_BASE64 is not set. Releases must be notarized."
+ exit 1
fi
-
- - name: Package app
+ echo "$NOTARY_KEY_P8" | base64 --decode > "$RUNNER_TEMP/notary.p8"
+ xcrun notarytool store-credentials tickerbar-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 }}
+ TEAM_ID: ${{ secrets.TEAM_ID }}
+ NOTARY_PROFILE: tickerbar-notary
+ run: ./scripts/build-release.sh
+
+ - name: Checksum
+ id: pkg
run: |
- cd build/Build/Products/Release
- zip -r -y TickerBar.zip TickerBar.app
+ echo "sha256=$(shasum -a 256 "$ZIP" | awk '{print $1}')" >> "$GITHUB_OUTPUT"
+ echo "size=$(stat -f%z "$ZIP")" >> "$GITHUB_OUTPUT"
- name: Sign update with Sparkle
+ id: sparkle
env:
SPARKLE_PRIVATE_KEY: ${{ secrets.SPARKLE_PRIVATE_KEY }}
run: |
- SPARKLE_BIN="build/SourcePackages/artifacts/sparkle/Sparkle/bin"
- if [ ! -f "$SPARKLE_BIN/sign_update" ]; then
- xcodebuild -project TickerBar.xcodeproj \
- -scheme TickerBar \
- -configuration Release \
- -derivedDataPath build \
- -resolvePackageDependencies
- SPARKLE_BIN="build/SourcePackages/artifacts/sparkle/Sparkle/bin"
+ SIGN_UPDATE="build/SourcePackages/artifacts/sparkle/Sparkle/bin/sign_update"
+ if [ ! -x "$SIGN_UPDATE" ]; then
+ echo "::error::sign_update not found at $SIGN_UPDATE"
+ exit 1
fi
- SIGNATURE=$("$SPARKLE_BIN/sign_update" \
- build/Build/Products/Release/TickerBar.zip \
+ SIGNATURE=$("$SIGN_UPDATE" "$ZIP" \
--ed-key-file <(echo "$SPARKLE_PRIVATE_KEY") \
- | grep "sparkle:edSignature" | sed 's/.*sparkle:edSignature="\([^"]*\)".*/\1/')
- echo "SPARKLE_SIGNATURE=$SIGNATURE" >> "$GITHUB_ENV"
- ZIP_SIZE=$(stat -f%z build/Build/Products/Release/TickerBar.zip)
- echo "ZIP_SIZE=$ZIP_SIZE" >> "$GITHUB_ENV"
+ | sed -n 's/.*sparkle:edSignature="\([^"]*\)".*/\1/p')
+ if [ -z "$SIGNATURE" ]; then
+ echo "::error::sign_update produced no signature"
+ exit 1
+ fi
+ echo "signature=$SIGNATURE" >> "$GITHUB_OUTPUT"
- name: Generate appcast.xml
env:
- TAG: ${{ github.ref_name }}
+ VERSION: ${{ steps.v.outputs.version }}
+ TAG: ${{ steps.v.outputs.tag }}
+ SIGNATURE: ${{ steps.sparkle.outputs.signature }}
+ SIZE: ${{ steps.pkg.outputs.size }}
run: |
- VERSION="${TAG#v}"
- cat > appcast.xml << XMLEOF
+ cat > appcast.xml <
@@ -52,27 +52,21 @@ Trusting the whole tap applies to every current and future formula, cask, and co
### Manual download
-1. Download `TickerBar.zip` from the [latest release](https://github.com/TerrifiedBug/TickerBar/releases/latest).
+1. Download `tickerbar.zip` from the [latest release](https://github.com/TerrifiedBug/tickerbar/releases/latest).
2. Unzip it and move `TickerBar.app` into Applications.
3. Open TickerBar from Applications.
### First launch on macOS
-TickerBar currently uses ad hoc signing instead of a paid Apple Developer ID, so macOS may block the first launch. Control-click `TickerBar.app` in Applications, choose **Open**, then confirm **Open** in the dialog.
-
-If macOS still blocks it, remove the quarantine flag:
-
-```bash
-xattr -dr com.apple.quarantine /Applications/TickerBar.app
-```
+TickerBar is signed with an Apple Developer ID certificate and notarized by Apple. It opens straight from Applications with no Gatekeeper prompt and no quarantine workaround.
## Build it yourself
You need Xcode 15 or newer and macOS 14 or newer.
```bash
-git clone https://github.com/TerrifiedBug/TickerBar.git
-cd TickerBar
+git clone https://github.com/TerrifiedBug/tickerbar.git
+cd tickerbar
xcodebuild -project TickerBar.xcodeproj -scheme TickerBar -configuration Release -derivedDataPath build build
```
diff --git a/TickerBar.xcodeproj/project.pbxproj b/TickerBar.xcodeproj/project.pbxproj
index ee8d9af..297a688 100644
--- a/TickerBar.xcodeproj/project.pbxproj
+++ b/TickerBar.xcodeproj/project.pbxproj
@@ -55,7 +55,6 @@
D1E2F3A4B5C6D7E8F9A0B1C3 /* PriceAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PriceAlert.swift; sourceTree = "