Skip to content

ci: run macOS jobs on the Studio self-hosted runner (toggleable, Xcode 27) - #48

Merged
JoeMatt merged 1 commit into
developfrom
ci/studio-runner
Oct 15, 2026
Merged

JoeMatt merged 1 commit into
developfrom
ci/studio-runner

Conversation

@JoeMatt

@JoeMatt JoeMatt commented Oct 15, 2026 •

Copy link
Copy Markdown
Member

ci: let macOS jobs run on the self-hosted Mac Studio (Xcode 27), toggled by a repo variable

Applies the same pattern already merged in Provenance-Emu/Provenance (testflight.yml, build.yml) and JoeMatt/iFly to iCube's macOS jobs.

Toggle

Set repo variable MACOS_RUNNER_LABELS = ["self-hosted","studio"] to run on the Studio. Leave it unset/empty and nothing changes (GitHub-hosted macos-26 / macos-latest).

Pull requests from forks always stay GitHub-hosted (github.event.pull_request.head.repo.full_name == github.repository check), so untrusted code never touches the persistent machine. No pull_request_target is involved.

Jobs switched

File Job Hosted default
build.yml build macos-26
release.yml build macos-26
testflight.yml testflight (ios + tvos matrix) macos-26
tests.yml test macos-26
lint.yml swift, clang-format macos-latest

Left alone: every ubuntu-latest job (testflight.yml plan/tag, build.yml alpha-release) and mcp-tests.yml.

Xcode

  • Hosted: unchanged. maxim-lobanov/setup-xcode + XCODE_VERSION: 26.3 (steps now guarded with if: runner.environment == 'github-hosted').
  • Self-hosted: Xcode 27 only. A first step, before checkout, writes DEVELOPER_DIR=<XCODE_PATH>/Contents/Developer to $GITHUB_ENV, fails with a clear message if it is missing, and prints xcodebuild -version. XCODE_PATH is an optional repo variable, default /Applications/Xcode.app. It has to come before checkout because the job-level DEVELOPER_DIR names the hosted-only Xcode_26.3.app, and a missing Xcode breaks the /usr/bin/git shim.
  • No sudo xcode-select anywhere. The only Xcode_14.2.app reference (in build.yml) is a long-dead commented-out step; nothing depends on it, so it is left as is.

Self-hosted hygiene

  • Brew installs guarded with command -v X >/dev/null || brew install X; HOMEBREW_NO_AUTO_UPDATE and HOMEBREW_NO_INSTALL_CLEANUP set (the Studio's Homebrew is owned by another user and already has the tools).
  • testflight.yml: log moved from /tmp to $RUNNER_TEMP; the existing if: always() keychain cleanup now also removes the p12, the ASC .p8, the export dir and the archives.
  • tests.yml: on self-hosted, ccache lives in ~/Library/Caches/ccache-icube (checkout's git clean -ffdx would wipe a workspace-local cache every run) and actions/cache for ccache is skipped there; the booted simulator is shut down in an if: always() step (it is picked, not created, so it is not deleted).
  • The simulator picker in tests.yml already selects any available iPhone on an iOS runtime at or below the selected SDK, so with Xcode 27 it resolves to an iOS 27 runtime. There are no OS=26 destinations in the workflows.

Not changed

Build logic, secrets, triggers, schedules, timeouts. actionlint output is identical to develop (pre-existing findings only, e.g. actions/checkout@v2 in build.yml).

Risks / things to check on first self-hosted run

  • Xcode 27 vs Xcode 26.3 is a real toolchain change for the self-hosted legs only (hosted stays on 26.3). Tuist.swift has compatibleXcodeVersions: .all; the first run is the test.

  • actions/checkout's git clean -ffdx still wipes build-Xcode* DerivedData and build/ in the workspace each run. build.yml keeps restoring build/ through actions/cache. Setting clean: false on self-hosted would keep them, but build.yml's mkdir products is not idempotent, so that is a follow-up.

  • Tuist is installed via mise into ~/.local and mise use -g writes the runner user's global config; fine on a dedicated runner user, but jobs share it.

  • -allowProvisioningUpdates leaves Xcode-managed provisioning profiles in ~/Library/MobileDevice; the signing keychain, p12 and ASC key are removed.

  • release.yml (on release: published) and testflight.yml (nightly schedule) will use the Studio too when the variable is set; the TestFlight run is the highest-stakes one, so consider a workflow_dispatch dry run first.

  • Lint latency: with a single Studio runner instance, the two lint jobs queue behind multi-hour build/tests/testflight jobs, which defeats their quick-feedback purpose. Consider leaving lint on hosted (drop it from the toggle or give it its own variable).

  • Parallel runner instances: the keychain search list is per user; if more than one runner runs as ghrunner, the iOS and tvOS testflight legs could race on it.

  • TestFlight on Xcode 27: confirm xcodebuild -version on the Studio is a GA build, since App Store Connect rejects beta-toolchain builds for external TestFlight.

  • Simulator devices are per user: ghrunner needs an iPhone device in its own device set, or the picker in tests.yml fails with "No available iPhone simulator".

  • Prerequisite: a runner labelled self-hosted, macOS, ARM64, studio must be registered for this repo (currently a repo-level runner studio-icube is online) before the variable is set; otherwise jobs sit queued.

Enable / disable

gh variable set MACOS_RUNNER_LABELS -R Provenance-Emu/iCube --body '["self-hosted","studio"]'
gh variable delete MACOS_RUNNER_LABELS -R Provenance-Emu/iCube   # back to GitHub-hosted

🤖 Generated with Claude Code


Note

Medium Risk
Toggling the Studio switches release/TestFlight builds to Xcode 27 and a shared persistent runner (signing material cleanup, simulator state, job queuing), while hosted behavior stays on 26.3; misconfiguration or parallel TestFlight matrix legs could affect CI reliability or App Store uploads.

Overview
Adds an optional MACOS_RUNNER_LABELS repo variable so macOS jobs in build, release, testflight, tests, and lint can run on the Mac Studio instead of GitHub-hosted macos-26 / macos-latest. Fork PRs always stay on hosted runners so untrusted code never executes on the persistent machine.

Self-hosted path: a pre-checkout step sets DEVELOPER_DIR from XCODE_PATH (default /Applications/Xcode.app, Xcode 27) because the job-level path targets hosted-only Xcode_26.3.app. maxim-lobanov/setup-xcode runs only on github-hosted (still pinned to 26.3). Workflow-wide HOMEBREW_NO_AUTO_UPDATE / HOMEBREW_NO_INSTALL_CLEANUP and command -v … || brew install avoid disturbing the Studio’s shared Homebrew.

Hygiene on the Studio: TestFlight logs and failure artifacts use $RUNNER_TEMP; signing keychain cleanup also removes p12, ASC keys, archives, and export dirs. Unit tests relocate ccache outside the workspace (checkout would wipe it), skip actions/cache for ccache on self-hosted, and shut down the booted simulator after the job.

Reviewed by Cursor Bugbot for commit 12463bd. Configure here.

…led by a repo variable

What: build.yml (build), release.yml (build), testflight.yml (testflight),
tests.yml (test) and lint.yml (swift, clang-format) can now run on the Mac
Studio runner. Same pattern as Provenance-Emu/Provenance and JoeMatt/iFly.

Toggle: repo variable MACOS_RUNNER_LABELS = ["self-hosted","studio"]. Unset
(or empty) keeps today's behaviour: macos-26 / macos-latest on GitHub-hosted.
Pull requests from forks always stay GitHub-hosted, so untrusted fork code
never reaches the persistent machine. Ubuntu jobs and mcp-tests.yml are
untouched.

Xcode: hosted keeps maxim-lobanov/setup-xcode with XCODE_VERSION 26.3 (those
steps are now guarded to github-hosted). Self-hosted uses Xcode 27 only: a first
step, before checkout, writes DEVELOPER_DIR=<XCODE_PATH>/Contents/Developer to
GITHUB_ENV (XCODE_PATH is an optional repo variable, default
/Applications/Xcode.app), fails clearly if it is missing and prints
xcodebuild -version. It must precede checkout because the job-level
DEVELOPER_DIR names the hosted-only Xcode_26.3.app and a missing Xcode breaks
the /usr/bin/git shim. No sudo xcode-select anywhere.

Other self-hosted hygiene: brew installs are guarded with command -v and
HOMEBREW_NO_AUTO_UPDATE / HOMEBREW_NO_INSTALL_CLEANUP are set (the Studio's
Homebrew belongs to another user). testflight.yml now logs to RUNNER_TEMP
instead of /tmp and its if: always() cleanup also removes the p12, ASC .p8,
export dir and archives besides the temporary keychain. tests.yml keeps ccache
in ~/Library/Caches/ccache-icube on self-hosted (checkout's git clean would
wipe a workspace cache) and skips actions/cache there, and shuts down the
simulator it booted. The simulator picker already chooses any iPhone runtime
at or below the selected SDK, so it resolves to an iOS 27 runtime with Xcode 27.

No build logic, secrets, triggers or schedules changed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 15, 2026 23:17

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@qodo-code-review

Copy link
Copy Markdown

ⓘ Qodo reviews are paused because the subscription is no longer active. Ask your workspace admin to reactivate the subscription to resume reviews. Manage billing

@JoeMatt
JoeMatt merged commit 22bd603 into develop Oct 15, 2026
5 checks passed
@JoeMatt
JoeMatt deleted the ci/studio-runner branch October 15, 2026 23:18
@cursor

cursor Bot commented Oct 15, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: c9bca5db-0db7-4419-93b5-0a5424784d12)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants