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
66 changes: 66 additions & 0 deletions .github/scripts/runner_guard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
"""Fail when a workflow job could run on a billed or unapproved runner.

This repository is public. GitHub's standard hosted runners are free here, but
larger runners (-N-cores, -arm, -gpu) and third-party runners such as
Blacksmith are billed, and self-hosted runners must never run untrusted PR
code. Self-hosted jobs are allowed only when listed in SELF_HOSTED_OK as
"<workflow file>:<job id>".

Fail-closed: runs-on must be literal labels or ${{ matrix.os }} over a literal
os list. Any other shape raises and is rejected, including other expressions,
expression-built matrices, runner groups and reusable workflows from other
repositories.
"""

import os
import pathlib
import sys

import yaml

FREE = {
"ubuntu-latest", "ubuntu-24.04", "ubuntu-22.04",
"windows-latest", "windows-2025", "windows-2022",
"macos-latest", "macos-15", "macos-14",
}
UNPROVABLE = (KeyError, TypeError, ValueError, AttributeError)


def runner_labels(job: dict) -> set:
"""Every label the job can select; raises when that cannot be proven."""
if "uses" in job:
if not job["uses"].startswith("./"):
raise ValueError("reusable workflow from another repository")
return set()
runs_on = job["runs-on"]
if runs_on == "${{ matrix.os }}":
matrix = job["strategy"]["matrix"]
runs_on = matrix["os"] + [x["os"] for x in matrix.get("include", []) if "os" in x]
labels = [runs_on] if isinstance(runs_on, str) else list(runs_on)
if not labels or any("${{" in label for label in labels):
raise ValueError(f"runs-on {runs_on!r} is not literal")
return set(labels)


def problems(workflows: pathlib.Path, approved: set) -> list:
found = []
for workflow in sorted(workflows.glob("*.y*ml")):
for job_id, job in (yaml.safe_load(workflow.read_text()).get("jobs") or {}).items():
key = f"{workflow.name}:{job_id}"
try:
labels = runner_labels(job)
except UNPROVABLE as error:
found.append(f"{key}: cannot prove the runner is free ({error!r})")
continue
if "self-hosted" in labels:
labels = set() if key in approved else {"unapproved self-hosted"}
if labels - FREE:
found.append(f"{key}: may run on {sorted(labels - FREE)}")
return found


if __name__ == "__main__":
approved = set(filter(None, os.environ.get("SELF_HOSTED_OK", "").replace(" ", "").split(",")))
found = problems(pathlib.Path(".github/workflows"), approved)
print("\n".join(found) or "All jobs use free standard runners or approved self-hosted ones.")
sys.exit(1 if found else 0)
4 changes: 2 additions & 2 deletions .github/workflows/deploy-pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ concurrency:

jobs:
build:
runs-on: blacksmith-4vcpu-ubuntu-2404
runs-on: ubuntu-24.04

steps:
- uses: actions/checkout@v4
Expand All @@ -41,7 +41,7 @@ jobs:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: blacksmith-4vcpu-ubuntu-2404
runs-on: ubuntu-24.04
needs: build

steps:
Expand Down
24 changes: 24 additions & 0 deletions .github/workflows/runner-guard.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: Runner guard

# Public repo: keep jobs on GitHub's free standard runners. See
# .github/scripts/runner_guard.py for what is allowed and why.

on:
pull_request:
paths: [".github/workflows/**", ".github/scripts/runner_guard.py"]
push:
branches: [main]
paths: [".github/workflows/**", ".github/scripts/runner_guard.py"]

permissions:
contents: read

jobs:
runner-guard:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- name: Allow only free standard or approved self-hosted runners
env:
SELF_HOSTED_OK: ""
run: python3 .github/scripts/runner_guard.py
2 changes: 1 addition & 1 deletion .github/workflows/verify-pages-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ on:

jobs:
verify:
runs-on: blacksmith-4vcpu-ubuntu-2404
runs-on: ubuntu-24.04

steps:
- uses: actions/checkout@v4
Expand Down