Self-hosted macOS CI Runner

Idea

Use the M1 MacBook Pro as a self-hosted GitHub Actions runner for iOS-specific CI jobs instead of paying for GitHub-hosted macOS runners.

Motivation

GitHub-hosted macOS runners cost 10x the Linux rate (0.008/min). With a 10x free-tier multiplier, the 2,000 free minutes effectively become 200 macOS minutes/month. At moderate usage (20 pushes/day), hosted macOS CI could cost ~0 and avoids cold-start VM spin-up time.

Plan

Only run macOS-required jobs on the self-hosted runner

  • rangeday bindings swift — UniFFI generation
  • rangeday test ios — Swift/xcodebuild tests
  • rangeday build ios — XCFramework cross-compilation
  • rangeday check ios / rangeday fmt ios — SwiftLint/swift-format

Everything else (core, server, proto, Android) stays on cheap GitHub-hosted Linux runners.

Split CI workflow

jobs:
  rust-and-proto:
    runs-on: ubuntu-latest
    steps:
      - run: rangeday test core
      - run: rangeday test server
      - run: rangeday test proto
      - run: rangeday check all
 
  ios:
    runs-on: self-hosted
    needs: rust-and-proto            # skip if Rust tests fail
    steps:
      - run: rangeday bindings swift
      - run: rangeday test ios
      - run: rangeday build ios
  • Path filtering — only trigger iOS job when libs/core/ or apps/ios/ change.
  • Concurrency control — single machine, so cancel in-progress runs:
    concurrency:
      group: ios-ci
      cancel-in-progress: true

Runner setup

# Download runner agent from repo Settings > Actions > Runners
./config.sh --url https://github.com/<org>/range-day --token <TOKEN>
 
# Install as launch daemon (survives reboots, runs with lid closed)
sudo ./svc.sh install
sudo ./svc.sh start

Label: self-hosted, macOS, ARM64

Gotchas

ConcernMitigation
Mac sleeping / lid closedLaunch daemon keeps runner alive; disable sleep in Energy settings
Xcode updates break buildsPin Xcode version with xcode-select, update intentionally
Dirty state between runsRunner uses fresh _work directory per job; cache Rust builds for speed
Mac offline / rebootingGitHub queues the job and retries when runner reconnects
Security (public repos)Only use self-hosted runners on private repos

Open Questions

  • Do we need the runner available 24/7 or only during working hours?
  • Should we add a second Mac later for redundancy, or is one sufficient for MVP?