Skip to content

Commit ce3681a

Browse files
committed
ci: block fork PRs on persistent runners
1 parent 4d3a5be commit ce3681a

5 files changed

Lines changed: 59 additions & 3 deletions

File tree

.github/workflows/ci-self-hosted.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ concurrency:
1414
jobs:
1515
mac-contracts:
1616
name: macOS MLX data, model, and eval contracts
17+
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }}
1718
runs-on: [self-hosted, macOS, ARM64, cppmega-mlx-macos]
1819
timeout-minutes: 30
1920
steps:
@@ -42,6 +43,7 @@ jobs:
4243
4344
linux-portable:
4445
name: Linux portable syntax and conveyor contracts
46+
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }}
4547
runs-on: [self-hosted, Linux, X64, cppmega-mlx]
4648
timeout-minutes: 20
4749
steps:

.github/workflows/e2e-matrix.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ jobs:
3131
# this runs on the repository's persistent Apple Silicon runner.
3232
preset-matrix:
3333
name: Preset matrix (smoke, self-hosted macOS)
34+
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }}
3435
runs-on: [self-hosted, macOS, ARM64, cppmega-mlx-macos]
3536
timeout-minutes: 25
3637
strategy:
@@ -92,6 +93,7 @@ jobs:
9293
# Specialised + canvas smoke scenarios — run on every push (lightweight).
9394
specialised:
9495
name: Specialised + canvas smoke
96+
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }}
9597
runs-on: [self-hosted, macOS, ARM64, cppmega-mlx-macos]
9698
timeout-minutes: 15
9799
steps:
@@ -190,7 +192,7 @@ jobs:
190192
name: Matrix report
191193
needs: [preset-matrix, specialised]
192194
runs-on: [self-hosted, Linux, X64, cppmega-mlx]
193-
if: always()
195+
if: ${{ always() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }}
194196
steps:
195197
- uses: actions/checkout@v4
196198
- uses: actions/download-artifact@v4

.github/workflows/gemini-dispatch.yml

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,13 @@ defaults:
2525
jobs:
2626
debugger:
2727
if: |-
28-
${{ fromJSON(vars.GEMINI_DEBUG || vars.ACTIONS_STEP_DEBUG || false) }}
28+
${{
29+
fromJSON(vars.GEMINI_DEBUG || vars.ACTIONS_STEP_DEBUG || false) &&
30+
(
31+
github.event_name != 'pull_request' ||
32+
github.event.pull_request.head.repo.full_name == github.repository
33+
)
34+
}}
2935
runs-on: [self-hosted, Linux, X64, cppmega-mlx]
3036
permissions:
3137
contents: 'read'
@@ -189,7 +195,15 @@ jobs:
189195
- 'invoke'
190196
- 'plan-execute'
191197
if: |-
192-
${{ always() && !cancelled() && (failure() || needs.dispatch.outputs.command == 'fallthrough') }}
198+
${{
199+
always() &&
200+
!cancelled() &&
201+
(
202+
github.event_name != 'pull_request' ||
203+
github.event.pull_request.head.repo.full_name == github.repository
204+
) &&
205+
(failure() || needs.dispatch.outputs.command == 'fallthrough')
206+
}}
193207
runs-on: [self-hosted, Linux, X64, cppmega-mlx]
194208
permissions:
195209
contents: 'read'

.github/workflows/gemini-scheduled-triage.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ defaults:
2727

2828
jobs:
2929
triage:
30+
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }}
3031
runs-on: [self-hosted, Linux, X64, cppmega-mlx]
3132
timeout-minutes: 7
3233
permissions:
@@ -140,6 +141,8 @@ jobs:
140141
needs:
141142
- 'triage'
142143
if: |-
144+
(github.event_name != 'pull_request' ||
145+
github.event.pull_request.head.repo.full_name == github.repository) &&
143146
needs.triage.outputs.available_labels != '' &&
144147
needs.triage.outputs.available_labels != '[]' &&
145148
needs.triage.outputs.triaged_issues != '' &&

tests/test_workflow_runner_policy.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,17 @@
1010
r"^\s*runs-on:\s*.*(?:ubuntu|macos|windows)-latest\s*$",
1111
re.MULTILINE,
1212
)
13+
JOB_BLOCK = re.compile(
14+
r"(?ms)^ (?P<name>[A-Za-z0-9_-]+):\n(?P<body>.*?)(?=^ [A-Za-z0-9_-]+:\n|\Z)"
15+
)
16+
TRUSTED_PR_GUARDS = (
17+
"github.event.pull_request.head.repo.full_name == github.repository",
18+
"github.event.pull_request.head.repo.fork == false",
19+
)
20+
NON_PR_ONLY_GUARD = (
21+
"github.event_name == 'schedule' || "
22+
"github.event_name == 'workflow_dispatch'"
23+
)
1324

1425

1526
def test_workflows_do_not_use_github_hosted_runners() -> None:
@@ -21,6 +32,30 @@ def test_workflows_do_not_use_github_hosted_runners() -> None:
2132
assert not violations, f"GitHub-hosted runners are forbidden: {violations}"
2233

2334

35+
def test_pull_requests_cannot_execute_on_persistent_self_hosted_runners() -> None:
36+
violations = []
37+
for workflow in sorted((REPO_ROOT / ".github" / "workflows").glob("*.yml")):
38+
text = workflow.read_text(encoding="utf-8")
39+
if not re.search(r"(?m)^ pull_request:\s*$", text):
40+
continue
41+
jobs = text.partition("\njobs:\n")[2]
42+
for match in JOB_BLOCK.finditer(jobs):
43+
body = match.group("body")
44+
if "runs-on: [self-hosted" not in body:
45+
continue
46+
guarded = any(marker in body for marker in TRUSTED_PR_GUARDS)
47+
guarded = guarded or NON_PR_ONLY_GUARD in body
48+
if not guarded:
49+
violations.append(
50+
f"{workflow.relative_to(REPO_ROOT).as_posix()}:{match.group('name')}"
51+
)
52+
53+
assert not violations, (
54+
"pull_request jobs may not execute untrusted code on persistent "
55+
f"self-hosted runners: {violations}"
56+
)
57+
58+
2459
def test_macos_e2e_uses_an_isolated_job_venv() -> None:
2560
workflow = (REPO_ROOT / ".github" / "workflows" / "e2e-matrix.yml").read_text(
2661
encoding="utf-8"

0 commit comments

Comments
 (0)