Search/browse performance backbone + ⌘K rewritten as an indexed palette #38
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Build & Release | |
| on: | |
| push: | |
| branches: [main] | |
| tags: ['v*'] | |
| pull_request: | |
| branches: [main] | |
| # Cancel in-progress runs for the same branch | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| env: | |
| SCHEME: TopPresenter | |
| PRODUCT_NAME: TopPresenter | |
| # EXACT parity with the dev Mac: Xcode 26.3 (17C529) on the macos-26 arm64 | |
| # image. Older Xcode (16.x) silently IGNORES the project's Swift 6.x settings | |
| # (SWIFT_DEFAULT_ACTOR_ISOLATION, approachable concurrency) — never build the | |
| # release with a different major Xcode than the one used for development. | |
| XCODE_VERSION: '26.3' | |
| jobs: | |
| build: | |
| name: Build & Package | |
| runs-on: macos-26 | |
| timeout-minutes: 40 | |
| outputs: | |
| version: ${{ steps.package.outputs.VERSION }} | |
| prerelease_tag: ${{ steps.package.outputs.PRERELEASE_TAG }} | |
| prerelease_version: ${{ steps.package.outputs.PRERELEASE_VERSION }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v7 | |
| - name: Select Xcode | |
| run: | | |
| sudo xcode-select -s "/Applications/Xcode_${XCODE_VERSION}.app/Contents/Developer" || { | |
| echo "::error::Xcode ${XCODE_VERSION} not on this image. Available:" | |
| ls /Applications | grep -i '^Xcode' || true | |
| exit 1 | |
| } | |
| xcodebuild -version | |
| - name: Resolve dependencies | |
| run: xcodebuild -resolvePackageDependencies -scheme "$SCHEME" -project "$PRODUCT_NAME.xcodeproj" | |
| - name: Build | |
| run: | | |
| # Monotonic CFBundleVersion so Sparkle can tell releases apart (the project's | |
| # CURRENT_PROJECT_VERSION is a fixed 1). Ad-hoc code signing ("-") so the app's | |
| # entitlements (sandbox + Sparkle XPC) apply with no Apple certs; the bundle is | |
| # uniformly RE-signed below so every nested binary carries the same identity. | |
| xcodebuild \ | |
| -scheme "$SCHEME" \ | |
| -project "$PRODUCT_NAME.xcodeproj" \ | |
| -configuration Release \ | |
| -derivedDataPath build \ | |
| -destination 'platform=macOS' \ | |
| CODE_SIGN_IDENTITY="-" \ | |
| CODE_SIGN_STYLE=Manual \ | |
| CODE_SIGNING_REQUIRED=NO \ | |
| CODE_SIGNING_ALLOWED=YES \ | |
| DEVELOPMENT_TEAM="" \ | |
| CURRENT_PROJECT_VERSION=${{ github.run_number }} \ | |
| clean build | |
| - name: Run unit tests (gate — failures fail the build) | |
| run: | | |
| xcodebuild \ | |
| -scheme "$SCHEME" \ | |
| -project "$PRODUCT_NAME.xcodeproj" \ | |
| -destination 'platform=macOS' \ | |
| -only-testing:TopPresenterTests \ | |
| CODE_SIGN_IDENTITY="-" \ | |
| CODE_SIGNING_REQUIRED=NO \ | |
| CODE_SIGNING_ALLOWED=NO \ | |
| DEVELOPMENT_TEAM="" \ | |
| test | |
| - name: Re-sign uniformly (ad-hoc, inside-out) & verify | |
| run: | | |
| # DYLD kills the app if the process and a mapped framework carry DIFFERENT | |
| # Team IDs ("code signature not valid ... different Team IDs"). Guarantee | |
| # uniformity: re-sign every nested Sparkle binary and then the app itself | |
| # with the SAME ad-hoc identity, inside-out. No --options runtime here: | |
| # hardened runtime + ad-hoc (team-less) signatures = library validation | |
| # rejects the frameworks; plain ad-hoc launches everywhere. | |
| APP="build/Build/Products/Release/$PRODUCT_NAME.app" | |
| SPK="$APP/Contents/Frameworks/Sparkle.framework" | |
| for target in \ | |
| "$SPK/Versions/B/XPCServices/Downloader.xpc" \ | |
| "$SPK/Versions/B/XPCServices/Installer.xpc" \ | |
| "$SPK/Versions/B/Autoupdate" \ | |
| "$SPK/Versions/B/Updater.app" \ | |
| "$SPK"; do | |
| if [ -e "$target" ]; then | |
| codesign --force --sign - --preserve-metadata=entitlements "$target" | |
| fi | |
| done | |
| codesign --force --sign - --preserve-metadata=entitlements "$APP" | |
| echo "── verify ──" | |
| codesign --verify --deep --strict --verbose=2 "$APP" | |
| echo "── identities (must ALL be adhoc, no TeamIdentifier) ──" | |
| for bin in "$APP" "$SPK" "$SPK/Versions/B/Autoupdate" "$SPK/Versions/B/Updater.app"; do | |
| codesign -dv "$bin" 2>&1 | grep -E 'Signature|TeamIdentifier' | sed "s|^|$(basename "$bin"): |" | |
| done | |
| - name: Package app | |
| id: package | |
| run: | | |
| APP_PATH="build/Build/Products/Release/$PRODUCT_NAME.app" | |
| if [ ! -d "$APP_PATH" ]; then | |
| echo "❌ App not found at $APP_PATH" | |
| find build -name "*.app" -type d | |
| exit 1 | |
| fi | |
| # Get version info | |
| VERSION=$(/usr/libexec/PlistBuddy -c "Print :CFBundleShortVersionString" "$APP_PATH/Contents/Info.plist" 2>/dev/null || echo "1.0") | |
| BUILD=$(/usr/libexec/PlistBuddy -c "Print :CFBundleVersion" "$APP_PATH/Contents/Info.plist" 2>/dev/null || echo "1") | |
| # Alpha counter is PER VERSION: bumping MARKETING_VERSION restarts at alpha.1 | |
| git fetch --tags --force --quiet | |
| ALPHA_N=$(( $(git tag -l "v$VERSION-alpha.*" | wc -l | tr -d ' ') + 1 )) | |
| echo "VERSION=$VERSION" >> $GITHUB_ENV | |
| echo "BUILD=$BUILD" >> $GITHUB_ENV | |
| echo "PRERELEASE_VERSION=$VERSION-alpha.${ALPHA_N}" >> $GITHUB_ENV | |
| echo "PRERELEASE_TAG=v$VERSION-alpha.${ALPHA_N}" >> $GITHUB_ENV | |
| echo "VERSION=$VERSION" >> $GITHUB_OUTPUT | |
| echo "PRERELEASE_VERSION=$VERSION-alpha.${ALPHA_N}" >> $GITHUB_OUTPUT | |
| echo "PRERELEASE_TAG=v$VERSION-alpha.${ALPHA_N}" >> $GITHUB_OUTPUT | |
| # Create ZIP | |
| cd "build/Build/Products/Release" | |
| ditto -c -k --keepParent "$PRODUCT_NAME.app" "$PRODUCT_NAME-$VERSION.zip" | |
| mv "$PRODUCT_NAME-$VERSION.zip" "$GITHUB_WORKSPACE/" | |
| # Create DMG | |
| hdiutil create -volname "$PRODUCT_NAME" \ | |
| -srcfolder "$PRODUCT_NAME.app" \ | |
| -ov -format UDZO \ | |
| "$GITHUB_WORKSPACE/$PRODUCT_NAME-$VERSION.dmg" | |
| - name: Upload artifact | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: TopPresenter-${{ env.VERSION }} | |
| path: | | |
| TopPresenter-*.zip | |
| TopPresenter-*.dmg | |
| retention-days: 30 | |
| # Unique pre-release on every push to main | |
| pre-release: | |
| name: Pre-release (Alpha) | |
| needs: build | |
| if: github.ref == 'refs/heads/main' && github.event_name == 'push' | |
| runs-on: macos-26 | |
| permissions: | |
| contents: write | |
| pages: write # deploy the appcast to Pages (no gh-pages branch) | |
| id-token: write # required by actions/deploy-pages | |
| environment: | |
| name: github-pages | |
| env: | |
| # Empty until you add the repo secret — the appcast steps then no-op (CI stays green). | |
| SPARKLE_PRIVATE_KEY: ${{ secrets.SPARKLE_PRIVATE_KEY }} | |
| steps: | |
| - name: Checkout (for appcast scripts) | |
| uses: actions/checkout@v7 | |
| - name: Download artifact | |
| uses: actions/download-artifact@v8 | |
| with: | |
| pattern: TopPresenter-* | |
| merge-multiple: true | |
| - name: Get short SHA | |
| run: echo "SHORT_SHA=${GITHUB_SHA::7}" >> $GITHUB_ENV | |
| - name: Create alpha pre-release | |
| uses: softprops/action-gh-release@v3 | |
| with: | |
| tag_name: ${{ needs.build.outputs.prerelease_tag }} | |
| name: "TopPresenter ${{ needs.build.outputs.prerelease_version }}" | |
| prerelease: true | |
| generate_release_notes: true | |
| body: | | |
| ### ⚡ Automatic Alpha Build | |
| **Version:** `${{ needs.build.outputs.prerelease_version }}` | |
| **Tag:** `${{ needs.build.outputs.prerelease_tag }}` | |
| **Commit:** `${{ github.sha }}` | |
| **Branch:** `main` | |
| **Built:** ${{ github.event.head_commit.timestamp }} | |
| > ⚠️ This is an unsigned build. To open on macOS: | |
| > Right-click the app → Open → Open (bypass Gatekeeper). | |
| > Or run: `xattr -cr TopPresenter.app` | |
| --- | |
| **Commit message:** ${{ github.event.head_commit.message }} | |
| files: | | |
| TopPresenter-*.zip | |
| TopPresenter-*.dmg | |
| - name: Build signed appcast (beta channel) | |
| if: ${{ env.SPARKLE_PRIVATE_KEY != '' }} | |
| env: | |
| ZIP: TopPresenter-${{ needs.build.outputs.version }}.zip | |
| DOWNLOAD_URL: https://github.com/${{ github.repository }}/releases/download/${{ needs.build.outputs.prerelease_tag }}/TopPresenter-${{ needs.build.outputs.version }}.zip | |
| SHORT_VERSION: ${{ needs.build.outputs.prerelease_version }} | |
| BUILD_VERSION: ${{ github.run_number }} | |
| CHANNEL: beta | |
| FEED_URL: https://robyrew.github.io/TopPresenter/appcast.xml | |
| NOTES: ${{ github.event.head_commit.message }} | |
| run: | | |
| chmod +x scripts/publish_appcast.sh | |
| scripts/publish_appcast.sh | |
| - name: Setup Pages | |
| if: ${{ env.SPARKLE_PRIVATE_KEY != '' }} | |
| uses: actions/configure-pages@v6 | |
| - name: Upload appcast artifact | |
| if: ${{ env.SPARKLE_PRIVATE_KEY != '' }} | |
| uses: actions/upload-pages-artifact@v5 | |
| with: | |
| path: site | |
| - name: Deploy appcast to Pages | |
| if: ${{ env.SPARKLE_PRIVATE_KEY != '' }} | |
| uses: actions/deploy-pages@v5 | |
| # Stable release on final version tags (v1.0.0, v1.1.0, etc.) | |
| release: | |
| name: Stable Release | |
| needs: build | |
| if: startsWith(github.ref, 'refs/tags/v') && !contains(github.ref_name, '-') | |
| runs-on: macos-26 | |
| permissions: | |
| contents: write | |
| pages: write # deploy the appcast to Pages (no gh-pages branch) | |
| id-token: write # required by actions/deploy-pages | |
| environment: | |
| name: github-pages | |
| env: | |
| SPARKLE_PRIVATE_KEY: ${{ secrets.SPARKLE_PRIVATE_KEY }} | |
| steps: | |
| - name: Checkout (for appcast scripts) | |
| uses: actions/checkout@v7 | |
| - name: Download artifact | |
| uses: actions/download-artifact@v8 | |
| with: | |
| pattern: TopPresenter-* | |
| merge-multiple: true | |
| - name: Create stable release | |
| uses: softprops/action-gh-release@v3 | |
| with: | |
| name: "TopPresenter ${{ github.ref_name }}" | |
| generate_release_notes: true | |
| body: | | |
| ### 🎉 TopPresenter ${{ github.ref_name }} | |
| > ⚠️ This is an unsigned build. To open on macOS: | |
| > Right-click the app → Open → Open (bypass Gatekeeper). | |
| > Or run: `xattr -cr TopPresenter.app` | |
| files: | | |
| TopPresenter-*.zip | |
| TopPresenter-*.dmg | |
| - name: Build signed appcast (stable channel) | |
| if: ${{ env.SPARKLE_PRIVATE_KEY != '' }} | |
| env: | |
| ZIP: TopPresenter-${{ needs.build.outputs.version }}.zip | |
| DOWNLOAD_URL: https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/TopPresenter-${{ needs.build.outputs.version }}.zip | |
| SHORT_VERSION: ${{ needs.build.outputs.version }} | |
| BUILD_VERSION: ${{ github.run_number }} | |
| CHANNEL: "" | |
| FEED_URL: https://robyrew.github.io/TopPresenter/appcast.xml | |
| run: | | |
| chmod +x scripts/publish_appcast.sh | |
| scripts/publish_appcast.sh | |
| - name: Setup Pages | |
| if: ${{ env.SPARKLE_PRIVATE_KEY != '' }} | |
| uses: actions/configure-pages@v6 | |
| - name: Upload appcast artifact | |
| if: ${{ env.SPARKLE_PRIVATE_KEY != '' }} | |
| uses: actions/upload-pages-artifact@v5 | |
| with: | |
| path: site | |
| - name: Deploy appcast to Pages | |
| if: ${{ env.SPARKLE_PRIVATE_KEY != '' }} | |
| uses: actions/deploy-pages@v5 |