Skip to content

feat: self-hosted runner support, private-repos-only by design - #6

Merged
AdamXweb merged 2 commits into
mainfrom
feat/self-hosted-runner-support
Aug 7, 2026
Merged

feat: self-hosted runner support, private-repos-only by design#6
AdamXweb merged 2 commits into
mainfrom
feat/self-hosted-runner-support

Conversation

@adamXbot

@adamXbot adamXbot commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

Adds the option to run macOS jobs on self-hosted hardware, with the constraint that makes it safe built into the workflows rather than left to each consumer to remember.

The rule

Self-hosted runners are for private repositories only.

A self-hosted runner is a persistent machine that isn't reset between jobs. A public repository accepts pull requests from anyone, and a pull request is a proposal to run the author's code. Public repositories also get free Actions minutes — so pointing one at owned hardware takes on the entire risk in exchange for nothing.

This is why the migration targets private repos: it captures the whole bill (they're the only ones billed) and removes the untrusted-code surface structurally, rather than via a gate that has to be written correctly in every workflow forever.

Two gates

  1. Job-level if: — GitHub evaluates it before allocating a runner, so a fork PR never reaches the host.
  2. actions/assert-trusted-runner — first step of every job, ahead of checkout. Re-checks visibility and fork status on the runner itself.

Visibility is deliberately not in the job-level if:. A skipped job reports success, and a public repo wired to the fleet is a configuration mistake that must be loud — so it fails in the step instead.

The guard is a no-op on GitHub-hosted runners, which is what lets it live in a shared workflow whose defaults stay hosted. It fails closed: unknown visibility stops the job.

Inputs

Input Default Notes
runner macos-15 Plain label, or JSON array for the fleet
release_runner same as runner Route signing to its own runner/account
macos_runner empty Deprecated; non-empty still wins, so no caller changes behaviour

Defaults stay hosted. The fleet is opt-in per consumer and never inherited, so a public repo can't land on owned hardware by accident.

Self-hosted behaviour changes (automatic)

  • setup-xcode is skipped — it re-points the machine's selected Xcode, which on a shared host reaches into every other repo's builds.
  • xcodegen is expected preinstalled instead of brew installed per job, with a clear failure if missing.
  • The release job's checkout keeps push credentials (it publishes the appcast), so cleanup now also unsets them from .git/config — on a self-hosted host the disk outlives the job.

Verification

  • actionlint: no new findings (the two reported are pre-existing in ios-release.yml, confirmed against the base commit).
  • Guard logic exercised as a script across 17 cases — hosted no-op, private push/PR, fork PR, pull_request_target fork, public repo, unknown visibility, the allow-public escape hatch, and two command-injection attempts via fork names containing $(...) and backticks. All pass; no injection artefacts created. Every value is passed through the environment, never interpolated.
  • runs-on precedence evaluated against GitHub's &&/|| semantics across 10 cases, including every existing calling convention. Confirmed: a caller that passes nothing still resolves to macos-15.
  • shellcheck clean on the guard script; YAML parses on all four files.

Note on @v1

The workflows reference privacykey/gh-workflows/actions/assert-trusted-runner@v1, which resolves once v1 is moved onto this commit after merge. Retag v1.1.0 and move v1 as part of merging.

🤖 Generated with Claude Code

adamXbot and others added 2 commits August 7, 2026 12:28
macOS Actions minutes bill at 10x and are the portfolio's main CI cost.
This adds the option to run them on owned hardware, with the constraint
that makes that safe built into the workflows rather than left to each
consumer to remember.

Self-hosted runners are for PRIVATE repositories only. A self-hosted
runner is persistent and not reset between jobs; a public repository
accepts pull requests from anyone, and a pull request is a proposal to
run the author's code. Public repositories also get free minutes — so
pointing one at owned hardware takes the whole risk for no saving.

Two independent gates:

  - job-level `if:`, which GitHub evaluates before allocating a runner,
    so fork pull requests never reach the host;
  - actions/assert-trusted-runner, the first step of every job and ahead
    of checkout, which re-checks visibility and fork status on the runner
    and fails closed when visibility can't be determined.

Visibility is deliberately NOT part of the job-level `if:`: a skipped job
reports success, and a public repository wired to the fleet is a
configuration mistake that has to be loud.

The guard is a no-op on GitHub-hosted runners, which is what lets it sit
in a shared workflow whose defaults stay hosted. Every value it reads is
passed through the environment, never interpolated — a fork can name
itself `$(...)`.

Also:

  - `runner` / `release_runner` inputs accept a JSON array of labels,
    reusing the startsWith/fromJSON convention ios-release.yml already
    used. `release_runner` exists so signing can be routed to a runner in
    its own macOS account rather than sharing the CI machine.
  - `macos_runner` is deprecated but still wins when non-empty, so no
    existing caller changes behaviour. Defaults remain macos-15: the
    fleet is opt-in, never inherited.
  - On self-hosted runners, setup-xcode is skipped (it re-points the
    machine's selected Xcode, which on a shared host would affect every
    other repository's builds) and xcodegen is expected preinstalled,
    with a clear failure if it isn't.
  - The release job's checkout keeps its push credentials, so cleanup now
    also unsets them from .git/config on self-hosted hosts, where the
    disk outlives the job.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@AdamXweb
AdamXweb merged commit ee44487 into main Aug 7, 2026
@AdamXweb
AdamXweb deleted the feat/self-hosted-runner-support branch August 7, 2026 03:06
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