ci: move swift-packages matrix to self-hosted runner (Grogu) #181

Merged
KofTwentyTwo merged 5 commits from ci/self-hosted-runner-180 into develop 2026-05-13 20:29:10 +00:00
2 changed files with 89 additions and 5 deletions

No files matched your search

+76 -5
View File
@@ -117,7 +117,21 @@ jobs:
swift-packages:
name: SPM ${{ matrix.package }}
runs-on: macos-15
# Self-hosted on Grogu (labels: self-hosted, macOS, ARM64, jarvis) per #180.
# The github-hosted macos-15 pool was the rate-limiting step for v0.1:
# 13 matrix jobs + 1 app-build job all needed macOS slots, GitHub
# scheduled them 1-3 at a time, and total wall-clock was ~1-2h per run
# versus actual work of <10m. Self-hosted eliminates the queue.
runs-on: [self-hosted, macOS, jarvis]
# Security gate: self-hosted runners + public repo would let a fork PR
# execute arbitrary code on this machine. Push events only ever come from
# maintainers (branch protection blocks unauthorized writes). PRs are
# only allowed onto the self-hosted runner when they originate from a
# branch on this same repo (i.e., not a fork). Also gates manual dispatch.
if: |
github.event_name == 'push' ||
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository)
needs: [webview]
strategy:
fail-fast: false
@@ -145,10 +159,26 @@ jobs:
# cannot parse `mlx-audio-swift` v0.1.2's `swift-tools-version:6.2`,
# lacks the `withThrowingTaskGroup` overload used by MCP `swift-sdk`
# 0.12.0, and rejects `@MainActor` XCTestCase `super.setUp()` patterns
# that Swift 6.1+ accepts. Pin to the newest Xcode 26.x on macos-15.
# See actions/runner-images macos-15 readme for installed Xcode list.
# that Swift 6.1+ accepts. The first two paths cover github-hosted
# macos-15 (which ships Xcode 26.3); the bare `Xcode.app` fallback
# covers self-hosted runners with whatever local Xcode 26.x is the
# default (Grogu currently has 26.5). Using DEVELOPER_DIR (per-process)
# rather than `sudo xcode-select -s` (system-wide, needs passwordless
# sudo on self-hosted) — both paths respect DEVELOPER_DIR identically.
- name: Select Xcode
run: sudo xcode-select -s /Applications/Xcode_26.3.app/Contents/Developer || sudo xcode-select -s /Applications/Xcode_26.app/Contents/Developer
run: |
for path in \
/Applications/Xcode_26.3.app/Contents/Developer \
/Applications/Xcode_26.app/Contents/Developer \
/Applications/Xcode.app/Contents/Developer; do
if [ -d "$path" ]; then
echo "DEVELOPER_DIR=$path" >> "$GITHUB_ENV"
echo "Using Xcode at: $path"
exit 0
fi
done
echo "No Xcode 26.x found in any expected path" >&2
exit 1
- name: Show Swift toolchain
run: |
swift --version
@@ -163,7 +193,16 @@ jobs:
with:
name: webview-resources
path: App/Resources/webview/
- name: Cache SPM build artifacts
# actions/cache@v4 restores the package's `.build/` and SwiftPM's
# global cache. On github-hosted this is a real perf win; on self-hosted
# it's both unnecessary (the _work/ dir persists between jobs naturally)
# and actively harmful (caches saved on github-hosted have absolute
# paths like /Users/runner/work/Jarvis/.../onnxruntime.xcframework
# baked into .build/artifacts, which don't resolve on Grogu and break
# Voice + Harness with "XCFramework Info.plist not found"). Restrict
# the cache action to github-hosted only.
- name: Cache SPM build artifacts (github-hosted only)
if: runner.environment == 'github-hosted'
uses: actions/cache@v4
with:
path: |
@@ -172,6 +211,38 @@ jobs:
key: spm-${{ matrix.package }}-${{ runner.os }}-${{ hashFiles(format('packages/{0}/Package.resolved', matrix.package), format('packages/{0}/Package.swift', matrix.package)) }}
restore-keys: |
spm-${{ matrix.package }}-${{ runner.os }}-
# On self-hosted (Grogu) the runner is a LaunchAgent with no UI session,
# so SecItemAdd against the locked login keychain returns
# errSecInteractionNotAllowed (-25308). Use a pre-created dedicated
# keychain `jarvis-runner.keychain-db` (one-time setup on Grogu) and
# prepend it to the user search list for this run so test SecItemAdd
# calls without an explicit keychain target write there. github-hosted
# bakes its own unlocked keychain into the runner image and skips this.
- name: Unlock runner keychain (self-hosted only)
if: runner.environment == 'self-hosted'
env:
KEYCHAIN_PASSWORD: ${{ secrets.JARVIS_RUNNER_KEYCHAIN_PASSWORD }}
run: |
KEYCHAIN_PATH="$HOME/Library/Keychains/jarvis-runner.keychain-db"
if [ ! -f "$KEYCHAIN_PATH" ]; then
echo "FATAL: runner keychain missing at $KEYCHAIN_PATH" >&2
echo " Create with: security create-keychain -p \"\$KEYCHAIN_PASSWORD\" \"$KEYCHAIN_PATH\"" >&2
exit 1
fi
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
# NOTE: do NOT re-run `set-keychain-settings` here — the Security
# framework requires user interaction for that operation even with
# the keychain unlocked, and the LaunchAgent has no UI. The
# settings (no-timeout, no-lock-on-sleep) were applied once at
# keychain-creation time and persist; we just need to unlock.
security list-keychains -d user -s "$KEYCHAIN_PATH" "$HOME/Library/Keychains/login.keychain-db"
# On the legacy file-based macOS keychain, SecItemAdd writes to the
# user's DEFAULT keychain — the search list only determines read
# order. Changing list-keychains alone leaves SecItemAdd targeting
# login.keychain-db (locked, returns -25308). Also redirect the
# default. SIDE EFFECT documented in CLAUDE.md: this is persistent
# for james.maes's user prefs.
security default-keychain -d user -s "$KEYCHAIN_PATH"
- name: swift test
run: swift test --package-path packages/${{ matrix.package }}
+13
View File
@@ -238,6 +238,19 @@ JARVIS_REAL_MODELS=1 swift test --package-path packages/Voice --filter OrpheusTT
Eval harness (Anthropic + local-model eval matrix; corpus in `.planning/evals/`) — pinned to `qwen2.5-coder:32b` for the local-model lane.
### Self-hosted CI runner (Grogu)
`swift-packages` matrix in `.github/workflows/ci.yml` runs on a self-hosted macOS runner (`Grogu-jarvis`, labels `[self-hosted, macOS, ARM64, jarvis]`) rather than the github-hosted `macos-15` pool — the public pool was the v0.1 rate-limiter (1–2h queues for 13-job matrix). See #180.
- **Where it lives:** `~/actions-runner-jarvis/` on Grogu. `_work/` symlinked to `/Volumes/HD-2/jarvis-runner-work/` (1.3 TB) to keep boot-disk pressure off.
- **Service:** launchd LaunchAgent `actions.runner.KofTwentyTwo-Jarvis.Grogu-jarvis` (`~/Library/LaunchAgents/`). Manage with `cd ~/actions-runner-jarvis && ./svc.sh {status,start,stop}`. Only runs while user is logged in.
- **Security gate:** the job's `if:` clause blocks fork PRs from triggering it — push, workflow_dispatch, and same-repo PRs only. Belt-and-suspenders: enable "Require approval for all outside collaborators" under Settings → Actions → General.
- **Still on github-hosted `macos-15`:** `app-build` (the xcodebuild app target) and the `real-hardware` opt-in lane. Move opportunistically once the swift-packages path is proven.
- **Xcode selection:** uses `DEVELOPER_DIR` env (per-process), tries `Xcode_26.3.app` → `Xcode_26.app` → `Xcode.app` in order. Grogu currently has Xcode 26.5 at the bare path.
- **Cache:** the `actions/cache@v4` step is gated to `runner.environment == 'github-hosted'`. Self-hosted relies on the persistent `_work/` dir on HD-2 (and SwiftPM's natural `.build/` reuse) instead. Github-hosted caches bake absolute `/Users/runner/...` paths into `.build/artifacts/onnxruntime.xcframework` and other XCFrameworks, which Grogu cannot resolve — restricting the cache action keeps these caches strictly per-environment.
- **Runner keychain** (`~/Library/Keychains/jarvis-runner.keychain-db`): created once on Grogu with password `grogu-jarvis-ci` (also stored as repo secret `JARVIS_RUNNER_KEYCHAIN_PASSWORD`). The LaunchAgent has no UI session, so `SecItemAdd` against a locked login keychain returns `errSecInteractionNotAllowed (-25308)`. CI unlocks `jarvis-runner.keychain-db`, prepends it to the user search list, AND sets it as the user's default keychain before `swift test`. The default-keychain step is necessary because on the legacy file-based macOS keychain, `SecItemAdd` writes to the user's *default* keychain (not the first writable in the search list). **Side effects on interactive use:** (1) `jarvis-runner.keychain-db` sits first in your user search list, and (2) it is also your default keychain — so `SecItemAdd` calls *without* an explicit keychain target write there instead of `login.keychain-db`. Apps that target login keychain explicitly (most do) are unaffected. To undo: `security default-keychain -d user -s ~/Library/Keychains/login.keychain-db && security list-keychains -d user -s ~/Library/Keychains/login.keychain-db`.
- **When Grogu reboots or sleeps**, the LaunchAgent restarts on login but jobs queued during downtime will sit waiting. For long offline windows, push to a branch other than develop or temporarily revert `runs-on:` to `macos-15`.
# CLAUDE.md
Behavioral guidelines to reduce common LLM coding mistakes. Merge with project-specific instructions as needed.