From edadf0efaa9e39141459ddf927d0bf753ef22b1c Mon Sep 17 00:00:00 2001 From: James Maes Date: Wed, 13 May 2026 13:50:12 -0500 Subject: [PATCH 1/5] ci(actions): move swift-packages matrix to self-hosted runner (Grogu) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. Grogu is registered with labels [self-hosted, macOS, ARM64, jarvis] and _work/ symlinked to HD-2 (1.3 TB) for headroom. Scope: - swift-packages matrix only — app-build stays on macos-15 for now. - `if:` clause blocks fork PRs from triggering self-hosted jobs (push, workflow_dispatch, and same-repo PRs only). - Xcode select switches from `sudo xcode-select -s` to a per-process DEVELOPER_DIR env so the runner doesn't need passwordless sudo. The fallback chain now also accepts /Applications/Xcode.app/Contents/Developer so Grogu's local Xcode 26.5 is picked up. Closes #180 Co-Authored-By: Claude Opus 4.7 --- .github/workflows/ci.yml | 38 ++++++++++++++++++++++++++++++++++---- CLAUDE.md | 11 +++++++++++ 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e2370f0..55d247f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/CLAUDE.md b/CLAUDE.md index 1afa409..2054b28 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -238,6 +238,17 @@ 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. +- **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. -- 2.54.0 From 3ba1d749faf7759562c9b11bd3658cec96d7c229 Mon Sep 17 00:00:00 2001 From: James Maes Date: Wed, 13 May 2026 14:41:34 -0500 Subject: [PATCH 2/5] ci(actions): fix self-hosted runner env quirks for keychain + cache Three SPM failures on PR #181's first self-hosted CI run, all environment-specific: - SPM Keychain: errSecInteractionNotAllowed (-25308). The LaunchAgent has no UI session and the login keychain is locked. - SPM Voice + SPM Harness: github-hosted actions/cache@v4 entries with absolute paths (/Users/runner/work/...) baked into .build/artifacts/onnxruntime.xcframework. Cache key only contained runner.os (= macOS for both environments), so self-hosted hit the poisoned github-hosted cache. Fixes: - Restrict actions/cache@v4 to runner.environment == 'github-hosted'. Self-hosted's _work/ on HD-2 persists between jobs naturally, so the cache action is unnecessary there and prevents path-baked cache poisoning between environments. - Add a "Unlock runner keychain" step gated to runner.environment == 'self-hosted'. Unlocks a pre-created jarvis-runner.keychain-db with password from the repo secret JARVIS_RUNNER_KEYCHAIN_PASSWORD and prepends it to the user search list so SecItemAdd writes there. Document both in CLAUDE.md "Self-hosted CI runner". Refs #180 --- .github/workflows/ci.yml | 32 +++++++++++++++++++++++++++++++- CLAUDE.md | 2 ++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 55d247f..f8662c8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -193,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: | @@ -202,6 +211,27 @@ 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" + security set-keychain-settings -t 0 "$KEYCHAIN_PATH" + security list-keychains -d user -s "$KEYCHAIN_PATH" "$HOME/Library/Keychains/login.keychain-db" - name: swift test run: swift test --package-path packages/${{ matrix.package }} diff --git a/CLAUDE.md b/CLAUDE.md index 2054b28..60b4b13 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -247,6 +247,8 @@ Eval harness (Anthropic + local-model eval matrix; corpus in `.planning/evals/`) - **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` and prepends it to the user search list before `swift test`. **Side effect on interactive use:** `jarvis-runner.keychain-db` sits first in your user search list after CI runs, so `SecItemAdd` calls *without* an explicit keychain target may write there instead of `login.keychain-db`. Apps that target login keychain explicitly (most do) are unaffected. To undo: `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 -- 2.54.0 From ca76e7761c06f621559af13408ec632f143be05e Mon Sep 17 00:00:00 2001 From: James Maes Date: Wed, 13 May 2026 14:53:37 -0500 Subject: [PATCH 3/5] ci: retrigger after JARVIS_RUNNER_KEYCHAIN_PASSWORD secret fix Previous run failed all 13 SPM jobs with 'passphrase you entered is not correct' because the secret value was set to literal '-' (gh secret set parsed '--body -' as the body value, not as 'read from stdin'). Secret now contains the correct password. This empty commit triggers a fresh CI run that will resolve the corrected secret. Refs #180 -- 2.54.0 From 9f4bc5fe2d156dca14d6e1b92709cc465bec0a7c Mon Sep 17 00:00:00 2001 From: James Maes Date: Wed, 13 May 2026 14:56:45 -0500 Subject: [PATCH 4/5] ci(actions): drop set-keychain-settings from unlock step The Security framework requires user interaction for set-keychain-settings even when the keychain is unlocked, and the LaunchAgent on Grogu has no UI. The settings (no-timeout, no-lock-on-sleep) are persistent and were applied once at keychain-creation time; only the unlock step needs to run every job. Refs #180 --- .github/workflows/ci.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f8662c8..b717512 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -230,7 +230,11 @@ jobs: exit 1 fi security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" - security set-keychain-settings -t 0 "$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" - name: swift test run: swift test --package-path packages/${{ matrix.package }} -- 2.54.0 From 42ffb72add937d102e1c34ff82c0c0eea8c18afb Mon Sep 17 00:00:00 2001 From: James Maes Date: Wed, 13 May 2026 15:11:07 -0500 Subject: [PATCH 5/5] ci(actions): also set default-keychain so SecItemAdd targets jarvis-runner MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SecItemAdd on the legacy file-based macOS keychain writes to the user's DEFAULT keychain, not the first writable keychain in the search list. Changing list-keychains alone left SecItemAdd targeting the locked login.keychain-db, still returning errSecInteractionNotAllowed (-25308). Also set the user's default keychain to jarvis-runner.keychain-db in the unlock step. This is persistent for the runner user (james.maes), so SecItemAdd from interactive use will also go there — documented as a side effect in CLAUDE.md with the undo command. Refs #180 --- .github/workflows/ci.yml | 7 +++++++ CLAUDE.md | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b717512..13815a5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -236,6 +236,13 @@ jobs: # 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 }} diff --git a/CLAUDE.md b/CLAUDE.md index 60b4b13..48a19cb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -248,7 +248,7 @@ Eval harness (Anthropic + local-model eval matrix; corpus in `.planning/evals/`) - **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` and prepends it to the user search list before `swift test`. **Side effect on interactive use:** `jarvis-runner.keychain-db` sits first in your user search list after CI runs, so `SecItemAdd` calls *without* an explicit keychain target may write there instead of `login.keychain-db`. Apps that target login keychain explicitly (most do) are unaffected. To undo: `security list-keychains -d user -s ~/Library/Keychains/login.keychain-db`. +- **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 -- 2.54.0