Skip to content

feat: add automatic disk cleanup (#79) #66

feat: add automatic disk cleanup (#79)

feat: add automatic disk cleanup (#79) #66

Workflow file for this run

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