macOS Forgejo CI runner (GitHub macos-15 toolchain parity) via docker-osx & installer + toolchain provisioning
  • Shell 88.1%
  • PowerShell 11.9%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
FiLL bcf1aa282b
All checks were successful
macOS runner self-test / toolchain (push) Successful in 19s
feat: macOS Forgejo runner (GitHub macos-15 parity)
2026-06-03 04:18:12 +02:00
.forgejo/workflows feat: macOS Forgejo runner (GitHub macos-15 parity) 2026-06-03 04:18:12 +02:00
hypervisor feat: macOS Forgejo runner (GitHub macos-15 parity) 2026-06-03 04:18:12 +02:00
.gitignore feat: macOS Forgejo runner (GitHub macos-15 parity) 2026-06-03 04:18:12 +02:00
bootstrap.sh feat: macOS Forgejo runner (GitHub macos-15 parity) 2026-06-03 04:18:12 +02:00
configure_runner.sh feat: macOS Forgejo runner (GitHub macos-15 parity) 2026-06-03 04:18:12 +02:00
docker_osx_compose.yml feat: macOS Forgejo runner (GitHub macos-15 parity) 2026-06-03 04:18:12 +02:00
forgejo-runner-darwin-amd64 feat: macOS Forgejo runner (GitHub macos-15 parity) 2026-06-03 04:18:12 +02:00
forgejo-runner-darwin-arm64 feat: macOS Forgejo runner (GitHub macos-15 parity) 2026-06-03 04:18:12 +02:00
install.sh feat: macOS Forgejo runner (GitHub macos-15 parity) 2026-06-03 04:18:12 +02:00
install_into_vm.sh feat: macOS Forgejo runner (GitHub macos-15 parity) 2026-06-03 04:18:12 +02:00
provision_toolchain.sh feat: macOS Forgejo runner (GitHub macos-15 parity) 2026-06-03 04:18:12 +02:00
README.md feat: macOS Forgejo runner (GitHub macos-15 parity) 2026-06-03 04:18:12 +02:00
runner.env.example feat: macOS Forgejo runner (GitHub macos-15 parity) 2026-06-03 04:18:12 +02:00
uninstall.sh feat: macOS Forgejo runner (GitHub macos-15 parity) 2026-06-03 04:18:12 +02:00
verify_toolchain.sh feat: macOS Forgejo runner (GitHub macos-15 parity) 2026-06-03 04:18:12 +02:00

macOS Forgejo Runner (GitHub macos-15 parity)

Spin up a macOS CI runner for Forgejo that can do (almost) everything a GitHub-hosted macos-15 runner can: Xcode + iOS SDK, the full language/runtime zoo (Node, Python, Ruby, Go, Rust, Java, .NET, PHP, Kotlin, Swift), package managers (Homebrew, CocoaPods, Carthage, yarn/pnpm, Composer, vcpkg), Apple tooling (fastlane, swiftlint, swiftformat, xcbeautify, xcpretty), build tools (cmake, ninja, bazelisk, gradle, maven, ant, llvm, gcc), CLIs (gh, git-lfs, aws, sam, az, jq, yq, packer, …) and browsers + WebDrivers (Chrome, Firefox, Edge).

It runs macOS inside sickcodes/docker-osx (QEMU/KVM) and registers a forgejo-runner LaunchDaemon inside the guest, labelled macos-15/14/13/latest.


Apple's macOS Software License only permits running macOS on Apple-branded hardware. Running it in QEMU on non-Apple hardware (what this does) violates that EULA. This project is provided for lab/educational use; you accept that risk. Using your Apple ID for automated sign-in may also get the account flagged. Consider a dedicated Apple ID. For production/legal iOS CI, use real Apple hardware (Mac mini/Studio, MacStadium, etc.).


Requirements (host)

  • A Linux host with /dev/kvm (bare metal, or a VM with nested virt enabled). On Windows that means an Ubuntu Hyper-V VM with -ExposeVirtualizationExtensions $true — see hypervisor/ notes. ⚠️ Do not try to do this via the WSL2 kernel; that route breaks Docker Desktop.
  • docker + docker compose, and sshpass.
  • ~8 GB RAM and ~90 GB free disk (macOS + Xcode + toolchain).
  • A Forgejo instance and a runner registration token.

Quick start

cp runner.env.example runner.env      # then edit: instance URL, token, specs
./install.sh                          # turnkey: VM up → register → toolchain → Xcode → configure → verify

install.sh is a wizard and an orchestrator — it prompts for anything missing in runner.env. The only interactive moment is the Xcode step: when Apple ID: appears, you type your email, password (hidden) and 2FA code (type sms if you have no trusted device). Credentials are never stored.

Then in any Forgejo workflow:

jobs:
  build:
    runs-on: macos-15
    steps:
      - uses: actions/checkout@v4
      - run: xcodebuild -version && swift --version

Power-user / manual install

Prefer running the idempotent steps yourself? They live as standalone scripts:

Step Script Where it runs
1. Boot macOS VM docker compose -f docker_osx_compose.yml up -d host
2. Register runner install_into_vm.sh (uses bootstrap.sh + the forgejo-runner-darwin-* binaries) host → guest
3. Install toolchain provision_toolchain.sh guest
4. Install Xcode xcodes install <ver> (interactive) guest
5. Configure env configure_runner.sh guest
6. Verify verify_toolchain.sh guest

All are idempotent and re-runnable.

What configure_runner.sh does (the "full runner" glue)

A bare LaunchDaemon runs with a minimal PATH, so jobs wouldn't see Homebrew or the toolchain. This script:

  • injects EnvironmentVariables into the runner plist: a full PATH (Homebrew + ~/bin for xcodes + rust proxies + pyenv/rbenv shims), LANG/LC_ALL=en_US.UTF-8, and RUNNER_TOOL_CACHE/AGENT_TOOLSDIRECTORY (so setup-node/setup-python/… work), then reloads the daemon;
  • pins Xcode as the active developer dir (Homebrew's CLT install hijacks it);
  • disables the screen lock / screensaver / sleep and installs a caffeinate LaunchAgent so the headless guest never locks.

Uninstall

./uninstall.sh            # unregister runner, undo guest config, remove container (keeps the macOS image)
./uninstall.sh --purge    # ALSO delete the persistent macOS image (vm_data) — irreversible

Configuration

All knobs live in runner.env (see runner.env.example). Notable:

  • XCODE_VERSION16.4 is the newest Xcode that runs on Intel guests; bump it on an Apple-Silicon host.
  • TOOLCHAIN_NOPASSWD1 (temp passwordless sudo during provisioning, removed after), keep (leave it on — convenient for a dedicated runner, weaker security), or 0 (babysit sudo prompts via VNC).

Reproducing fast: golden image

The slow, interactive parts (macOS install + Apple ID/Xcode) are best done once. Snapshot the resulting vm_data/ disk image and reuse it for new runners — then bring-up is just copy image → compose up → register. The image is large and personal (contains your Apple ID/data) — keep it on a NAS / artifact store, never in git, and don't share an image that holds your credentials.

Troubleshooting

  • Slow installs — expected; the guest CPU is emulated. unxip + Gatekeeper
    • single-thread Homebrew pours dominate. KVM is on; it's not pure emulation.
  • xcodebuild says "requires Xcode" — CLT hijacked the active dir; re-run configure_runner.sh (or sudo xcode-select -s /Applications/Xcode-*.app/Contents/Developer).
  • Repeated macOS login screen — that's the screen lock; configure_runner.sh disables it.
  • zsh quirks in the guest — it mangles ===, leading =, ? and [ ] in unquoted echo; quote your markers in ad-hoc commands.