diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c3688d9..e30dcd5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,6 +4,8 @@ on: push: branches: [main] pull_request: + # The gate skips drafts; ready_for_review runs it when a draft is marked ready. + types: [opened, synchronize, reopened, ready_for_review] permissions: contents: read diff --git a/log/devlog.md b/log/devlog.md index c7e1848..92bcad8 100644 --- a/log/devlog.md +++ b/log/devlog.md @@ -2,6 +2,12 @@ Newest entry first. Format: `standards/devlog.md` in emolysis/rules (private). +## 2026-10-23 · ci: a draft pull request runs its checks when it is marked ready (green) + +- Landed: `ci.yml` lists `ready_for_review` in its `pull_request` types, so a draft runs the gate when it is marked ready instead of on its next push; `scripts/check.py` refuses a workflow whose jobs skip drafts without it (`draft_trigger_problems`), with three tests. +- Numbers: one workflow line, one check, 53 tests. +- Still open: none. + ## 2026-10-20 · ci: minute saving in the workflows (green) - Landed: `claude-review.yml` reviews once per pull request (`opened`, `ready_for_review`, the `review` label), never on every push, with a cancel group per pull request; `ci.yml` cancels the run a newer push supersedes on every ref and skips draft pull requests; the `review` label; the starter workflow ignores Markdown and `docs/` on push and pull request and cancels on every branch (spec updated); `scripts/check.py` refuses a review workflow that runs on push events (`review_trigger_problems`), with four tests. Founder decision of 2026-10-20 after the organization ran out of Action minutes. diff --git a/scripts/check.py b/scripts/check.py index 294428e..bc5752e 100644 --- a/scripts/check.py +++ b/scripts/check.py @@ -325,9 +325,28 @@ def check_workflows() -> Problems: problems.append(f"{path}: set top-level permissions explicitly") if path == REVIEW_WORKFLOW: problems += review_trigger_problems(path, doc) + problems += draft_trigger_problems(path, doc) return problems +def draft_trigger_problems(path: Path, doc: Any) -> Problems: + """A workflow whose jobs skip drafts lists ready_for_review in its pull_request types: + the default types never fire when a draft is marked ready, so it would run nothing until + the next push (emolysis/rules standards/ci.md, Minutes, Drafts).""" + doc = doc or {} + jobs = doc.get("jobs") or {} + skips = any( + "github.event.pull_request.draft" in str((job or {}).get("if", "")) for job in jobs.values() + ) + on = workflow_on(doc) + if not skips or not isinstance(on, dict) or "pull_request" not in on: + return [] + types = (on.get("pull_request") or {}).get("types") or [] + if "ready_for_review" not in types: + return [f"{path}: jobs skip drafts, so pull_request.types must list ready_for_review"] + return [] + + def review_trigger_problems(path: Path, doc: Any) -> Problems: """The automated review runs once per pull request, never on every push: a review spends runner minutes and API credit (emolysis/rules standards/ci.md, Minutes).""" diff --git a/tests/test_check.py b/tests/test_check.py index a3b060c..05828d7 100644 --- a/tests/test_check.py +++ b/tests/test_check.py @@ -95,6 +95,32 @@ def test_review_trigger_default_types() -> None: assert "synchronize" in problem +# --- a draft marked ready runs the jobs that skip drafts --------------------------------- + + +DRAFT_JOB = """ +jobs: + gate: + if: ${{ !github.event.pull_request.draft }} + runs-on: ubuntu-latest +""" +READY = "on:\n pull_request:\n types: [opened, synchronize, reopened, ready_for_review]\n" + + +def test_draft_trigger_ok() -> None: + assert check.draft_trigger_problems(P, doc(READY + DRAFT_JOB)) == [] + + +def test_draft_trigger_default_types() -> None: + [problem] = check.draft_trigger_problems(P, doc("on:\n pull_request:\n" + DRAFT_JOB)) + assert "ready_for_review" in problem + + +def test_draft_trigger_ignores_workflows_without_a_draft_guard() -> None: + plain = doc("on:\n pull_request:\njobs:\n gate:\n runs-on: ubuntu-latest\n") + assert check.draft_trigger_problems(P, plain) == [] + + # --- labels that start automation stay off issue forms ------------------------------------