feat: add automatic disk cleanup (#79) #66
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: Release | |
| on: | |
| push: | |
| branches: [main] | |
| workflow_dispatch: | |
| concurrency: | |
| group: release | |
| cancel-in-progress: false | |
| permissions: | |
| contents: write | |
| issues: write | |
| pull-requests: write | |
| jobs: | |
| # Step 1: Select runner (lightweight, runs on ubuntu) | |
| select-runner: | |
| name: Select runner | |
| runs-on: ubuntu-latest | |
| outputs: | |
| runner: ${{ steps.runner.outputs.use-runner }} | |
| steps: | |
| # Automatically detect if a self-hosted mac-runner is online. | |
| # If yes → build on local Mac (faster, free). If no → fall back to cloud. | |
| # Requires a fine-grained PAT with Administration:Read permission. | |
| # https://github.com/mikehardy/runner-fallback-action | |
| - name: Select runner | |
| id: runner | |
| uses: mikehardy/runner-fallback-action@v1 | |
| with: | |
| primary-runner: mac-runner | |
| fallback-runner: macos-latest | |
| fallback-on-error: true | |
| github-token: ${{ secrets.RUNNER_TOKEN }} | |
| # Step 2: Check if a release is needed (runs on mac-runner or fallback) | |
| check-release: | |
| name: Check for release | |
| needs: select-runner | |
| runs-on: ${{ fromJson(needs.select-runner.outputs.runner) }} | |
| outputs: | |
| new_release: ${{ steps.semantic.outputs.new_release_published }} | |
| version: ${{ steps.semantic.outputs.new_release_version }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Add Homebrew to PATH | |
| run: echo "/opt/homebrew/bin" >> "$GITHUB_PATH" | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 22 | |
| - name: Check if release is needed | |
| id: semantic | |
| uses: cycjimmy/semantic-release-action@v4 | |
| with: | |
| dry_run: true | |
| extra_plugins: | | |
| @semantic-release/changelog | |
| @semantic-release/git | |
| conventional-changelog-conventionalcommits | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Result | |
| run: | | |
| echo "New release: ${{ steps.semantic.outputs.new_release_published }}" | |
| echo "Version: ${{ steps.semantic.outputs.new_release_version }}" | |
| # Step 3: Build and release (runs on same runner as step 2) | |
| build-and-release: | |
| name: Build & Release | |
| needs: [select-runner, check-release] | |
| if: needs.check-release.outputs.new_release == 'true' | |
| runs-on: ${{ fromJson(needs.select-runner.outputs.runner) }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Add Homebrew to PATH | |
| run: echo "/opt/homebrew/bin" >> "$GITHUB_PATH" | |
| - name: Setup Xcode | |
| uses: maxim-lobanov/setup-xcode@v1 | |
| with: | |
| xcode-version: latest-stable | |
| - name: Resolve dependencies | |
| run: swift package resolve | |
| - name: Build universal binary | |
| run: | | |
| # Using --disable-sandbox because Apple's Containerization framework | |
| # requires write access during compilation, which the default SPM sandbox blocks. | |
| # This is a standard workaround for Swift projects using native dependencies. | |
| # Security note: Only affects build-time isolation, not runtime security. | |
| echo "Building for arm64..." | |
| swift build -c release --arch arm64 --disable-sandbox | |
| echo "Building for x86_64..." | |
| swift build -c release --arch x86_64 --disable-sandbox | |
| echo "Creating universal binary..." | |
| mkdir -p .build/universal | |
| lipo -create \ | |
| .build/arm64-apple-macosx/release/mac-runner \ | |
| .build/x86_64-apple-macosx/release/mac-runner \ | |
| -output .build/universal/mac-runner | |
| - name: Create app bundle | |
| env: | |
| VERSION: ${{ needs.check-release.outputs.version }} | |
| run: | | |
| mkdir -p build/MacRunner.app/Contents/MacOS | |
| mkdir -p build/MacRunner.app/Contents/Resources | |
| cp .build/universal/mac-runner build/MacRunner.app/Contents/MacOS/MacRunner | |
| chmod +x build/MacRunner.app/Contents/MacOS/MacRunner | |
| ./scripts/generate-info-plist.sh > build/MacRunner.app/Contents/Info.plist | |
| # ── Code Signing ────────────────────────────────────────────────── | |
| - name: Import signing certificate | |
| env: | |
| APPLE_CERTIFICATE_BASE64: ${{ secrets.APPLE_CERTIFICATE_BASE64 }} | |
| APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} | |
| run: | | |
| set -euo pipefail | |
| # Generate a random password for the temporary keychain | |
| KEYCHAIN_PASSWORD="$(uuidgen)" | |
| # Create a temporary keychain | |
| KEYCHAIN_PATH="$RUNNER_TEMP/signing.keychain-db" | |
| 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 the certificate | |
| CERT_PATH="$RUNNER_TEMP/certificate.p12" | |
| echo "$APPLE_CERTIFICATE_BASE64" | base64 --decode > "$CERT_PATH" | |
| security import "$CERT_PATH" \ | |
| -k "$KEYCHAIN_PATH" \ | |
| -P "$APPLE_CERTIFICATE_PASSWORD" \ | |
| -T /usr/bin/codesign \ | |
| -T /usr/bin/security | |
| rm -f "$CERT_PATH" | |
| # Allow codesign to access the keychain without prompting | |
| security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" | |
| # Add temporary keychain to the search list | |
| security list-keychains -d user -s "$KEYCHAIN_PATH" $(security list-keychains -d user | tr -d '"') | |
| IDENTITY="$(security find-identity -v -p codesigning "$KEYCHAIN_PATH" | grep 'Developer ID Application' | head -1 | awk -F '"' '{print $2}')" | |
| if [ -z "$IDENTITY" ]; then | |
| echo "Developer ID Application identity not found in imported keychain" | |
| exit 1 | |
| fi | |
| # Save keychain metadata for later steps and cleanup | |
| echo "KEYCHAIN_PATH=$KEYCHAIN_PATH" >> "$GITHUB_ENV" | |
| echo "KEYCHAIN_PASSWORD=$KEYCHAIN_PASSWORD" >> "$GITHUB_ENV" | |
| echo "SIGNING_IDENTITY=$IDENTITY" >> "$GITHUB_ENV" | |
| - name: Prepare notarization credentials | |
| env: | |
| APPLE_API_KEY_BASE64: ${{ secrets.APPLE_API_KEY_BASE64 }} | |
| APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} | |
| APPLE_API_ISSUER_ID: ${{ secrets.APPLE_API_ISSUER_ID }} | |
| run: | | |
| set -euo pipefail | |
| if [ -n "${APPLE_API_KEY_BASE64:-}" ] && [ -n "${APPLE_API_KEY_ID:-}" ] && [ -n "${APPLE_API_ISSUER_ID:-}" ]; then | |
| API_KEY_PATH="$RUNNER_TEMP/AuthKey_${APPLE_API_KEY_ID}.p8" | |
| echo "$APPLE_API_KEY_BASE64" | base64 --decode > "$API_KEY_PATH" | |
| chmod 600 "$API_KEY_PATH" | |
| echo "NOTARY_API_KEY_PATH=$API_KEY_PATH" >> "$GITHUB_ENV" | |
| echo "Using App Store Connect API key for notarization" | |
| else | |
| echo "App Store Connect API key not configured; release workflow will fall back to Apple ID notarization credentials" | |
| fi | |
| - name: Code sign app bundle | |
| env: | |
| APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} | |
| SIGNING_IDENTITY: ${{ env.SIGNING_IDENTITY }} | |
| run: | | |
| set -euo pipefail | |
| xattr -cr build/MacRunner.app | |
| sign_path() { | |
| local target="$1" | |
| codesign --force --options runtime --timestamp --sign "$SIGNING_IDENTITY" "$target" | |
| } | |
| while IFS= read -r nested; do | |
| sign_path "$nested" | |
| done < <(find build/MacRunner.app \( \ | |
| -path '*/Contents/Frameworks/*.framework' -o \ | |
| -path '*/Contents/Frameworks/*.dylib' -o \ | |
| -path '*/Contents/PlugIns/*.appex' -o \ | |
| -path '*/Contents/XPCServices/*.xpc' -o \ | |
| -path '*/Contents/Helpers/*' -o \ | |
| -path '*/Contents/Library/LoginItems/*.app' \ | |
| \) -depth) | |
| echo "Signing the binary..." | |
| codesign --force --options runtime \ | |
| --sign "$SIGNING_IDENTITY" \ | |
| --timestamp \ | |
| build/MacRunner.app/Contents/MacOS/MacRunner | |
| echo "Signing the app bundle..." | |
| codesign --force --options runtime --deep \ | |
| --sign "$SIGNING_IDENTITY" \ | |
| --timestamp \ | |
| build/MacRunner.app | |
| echo "Verifying signature..." | |
| codesign --verify --deep --strict --verbose=2 build/MacRunner.app | |
| # ── Create ZIP ──────────────────────────────────────────────────── | |
| - name: Create ZIP | |
| id: zip | |
| env: | |
| VERSION: ${{ needs.check-release.outputs.version }} | |
| run: | | |
| set -euo pipefail | |
| cd build | |
| ditto -c -k --keepParent MacRunner.app "MacRunner-${VERSION}.zip" | |
| echo "sha256=$(shasum -a 256 "MacRunner-${VERSION}.zip" | awk '{print $1}')" >> "$GITHUB_OUTPUT" | |
| # ── Notarize ZIP ────────────────────────────────────────────────── | |
| - name: Notarize app (via ZIP) | |
| env: | |
| VERSION: ${{ needs.check-release.outputs.version }} | |
| APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} | |
| APPLE_API_ISSUER_ID: ${{ secrets.APPLE_API_ISSUER_ID }} | |
| NOTARY_API_KEY_PATH: ${{ env.NOTARY_API_KEY_PATH }} | |
| APPLE_ID: ${{ secrets.APPLE_ID }} | |
| APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} | |
| APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} | |
| run: | | |
| set -euo pipefail | |
| echo "Submitting ZIP for notarization..." | |
| ./scripts/notarize.sh "build/MacRunner-${VERSION}.zip" | |
| echo "Stapling notarization ticket to app..." | |
| xcrun stapler staple build/MacRunner.app | |
| xcrun stapler validate build/MacRunner.app | |
| echo "Re-creating ZIP with stapled app..." | |
| rm -f "build/MacRunner-${VERSION}.zip" | |
| cd build | |
| ditto -c -k --keepParent MacRunner.app "MacRunner-${VERSION}.zip" | |
| - name: Update ZIP SHA256 | |
| id: zip_final | |
| env: | |
| VERSION: ${{ needs.check-release.outputs.version }} | |
| run: | | |
| echo "sha256=$(shasum -a 256 "build/MacRunner-${VERSION}.zip" | awk '{print $1}')" >> "$GITHUB_OUTPUT" | |
| # ── Create DMG ──────────────────────────────────────────────────── | |
| - name: Create DMG | |
| env: | |
| VERSION: ${{ needs.check-release.outputs.version }} | |
| run: | | |
| set -euo pipefail | |
| brew install create-dmg || true | |
| create-dmg \ | |
| --volname "Mac Runner" \ | |
| --window-pos 200 120 \ | |
| --window-size 600 400 \ | |
| --icon-size 100 \ | |
| --icon "MacRunner.app" 175 120 \ | |
| --hide-extension "MacRunner.app" \ | |
| --app-drop-link 425 120 \ | |
| "build/MacRunner-${VERSION}.dmg" \ | |
| "build/MacRunner.app" || \ | |
| hdiutil create \ | |
| -volname "Mac Runner" \ | |
| -srcfolder build/MacRunner.app \ | |
| -ov -format UDZO \ | |
| "build/MacRunner-${VERSION}.dmg" | |
| # ── Sign & Notarize DMG ─────────────────────────────────────────── | |
| - name: Sign and notarize DMG | |
| env: | |
| VERSION: ${{ needs.check-release.outputs.version }} | |
| SIGNING_IDENTITY: ${{ env.SIGNING_IDENTITY }} | |
| APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} | |
| APPLE_API_ISSUER_ID: ${{ secrets.APPLE_API_ISSUER_ID }} | |
| NOTARY_API_KEY_PATH: ${{ env.NOTARY_API_KEY_PATH }} | |
| APPLE_ID: ${{ secrets.APPLE_ID }} | |
| APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} | |
| APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} | |
| run: | | |
| set -euo pipefail | |
| echo "Signing DMG..." | |
| codesign --force --sign "$SIGNING_IDENTITY" --timestamp "build/MacRunner-${VERSION}.dmg" | |
| echo "Submitting DMG for notarization..." | |
| ./scripts/notarize.sh "build/MacRunner-${VERSION}.dmg" | |
| echo "Stapling notarization ticket to DMG..." | |
| xcrun stapler staple "build/MacRunner-${VERSION}.dmg" | |
| xcrun stapler validate "build/MacRunner-${VERSION}.dmg" | |
| # ── Cleanup Keychain ────────────────────────────────────────────── | |
| - name: Clean up signing keychain | |
| if: always() | |
| run: | | |
| if [ -n "${NOTARY_API_KEY_PATH:-}" ]; then | |
| rm -f "$NOTARY_API_KEY_PATH" | |
| fi | |
| if [ -n "${KEYCHAIN_PATH:-}" ] && security list-keychains | grep -q "signing.keychain-db"; then | |
| security delete-keychain "$KEYCHAIN_PATH" | |
| echo "Temporary keychain removed." | |
| fi | |
| # ── Release ─────────────────────────────────────────────────────── | |
| - name: Update Homebrew cask | |
| env: | |
| VERSION: ${{ needs.check-release.outputs.version }} | |
| SHA256: ${{ steps.zip_final.outputs.sha256 }} | |
| run: | | |
| sed -i '' 's/version ".*"/version "'"${VERSION}"'"/' Casks/mac-runner.rb | |
| sed -i '' 's/sha256 .*/sha256 "'"${SHA256}"'"/' Casks/mac-runner.rb | |
| echo "Updated cask to version ${VERSION} with SHA256 ${SHA256}" | |
| cat Casks/mac-runner.rb | |
| - name: Run semantic-release | |
| uses: cycjimmy/semantic-release-action@v4 | |
| with: | |
| extra_plugins: | | |
| @semantic-release/changelog | |
| @semantic-release/git | |
| conventional-changelog-conventionalcommits | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Upload artifacts to release | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| VERSION: ${{ needs.check-release.outputs.version }} | |
| run: | | |
| gh release upload "v${VERSION}" \ | |
| "build/MacRunner-${VERSION}.zip" \ | |
| "build/MacRunner-${VERSION}.dmg" \ | |
| --clobber | |
| # Step 4: Update Homebrew tap (runs on ubuntu, after release) | |
| update-homebrew-tap: | |
| name: Update Homebrew tap | |
| needs: [check-release, build-and-release] | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout mac-runner (for cask source) | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: main | |
| fetch-depth: 1 | |
| - name: Checkout homebrew-tap | |
| uses: actions/checkout@v4 | |
| with: | |
| repository: omniaura/homebrew-tap | |
| token: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }} | |
| path: homebrew-tap | |
| - name: Update cask in homebrew-tap | |
| run: | | |
| cp Casks/mac-runner.rb homebrew-tap/Casks/mac-runner.rb | |
| - name: Commit and push | |
| env: | |
| VERSION: ${{ needs.check-release.outputs.version }} | |
| run: | | |
| cd homebrew-tap | |
| git config user.name "github-actions[bot]" | |
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| if git diff --quiet; then | |
| echo "No changes to cask" | |
| exit 0 | |
| fi | |
| git add Casks/mac-runner.rb | |
| git commit -m "chore: update mac-runner cask to ${VERSION}" | |
| git push origin main |