From 045af4b37300640f6472d3dda7a215a2b236ed9c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 25 Sep 2026 16:13:42 +0000 Subject: [PATCH 1/4] fix(ci): skip Draft heavy workflow admission Co-authored-by: Seongho Bae --- .../agent-review-runtime-quality-ci.yml | 2 + .github/workflows/codeql-pr.yml | 4 +- .github/workflows/python-security.yml | 7 ++- .github/workflows/sast-semgrep.yml | 7 ++- .github/workflows/security-scan.yml | 4 +- ...60925-heavy-pr-workflow-draft-admission.md | 8 ++++ .../heavy-pr-workflow-draft-admission.md | 44 +++++++++++++++++++ .../test_required_workflow_queue_contract.py | 32 ++++++++++++++ 8 files changed, 100 insertions(+), 8 deletions(-) create mode 100644 CHANGELOG.d/20260925-heavy-pr-workflow-draft-admission.md create mode 100644 docs/doctoring/heavy-pr-workflow-draft-admission.md diff --git a/.github/workflows/agent-review-runtime-quality-ci.yml b/.github/workflows/agent-review-runtime-quality-ci.yml index a601e25522..9f3850031b 100644 --- a/.github/workflows/agent-review-runtime-quality-ci.yml +++ b/.github/workflows/agent-review-runtime-quality-ci.yml @@ -2,6 +2,7 @@ name: Agent Review Runtime Quality CI on: pull_request: + types: [opened, synchronize, reopened, ready_for_review, converted_to_draft, closed] branches: [main] paths: - ".github/workflows/agent-review-runtime-quality-ci.yml" @@ -126,6 +127,7 @@ permissions: jobs: agent_review_runtime_quality: name: agent-review-runtime-quality + if: github.event.action != 'closed' && github.event.pull_request.draft == false runs-on: ubuntu-24.04 timeout-minutes: 25 env: diff --git a/.github/workflows/codeql-pr.yml b/.github/workflows/codeql-pr.yml index cc13d2d87e..2f63467a79 100644 --- a/.github/workflows/codeql-pr.yml +++ b/.github/workflows/codeql-pr.yml @@ -15,7 +15,7 @@ name: CodeQL PR on: pull_request: - types: [opened, synchronize, reopened, ready_for_review, closed] + types: [opened, synchronize, reopened, ready_for_review, converted_to_draft, closed] # Do not restrict the base ref: the org required-workflow ruleset already # scopes this to each repository's actual default branch via # ref_name: ["~DEFAULT_BRANCH"], whatever it is named. A hardcoded @@ -55,7 +55,7 @@ permissions: jobs: detect-languages: name: Detect CodeQL languages - if: github.event.action != 'closed' + if: github.event.action != 'closed' && github.event.pull_request.draft == false runs-on: ubuntu-24.04 permissions: contents: read diff --git a/.github/workflows/python-security.yml b/.github/workflows/python-security.yml index 1788cfd40b..3e3792ab5a 100644 --- a/.github/workflows/python-security.yml +++ b/.github/workflows/python-security.yml @@ -24,7 +24,7 @@ name: Python Security on: pull_request: - types: [opened, synchronize, reopened, ready_for_review, closed] + types: [opened, synchronize, reopened, ready_for_review, converted_to_draft, closed] branches: [main, master, develop] push: branches: [main, master, develop] @@ -45,7 +45,10 @@ permissions: jobs: detect-python: name: Detect Python - if: github.event.action != 'closed' + if: >- + github.event_name != 'pull_request' || + (github.event.action != 'closed' && + github.event.pull_request.draft == false) runs-on: ubuntu-24.04 outputs: has_python: ${{ steps.detect.outputs.has_python }} diff --git a/.github/workflows/sast-semgrep.yml b/.github/workflows/sast-semgrep.yml index f8ab04b865..a4ae6b4694 100644 --- a/.github/workflows/sast-semgrep.yml +++ b/.github/workflows/sast-semgrep.yml @@ -21,7 +21,7 @@ name: SAST Semgrep on: pull_request: - types: [opened, synchronize, reopened, ready_for_review, closed] + types: [opened, synchronize, reopened, ready_for_review, converted_to_draft, closed] # Scan every PR base ref, including feature branches used by stacked PRs. push: branches: [main, master, develop] @@ -49,7 +49,10 @@ jobs: # docs/doctoring/required-workflow-path-filter-boundary.md. # Fails OPEN: an unreadable, empty, or truncated file list scans everything. # The gate lives inside this job as a step-level guard (one runner, not two). - if: github.event.action != 'closed' + if: >- + github.event_name != 'pull_request' || + (github.event.action != 'closed' && + github.event.pull_request.draft == false) runs-on: ubuntu-24.04 permissions: contents: read diff --git a/.github/workflows/security-scan.yml b/.github/workflows/security-scan.yml index e04d7bf8f3..9803dc5baf 100644 --- a/.github/workflows/security-scan.yml +++ b/.github/workflows/security-scan.yml @@ -41,7 +41,7 @@ name: Security Scan on: pull_request: - types: [opened, synchronize, reopened, ready_for_review, closed] + types: [opened, synchronize, reopened, ready_for_review, converted_to_draft, closed] # Do not restrict the base ref: stacked PRs must receive the same # diff-scoped OSV/dependency and repo-wide Trivy gate as default-branch PRs. @@ -70,7 +70,7 @@ jobs: # here and consumed through `needs`. See # docs/doctoring/required-workflow-path-filter-boundary.md. # Fails OPEN: an unreadable, empty, or truncated file list scans everything. - if: github.event.action != 'closed' + if: github.event.action != 'closed' && github.event.pull_request.draft == false runs-on: ubuntu-24.04 timeout-minutes: 5 permissions: diff --git a/CHANGELOG.d/20260925-heavy-pr-workflow-draft-admission.md b/CHANGELOG.d/20260925-heavy-pr-workflow-draft-admission.md new file mode 100644 index 0000000000..21d6ec8b69 --- /dev/null +++ b/CHANGELOG.d/20260925-heavy-pr-workflow-draft-admission.md @@ -0,0 +1,8 @@ +### Heavy required PR workflows skip Draft admission + +- Security Scan, SAST Semgrep, CodeQL PR, Python Security, and Agent Review + Runtime Quality now skip their first runner-consuming job while a PR is + Draft and re-enter on `ready_for_review`. +- `converted_to_draft` creates a runner-free replacement generation while + existing PR concurrency cancellation retires the prior Ready work. +- Push, schedule, and repository-dispatch paths remain unchanged. diff --git a/docs/doctoring/heavy-pr-workflow-draft-admission.md b/docs/doctoring/heavy-pr-workflow-draft-admission.md new file mode 100644 index 0000000000..136b7d0195 --- /dev/null +++ b/docs/doctoring/heavy-pr-workflow-draft-admission.md @@ -0,0 +1,44 @@ +# Heavy pull-request workflow Draft admission + +## Decision + +The first runner-consuming job in each of these pull-request workflows now +requires a non-Draft pull request: + +- `security-scan.yml`: `changed-scope` +- `sast-semgrep.yml`: `semgrep` +- `codeql-pr.yml`: `detect-languages` +- `python-security.yml`: `detect-python` +- `agent-review-runtime-quality-ci.yml`: `agent_review_runtime_quality` + +The PR triggers retain `ready_for_review` and now also subscribe to +`converted_to_draft`. A Draft `opened`, `synchronize`, or `reopened` event +therefore produces skipped job conclusions without admitting a runner. A +`ready_for_review` event has `draft == false` and creates a fresh generation. +The Draft conversion generation remains runner-free while each workflow's +repository-and-PR `cancel-in-progress: true` concurrency group retires the +previous Ready generation. Closed events remain subscribed where needed so +workflow cancellation still retires superseded work. + +Mixed-event workflows use a non-PR bypass in their job condition. Push, +schedule, and `repository_dispatch` scans in SAST Semgrep and Python Security +therefore remain unchanged. Security Scan, CodeQL PR, and Runtime Quality are +pull-request workflows, so their new condition is PR-specific by construction. + +CodeQL gates `detect-languages`, not the matrix-consuming `analyze-head` job. +The existing matrix safety boundary remains intact: `analyze-head` has no +needs-output-dependent job-level condition, avoiding literal unexpanded +matrix check names. When language detection is skipped for a Draft, downstream +analysis is skipped through its existing dependency. + +Every changed workflow still has a job-level conclusion for the lifecycle run; +no trigger-level Draft filter was introduced. Required contexts therefore +receive skipped conclusions rather than being left Pending by an absent +workflow invocation. + +## Scope + +This change does not alter exact-head checkout or admission logic, scanner +steps, concurrency keys, non-PR triggers, timeout budgets, or metadata-job +consolidation. The Agent Review Runtime Quality path filters and `main` base +branch restriction remain unchanged. diff --git a/tests/test_required_workflow_queue_contract.py b/tests/test_required_workflow_queue_contract.py index 87277d45f5..0f3d8a89e5 100644 --- a/tests/test_required_workflow_queue_contract.py +++ b/tests/test_required_workflow_queue_contract.py @@ -1142,6 +1142,38 @@ def test_pull_request_close_events_cancel_superseded_runs_without_heavy_jobs() - assert workflow_level_cancels_in_progress(strix_workflow) +def test_heavy_pr_workflows_skip_drafts_and_reenter_on_ready() -> None: + """Heavy PR workflows gate their first runner job on Draft state.""" + workflow_specs = ( + ("security-scan.yml", "changed-scope", False), + ("sast-semgrep.yml", "semgrep", True), + ("codeql-pr.yml", "detect-languages", False), + ("python-security.yml", "detect-python", True), + ("agent-review-runtime-quality-ci.yml", "agent_review_runtime_quality", False), + ) + + for filename, entry_job, mixed_events in workflow_specs: + workflow = workflow_text(filename) + job_match = re.search( + rf"(?ms)^ {re.escape(entry_job)}:\n(.*?)(?=^ [A-Za-z0-9_-]+:\s*$|\Z)", + workflow, + ) + assert job_match is not None, (filename, entry_job) + job = job_match.group(1) + + assert "github.event.pull_request.draft == false" in job, filename + assert not re.search(r"(?m)^ needs:", job), filename + assert "ready_for_review" in workflow, filename + assert "converted_to_draft" in workflow, filename + assert workflow_level_cancels_in_progress(workflow), filename + + if mixed_events: + assert "github.event_name != 'pull_request'" in job, filename + assert "push:" in workflow, filename + assert "schedule:" in workflow, filename + assert "repository_dispatch:" in workflow, filename + + def test_merge_scheduler_owns_empty_pr_cleanup_without_checkout() -> None: """Keep empty-PR cleanup in the existing metadata-only scheduler job.""" workflow = workflow_text("pr-review-merge-scheduler.yml") From ad5d218cd6dad94919f74680369d4533bb304322 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 25 Sep 2026 16:14:37 +0000 Subject: [PATCH 2/4] test(ci): align Semgrep Draft guard contract Co-authored-by: Seongho Bae --- .github/workflows/sast-semgrep.yml | 5 +---- tests/test_docs_only_pr_runner_admission.py | 6 +++++- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/sast-semgrep.yml b/.github/workflows/sast-semgrep.yml index a4ae6b4694..0bd09d720f 100644 --- a/.github/workflows/sast-semgrep.yml +++ b/.github/workflows/sast-semgrep.yml @@ -49,10 +49,7 @@ jobs: # docs/doctoring/required-workflow-path-filter-boundary.md. # Fails OPEN: an unreadable, empty, or truncated file list scans everything. # The gate lives inside this job as a step-level guard (one runner, not two). - if: >- - github.event_name != 'pull_request' || - (github.event.action != 'closed' && - github.event.pull_request.draft == false) + if: github.event_name != 'pull_request' || (github.event.action != 'closed' && github.event.pull_request.draft == false) runs-on: ubuntu-24.04 permissions: contents: read diff --git a/tests/test_docs_only_pr_runner_admission.py b/tests/test_docs_only_pr_runner_admission.py index 674b984b63..ec69fc161a 100644 --- a/tests/test_docs_only_pr_runner_admission.py +++ b/tests/test_docs_only_pr_runner_admission.py @@ -248,7 +248,11 @@ def test_sast_semgrep_folds_the_gate_into_its_single_consumer_at_step_level(): assert not re.search(r"(?m)^ needs:", semgrep) job_if = re.search(r"(?m)^ if: (.*)$", semgrep) assert job_if is not None - assert job_if.group(1) == "github.event.action != 'closed'" + assert job_if.group(1) == ( + "github.event_name != 'pull_request' || " + "(github.event.action != 'closed' && " + "github.event.pull_request.draft == false)" + ) assert "pull-requests: read" in semgrep assert "id: scope" in semgrep assert semgrep.count("steps.scope.outputs.code == 'true'") == 5 From 14aea0d97ac5bcbe29af603874d06841076039df Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 25 Sep 2026 16:37:55 +0000 Subject: [PATCH 3/4] fix(ci): scope Draft skips to native workflows Co-authored-by: Seongho Bae --- .github/workflows/codeql-pr.yml | 2 +- .github/workflows/sast-semgrep.yml | 2 +- .github/workflows/security-scan.yml | 2 +- ...60925-heavy-pr-workflow-draft-admission.md | 9 ++-- .../heavy-pr-workflow-draft-admission.md | 33 +++++++------ .../required-workflow-path-filter-boundary.md | 46 +++++++++++++------ tests/test_docs_only_pr_runner_admission.py | 3 +- .../test_required_workflow_queue_contract.py | 16 ++++--- 8 files changed, 71 insertions(+), 42 deletions(-) diff --git a/.github/workflows/codeql-pr.yml b/.github/workflows/codeql-pr.yml index 2f63467a79..f3639ce021 100644 --- a/.github/workflows/codeql-pr.yml +++ b/.github/workflows/codeql-pr.yml @@ -55,7 +55,7 @@ permissions: jobs: detect-languages: name: Detect CodeQL languages - if: github.event.action != 'closed' && github.event.pull_request.draft == false + if: github.event.action != 'closed' && (github.event.pull_request.draft == false || github.repository != 'ContextualWisdomLab/.github') runs-on: ubuntu-24.04 permissions: contents: read diff --git a/.github/workflows/sast-semgrep.yml b/.github/workflows/sast-semgrep.yml index 0bd09d720f..1e58d14e5b 100644 --- a/.github/workflows/sast-semgrep.yml +++ b/.github/workflows/sast-semgrep.yml @@ -49,7 +49,7 @@ jobs: # docs/doctoring/required-workflow-path-filter-boundary.md. # Fails OPEN: an unreadable, empty, or truncated file list scans everything. # The gate lives inside this job as a step-level guard (one runner, not two). - if: github.event_name != 'pull_request' || (github.event.action != 'closed' && github.event.pull_request.draft == false) + if: github.event_name != 'pull_request' || (github.event.action != 'closed' && (github.event.pull_request.draft == false || github.repository != 'ContextualWisdomLab/.github')) runs-on: ubuntu-24.04 permissions: contents: read diff --git a/.github/workflows/security-scan.yml b/.github/workflows/security-scan.yml index 9803dc5baf..76082c3cf0 100644 --- a/.github/workflows/security-scan.yml +++ b/.github/workflows/security-scan.yml @@ -70,7 +70,7 @@ jobs: # here and consumed through `needs`. See # docs/doctoring/required-workflow-path-filter-boundary.md. # Fails OPEN: an unreadable, empty, or truncated file list scans everything. - if: github.event.action != 'closed' && github.event.pull_request.draft == false + if: github.event.action != 'closed' && (github.event.pull_request.draft == false || github.repository != 'ContextualWisdomLab/.github') runs-on: ubuntu-24.04 timeout-minutes: 5 permissions: diff --git a/CHANGELOG.d/20260925-heavy-pr-workflow-draft-admission.md b/CHANGELOG.d/20260925-heavy-pr-workflow-draft-admission.md index 21d6ec8b69..644bfd8af8 100644 --- a/CHANGELOG.d/20260925-heavy-pr-workflow-draft-admission.md +++ b/CHANGELOG.d/20260925-heavy-pr-workflow-draft-admission.md @@ -1,8 +1,11 @@ ### Heavy required PR workflows skip Draft admission -- Security Scan, SAST Semgrep, CodeQL PR, Python Security, and Agent Review - Runtime Quality now skip their first runner-consuming job while a PR is - Draft and re-enter on `ready_for_review`. +- Security Scan, SAST Semgrep, and CodeQL PR skip their first + runner-consuming job for Draft PRs only in native `.github` runs; ruleset + launches bypass the Draft condition because they do not re-enter on + `ready_for_review`. Python Security and Agent Review Runtime Quality retain + their direct-run Draft guards. +- Native `.github` runs re-enter on `ready_for_review`. - `converted_to_draft` creates a runner-free replacement generation while existing PR concurrency cancellation retires the prior Ready work. - Push, schedule, and repository-dispatch paths remain unchanged. diff --git a/docs/doctoring/heavy-pr-workflow-draft-admission.md b/docs/doctoring/heavy-pr-workflow-draft-admission.md index 136b7d0195..77ea3569da 100644 --- a/docs/doctoring/heavy-pr-workflow-draft-admission.md +++ b/docs/doctoring/heavy-pr-workflow-draft-admission.md @@ -3,27 +3,32 @@ ## Decision The first runner-consuming job in each of these pull-request workflows now -requires a non-Draft pull request: +has a Draft lifecycle guard: -- `security-scan.yml`: `changed-scope` -- `sast-semgrep.yml`: `semgrep` -- `codeql-pr.yml`: `detect-languages` -- `python-security.yml`: `detect-python` -- `agent-review-runtime-quality-ci.yml`: `agent_review_runtime_quality` +- Native `.github` runs: `security-scan.yml` (`changed-scope`), SAST + Semgrep (`semgrep`), and CodeQL PR (`detect-languages`) require a + non-Draft pull request. +- Direct-run-only workflows: Python Security (`detect-python`) and Agent + Review Runtime Quality (`agent_review_runtime_quality`) retain their + non-Draft guards without a repository bypass. The PR triggers retain `ready_for_review` and now also subscribe to `converted_to_draft`. A Draft `opened`, `synchronize`, or `reopened` event -therefore produces skipped job conclusions without admitting a runner. A -`ready_for_review` event has `draft == false` and creates a fresh generation. -The Draft conversion generation remains runner-free while each workflow's +in native `.github` runs therefore produces skipped job conclusions without +admitting a runner. A native `ready_for_review` event has `draft == false` and +creates a fresh generation. Ruleset-launched Security Scan, SAST, and CodeQL +runs bypass the Draft condition because their ignored `types` filter does not +re-trigger them on `ready_for_review`. The Draft conversion generation +remains runner-free while each workflow's repository-and-PR `cancel-in-progress: true` concurrency group retires the previous Ready generation. Closed events remain subscribed where needed so workflow cancellation still retires superseded work. Mixed-event workflows use a non-PR bypass in their job condition. Push, schedule, and `repository_dispatch` scans in SAST Semgrep and Python Security -therefore remain unchanged. Security Scan, CodeQL PR, and Runtime Quality are -pull-request workflows, so their new condition is PR-specific by construction. +therefore remain unchanged. Security Scan and CodeQL PR use the repository +bypass because they are ruleset-required; Runtime Quality remains direct-run +only and keeps its existing branch/path filters. CodeQL gates `detect-languages`, not the matrix-consuming `analyze-head` job. The existing matrix safety boundary remains intact: `analyze-head` has no @@ -32,9 +37,9 @@ matrix check names. When language detection is skipped for a Draft, downstream analysis is skipped through its existing dependency. Every changed workflow still has a job-level conclusion for the lifecycle run; -no trigger-level Draft filter was introduced. Required contexts therefore -receive skipped conclusions rather than being left Pending by an absent -workflow invocation. +no trigger-level Draft filter was introduced. Native `.github` Draft runs +receive skipped conclusions, while ruleset-targeted required contexts remain +admitted and continue to produce security evidence. ## Scope diff --git a/docs/doctoring/required-workflow-path-filter-boundary.md b/docs/doctoring/required-workflow-path-filter-boundary.md index 65abac141a..d6245b7fba 100644 --- a/docs/doctoring/required-workflow-path-filter-boundary.md +++ b/docs/doctoring/required-workflow-path-filter-boundary.md @@ -163,24 +163,37 @@ an output check into their existing `if:`. `codeql-pr.yml`'s step-level guards on `analyze-head` and a job-level guard on `analyze-merge`. This works in both contexts that trigger-level filtering could not satisfy -simultaneously: +simultaneously, but Draft lifecycle admission has an explicit repository +boundary: - **Ruleset-injected repos:** the ruleset ignores `on:` filters, but it cannot skip a job's own `if:` evaluation -- that happens inside the run GitHub Actions actually executes, after admission, using that target - repository's real PR event payload. -- **`.github` classic protection:** the job **always runs** (its own `if:` - is event-based, not output-based) and always reports a conclusion -- - `success` when in scope, `skipped` when not -- so the named context is - never left Pending. + repository's real PR event payload. Draft guards therefore include a + `github.repository != 'ContextualWisdomLab/.github'` bypass. Ruleset-launched + runs do not re-trigger on `ready_for_review`, so skipping there would leave a + Draft-origin PR with no later required scan. +- **`.github` native protection:** the repository is excluded from the central + ruleset, so its native PR event types are honored. The same workflows may + skip their first job for Draft PRs locally; native `ready_for_review` then + creates the fresh generation and scan. The skipped job reports a terminal + conclusion rather than leaving a trigger-filtered context Pending. + +The Draft exception is deliberately `.github`-local. It is not a general +required-workflow policy: the guard must always pass in ruleset-targeted +repositories because GitHub does not replay those required workflows on +`ready_for_review`. The classifier fails **open**: an unreadable, empty, or truncated file list (including one that doesn't match the PR's own `changed_files` count, which -GitHub caps at 3000 entries per page) scans everything. Every one of the five -workflows keeps at least one job with no `needs:` and no output-dependent -`if:` (the `changed-scope` job itself, `cancel-superseded-pr-runs` also -qualifying in `strix.yml`), so a fully-skipped run still concludes -`success`, not the undocumented `skipped` conclusion. +GitHub caps at 3000 entries per page) scans everything. Every +ruleset-targeted gate workflow keeps at least one job with no `needs:` and no +output-dependent `if:` (the `changed-scope` job itself, +`cancel-superseded-pr-runs` also qualifying in `strix.yml`), so its run still +concludes `success`, not the undocumented `skipped` conclusion. Native +`.github` Draft lifecycle runs are the explicit exception: their job-level +Draft guards may produce skipped conclusions, and native `ready_for_review` +supplies the later scan. `LICENSE.*` was deliberately **not** reused from `strix.yml`'s existing doc-pattern list: it matches `LICENSE.py`, which is executable. The @@ -199,7 +212,8 @@ with `codeql-pr.yml`'s classifier step, `runs-on: ubuntu-24.04` on every gate job, no trigger-level `paths`/`paths-ignore` on any of the nine other required-adjacent workflows, the `closed`-guard-plus-needs-output shape on every gated job, `codeql-pr.yml`'s step-vs-job gating split, and the -always-admitted job in each of the five gate workflows. +always-admitted ruleset-target job in each gate workflow. Native `.github` +Draft skips are asserted separately with the repository guard. Post-merge, the operational proof is a docs-only PR in one ruleset-covered repository: `changed-scope` (and `detect-languages` for CodeQL) succeed while @@ -216,6 +230,8 @@ that make each skip safe are unchanged: `scheduled-security-scan.yml` run full, unfiltered scans of the default branch. `secret-scan.yml` is intentionally untouched (already diff-scoped and cheap; a leaked key in a `README.md` is the canonical case a doc-only skip would otherwise miss). -`codeql-pr.yml`'s `detect-languages` job keeps its unconditional `if:` -because gating it would destroy the two required CodeQL contexts, per the -matrix hazard above. +`codeql-pr.yml`'s `detect-languages` job remains unconditional with respect +to changed-scope output because gating the matrix-consuming analysis job would +destroy the two required CodeQL contexts, per the matrix hazard above. Its +Draft guard is native `.github`-only; the repository bypass keeps +ruleset-targeted detection admitted. diff --git a/tests/test_docs_only_pr_runner_admission.py b/tests/test_docs_only_pr_runner_admission.py index ec69fc161a..dab18392ec 100644 --- a/tests/test_docs_only_pr_runner_admission.py +++ b/tests/test_docs_only_pr_runner_admission.py @@ -251,7 +251,8 @@ def test_sast_semgrep_folds_the_gate_into_its_single_consumer_at_step_level(): assert job_if.group(1) == ( "github.event_name != 'pull_request' || " "(github.event.action != 'closed' && " - "github.event.pull_request.draft == false)" + "(github.event.pull_request.draft == false || " + "github.repository != 'ContextualWisdomLab/.github'))" ) assert "pull-requests: read" in semgrep assert "id: scope" in semgrep diff --git a/tests/test_required_workflow_queue_contract.py b/tests/test_required_workflow_queue_contract.py index 0f3d8a89e5..e3951af66c 100644 --- a/tests/test_required_workflow_queue_contract.py +++ b/tests/test_required_workflow_queue_contract.py @@ -1145,14 +1145,14 @@ def test_pull_request_close_events_cancel_superseded_runs_without_heavy_jobs() - def test_heavy_pr_workflows_skip_drafts_and_reenter_on_ready() -> None: """Heavy PR workflows gate their first runner job on Draft state.""" workflow_specs = ( - ("security-scan.yml", "changed-scope", False), - ("sast-semgrep.yml", "semgrep", True), - ("codeql-pr.yml", "detect-languages", False), - ("python-security.yml", "detect-python", True), - ("agent-review-runtime-quality-ci.yml", "agent_review_runtime_quality", False), + ("security-scan.yml", "changed-scope", False, True), + ("sast-semgrep.yml", "semgrep", True, True), + ("codeql-pr.yml", "detect-languages", False, True), + ("python-security.yml", "detect-python", True, False), + ("agent-review-runtime-quality-ci.yml", "agent_review_runtime_quality", False, False), ) - for filename, entry_job, mixed_events in workflow_specs: + for filename, entry_job, mixed_events, ruleset_required in workflow_specs: workflow = workflow_text(filename) job_match = re.search( rf"(?ms)^ {re.escape(entry_job)}:\n(.*?)(?=^ [A-Za-z0-9_-]+:\s*$|\Z)", @@ -1162,6 +1162,10 @@ def test_heavy_pr_workflows_skip_drafts_and_reenter_on_ready() -> None: job = job_match.group(1) assert "github.event.pull_request.draft == false" in job, filename + if ruleset_required: + assert "github.repository != 'ContextualWisdomLab/.github'" in job, filename + else: + assert "github.repository != 'ContextualWisdomLab/.github'" not in job, filename assert not re.search(r"(?m)^ needs:", job), filename assert "ready_for_review" in workflow, filename assert "converted_to_draft" in workflow, filename From 2633197d3ded3417c3acf97291a15a053d2b214b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 25 Sep 2026 16:38:49 +0000 Subject: [PATCH 4/4] test(ci): isolate follow-up admission contract Co-authored-by: Seongho Bae --- .../test_required_workflow_queue_contract.py | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/test_required_workflow_queue_contract.py b/tests/test_required_workflow_queue_contract.py index e3951af66c..603bdc192f 100644 --- a/tests/test_required_workflow_queue_contract.py +++ b/tests/test_required_workflow_queue_contract.py @@ -1142,6 +1142,16 @@ def test_pull_request_close_events_cancel_superseded_runs_without_heavy_jobs() - assert workflow_level_cancels_in_progress(strix_workflow) +def test_merge_scheduler_owns_empty_pr_cleanup_without_checkout() -> None: + """Keep empty-PR cleanup in the existing metadata-only scheduler job.""" + workflow = workflow_text("pr-review-merge-scheduler.yml") + scheduler = workflow_step(workflow, "Inspect PR review and merge queue") + + assert not (REPO_ROOT / ".github/workflows/close-empty-pr.yml").exists() + assert "pr_review_merge_scheduler.py" in scheduler + assert "actions/checkout" not in workflow + + def test_heavy_pr_workflows_skip_drafts_and_reenter_on_ready() -> None: """Heavy PR workflows gate their first runner job on Draft state.""" workflow_specs = ( @@ -1178,16 +1188,6 @@ def test_heavy_pr_workflows_skip_drafts_and_reenter_on_ready() -> None: assert "repository_dispatch:" in workflow, filename -def test_merge_scheduler_owns_empty_pr_cleanup_without_checkout() -> None: - """Keep empty-PR cleanup in the existing metadata-only scheduler job.""" - workflow = workflow_text("pr-review-merge-scheduler.yml") - scheduler = workflow_step(workflow, "Inspect PR review and merge queue") - - assert not (REPO_ROOT / ".github/workflows/close-empty-pr.yml").exists() - assert "pr_review_merge_scheduler.py" in scheduler - assert "actions/checkout" not in workflow - - def test_review_workflow_completions_do_not_spawn_scheduler_runs() -> None: """Required checks rely on GitHub auto-merge instead of a follow-up workflow.""" workflow = workflow_text("pr-review-merge-scheduler.yml")