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.

- GitHub release downloads + GitHub release downloads

@@ -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)