From 1853e36c72b4322aa6217ba23f5e3bf2873e6d79 Mon Sep 17 00:00:00 2001
From: Joshua Sattler <34030048+jsattler@users.noreply.github.com>
Date: Sat, 7 Feb 2026 20:18:14 +0100
Subject: [PATCH 1/2] feat(ci): add release, prerelease, and PR workflows
- Add release.yml for production releases with signing and notarization
- Add prerelease.yml for beta releases with signing (no notarization)
- Add pull-request.yml to verify builds on PRs
- Add Homebrew cask formula in Casks/bettercapture.rb
---
.github/workflows/prerelease.yml | 168 ++++++++++++++++++++++
.github/workflows/pull-request.yml | 64 +++++++++
.github/workflows/release.yml | 216 +++++++++++++++++++++++++++++
Casks/bettercapture.rb | 20 +++
4 files changed, 468 insertions(+)
create mode 100644 .github/workflows/prerelease.yml
create mode 100644 .github/workflows/pull-request.yml
create mode 100644 .github/workflows/release.yml
create mode 100644 Casks/bettercapture.rb
diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml
new file mode 100644
index 0000000..085179f
--- /dev/null
+++ b/.github/workflows/prerelease.yml
@@ -0,0 +1,168 @@
+name: Build Pre-release
+
+on:
+ release:
+ types: [prereleased]
+ workflow_dispatch:
+ inputs:
+ version:
+ description: 'Version number (e.g., 1.0.0-beta.1)'
+ required: true
+ type: string
+
+permissions:
+ contents: write
+
+env:
+ APP_NAME: BetterCapture
+ SCHEME: BetterCapture
+ XCODE_VERSION: '26.0'
+
+jobs:
+ build:
+ runs-on: macos-15
+ timeout-minutes: 20
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Select Xcode version
+ run: sudo xcode-select -s /Applications/Xcode_${{ env.XCODE_VERSION }}.app
+
+ - name: Show Xcode version
+ run: xcodebuild -version
+
+ - name: Import signing certificate
+ env:
+ APPLE_CERTIFICATE_BASE64: ${{ secrets.APPLE_CERTIFICATE_BASE64 }}
+ APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
+ run: |
+ # Create temporary keychain
+ KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
+ KEYCHAIN_PASSWORD=$(openssl rand -base64 32)
+
+ # Create keychain
+ security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
+ security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
+ security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
+
+ # Import certificate
+ CERTIFICATE_PATH=$RUNNER_TEMP/certificate.p12
+ echo "$APPLE_CERTIFICATE_BASE64" | base64 --decode -o "$CERTIFICATE_PATH"
+ security import "$CERTIFICATE_PATH" -P "$APPLE_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k "$KEYCHAIN_PATH"
+ security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
+ security list-keychain -d user -s "$KEYCHAIN_PATH"
+
+ # Clean up certificate file
+ rm "$CERTIFICATE_PATH"
+
+ - name: Determine version
+ id: version
+ run: |
+ if [ "${{ github.event_name }}" == "release" ]; then
+ VERSION="${{ github.event.release.tag_name }}"
+ # Remove 'v' prefix if present
+ VERSION="${VERSION#v}"
+ else
+ VERSION="${{ inputs.version }}"
+ fi
+ echo "version=$VERSION" >> $GITHUB_OUTPUT
+ echo "Building pre-release version: $VERSION"
+
+ - name: Update version in project
+ run: |
+ VERSION=${{ steps.version.outputs.version }}
+ # Extract major.minor for MARKETING_VERSION (strip pre-release suffix)
+ MARKETING_VERSION=$(echo "$VERSION" | grep -oE '^[0-9]+\.[0-9]+')
+ agvtool new-marketing-version "$MARKETING_VERSION"
+ agvtool new-version -all 1
+
+ - name: Build application
+ run: |
+ xcodebuild archive \
+ -project "${{ env.APP_NAME }}.xcodeproj" \
+ -scheme "${{ env.SCHEME }}" \
+ -configuration Release \
+ -archivePath "$RUNNER_TEMP/${{ env.APP_NAME }}.xcarchive" \
+ -destination "generic/platform=macOS" \
+ DEVELOPMENT_TEAM="${{ secrets.APPLE_TEAM_ID }}" \
+ CODE_SIGN_IDENTITY="Developer ID Application" \
+ CODE_SIGN_STYLE=Manual \
+ OTHER_CODE_SIGN_FLAGS="--timestamp --options=runtime" \
+ | xcbeautify --renderer github-actions || true
+
+ - name: Export application
+ run: |
+ # Create export options plist
+ cat > "$RUNNER_TEMP/ExportOptions.plist" << EOF
+
+
+
+
+ method
+ developer-id
+ teamID
+ ${{ secrets.APPLE_TEAM_ID }}
+ signingStyle
+ manual
+ signingCertificate
+ Developer ID Application
+
+
+ EOF
+
+ xcodebuild -exportArchive \
+ -archivePath "$RUNNER_TEMP/${{ env.APP_NAME }}.xcarchive" \
+ -exportPath "$RUNNER_TEMP/export" \
+ -exportOptionsPlist "$RUNNER_TEMP/ExportOptions.plist"
+
+ - name: Create DMG
+ run: |
+ APP_PATH="$RUNNER_TEMP/export/${{ env.APP_NAME }}.app"
+ DMG_PATH="$RUNNER_TEMP/${{ env.APP_NAME }}-${{ steps.version.outputs.version }}-arm64.dmg"
+
+ # Create temporary directory for DMG contents
+ DMG_TEMP="$RUNNER_TEMP/dmg-contents"
+ mkdir -p "$DMG_TEMP"
+
+ # Copy app to temporary directory
+ cp -R "$APP_PATH" "$DMG_TEMP/"
+
+ # Create symbolic link to Applications
+ ln -s /Applications "$DMG_TEMP/Applications"
+
+ # Create DMG
+ hdiutil create -volname "${{ env.APP_NAME }}" \
+ -srcfolder "$DMG_TEMP" \
+ -ov -format UDZO \
+ "$DMG_PATH"
+
+ # Clean up
+ rm -rf "$DMG_TEMP"
+
+ echo "DMG_PATH=$DMG_PATH" >> $GITHUB_ENV
+
+ - name: Upload DMG artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: ${{ env.APP_NAME }}-${{ steps.version.outputs.version }}-arm64.dmg
+ path: ${{ env.DMG_PATH }}
+ if-no-files-found: error
+
+ - name: Upload to pre-release
+ if: github.event_name == 'release'
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: |
+ gh release upload "${{ github.event.release.tag_name }}" \
+ "${{ env.DMG_PATH }}" \
+ --clobber
+
+ - name: Clean up keychain
+ if: always()
+ run: |
+ KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
+ if [ -f "$KEYCHAIN_PATH" ]; then
+ security delete-keychain "$KEYCHAIN_PATH"
+ fi
diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml
new file mode 100644
index 0000000..0b58f47
--- /dev/null
+++ b/.github/workflows/pull-request.yml
@@ -0,0 +1,64 @@
+name: Pull Request
+
+on:
+ pull_request:
+ branches: [main]
+ paths:
+ - '**.swift'
+ - '**.xcodeproj/**'
+ - '.github/workflows/pull-request.yml'
+
+env:
+ APP_NAME: BetterCapture
+ SCHEME: BetterCapture
+ XCODE_VERSION: '26.0'
+
+jobs:
+ build:
+ name: Build
+ runs-on: macos-15
+ timeout-minutes: 15
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Select Xcode version
+ run: sudo xcode-select -s /Applications/Xcode_${{ env.XCODE_VERSION }}.app
+
+ - name: Show Xcode version
+ run: xcodebuild -version
+
+ - name: Build application
+ run: |
+ xcodebuild build \
+ -project "${{ env.APP_NAME }}.xcodeproj" \
+ -scheme "${{ env.SCHEME }}" \
+ -configuration Debug \
+ -destination "platform=macOS" \
+ CODE_SIGN_IDENTITY="-" \
+ CODE_SIGNING_REQUIRED=NO \
+ | xcbeautify --renderer github-actions || true
+
+ test:
+ name: Test
+ runs-on: macos-15
+ timeout-minutes: 15
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Select Xcode version
+ run: sudo xcode-select -s /Applications/Xcode_${{ env.XCODE_VERSION }}.app
+
+ - name: Run tests
+ run: |
+ xcodebuild test \
+ -project "${{ env.APP_NAME }}.xcodeproj" \
+ -scheme "${{ env.SCHEME }}" \
+ -configuration Debug \
+ -destination "platform=macOS" \
+ CODE_SIGN_IDENTITY="-" \
+ CODE_SIGNING_REQUIRED=NO \
+ | xcbeautify --renderer github-actions || true
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..f24da31
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,216 @@
+name: Build and Release
+
+on:
+ release:
+ types: [published]
+ workflow_dispatch:
+ inputs:
+ version:
+ description: "Version number (e.g., 1.0.0)"
+ required: true
+ type: string
+
+permissions:
+ contents: write
+
+env:
+ APP_NAME: BetterCapture
+ SCHEME: BetterCapture
+ XCODE_VERSION: "26.0"
+
+jobs:
+ build:
+ runs-on: macos-15
+ timeout-minutes: 30
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Select Xcode version
+ run: sudo xcode-select -s /Applications/Xcode_${{ env.XCODE_VERSION }}.app
+
+ - name: Show Xcode version
+ run: xcodebuild -version
+
+ - name: Import signing certificate
+ env:
+ APPLE_CERTIFICATE_BASE64: ${{ secrets.APPLE_CERTIFICATE_BASE64 }}
+ APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
+ run: |
+ # Create temporary keychain
+ KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
+ KEYCHAIN_PASSWORD=$(openssl rand -base64 32)
+
+ # Create keychain
+ security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
+ security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
+ security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
+
+ # Import certificate
+ CERTIFICATE_PATH=$RUNNER_TEMP/certificate.p12
+ echo "$APPLE_CERTIFICATE_BASE64" | base64 --decode -o "$CERTIFICATE_PATH"
+ security import "$CERTIFICATE_PATH" -P "$APPLE_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k "$KEYCHAIN_PATH"
+ security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
+ security list-keychain -d user -s "$KEYCHAIN_PATH"
+
+ # Clean up certificate file
+ rm "$CERTIFICATE_PATH"
+
+ - name: Determine version
+ id: version
+ run: |
+ if [ "${{ github.event_name }}" == "release" ]; then
+ VERSION="${{ github.event.release.tag_name }}"
+ # Remove 'v' prefix if present
+ VERSION="${VERSION#v}"
+ else
+ VERSION="${{ inputs.version }}"
+ fi
+ echo "version=$VERSION" >> $GITHUB_OUTPUT
+ echo "Building version: $VERSION"
+
+ - name: Update version in project
+ run: |
+ VERSION=${{ steps.version.outputs.version }}
+ # Extract major.minor for MARKETING_VERSION
+ MARKETING_VERSION=$(echo "$VERSION" | grep -oE '^[0-9]+\.[0-9]+')
+ # Use full version for display if it has patch
+ agvtool new-marketing-version "$MARKETING_VERSION"
+ agvtool new-version -all 1
+
+ - name: Build application
+ run: |
+ xcodebuild archive \
+ -project "${{ env.APP_NAME }}.xcodeproj" \
+ -scheme "${{ env.SCHEME }}" \
+ -configuration Release \
+ -archivePath "$RUNNER_TEMP/${{ env.APP_NAME }}.xcarchive" \
+ -destination "generic/platform=macOS" \
+ DEVELOPMENT_TEAM="${{ secrets.APPLE_TEAM_ID }}" \
+ CODE_SIGN_IDENTITY="Developer ID Application" \
+ CODE_SIGN_STYLE=Manual \
+ OTHER_CODE_SIGN_FLAGS="--timestamp --options=runtime" \
+ | xcbeautify --renderer github-actions || true
+
+ - name: Export application
+ run: |
+ # Create export options plist
+ cat > "$RUNNER_TEMP/ExportOptions.plist" << EOF
+
+
+
+
+ method
+ developer-id
+ teamID
+ ${{ secrets.APPLE_TEAM_ID }}
+ signingStyle
+ manual
+ signingCertificate
+ Developer ID Application
+
+
+ EOF
+
+ xcodebuild -exportArchive \
+ -archivePath "$RUNNER_TEMP/${{ env.APP_NAME }}.xcarchive" \
+ -exportPath "$RUNNER_TEMP/export" \
+ -exportOptionsPlist "$RUNNER_TEMP/ExportOptions.plist"
+
+ - name: Notarize application
+ env:
+ APPLE_ID: ${{ secrets.APPLE_ID }}
+ APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
+ APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
+ run: |
+ APP_PATH="$RUNNER_TEMP/export/${{ env.APP_NAME }}.app"
+
+ # Create zip for notarization
+ ditto -c -k --keepParent "$APP_PATH" "$RUNNER_TEMP/${{ env.APP_NAME }}-notarize.zip"
+
+ # Submit for notarization
+ xcrun notarytool submit "$RUNNER_TEMP/${{ env.APP_NAME }}-notarize.zip" \
+ --apple-id "$APPLE_ID" \
+ --password "$APPLE_ID_PASSWORD" \
+ --team-id "$APPLE_TEAM_ID" \
+ --wait
+
+ # Staple the notarization ticket
+ xcrun stapler staple "$APP_PATH"
+
+ # Clean up
+ rm "$RUNNER_TEMP/${{ env.APP_NAME }}-notarize.zip"
+
+ - name: Create DMG
+ run: |
+ APP_PATH="$RUNNER_TEMP/export/${{ env.APP_NAME }}.app"
+ DMG_PATH="$RUNNER_TEMP/${{ env.APP_NAME }}-${{ steps.version.outputs.version }}-arm64.dmg"
+
+ # Create temporary directory for DMG contents
+ DMG_TEMP="$RUNNER_TEMP/dmg-contents"
+ mkdir -p "$DMG_TEMP"
+
+ # Copy app to temporary directory
+ cp -R "$APP_PATH" "$DMG_TEMP/"
+
+ # Create symbolic link to Applications
+ ln -s /Applications "$DMG_TEMP/Applications"
+
+ # Create DMG
+ hdiutil create -volname "${{ env.APP_NAME }}" \
+ -srcfolder "$DMG_TEMP" \
+ -ov -format UDZO \
+ "$DMG_PATH"
+
+ # Clean up
+ rm -rf "$DMG_TEMP"
+
+ echo "DMG_PATH=$DMG_PATH" >> $GITHUB_ENV
+
+ - name: Upload DMG artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: ${{ env.APP_NAME }}-${{ steps.version.outputs.version }}-arm64.dmg
+ path: ${{ env.DMG_PATH }}
+ if-no-files-found: error
+
+ - name: Upload to release
+ if: github.event_name == 'release'
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: |
+ gh release upload "${{ github.event.release.tag_name }}" \
+ "${{ env.DMG_PATH }}" \
+ --clobber
+
+ - name: Calculate SHA256 and update Homebrew cask
+ if: github.event_name == 'release'
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: |
+ # Calculate SHA256
+ SHA256=$(shasum -a 256 "${{ env.DMG_PATH }}" | awk '{print $1}')
+ VERSION=${{ steps.version.outputs.version }}
+
+ echo "SHA256: $SHA256"
+ echo "Version: $VERSION"
+
+ # Update cask file
+ sed -i '' "s/version \".*\"/version \"$VERSION\"/" Casks/bettercapture.rb
+ sed -i '' "s/sha256 .*/sha256 \"$SHA256\"/" Casks/bettercapture.rb
+
+ # Commit and push the updated cask
+ git config user.name "github-actions[bot]"
+ git config user.email "github-actions[bot]@users.noreply.github.com"
+ git add Casks/bettercapture.rb
+ git commit -m "chore(brew): update cask to v$VERSION" || echo "No changes to commit"
+ git push origin HEAD:main
+
+ - name: Clean up keychain
+ if: always()
+ run: |
+ KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
+ if [ -f "$KEYCHAIN_PATH" ]; then
+ security delete-keychain "$KEYCHAIN_PATH"
+ fi
diff --git a/Casks/bettercapture.rb b/Casks/bettercapture.rb
new file mode 100644
index 0000000..a551777
--- /dev/null
+++ b/Casks/bettercapture.rb
@@ -0,0 +1,20 @@
+cask "bettercapture" do
+ version "1.0.0"
+ sha256 :no_check # Updated automatically by release workflow
+
+ url "https://github.com/jsattler/BetterCapture/releases/download/v#{version}/BetterCapture-#{version}-arm64.dmg"
+ name "BetterCapture"
+ desc "The macOS screen recorder you deserve - always free and open source"
+ homepage "https://github.com/jsattler/BetterCapture"
+
+ depends_on macos: ">= :sequoia"
+ depends_on arch: :arm64
+
+ app "BetterCapture.app"
+
+ zap trash: [
+ "~/Library/Application Support/BetterCapture",
+ "~/Library/Caches/com.sattlerjoshua.BetterCapture",
+ "~/Library/Preferences/com.sattlerjoshua.BetterCapture.plist",
+ ]
+end
From 9d9487d37efceba03fc9d89f5a1e7f5540fe896b Mon Sep 17 00:00:00 2001
From: Joshua Sattler <34030048+jsattler@users.noreply.github.com>
Date: Sat, 7 Feb 2026 20:19:37 +0100
Subject: [PATCH 2/2] docs(readme): update Homebrew install instructions
---
README.md | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 5f440a7..f5a7c4d 100644
--- a/README.md
+++ b/README.md
@@ -27,7 +27,8 @@
### Homebrew
```bash
-brew install bettercapture
+brew tap jsattler/bettercapture https://github.com/jsattler/BetterCapture
+brew install --cask bettercapture
```
### Direct Download