From 3d04901a4357a05a2c6f6632668654cc896ea39d Mon Sep 17 00:00:00 2001
From: TerrifiedBug
Date: Sun, 2 Aug 2026 18:24:23 +0100
Subject: [PATCH 1/3] ci: sign and notarize releases with Developer ID
Signing moves from a hand-rolled 'codesign --deep' to xcodebuild archive and
export. The app is sandboxed and its entitlements reference
$(PRODUCT_BUNDLE_IDENTIFIER) for Sparkle's installer XPC mach-lookup
exceptions. codesign does not expand build settings, so the old command
signed a literal '$(PRODUCT_BUNDLE_IDENTIFIER)-spks' into the shipped app.
xcodebuild expands it and signs Sparkle.framework, Updater.app, Installer.xpc
and Downloader.xpc inside-out with their own entitlements.
Releases now fail loudly when signing or notary credentials are missing,
instead of falling back to ad-hoc signing and leaving users to clear
quarantine by hand. Signing secrets are scoped to a 'release' environment
that only v* tags can read.
scripts/build-release.sh holds the build so local and CI runs produce the
same artifact. Repository is now TerrifiedBug/tickerbar and the asset is
tickerbar.zip.
---
.github/workflows/release.yml | 238 ++++++++++++++++++++--------------
.gitignore | 3 +
CHANGELOG.md | 7 +
README.md | 16 +--
TickerBar/Info.plist | 12 +-
scripts/build-release.sh | 118 +++++++++++++++++
6 files changed, 282 insertions(+), 112 deletions(-)
create mode 100755 scripts/build-release.sh
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index d778677..9591090 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -1,103 +1,150 @@
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`. That matters here: the app is sandboxed, and its
+# entitlements reference $(PRODUCT_BUNDLE_IDENTIFIER) for Sparkle's installer
+# XPC mach-lookup exceptions. codesign does not expand build settings, so
+# signing the raw .entitlements file by hand bakes in a literal
+# "$(PRODUCT_BUNDLE_IDENTIFIER)-spks" and breaks the updater. xcodebuild
+# expands it and signs nested code (Sparkle.framework, Updater.app,
+# Installer.xpc, Downloader.xpc) inside-out with their own entitlements.
+#
+# 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
- - name: Set version from tag
+ - name: Select Xcode
+ uses: maxim-lobanov/setup-xcode@v1
+ 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 <
@@ -106,11 +153,12 @@ jobs:
Version $VERSION
$VERSION
$VERSION
+ 14.0
$(date -R)
@@ -119,66 +167,66 @@ jobs:
- name: Extract release notes from CHANGELOG
env:
- TAG: ${{ github.ref_name }}
+ VERSION: ${{ steps.v.outputs.version }}
run: |
- VERSION="${TAG#v}"
- # Extract the section for this version from CHANGELOG.md
- # Falls back to auto-generated notes if version not found
- awk "/^## \\[${VERSION}\\]/{found=1; next} /^## \\[/{if(found) exit} found{print}" CHANGELOG.md > release_notes.md
+ awk "/^## \\[${VERSION}\\]/{found=1; next} /^## \\[/{if(found) exit} found{print}" \
+ CHANGELOG.md > release_notes.md
if [ ! -s release_notes.md ]; then
- echo "See [CHANGELOG.md](https://github.com/TerrifiedBug/TickerBar/blob/master/CHANGELOG.md) for details." > release_notes.md
+ echo "See [CHANGELOG.md](https://github.com/TerrifiedBug/tickerbar/blob/master/CHANGELOG.md) for details." > release_notes.md
fi
- name: Create Release
uses: softprops/action-gh-release@v2
with:
- files: build/Build/Products/Release/TickerBar.zip
+ tag_name: ${{ steps.v.outputs.tag }}
+ files: ${{ env.ZIP }}
body_path: release_notes.md
+ fail_on_unmatched_files: true
- name: Commit appcast.xml
env:
- TAG: ${{ github.ref_name }}
+ TAG: ${{ steps.v.outputs.tag }}
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
- cp appcast.xml /tmp/appcast.xml
+ cp appcast.xml "$RUNNER_TEMP/appcast.xml"
git fetch origin master
git checkout -f master
- cp /tmp/appcast.xml appcast.xml
+ cp "$RUNNER_TEMP/appcast.xml" appcast.xml
git add appcast.xml
git commit -m "Update appcast.xml for $TAG" || echo "No changes to commit"
git push origin master
- name: Update Homebrew cask
- continue-on-error: true
env:
- TAG: ${{ github.ref_name }}
+ VERSION: ${{ steps.v.outputs.version }}
+ SHA256: ${{ steps.pkg.outputs.sha256 }}
GH_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
run: |
- VERSION="${TAG#v}"
- SHA256=$(shasum -a 256 build/Build/Products/Release/TickerBar.zip | awk '{print $1}')
- git clone https://x-access-token:${GH_TOKEN}@github.com/TerrifiedBug/homebrew-tap.git /tmp/homebrew-tap
- cd /tmp/homebrew-tap
- cat > Casks/tickerbar.rb << 'CASKEOF'
+ git clone "https://x-access-token:${GH_TOKEN}@github.com/TerrifiedBug/homebrew-tap.git" "$RUNNER_TEMP/tap"
+ cd "$RUNNER_TEMP/tap"
+ cat > Casks/tickerbar.rb <A free macOS menu bar app for keeping an eye on stocks. No subscription and no paid tier.
-
+
@@ -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/Info.plist b/TickerBar/Info.plist
index ddcba33..242fce5 100644
--- a/TickerBar/Info.plist
+++ b/TickerBar/Info.plist
@@ -17,20 +17,20 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 1.0.0
+ 1.5.0
CFBundleVersion
- 1.0.0
+ 1.5.0
LSMinimumSystemVersion
14.0
LSUIElement
- SUPublicEDKey
- XSwbLtdr2wVhhFCaOV/FJuGEtpTngVBU098oOwazgtk=
- SUFeedURL
- https://raw.githubusercontent.com/TerrifiedBug/TickerBar/master/appcast.xml
SUEnableAutomaticChecks
SUEnableInstallerLauncherService
+ SUFeedURL
+ https://raw.githubusercontent.com/TerrifiedBug/tickerbar/master/appcast.xml
+ SUPublicEDKey
+ XSwbLtdr2wVhhFCaOV/FJuGEtpTngVBU098oOwazgtk=
diff --git a/scripts/build-release.sh b/scripts/build-release.sh
new file mode 100755
index 0000000..52a7171
--- /dev/null
+++ b/scripts/build-release.sh
@@ -0,0 +1,118 @@
+#!/usr/bin/env bash
+#
+# Build, Developer ID-sign, notarize and package TickerBar.app into
+# dist/tickerbar.zip. The release workflow runs this same script, so a local
+# run and a CI run produce the same artifact.
+#
+# Signing is delegated to xcodebuild rather than a hand-rolled
+# `codesign --deep`. TickerBar is sandboxed and its entitlements reference
+# $(PRODUCT_BUNDLE_IDENTIFIER) for Sparkle's installer XPC mach-lookup
+# exceptions. codesign does not expand build settings, so signing the raw
+# .entitlements file by hand bakes in a literal
+# "$(PRODUCT_BUNDLE_IDENTIFIER)-spks" and the sandboxed updater cannot reach
+# its XPC services. xcodebuild expands it, and signs nested code
+# (Sparkle.framework, Updater.app, Installer.xpc, Downloader.xpc) inside-out
+# with each target's own entitlements.
+#
+# Configuration, all via environment:
+# VERSION release version, e.g. 1.5.0 (default: read from Info.plist)
+# APP_IDENTITY "Developer ID Application: NAME (TEAMID)"
+# TEAM_ID Apple Developer team id (default: parsed from APP_IDENTITY)
+# NOTARY_PROFILE notarytool keychain profile (default: tickerbar-notary)
+# SKIP_NOTARIZE=1 build and sign, but do not notarize (local testing)
+set -euo pipefail
+
+ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+cd "$ROOT"
+
+INFO_PLIST="$ROOT/TickerBar/Info.plist"
+VERSION="${VERSION:-$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' "$INFO_PLIST")}"
+APP_IDENTITY="${APP_IDENTITY:-}"
+NOTARY_PROFILE="${NOTARY_PROFILE:-tickerbar-notary}"
+
+BUILD="$ROOT/build"
+DIST="$ROOT/dist"
+ARCHIVE="$BUILD/TickerBar.xcarchive"
+APP="$DIST/TickerBar.app"
+ZIP="$DIST/tickerbar.zip"
+
+step() { printf '\n\033[1;34m==>\033[0m %s\n' "$1"; }
+
+if [[ -z "$APP_IDENTITY" ]]; then
+ echo "APP_IDENTITY is unset." >&2
+ echo "Set it to your Developer ID Application identity, e.g.:" >&2
+ echo ' APP_IDENTITY="Developer ID Application: Your Name (TEAMID)"' >&2
+ echo "List what you have with: security find-identity -v -p codesigning" >&2
+ exit 1
+fi
+
+# "Developer ID Application: Name (TEAMID)" -> TEAMID
+TEAM_ID="${TEAM_ID:-$(sed -n 's/.*(\([A-Z0-9]*\))$/\1/p' <<<"$APP_IDENTITY")}"
+if [[ -z "$TEAM_ID" ]]; then
+ echo "TEAM_ID is unset and could not be parsed from APP_IDENTITY." >&2
+ exit 1
+fi
+
+step "Stamping version $VERSION"
+/usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $VERSION" "$INFO_PLIST"
+/usr/libexec/PlistBuddy -c "Set :CFBundleVersion $VERSION" "$INFO_PLIST"
+
+step "Archiving (Developer ID, hardened runtime)"
+rm -rf "$ARCHIVE" "$DIST"
+xcodebuild archive \
+ -project TickerBar.xcodeproj \
+ -scheme TickerBar \
+ -configuration Release \
+ -derivedDataPath "$BUILD" \
+ -archivePath "$ARCHIVE" \
+ CODE_SIGN_STYLE=Manual \
+ CODE_SIGN_IDENTITY="$APP_IDENTITY" \
+ DEVELOPMENT_TEAM="$TEAM_ID" \
+ PROVISIONING_PROFILE_SPECIFIER= \
+ OTHER_CODE_SIGN_FLAGS=--timestamp
+
+step "Exporting app"
+EXPORT_OPTIONS="$(mktemp -t tickerbar-export).plist"
+cat > "$EXPORT_OPTIONS" <
+
+
+
+ methoddeveloper-id
+ signingStylemanual
+ teamID$TEAM_ID
+ signingCertificateDeveloper ID Application
+
+
+PLIST
+xcodebuild -exportArchive \
+ -archivePath "$ARCHIVE" \
+ -exportPath "$DIST" \
+ -exportOptionsPlist "$EXPORT_OPTIONS"
+rm -f "$EXPORT_OPTIONS"
+codesign --verify --deep --strict --verbose=2 "$APP"
+
+if [[ "${SKIP_NOTARIZE:-}" == "1" ]]; then
+ step "Skipping notarization (SKIP_NOTARIZE=1)"
+ echo "WARNING: not notarized, local testing only. Gatekeeper will complain." >&2
+else
+ step "Notarizing"
+ ditto -c -k --keepParent "$APP" "$DIST/notarize.zip"
+ xcrun notarytool submit "$DIST/notarize.zip" --keychain-profile "$NOTARY_PROFILE" --wait
+ rm -f "$DIST/notarize.zip"
+
+ step "Stapling"
+ xcrun stapler staple "$APP"
+ xcrun stapler validate "$APP"
+ # Must report "source=Notarized Developer ID". Anything else means users get
+ # a Gatekeeper prompt, so fail here rather than ship it.
+ spctl --assess --type exec --verbose=4 "$APP"
+fi
+
+step "Packaging"
+ditto -c -k --keepParent "$APP" "$ZIP"
+
+step "Done"
+echo "App: $APP"
+echo "Zip: $ZIP"
+echo "SHA256: $(shasum -a 256 "$ZIP" | awk '{print $1}')"
From b7dc9448faa0675c6a7fece8c837ee3c60713eb3 Mon Sep 17 00:00:00 2001
From: TerrifiedBug
Date: Sun, 2 Aug 2026 18:26:25 +0100
Subject: [PATCH 2/3] ci: pin actions to commit SHAs and enable dependabot
The release job imports the Developer ID private key, so a mutable action tag
is a live exfiltration path: whoever controls the tag controls code running in
the same job as the key. Pin every action to a full commit SHA and let
Dependabot keep the pins current.
---
.github/dependabot.yml | 10 ++++++++++
.github/workflows/ci.yml | 6 +++---
.github/workflows/release.yml | 9 ++++++---
3 files changed, 19 insertions(+), 6 deletions(-)
create mode 100644 .github/dependabot.yml
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 9591090..26af12b 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -48,10 +48,13 @@ jobs:
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: Select Xcode
- uses: maxim-lobanov/setup-xcode@v1
+ uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1.7.0
with:
xcode-version: latest-stable
@@ -176,7 +179,7 @@ jobs:
fi
- name: Create Release
- uses: softprops/action-gh-release@v2
+ uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2.6.2
with:
tag_name: ${{ steps.v.outputs.tag }}
files: ${{ env.ZIP }}
From 724ad2c596c9737ec458702f78783ffd4cc149a2 Mon Sep 17 00:00:00 2001
From: TerrifiedBug
Date: Sun, 2 Aug 2026 18:44:25 +0100
Subject: [PATCH 3/3] fix: drop unused App Sandbox before it activates and
orphans user data
No released build has ever been sandboxed. The old release workflow only
reached its '--deep --entitlements' path when signing secrets were present,
and they never were, so every shipped build came from the ad-hoc fallback
'codesign --force --deep --sign -', which applies no entitlements at all.
Signing correctly would therefore have enabled the sandbox for the first
time. That relocates preferences from ~/Library/Preferences into
~/Library/Containers, so every existing user would have opened v1.5.0 to an
empty watchlist with their holdings and alerts gone.
TickerBar ships through Developer ID for Homebrew, never the App Store, so
the sandbox is optional. Remove the entitlements file, its build setting and
the sandbox-only SUEnableInstallerLauncherService key. Hardened runtime stays
on, which is what notarization actually requires.
Verified: the notarized build reports no entitlements, creates no container,
and reads and writes the existing unsandboxed preferences.
---
.github/workflows/release.yml | 19 ++++++++++++-------
CHANGELOG.md | 5 ++++-
TickerBar.xcodeproj/project.pbxproj | 4 ----
TickerBar/Info.plist | 2 --
TickerBar/Services/UpdateChecker.swift | 9 ++++-----
TickerBar/TickerBar.entitlements | 17 -----------------
scripts/build-release.sh | 20 ++++++++++++--------
7 files changed, 32 insertions(+), 44 deletions(-)
delete mode 100644 TickerBar/TickerBar.entitlements
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 26af12b..a4bc50a 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -3,13 +3,18 @@ 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`. That matters here: the app is sandboxed, and its
-# entitlements reference $(PRODUCT_BUNDLE_IDENTIFIER) for Sparkle's installer
-# XPC mach-lookup exceptions. codesign does not expand build settings, so
-# signing the raw .entitlements file by hand bakes in a literal
-# "$(PRODUCT_BUNDLE_IDENTIFIER)-spks" and breaks the updater. xcodebuild
-# expands it and signs nested code (Sparkle.framework, Updater.app,
-# Installer.xpc, Downloader.xpc) inside-out with their own entitlements.
+# `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
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7d382f6..0288285 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,9 +8,12 @@ All notable changes to TickerBar will be documented in this file.
### Changed
- Releases are now signed with an Apple Developer ID certificate and notarized by Apple. macOS no longer blocks the first launch, so the `xattr -dr com.apple.quarantine` workaround is gone.
-- Signing is now done by `xcodebuild` archive and export instead of a hand-rolled `codesign --deep`. The previous command signed the raw entitlements file, which left the literal string `$(PRODUCT_BUNDLE_IDENTIFIER)-spks` in the sandboxed app's mach-lookup exceptions rather than the real service name. Sparkle's installer XPC services are now signed inside-out with their own entitlements.
+- Signing is now done by `xcodebuild` archive and export instead of `codesign --deep`. Sparkle documents `--deep` as a common source of signing errors, because the XPC services it bundles have different requirements from the rest of the app. Sparkle's framework, updater and helper tools are now signed inside-out.
- The repository moved to `TerrifiedBug/tickerbar` and the release asset is now `tickerbar.zip`. GitHub redirects the old paths, so existing installs keep updating.
+### Removed
+- Dropped the App Sandbox entitlements and the sandbox-only Sparkle installer service. No released build was ever sandboxed, because the old ad-hoc signing step applied no entitlements at all. Signing correctly would have switched the sandbox on for the first time and moved preferences into `~/Library/Containers`, losing every existing watchlist, holding and alert. TickerBar ships through Developer ID rather than the App Store, where the sandbox is optional.
+
## [1.4.1] - 2026-07-13
### Fixed
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 = ""; };
D4E5F6071819A1B2C3D4E5F6 /* MenuBarLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuBarLabel.swift; sourceTree = ""; };
F8B1157F816CFF9BBB9B3571 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; };
- FFA0DD350D92AFA182D7215C /* TickerBar.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = TickerBar.entitlements; sourceTree = ""; };
/* End PBXFileReference section */
/* Begin PBXGroup section */
@@ -83,7 +82,6 @@
children = (
DBDE6680A7FD9B348F4D20AA /* Assets.xcassets */,
B1A9E9CC5D7EB971811810C9 /* Info.plist */,
- FFA0DD350D92AFA182D7215C /* TickerBar.entitlements */,
ADFBB8F08EEDFB92601682F9 /* TickerBarApp.swift */,
7F0E226159EE2124503A1D18 /* Models */,
4C3B2A20AAEFBF8DC77B5988 /* Services */,
@@ -449,7 +447,6 @@
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
- CODE_SIGN_ENTITLEMENTS = TickerBar/TickerBar.entitlements;
COMBINE_HIDPI_IMAGES = YES;
ENABLE_HARDENED_RUNTIME = YES;
INFOPLIST_FILE = TickerBar/Info.plist;
@@ -467,7 +464,6 @@
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
- CODE_SIGN_ENTITLEMENTS = TickerBar/TickerBar.entitlements;
COMBINE_HIDPI_IMAGES = YES;
ENABLE_HARDENED_RUNTIME = YES;
INFOPLIST_FILE = TickerBar/Info.plist;
diff --git a/TickerBar/Info.plist b/TickerBar/Info.plist
index 242fce5..7eaa269 100644
--- a/TickerBar/Info.plist
+++ b/TickerBar/Info.plist
@@ -26,8 +26,6 @@
SUEnableAutomaticChecks
- SUEnableInstallerLauncherService
-
SUFeedURL
https://raw.githubusercontent.com/TerrifiedBug/tickerbar/master/appcast.xml
SUPublicEDKey
diff --git a/TickerBar/Services/UpdateChecker.swift b/TickerBar/Services/UpdateChecker.swift
index 22b7636..e59f98b 100644
--- a/TickerBar/Services/UpdateChecker.swift
+++ b/TickerBar/Services/UpdateChecker.swift
@@ -6,11 +6,10 @@ final class UpdateChecker: ObservableObject {
let updaterController: SPUStandardUpdaterController
init() {
- // Always run Sparkle — including for Homebrew installs. The app is
- // notarization-agnostic here; the cask sets `auto_updates true` so brew
- // defers to Sparkle instead of fighting it. (Previously this disabled
- // Sparkle whenever a /Caskroom/tickerbar dir existed, which left brew
- // users with no way to update from inside the app.)
+ // Always run Sparkle, including for Homebrew installs. The cask sets
+ // `auto_updates true` so brew defers to Sparkle instead of fighting
+ // it. (This used to disable Sparkle whenever a /Caskroom/tickerbar
+ // dir existed, which left brew users no way to update in-app.)
updaterController = SPUStandardUpdaterController(
startingUpdater: true,
updaterDelegate: nil,
diff --git a/TickerBar/TickerBar.entitlements b/TickerBar/TickerBar.entitlements
deleted file mode 100644
index 6226cd3..0000000
--- a/TickerBar/TickerBar.entitlements
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-
-
- com.apple.security.app-sandbox
-
- com.apple.security.network.client
-
- com.apple.security.files.user-selected.read-write
-
- com.apple.security.temporary-exception.mach-lookup.global-name
-
- $(PRODUCT_BUNDLE_IDENTIFIER)-spks
- $(PRODUCT_BUNDLE_IDENTIFIER)-spki
-
-
-
diff --git a/scripts/build-release.sh b/scripts/build-release.sh
index 52a7171..480227f 100755
--- a/scripts/build-release.sh
+++ b/scripts/build-release.sh
@@ -5,14 +5,18 @@
# run and a CI run produce the same artifact.
#
# Signing is delegated to xcodebuild rather than a hand-rolled
-# `codesign --deep`. TickerBar is sandboxed and its entitlements reference
-# $(PRODUCT_BUNDLE_IDENTIFIER) for Sparkle's installer XPC mach-lookup
-# exceptions. codesign does not expand build settings, so signing the raw
-# .entitlements file by hand bakes in a literal
-# "$(PRODUCT_BUNDLE_IDENTIFIER)-spks" and the sandboxed updater cannot reach
-# its XPC services. xcodebuild expands it, and signs nested code
-# (Sparkle.framework, Updater.app, Installer.xpc, Downloader.xpc) inside-out
-# with each target's own entitlements.
+# `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.
#
# Configuration, all via environment:
# VERSION release version, e.g. 1.5.0 (default: read from Info.plist)