- Shell 88.1%
- PowerShell 11.9%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
All checks were successful
macOS runner self-test / toolchain (push) Successful in 19s
|
||
| .forgejo/workflows | ||
| hypervisor | ||
| .gitignore | ||
| bootstrap.sh | ||
| configure_runner.sh | ||
| docker_osx_compose.yml | ||
| forgejo-runner-darwin-amd64 | ||
| forgejo-runner-darwin-arm64 | ||
| install.sh | ||
| install_into_vm.sh | ||
| provision_toolchain.sh | ||
| README.md | ||
| runner.env.example | ||
| uninstall.sh | ||
| verify_toolchain.sh | ||
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.
⚖️ Legal / EULA notice — read first
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— seehypervisor/notes. ⚠️ Do not try to do this via the WSL2 kernel; that route breaks Docker Desktop. docker+docker compose, andsshpass.- ~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
EnvironmentVariablesinto the runner plist: a fullPATH(Homebrew +~/binfor xcodes + rust proxies + pyenv/rbenv shims),LANG/LC_ALL=en_US.UTF-8, andRUNNER_TOOL_CACHE/AGENT_TOOLSDIRECTORY(sosetup-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
caffeinateLaunchAgent 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_VERSION—16.4is the newest Xcode that runs on Intel guests; bump it on an Apple-Silicon host.TOOLCHAIN_NOPASSWD—1(temp passwordless sudo during provisioning, removed after),keep(leave it on — convenient for a dedicated runner, weaker security), or0(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.
xcodebuildsays "requires Xcode" — CLT hijacked the active dir; re-runconfigure_runner.sh(orsudo xcode-select -s /Applications/Xcode-*.app/Contents/Developer).- Repeated macOS login screen — that's the screen lock;
configure_runner.shdisables it. - zsh quirks in the guest — it mangles
===, leading=,?and[ ]in unquotedecho; quote your markers in ad-hoc commands.