Self-hosted GitHub Actions runner setup¶
This guide covers setting up a Mac (or any machine) as a persistent
Pulp CI runner. The payoff is getting sanitizer + build jobs off
GitHub-hosted runners onto your own hardware — usually 4-8× faster,
and free. Related: the runner-selectable mechanism in
local-ci.md
and issue #412 step 6.
Who this is for¶
- Pulp contributors running a dedicated Mac mini / Mac Studio / Linux box as a persistent CI target
- Anyone opting into local sanitizer runs via the
PULP_SANITIZER_*_RUNS_ON_JSONrepo variables - Not for ephemeral or shared-dev-laptop runners — the install is designed to be left running
Prerequisites¶
- macOS 14+ (sanitizers), macOS 15+ preferred (Apple Clang 16+ has
C++20 P0960 paren-aggregate-init, needed by
core/view/include/pulp/view/lasso.hpp) - Xcode installed, with license accepted:
sudo xcodebuild -license accept - Homebrew in PATH for the runner user
- A non-root user account dedicated to the runner (don't reuse your
personal login account if you can avoid it; gate brittle env with a
fresh
$HOME) - git-lfs installed:
brew install git-lfs && git lfs install
Install the runner¶
- Create a dedicated user (optional but recommended):
sysadminctl -addUser pulpci -fullName "Pulp CI" -password - - Log in as that user, or use
sudo -iu pulpcito get a clean shell. - Follow GitHub's
Adding self-hosted runners
flow. Download the runner to
~/actions-runner/, run./config.sh, register against thedanielraffel/pulprepo. - Install as a launch agent so it survives reboot:
./svc.sh install; ./svc.sh start. - Add labels so Pulp workflows can target this runner specifically.
At minimum:
self-hosted,macos,arm64,sanitizer(or whatever repo var you wire up).
First-run gotchas (real traps we've hit)¶
1. git-lfs hook conflict on second checkout¶
Symptom: actions/checkout@v5 with lfs: true fails immediately:
Hook already exists: ~/actions-runner/_work/pulp/pulp/.git/hooks/pre-push
To resolve this, either:
1: run `git lfs update --manual` for instructions on how to merge hooks.
2: run `git lfs update --force` to overwrite your hook.
##[error]The process '/opt/homebrew/bin/git' failed with exit code 2
Root cause: Self-hosted runners don't get a fresh $HOME per run.
LFS hooks installed by your shell's git lfs install stick around in
_work/pulp/pulp/.git/hooks/* and conflict with the checkout action's
own hook setup on the second and subsequent runs.
Fix (one-time): On the runner machine, in the repo workspace:
This overwrites the conflicting hook so actions/checkout@v5 can
merge its own LFS hooks cleanly on the next run.
2. Xcode license not accepted¶
Symptom: CMake configure fails immediately with xcodebuild: error: You have not agreed to the Xcode license agreements.
Fix: On the runner machine, one time:
Required once per Xcode upgrade.
3. Homebrew not in PATH¶
Symptom: Steps installing ccache / shipyard / pluginval fail
with brew: command not found.
Fix: Verify the runner service starts with Homebrew's shim on PATH:
# On Apple Silicon:
echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> ~/.zshrc
# Or explicitly for the runner user's launchd environment:
launchctl setenv PATH "/opt/homebrew/bin:$PATH"
Restart the runner service after editing: ~/actions-runner/svc.sh stop && svc.sh start.
4. Apple Clang version skew¶
Symptom: Build fails with e.g. no matching constructor for initialization of 'canvas::Color' in lasso.hpp.
Root cause: Apple Clang 15 (macOS 14) lacks C++20 P0960 (paren
aggregate-init). lasso.hpp used Color(100, 150, 255, 40) which
requires Clang 16+ (macos-15).
Fix: Use macOS 15+. If you must stay on macOS 14, the code has
already been refactored to use Color::rgba8(...) (see PR #401), so
this should just work now — but watch for future regressions.
5. Git config "helpful" Xcode defaults¶
Symptom: actions/checkout hangs or misreads files; LFS pointers
don't resolve.
Root cause: Xcode's Source Control pane will occasionally set
core.fsmonitor = rider or core.untrackedCache = true on a global
git config. Both confuse actions/checkout.
Fix: On the runner user's account:
6. Running as root (do not)¶
Symptom: brew complains loudly, or CMake writes to paths you can't reach from your dev user later.
Fix: The actions-runner install should be done as a regular user,
not root. Re-run ./config.sh as yourself if you accidentally ran
as root.
Teardown / rotation¶
To decommission a runner:
Get the remove token from GitHub → Settings → Actions → Runners → the runner → Remove.
Wiring into Pulp's runner-selectable system¶
Once the runner is up and labeled self-hosted,macos,arm64,sanitizer:
# Route TSan to this runner:
gh variable set PULP_SANITIZER_TSAN_RUNS_ON_JSON \
--body '["self-hosted", "macos", "arm64", "sanitizer"]'
Next sanitizer run picks it up — no code change. See local-ci.md § Switching a job's runner for the full variable list and precedence rules.
Troubleshooting¶
If jobs dispatch to the runner but fail at a step that worked on GitHub-hosted before:
- Compare
$HOMEbetween runners (self-hosted often has Xcode/Homebrew state GitHub-hosted images lack). - Check
actions-runner/_diag/*.logfor the runner's perspective. - For LFS / checkout issues, delete
_work/<repo>/<repo>/.gitand let the next checkout reclone from scratch — destructive but fast recovery. - Re-run the offending PR's job manually via
gh run rerun <id>.
Refs: #429, #424, #412 step 6.