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 < @@ -106,11 +161,12 @@ jobs: Version $VERSION $VERSION $VERSION + 14.0 $(date -R) @@ -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 <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.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 ddcba33..7eaa269 100644 --- a/TickerBar/Info.plist +++ b/TickerBar/Info.plist @@ -17,20 +17,18 @@ 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/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 new file mode 100755 index 0000000..480227f --- /dev/null +++ b/scripts/build-release.sh @@ -0,0 +1,122 @@ +#!/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`. 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) +# 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}')"