Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .github/workflows/banner-guard.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,12 @@ concurrency:
jobs:
banner-scan:
name: "probeAddresses banner guard (main-repo notebooks)"
runs-on: ubuntu-latest
# Route vers la jambe Linux auto-hebergee (#13378 tranche 1, decision ai-01
# 2026-09-01) : garde Python pur, sans secret ni usage du GITHUB_TOKEN. La garde
# same-repo ci-dessous saute les PRs de fork (pr_gate.py compte `skipped` comme
# OK) : aucun code de fork n'atteint le runner. Retour arriere = revert de la PR.
runs-on: [self-hosted, coursia-ephemeral, coursia-linux]
if: github.event.pull_request.head.repo.full_name == null || github.event.pull_request.head.repo.full_name == github.repository
timeout-minutes: 10

steps:
Expand Down
8 changes: 7 additions & 1 deletion .github/workflows/cell-order-gate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,13 @@ concurrency:
jobs:
cell-order:
name: "No cell-ordering regression in changed notebooks"
runs-on: ubuntu-latest
# Route vers la jambe Linux auto-hebergee (#13378 tranche 1, decision ai-01
# 2026-09-01) : garde Python pur, sans secret ni usage du GITHUB_TOKEN. La garde
# same-repo ci-dessous saute les PRs de fork (pr_gate.py compte `skipped` comme
# OK) : aucun code de fork n'atteint le runner. Retour arriere = revert de la PR.
runs-on: [self-hosted, coursia-ephemeral, coursia-linux]
if: github.event.pull_request.head.repo.full_name == null || github.event.pull_request.head.repo.full_name == github.repository
timeout-minutes: 10

steps:
- name: Checkout
Expand Down
11 changes: 10 additions & 1 deletion .github/workflows/hooks-parity.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,19 @@ concurrency:
group: hooks-parity-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

permissions:
contents: read

jobs:
parity:
name: parity gate
runs-on: ubuntu-latest
# Route vers la jambe Linux auto-hebergee (#13378 tranche 1, decision ai-01
# 2026-09-01) : garde Python pur, sans secret ni usage du GITHUB_TOKEN. La garde
# same-repo ci-dessous saute les PRs de fork (pr_gate.py compte `skipped` comme
# OK) : aucun code de fork n'atteint le runner. Retour arriere = revert de la PR.
runs-on: [self-hosted, coursia-ephemeral, coursia-linux]
if: github.event.pull_request.head.repo.full_name == null || github.event.pull_request.head.repo.full_name == github.repository
timeout-minutes: 10
# Advisory: surfaces the warning without breaking the rest of the CI run.
# `continue-on-error` is only valid on a job or a step -- at the workflow
# root it is an unknown key, which Actions rejects with a startup_failure
Expand Down
8 changes: 7 additions & 1 deletion .github/workflows/notebook-cell-source-parses.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,13 @@ concurrency:

jobs:
cell-source-parses:
runs-on: ubuntu-latest
# Route vers la jambe Linux auto-hebergee (#13378 tranche 1, decision ai-01
# 2026-09-01) : garde Python pur, sans secret ni usage du GITHUB_TOKEN. La garde
# same-repo ci-dessous saute les PRs de fork (pr_gate.py compte `skipped` comme
# OK) : aucun code de fork n'atteint le runner. Retour arriere = revert de la PR.
runs-on: [self-hosted, coursia-ephemeral, coursia-linux]
if: github.event.pull_request.head.repo.full_name == null || github.event.pull_request.head.repo.full_name == github.repository
timeout-minutes: 10

steps:
- uses: actions/checkout@v4
Expand Down
8 changes: 7 additions & 1 deletion .github/workflows/notebook-exec-sequence-ratchet.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,13 @@ concurrency:
jobs:
ratchet:
name: Exec-sequence ratchet (base vs PR)
runs-on: ubuntu-latest
# Route vers la jambe Linux auto-hebergee (#13378 tranche 1, decision ai-01
# 2026-09-01) : garde Python pur, sans secret ni usage du GITHUB_TOKEN. La garde
# same-repo ci-dessous saute les PRs de fork (pr_gate.py compte `skipped` comme
# OK) : aucun code de fork n'atteint le runner. Retour arriere = revert de la PR.
runs-on: [self-hosted, coursia-ephemeral, coursia-linux]
if: github.event.pull_request.head.repo.full_name == null || github.event.pull_request.head.repo.full_name == github.repository
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
with:
Expand Down
7 changes: 6 additions & 1 deletion .github/workflows/notebook-interp-positioning.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,12 @@ concurrency:
jobs:
check-interp-positioning:
name: check_interp_positioning.py
runs-on: ubuntu-latest
# Route vers la jambe Linux auto-hebergee (#13378 tranche 1, decision ai-01
# 2026-09-01) : garde Python pur, sans secret ni usage du GITHUB_TOKEN. La garde
# same-repo ci-dessous saute les PRs de fork (pr_gate.py compte `skipped` comme
# OK) : aucun code de fork n'atteint le runner. Retour arriere = revert de la PR.
runs-on: [self-hosted, coursia-ephemeral, coursia-linux]
if: github.event.pull_request.head.repo.full_name == null || github.event.pull_request.head.repo.full_name == github.repository
timeout-minutes: 10
steps:
- name: Checkout
Expand Down
16 changes: 15 additions & 1 deletion .github/workflows/notebook-navlink-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,23 @@ concurrency:

jobs:
check-navlinks:
runs-on: ubuntu-latest
# Route vers la jambe Linux auto-hebergee (#13378 tranche 1, decision ai-01
# 2026-09-01) : garde Python pur, sans secret ni usage du GITHUB_TOKEN. La garde
# same-repo ci-dessous saute les PRs de fork (pr_gate.py compte `skipped` comme
# OK) : aucun code de fork n'atteint le runner. Retour arriere = revert de la PR.
runs-on: [self-hosted, coursia-ephemeral, coursia-linux]
if: github.event.pull_request.head.repo.full_name == null || github.event.pull_request.head.repo.full_name == github.repository
timeout-minutes: 10
steps:
- uses: actions/checkout@v4

# La jambe Linux conteneurisee n'expose pas de `python` systeme (job
# 100021313259, exit 127) : le checker est stdlib-only, mais l'interprete
# vient de setup-python, comme dans les dix autres gardes de la tranche 1.
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'

- name: Check notebook navlinks
run: python scripts/notebook_tools/check_notebook_navlinks.py --check
8 changes: 7 additions & 1 deletion .github/workflows/notebook-papermill-ratchet.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,13 @@ concurrency:
jobs:
ratchet:
name: Papermill ratchet (base vs PR)
runs-on: ubuntu-latest
# Route vers la jambe Linux auto-hebergee (#13378 tranche 1, decision ai-01
# 2026-09-01) : garde Python pur, sans secret ni usage du GITHUB_TOKEN. La garde
# same-repo ci-dessous saute les PRs de fork (pr_gate.py compte `skipped` comme
# OK) : aucun code de fork n'atteint le runner. Retour arriere = revert de la PR.
runs-on: [self-hosted, coursia-ephemeral, coursia-linux]
if: github.event.pull_request.head.repo.full_name == null || github.event.pull_request.head.repo.full_name == github.repository
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
with:
Expand Down
7 changes: 6 additions & 1 deletion .github/workflows/pip-leak-guard.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,12 @@ concurrency:
jobs:
pip-leak-delta:
name: "!pip install HIGH delta guard (#6314)"
runs-on: ubuntu-latest
# Route vers la jambe Linux auto-hebergee (#13378 tranche 1, decision ai-01
# 2026-09-01) : garde Python pur, sans secret ni usage du GITHUB_TOKEN. La garde
# same-repo ci-dessous saute les PRs de fork (pr_gate.py compte `skipped` comme
# OK) : aucun code de fork n'atteint le runner. Retour arriere = revert de la PR.
runs-on: [self-hosted, coursia-ephemeral, coursia-linux]
if: github.event.pull_request.head.repo.full_name == null || github.event.pull_request.head.repo.full_name == github.repository
timeout-minutes: 10

steps:
Expand Down
8 changes: 7 additions & 1 deletion .github/workflows/prose-counts-guard.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,13 @@ concurrency:

jobs:
prose-counts:
runs-on: ubuntu-latest
# Route vers la jambe Linux auto-hebergee (#13378 tranche 1, decision ai-01
# 2026-09-01) : garde Python pur, sans secret ni usage du GITHUB_TOKEN. La garde
# same-repo ci-dessous saute les PRs de fork (pr_gate.py compte `skipped` comme
# OK) : aucun code de fork n'atteint le runner. Retour arriere = revert de la PR.
runs-on: [self-hosted, coursia-ephemeral, coursia-linux]
if: github.event.pull_request.head.repo.full_name == null || github.event.pull_request.head.repo.full_name == github.repository
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
with:
Expand Down
7 changes: 6 additions & 1 deletion .github/workflows/solution-leak-guard.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,12 @@ concurrency:
jobs:
solution-leak-delta:
name: "Solution-leak HIGH delta (advisory, WARN phase, #8053)"
runs-on: ubuntu-latest
# Route vers la jambe Linux auto-hebergee (#13378 tranche 1, decision ai-01
# 2026-09-01) : garde Python pur, sans secret ni usage du GITHUB_TOKEN. La garde
# same-repo ci-dessous saute les PRs de fork (pr_gate.py compte `skipped` comme
# OK) : aucun code de fork n'atteint le runner. Retour arriere = revert de la PR.
runs-on: [self-hosted, coursia-ephemeral, coursia-linux]
if: github.event.pull_request.head.repo.full_name == null || github.event.pull_request.head.repo.full_name == github.repository
timeout-minutes: 15

steps:
Expand Down
81 changes: 73 additions & 8 deletions scripts/ci/check_self_hosted_runner_policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,12 @@
fails closed: every self-hosted job must belong to an explicitly allowed
workflow, use the exact dedicated label set, and avoid runner groups (unavailable
for this personal-account repository). Every pull_request job must also carry
the exact same-repository guard. Dynamic ``runs-on`` expressions are rejected
because their target cannot be proved statically.
the exact same-repository guard. Triggers whose payload can name a fork pull
request (pull_request_target, workflow_run, issue_comment, pull_request_review,
pull_request_review_comment) never reach a self-hosted job, whatever the guard
says; push / schedule / workflow_dispatch only run repository-owned refs and are
accepted. Dynamic ``runs-on`` expressions are rejected because their target
cannot be proved statically.

Exit codes:
0 policy satisfied (including the explicit baseline of zero self-hosted jobs)
Expand Down Expand Up @@ -51,10 +55,26 @@
# - linux-self-hosted-tests.yml: workflow_dispatch-ONLY pilot vehicle for the
# containerized Linux runner (#13378, po-2024, dispatch ai-01 2026-08-31),
# zero fan-out, --ephemeral inside a capped Docker container.
# - tranche 1 (#13378, decision ai-01 2026-09-01, owner myia-po-2024:CoursIA):
# pure-Python guards, no secret, no GITHUB_TOKEN use, universal same-repo
# guard (#13874) at job level so fork PRs are skipped (pr_gate.py counts
# `skipped` as OK). Routed to the containerized Linux leg to relieve the
# GitHub-hosted queue. Rollback = revert of the routing PR.
SELF_HOSTED_WORKFLOW_ALLOWLIST = {
"pr-gate-stale-sweep.yml",
"windows-self-hosted-tests.yml",
"linux-self-hosted-tests.yml",
"banner-guard.yml",
"solution-leak-guard.yml",
"prose-counts-guard.yml",
"notebook-interp-positioning.yml",
"notebook-cell-source-parses.yml",
"cell-order-gate.yml",
"pip-leak-guard.yml",
"hooks-parity.yml",
"notebook-exec-sequence-ratchet.yml",
"notebook-navlink-check.yml",
"notebook-papermill-ratchet.yml",
}
GITHUB_HOSTED_LABELS = {
"ubuntu-latest",
Expand Down Expand Up @@ -90,6 +110,18 @@
"|| github.event.pull_request.head.repo.full_name == github.repository"
)
ACCEPTED_SAME_REPO_GUARDS = frozenset({SAME_REPO_GUARD, SAME_REPO_GUARD_UNIVERSAL})
# Evenements dont le code de workflow vient de la branche par defaut mais dont
# le payload nomme une pull request potentiellement issue d'un fork : un job
# peut faire `checkout refs/pull/N/head` et executer du code de fork sur le
# runner. Refuses sur self-hosted au meme titre que pull_request_target
# (#14148, reserve NanoClaw n.2). A l'inverse, push / schedule /
# workflow_dispatch ne portent que des refs du depot : la branche `== null`
# de la garde universelle y est sure par construction, pas par accident.
FORK_REACHABLE_TRIGGERS = frozenset({
"issue_comment",
"pull_request_review",
"pull_request_review_comment",
})
DEFAULT_WORKFLOWS_DIR = Path(__file__).resolve().parents[2] / ".github" / "workflows"


Expand Down Expand Up @@ -217,12 +249,23 @@ def _normalise_condition(value: Any) -> str:

def _starts_with_accepted_guard(condition: str) -> bool:
"""True iff ``condition`` leads with one of the accepted same-repo
guards, optionally wrapped in parens and optionally followed by `&& ...`
selection predicates. The guard must be the LEAD predicate, and any
suffix joined by `||` is rejected (the `||` would let the guard fall
through). The standalone tests ``test_guard_weakened_by_or_is_rejected``
and ``test_universal_guard_weakened_by_or_is_rejected`` verify that
guards, optionally followed by `&& ...` selection predicates. The guard
must be the LEAD predicate, and any suffix joined by `||` is rejected
(the `||` would let the guard fall through). The standalone tests
``test_guard_weakened_by_or_is_rejected`` and
``test_universal_guard_weakened_by_or_is_rejected`` verify that
`|| always()` is refused.

Parentheses: the direct guard may be bare or wrapped. The UNIVERSAL guard
carries an internal `||`, so it is accepted bare only when it is the whole
condition; combined with `&& ...` it MUST be wrapped --
`(<universal>) && <selection>`. This is not a parser limitation to work
around: in GitHub expressions `&&` binds tighter than `||`, so the bare
form reads `A == null || (A == repo && sel)` and runs the job on every
non-pull_request event regardless of the selection. The bare scan below
splits at the first top-level operator (the internal `||`) and rejects;
``_guard_violation_message`` names the required parentheses
(#14148, NanoClaw concern 1).
"""
if not condition:
return False
Expand Down Expand Up @@ -277,6 +320,20 @@ def _starts_with_accepted_guard(condition: str) -> bool:
return False


def _guard_violation_message(condition: str) -> str:
"""Name the exact defect: a bare universal guard followed by `&&` is the
one rejection whose fix is not "add the guard" but "add parentheses"."""
if condition.startswith(SAME_REPO_GUARD_UNIVERSAL):
tail = condition[len(SAME_REPO_GUARD_UNIVERSAL):].lstrip()
if tail.startswith("&&"):
return (
"universal same-repo guard combined with `&&` must be parenthesised: "
"`(<universal guard>) && <selection>` -- `&&` binds tighter than `||`, "
"so the bare form bypasses the selection outside pull_request context"
)
return "pull_request self-hosted job must lead with the same-repo job guard"


def scan_workflows(workflows_dir: Path = DEFAULT_WORKFLOWS_DIR) -> ScanResult:
yaml = _load_yaml()
if yaml is None:
Expand Down Expand Up @@ -383,6 +440,14 @@ def scan_workflows(workflows_dir: Path = DEFAULT_WORKFLOWS_DIR) -> ScanResult:
"REUSABLE_SELF_HOSTED",
"self-hosted jobs must not hide inside reusable workflows",
))
for trigger in sorted(FORK_REACHABLE_TRIGGERS & triggers):
violations.append(Violation(
path.name,
str(job_name),
"FORK_REACHABLE_TRIGGER",
f"{trigger} can check out a fork pull request and must never "
"reach a self-hosted runner",
))

if path.name not in SELF_HOSTED_WORKFLOW_ALLOWLIST:
violations.append(Violation(
Expand Down Expand Up @@ -430,7 +495,7 @@ def scan_workflows(workflows_dir: Path = DEFAULT_WORKFLOWS_DIR) -> ScanResult:
path.name,
str(job_name),
"SAME_REPO_GUARD",
"pull_request self-hosted job must lead with the same-repo job guard",
_guard_violation_message(condition),
))

return ScanResult(len(paths), jobs_scanned, self_hosted_jobs, violations, broken)
Expand Down
Loading
Loading