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
10 changes: 10 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -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
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
250 changes: 153 additions & 97 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -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 <<XMLEOF
<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" xmlns:dc="http://purl.org/dc/elements/1.1/">
<channel>
Expand All @@ -106,11 +161,12 @@ jobs:
<title>Version $VERSION</title>
<sparkle:version>$VERSION</sparkle:version>
<sparkle:shortVersionString>$VERSION</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>14.0</sparkle:minimumSystemVersion>
<pubDate>$(date -R)</pubDate>
<enclosure
url="https://github.com/TerrifiedBug/TickerBar/releases/download/$TAG/TickerBar.zip"
sparkle:edSignature="$SPARKLE_SIGNATURE"
length="$ZIP_SIZE"
url="https://github.com/TerrifiedBug/tickerbar/releases/download/$TAG/tickerbar.zip"
sparkle:edSignature="$SIGNATURE"
length="$SIZE"
type="application/octet-stream" />
</item>
</channel>
Expand All @@ -119,66 +175,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
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2.6.2
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 <<CASKEOF
cask "tickerbar" do
version "REPLACE_VERSION"
sha256 "REPLACE_SHA256"
version "$VERSION"
sha256 "$SHA256"

url "https://github.com/TerrifiedBug/TickerBar/releases/download/v#{version}/TickerBar.zip"
url "https://github.com/TerrifiedBug/tickerbar/releases/download/v#{version}/tickerbar.zip"
name "TickerBar"
desc "Lightweight macOS menu bar stock ticker"
homepage "https://github.com/TerrifiedBug/TickerBar"
homepage "https://github.com/TerrifiedBug/tickerbar"

# Ad-hoc build self-updates via Sparkle, so let Homebrew defer to it.
# Signed, notarized, and self-updating via Sparkle.
auto_updates true

app "TickerBar.app"

zap trash: "~/Library/Preferences/com.tickerbar.app.plist"
zap trash: [
"~/Library/Caches/com.tickerbar.app",
"~/Library/HTTPStorages/com.tickerbar.app",
"~/Library/Preferences/com.tickerbar.app.plist",
]
end
CASKEOF
sed -i '' "s/REPLACE_VERSION/$VERSION/" Casks/tickerbar.rb
sed -i '' "s/REPLACE_SHA256/$SHA256/" Casks/tickerbar.rb
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add Casks/tickerbar.rb
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,6 @@ DerivedData/
# Internal docs
docs/
plans/

# Release artifacts
dist/
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@ All notable changes to TickerBar will be documented in this file.

## [Unreleased]

## [1.5.0] - 2026-08-02

### 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 `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
Expand Down
Loading
Loading