diff --git a/.github/workflows/strix.yml b/.github/workflows/strix.yml index f15b29f564..4ee8c668a9 100644 --- a/.github/workflows/strix.yml +++ b/.github/workflows/strix.yml @@ -129,7 +129,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_name != 'pull_request_target' || (github.event.action != 'closed' && github.event.action != 'converted_to_draft') + if: github.event_name != 'pull_request_target' || (github.event.action != 'closed' && github.event.action != 'converted_to_draft' && (github.event.pull_request.draft == false || github.repository != 'ContextualWisdomLab/.github')) runs-on: ubuntu-24.04 timeout-minutes: 5 permissions: @@ -184,9 +184,14 @@ jobs: admit-current-head: name: Admit current pull request head + # Keep exact-head admission for every non-Draft PR while leaving push, + # schedule, and repository_dispatch paths unchanged. if: >- github.event_name != 'pull_request_target' || - (github.event.action != 'closed' && github.event.action != 'converted_to_draft') + (github.event.action != 'closed' && + github.event.action != 'converted_to_draft' && + (github.event.pull_request.draft == false || + github.repository != 'ContextualWisdomLab/.github')) runs-on: ubuntu-24.04 timeout-minutes: 5 permissions: @@ -241,9 +246,15 @@ jobs: } >> "$GITHUB_OUTPUT" cancel-superseded-pr-runs: + # A converted_to_draft run is intentionally runner-free: workflow-level + # cancel-in-progress retires the prior Ready generation before admission. + # Keep the bounded API cleanup for non-Draft synchronize and closed events. if: >- github.event_name == 'pull_request_target' && - (github.event.action == 'synchronize' || github.event.action == 'converted_to_draft' || github.event.action == 'closed') + (github.event.action == 'closed' || + (github.event.action == 'synchronize' && + (github.event.pull_request.draft == false || + github.repository != 'ContextualWisdomLab/.github'))) # Idempotent per PR: a fresh sweep re-verifies live state (live_target_matches # below) before selecting or cancelling anything, so it fully subsumes # whatever an older, not-yet-run instance would have done. cancel-in-progress diff --git a/CHANGELOG.d/20260925-strix-draft-admission.md b/CHANGELOG.d/20260925-strix-draft-admission.md new file mode 100644 index 0000000000..5e4d3285a2 --- /dev/null +++ b/CHANGELOG.d/20260925-strix-draft-admission.md @@ -0,0 +1,12 @@ +### Strix skips Draft pull-request admission + +- Native `.github` Draft `pull_request_target` generations now skip Strix + metadata and scanning jobs without a runner; ruleset-launched runs in other + repositories always admit them because those runs do not re-trigger on + `ready_for_review`. Native `ready_for_review` creates the fresh exact-head + scan. +- Non-Draft PR pushes, forced `repository_dispatch`, push, and scheduled scans + remain admitted. `converted_to_draft` still cancels the prior Ready + generation through workflow concurrency without admitting a cleanup runner. +- The synchronous model job remains intentionally unbounded under the + repository's progress-based Strix occupancy policy. diff --git a/docs/doctoring/strix-draft-admission.md b/docs/doctoring/strix-draft-admission.md new file mode 100644 index 0000000000..24e6657c10 --- /dev/null +++ b/docs/doctoring/strix-draft-admission.md @@ -0,0 +1,52 @@ +# Strix Draft pull-request admission + +## Decision + +`.github/workflows/strix.yml` evaluates Draft state at job level only for +native runs in `ContextualWisdomLab/.github`. Draft `opened`, `synchronize`, +and `reopened` generations there skip both metadata jobs, so the dependent +`strix` job is skipped without a runner. `ready_for_review` remains in the +trigger types and carries `draft == false`, so it admits a fresh exact-head +metadata generation and then the real scan. In ruleset-covered repositories, +the repository guard always admits the metadata jobs: ruleset-launched runs +ignore `types` and do not re-trigger on `ready_for_review`, so skipping there +would leave a Draft PR with no later required scan. + +Non-PR `push`, `schedule`, and `repository_dispatch` events remain admitted. + +`converted_to_draft` remains a trigger event so workflow-level +`cancel-in-progress` retires an older Ready generation. Its replacement is +runner-free: the explicit API cleanup job is not admitted for that event. +Closed PRs and non-Draft synchronize events retain the existing cleanup path, +including its live-target and superseded-run checks. + +This preserves the required `strix` check shape for non-Draft PRs and forced +repository-dispatch scans. A native `.github` Draft run produces skipped job +conclusions rather than leaving an uncreated trigger-level check Pending; +ruleset-targeted repositories continue to run the required scan even while +Draft because their workflow cannot depend on `ready_for_review` re-entry. + +## Timeout decision + +The `strix` job still has no job-level `timeout-minutes`. This was verified +against the workflow and the existing timeout contracts. The job runs the +model synchronously and explicitly sets `LLM_TIMEOUT`, +`STRIX_MEMORY_COMPRESSOR_TIMEOUT`, `STRIX_PROCESS_TIMEOUT_SECONDS`, and +`STRIX_TOTAL_TIMEOUT_SECONDS` to zero. The repository's standing model-path +policy accepts central Strix work taking more than two hours and rejects +elapsed inference caps; the active Strix occupancy record therefore uses +progress-based transport release rather than a wall-clock job deadline. + +Adding a job timeout here would terminate legitimate large-repository analysis +and contradict that policy. The short job-level limits on `changed-scope`, +`admit-current-head`, and the superseded-run cleanup remain because those jobs +perform bounded metadata/API work, not model inference. + +## Scope and follow-up + +This repair changes only Strix Draft admission and documents the timeout +decision. Security Scan, SAST Semgrep, and CodeQL use the same +`.github`-only Draft boundary in the coordinated follow-up; Python Security +and Agent Review Runtime Quality are not ruleset-required and retain their +direct-run Draft guards. No metadata-job consolidation is included; that +remains a separate queue-reduction concern. diff --git a/tests/test_required_workflow_queue_contract.py b/tests/test_required_workflow_queue_contract.py index 87277d45f5..7bda18c76d 100644 --- a/tests/test_required_workflow_queue_contract.py +++ b/tests/test_required_workflow_queue_contract.py @@ -1158,6 +1158,42 @@ def test_review_workflow_completions_do_not_spawn_scheduler_runs() -> None: assert "github.event.workflow_run" not in workflow +def test_strix_draft_pr_events_skip_runner_admission_until_ready() -> None: + """Draft PR generations skip every Strix runner job until review admission.""" + workflow = workflow_text("strix.yml") + + def job_block(job_name: str) -> str: + match = re.search( + rf"(?ms)^ {re.escape(job_name)}:\n(.*?)(?=^ [A-Za-z0-9_-]+:\s*$|\Z)", + workflow, + ) + assert match is not None, job_name + return match.group(1) + + for job_name in ("changed-scope", "admit-current-head"): + block = job_block(job_name) + assert "github.event.pull_request.draft == false" in block + assert "github.repository != 'ContextualWisdomLab/.github'" in block + assert "github.event_name != 'pull_request_target'" in block + + cleanup_block = job_block("cancel-superseded-pr-runs") + cleanup_if_start = cleanup_block.index(" if:") + cleanup_header = cleanup_block[ + cleanup_if_start : cleanup_block.index(" runs-on:", cleanup_if_start) + ] + assert "github.event.action == 'closed'" in cleanup_header + assert "github.event.action == 'synchronize'" in cleanup_header + assert "github.event.pull_request.draft == false" in cleanup_header + assert "github.repository != 'ContextualWisdomLab/.github'" in cleanup_header + assert "github.event.action == 'converted_to_draft'" not in cleanup_header + + strix = job_block("strix") + assert "needs: [changed-scope, admit-current-head]" in strix + assert "needs.changed-scope.outputs.code == 'true'" in strix + assert "needs.admit-current-head.outputs.admitted == 'true'" in strix + assert "ready_for_review" in workflow + assert "converted_to_draft" in workflow + def test_required_workflow_trusted_source_refs_are_not_input_controlled() -> None: """Ensure privileged workflows resolve trusted source code independently of inputs.""" for filename in (