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: 7 additions & 0 deletions .github/workflows/pr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,18 @@ name: Pull request
on:
pull_request:
branches: [main]
# Draft PRs skip CI; marking a PR ready runs the full suite. Converting a
# PR back to draft starts a run whose jobs all skip, which cancels any
# in-progress run through the concurrency group below.
types: [opened, synchronize, reopened, ready_for_review, converted_to_draft]
# Concurrency: auto-cancels previous runs when new commits are pushed
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
ReleaseLock:
name: Check committed registry lock
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
Expand All @@ -26,6 +31,7 @@ jobs:
run: python .github/release_lock.py --rehearse
ReleaseLockTests:
name: Release lock guard tests
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
Expand Down Expand Up @@ -122,6 +128,7 @@ jobs:
run: uv run --no-sync ruff format --check .
check-changelog:
name: Check changelog fragment
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
Expand Down
1 change: 1 addition & 0 deletions changelog.d/ci-skip-draft-prs.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Skip the pull request workflow for draft PRs; marking a PR ready for review runs the full suite, and converting it back to draft cancels any run in progress.
53 changes: 53 additions & 0 deletions policyengine_us/tests/code_health/test_pr_workflow_skips_drafts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
"""Draft pull requests must not run the PR workflow; ready ones run all of it."""

from pathlib import Path

import yaml

WORKFLOW = Path(__file__).resolve().parents[3] / ".github" / "workflows" / "pr.yaml"
GUARD = "github.event.pull_request.draft == false"


def _workflow():
data = yaml.safe_load(WORKFLOW.read_text())
# PyYAML reads the bare `on` key as True.
return data[True]["pull_request"], data["jobs"]


def _needs(job):
needs = job.get("needs", [])
return [needs] if isinstance(needs, str) else needs


def test_ready_for_review_and_converted_to_draft_trigger_the_workflow():
trigger, _ = _workflow()
assert {"opened", "synchronize", "reopened", "ready_for_review"} <= set(
trigger["types"]
)
# Converting back to draft starts a skipped run that cancels the old one.
assert "converted_to_draft" in trigger["types"]


def test_every_job_is_skipped_for_drafts():
_, jobs = _workflow()
roots = [name for name, job in jobs.items() if not _needs(job)]
assert roots, "the workflow must have at least one job without needs"
for name in roots:
assert jobs[name].get("if") == GUARD, name

def reaches_guarded_root(name, seen=()):
if name in seen:
raise AssertionError(f"cycle at {name}")
job = jobs[name]
if not _needs(job):
return job.get("if") == GUARD
return all(reaches_guarded_root(n, seen + (name,)) for n in _needs(job))

for name, job in jobs.items():
# A job-level status function other than success() (always(),
# !cancelled(), failure(), cancelled()) runs the job even when a
# needed job was skipped, which would defeat the guard.
condition = str(job.get("if", "")).replace(" ", "")
for status_function in ("always()", "cancelled()", "failure()"):
assert status_function not in condition, (name, condition)
assert reaches_guarded_root(name), name
Loading