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
1526def 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 ("\n jobs:\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+
2459def 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