Skip to content

feat(ci): refuse fork-authored code before checkout, on every self-hosted job - #23

Merged
ywatanabe1989 merged 1 commit into
mainfrom
feat/route-fork-prs-off-self-hosted
Jul 30, 2026
Merged

feat(ci): refuse fork-authored code before checkout, on every self-hosted job#23
ywatanabe1989 merged 1 commit into
mainfrom
feat/route-fork-prs-off-self-hosted

Conversation

@ywatanabe1989

Copy link
Copy Markdown
Contributor

What

Every self-hosted job here that runs actions/checkout now refuses fork-authored pull requests before checkout. Five workflows: pytest-matrix, import-smoke, quality-audit, rtd-sphinx-build, self-test.

No repo or org settings are changed by this PR.

Why

These jobs run bare on shared University of Melbourne HPC nodes — no container, no overlay, two concurrent jobs sharing one $HOME (measured by scitex-hpc on the CI supervisor allocation, job 28161762, spartan-bm062). uv pip install -e . alone executes a pull request's own build-backend hooks there, and rtd-sphinx-build executes the fork's conf.py as plain Python.

Operator, 2026-07-30: 「大学の資源を外部の人にも使わせる形になったら一発でアウトなのでそれはだめですね」

Why refuse rather than re-route to ubuntu-latest

That was the obvious fix and it is forbidden. Operator mandate 2026-07-14, enforced as PS-169: 「github側のランナーというのは本当にもう一切使わないでください…強制です、例外なしです」.

Two mandates, nothing in the intersection — so fork-authored code is refused, not relocated. The durable resolution is a runner we own (neither GitHub's nor the university's); that is a hardware conversation, not this patch.

Shape of the guard

  • First step, ahead of actions/checkout — checkout is what first puts fork content on the node, so a guard after it is a report, not a barrier.
  • Fails, does not skip — a skipped job's check can be reported as successful to branch protection, which is a red that looks green.
  • Names the remedy (gh pr checkout + push to a review branch) — an error that only says what broke is half-written.
  • Job names untouched, so every repo's required status checks keep reporting.

Scope correction worth recording

The first search reported one self-hosted runs-on. An unfiltered re-run found nine, across seven files — eight rows had been dropped with no truncation marker. auto-merge-to-develop and cla are excluded because they check out nothing (cla measured 2026-07-30 on scitex-app), so no fork code runs there.

Tests

tests/test_fork_guard.py derives the covered set rather than listing it: it parses every workflow, selects jobs that are self-hosted and check out, and requires the guard on exactly those. Add a self-hosted workflow that checks out a PR and these tests go red on that change — a hardcoded list would go stale silently and keep passing. It also asserts locally that no job resolves to a hosted image, so a future "just use ubuntu-latest for forks" cannot land here quietly.

Mutation-probed, each restoring the tree afterwards:

mutation result
guard moved after checkout test_guard_step_precedes_every_checkout fails
predicate relaxed to head.repo.fork test_guard_predicate_is_the_sanctioned_one fails
a job flipped to ubuntu-latest selector count + PS-169 arm fail (2 tests)

Baseline and post-restore: 74 passed.

What this does NOT do

For pull_request, GitHub runs the workflow definition from the PR's own head. A hostile fork can therefore edit the caller's ci.yml to bypass these reusable workflows entirely and declare its own self-hosted job. This closes the default path; it is not a boundary.

The boundary is the fork-PR approval policy — measured all_external_contributors on 74 of 74 public scitex-ai repos and as the org default for new repos (2026-07-30), so no external contributor's workflow runs without a maintainer's click. The README now says plainly what that click means, and gives the safe alternative: adopt the branch into the repo and open a same-repo PR.

…sted job

These jobs run BARE on shared University of Melbourne HPC nodes — no
container, no overlay, two concurrent jobs sharing one $HOME (measured by
scitex-hpc on the CI supervisor allocation, job 28161762, spartan-bm062). So
`actions/checkout` + `uv pip install -e .` executes a pull request's own
build-backend hooks on university hardware, and rtd-sphinx-build executes the
fork's conf.py as plain Python.

Operator, 2026-07-30: 「大学の資源を外部の人にも使わせる形になったら一発で
アウトなのでそれはだめですね」.

WHY REFUSE RATHER THAN RE-ROUTE. Routing fork PRs to ubuntu-latest was the
obvious fix and it is forbidden: operator mandate 2026-07-14, enforced as
PS-169 — 「github側のランナーというのは本当にもう一切使わないでください…
強制です、例外なしです」. Two mandates, no runner in the intersection, so
fork-authored code is refused rather than sent somewhere else. (The durable
resolution is a runner we OWN — neither GitHub's nor the university's. That
is a hardware conversation, not this patch.)

The guard is the FIRST step, ahead of actions/checkout, because checkout is
what first puts fork content on the node — a guard after it is a report, not
a barrier. It FAILS rather than skipping: a skipped job's check can be
reported as successful to branch protection, which is a red that looks green.
It names the remedy (`gh pr checkout` + push to a review branch), because an
error that only says what broke is half-written.

FIVE workflows, not one. The first search reported a single self-hosted
`runs-on` and an unfiltered re-run found nine across seven files — eight rows
had been dropped silently. auto-merge and cla are excluded because they check
out nothing (cla measured 2026-07-30 on scitex-app), so no fork code runs
there.

tests/test_fork_guard.py DERIVES the covered set instead of listing it: it
parses every workflow, selects jobs that are self-hosted AND check out, and
requires the guard on exactly those. Add a self-hosted workflow that checks
out a PR and these tests go red on that change — a hardcoded list would go
stale silently and keep passing. It also asserts locally that no job resolves
to a hosted image, so a future "just use ubuntu-latest for forks" cannot land
here quietly.

Mutation-probed, each restoring the tree afterwards:
  guard moved after checkout  -> test_guard_step_precedes_every_checkout      FAILS
  predicate -> head.repo.fork -> test_guard_predicate_is_the_sanctioned_one   FAILS
  a job -> ubuntu-latest      -> selector count + PS-169 arm                  FAIL (2)
Baseline and post-restore: 74 passed.

WHAT THIS DOES NOT DO, stated in the README next to the guard so nobody
mistakes it for a boundary: for `pull_request`, GitHub runs the workflow
definition from the PR's own head, so a hostile fork can edit the CALLER's
ci.yml to bypass these reusable workflows and declare its own self-hosted
job. This closes the DEFAULT path. The boundary is the fork-PR approval
policy — measured `all_external_contributors` on 74 of 74 public scitex-ai
repos and as the org default for new repos, 2026-07-30 — i.e. a maintainer's
click. The README says plainly what that click means.

No repo or org settings are changed by this PR.
@ywatanabe1989
ywatanabe1989 merged commit e1b4a71 into main Jul 30, 2026
1 check passed
@ywatanabe1989
ywatanabe1989 deleted the feat/route-fork-prs-off-self-hosted branch July 30, 2026 13:26
ywatanabe1989 added a commit to scitex-ai/scitex-ui that referenced this pull request Jul 30, 2026
…tself (#120)

scitex-ai/.github#23 put a pre-checkout fork guard on the five REUSABLE
workflows this repo calls. A reusable workflow cannot guard a job declared
LOCALLY, and this repo declares two that are fork-reachable:

  typecheck.yml    on: pull_request [main, develop]   self-hosted + checkout
  docs-sphinx.yml  on: pull_request                   self-hosted + checkout

Both were outside that PR's reach entirely. Found by auditing this repo
afterwards instead of assuming the shared fix covered it — the mechanism
shipped, and it reached five workflows out of seven.

`typecheck` is a REQUIRED status check on main and develop, which makes it the
most reachable self-hosted job in the repo. `npm ci` there runs the pull
request's own lifecycle scripts. docs-sphinx is worse per-run despite gating
nothing: it declares `permissions: contents: write`, passes a token to
checkout, and sphinx EXECUTES the PR's conf.py as plain Python.

These jobs run BARE on shared University of Melbourne HPC nodes — no
container, no overlay, two concurrent jobs sharing one $HOME (measured by
scitex-hpc, job 28161762, spartan-bm062).

  operator 2026-07-30  「大学の資源を外部の人にも使わせる形になったら一発で
                        アウトなのでそれはだめですね」
  operator 2026-07-14  「github側のランナーというのは本当にもう一切使わないで
                        ください…強制です、例外なしです」  (PS-169)

Nothing sits in the intersection of those two, so fork code is REFUSED here
rather than re-routed to a hosted runner. The guard is the first step, ahead
of checkout, because checkout is what first puts fork content on the node. It
FAILS rather than skipping: a skipped job's check can be reported as
successful to branch protection, which is a red that looks green.

tests/develop/test_fork_guard_own_workflows.py DERIVES the covered set: a job
needs the guard when its workflow has a pull_request trigger AND the job
resolves to self-hosted AND the job checks out. The release workflow is
excluded by the trigger arm (tags only), not by an exemption. The self-hosted
test handles BOTH spellings — the label list and the fleet's
`fromJSON(vars.CI_RUNS_ON || '[...]')` form — because a membership test alone
would silently exempt every job using the second, which is most of the release
workflow. That helper has its own unit test rather than being trusted.

Mutation-probed, each restoring the tree:
  guard deleted from typecheck        -> 5 failures
  guard moved after checkout (docs)   -> test_guard_step_precedes_every_checkout
  a NEW unguarded self-hosted PR job  -> 5 failures  (the drift arm: adding one
                                         goes red on the change that adds it)
Baseline and post-restore: 26 passed.

WHAT THIS DOES NOT DO: for `pull_request`, GitHub runs workflow definitions
from the PR's own head, so a fork can delete these guards in its own PR. This
closes the default path; it is not a boundary. The boundary is the fork-PR
approval policy — measured `all_external_contributors` on 74 of 74 public
scitex-ai repos, 2026-07-30 — i.e. a maintainer's click.

No repo or org settings are changed.
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.

1 participant