Skip to content
Open
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
8 changes: 6 additions & 2 deletions .github/workflows/pr-review-merge-scheduler.yml
Original file line number Diff line number Diff line change
Expand Up @@ -854,8 +854,12 @@ jobs:
# PR_REVIEW_MERGE_TOKEN does not cover it. The automation can never
# merge those PRs regardless, so this is a skipped, non-fatal
# "unavailable" repository, not a failure the sweep can act on. Any
# other non-zero exit is a genuine per-repository failure.
if printf '%s' "$sweep_output" | grep -qF "Resource not accessible by integration"; then
# other non-zero exit is a genuine per-repository failure. A
# versioned scheduler payload proves the repository was readable
# and the failure was an attempted per-PR action, even when that
# action's API error contains the same 403 wording.
if printf '%s' "$sweep_output" | grep -qF "Resource not accessible by integration" &&
! printf '%s' "$sweep_output" | grep -qF '"schema_version": "pr-review-merge-scheduler/v2"'; then
echo "::warning::Skipping ${repo_full_name}: the sweep credential lacks access (HTTP 403 Resource not accessible by integration). Install the OpenCode app on this repository or grant PR_REVIEW_MERGE_TOKEN access to include it in the sweep."
unavailable=$((unavailable + 1))
unavailable_repos+=("$repo_full_name")
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ Semantic Versioning where the repository publishes a release.

### Fixed

- Made pull-request scheduler mutation and dispatch failures fail the targeted
workflow and organization sweep after the complete structured decision
summary is emitted, while ordinary policy waits remain successful.
- Bounded the Strix quality self-test's deterministic timeout fixtures to 3-second process and 5-second fake-sleep budgets so exact-head policy evidence completes inside the existing job limit without changing production Strix scanner timeouts, providers, credentials, or review semantics.
- Allowed commas and ASCII parentheses in the bounded Strix changed-file path policy so legal tracked Packrat fixtures can receive exact-head security analysis, while rejecting raw `..` components before normalization and keeping controls, backslashes, whitespace ambiguity, and shell punctuation fail-closed.
- Bound each review-agent invocation key to the wrapper's complete canonical payload, including the base branch and requesting actor; altered fields with a valid-format key now fail before durable-leader election or forwarding, and wrapper write permission is job-scoped.
Expand Down
24 changes: 24 additions & 0 deletions docs/doctoring/pr-review-merge-scheduler.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# PR review and merge scheduler

## Terminal result policy

The scheduler isolates a failed mutation or dispatch to its pull request and
continues the bounded scan. It emits the human-readable lines, job summary, and
versioned JSON decision payload for every inspected pull request before choosing
the process result.

An `action_error` is a material execution failure, so one or more such decisions
produce a non-zero terminal result after the summary is written. Policy outcomes
such as `wait`, `block`, `skip`, and deferred capacity do not make an otherwise
healthy scheduler invocation fail.

A targeted single-pull-request run and the organization sweep use the same
terminal policy. The organization sweep preserves each repository's captured
summary, records that repository as failed, finishes its bounded repository
walk, and then fails the job. A repository is classified as unavailable only
when the scheduler fails before emitting its versioned structured payload and
the error proves that the sweep credential cannot read the repository.

This separation keeps ordinary governance waits visible without reporting them
as incidents, while preventing a failed merge, update, auto-merge, or review
dispatch from producing a passing workflow result.
13 changes: 12 additions & 1 deletion scripts/ci/pr_review_merge_scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -2774,6 +2774,17 @@ def print_summary(
)


def scheduler_exit_code(decisions: list[Decision]) -> int:
"""Return failure after a complete scan when a requested action failed.

Ordinary policy outcomes remain successful scheduler executions. A caught
``action_error`` is different: the scheduler attempted a mutation or
dispatch and could not complete it. The caller must receive that failure
only after :func:`print_summary` has preserved every per-PR decision.
"""
return 1 if any(decision.action == "action_error" for decision in decisions) else 0


def markdown_cell(value: object) -> str:
"""Escape a value for a compact GitHub Actions summary table cell."""
return str(value).replace("|", "\\|").replace("\n", "<br>")
Expand Down Expand Up @@ -3792,7 +3803,7 @@ def main(argv: list[str]) -> int:
base_branch=args.base_branch,
project_flow=args.project_flow,
)
return 0
return scheduler_exit_code(decisions)


if __name__ == "__main__": # pragma: no cover
Expand Down
4 changes: 2 additions & 2 deletions tests/test_pr_review_merge_scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -4472,7 +4472,7 @@ def fake_inspect(repo, pr, **kwargs):
monkeypatch.setattr(sched, "fetch_open_prs", lambda repo, max_prs: prs)
monkeypatch.setattr(sched, "inspect_pr", fake_inspect)

assert sched.main(["--repo", "owner/repo", "--base-branch", "main", "--project-flow", "github"]) == 0
assert sched.main(["--repo", "owner/repo", "--base-branch", "main", "--project-flow", "github"]) == 1
assert seen == [1, 2]
output = capsys.readouterr().out
assert "PR #1: action_error: Command failed (1): gh pr merge 1; GraphQL: Resource not accessible by integration" in output
Expand Down Expand Up @@ -4561,7 +4561,7 @@ def fake_inspect(repo, pr, **kwargs):
monkeypatch.setattr(sched, "fetch_open_prs", lambda repo, max_prs: prs)
monkeypatch.setattr(sched, "inspect_pr", fake_inspect)

assert sched.main(["--repo", "owner/repo", "--base-branch", "main", "--project-flow", "github"]) == 0
assert sched.main(["--repo", "owner/repo", "--base-branch", "main", "--project-flow", "github"]) == 1
assert seen == [1, 2, 3]
output = capsys.readouterr().out
assert "PR #1: action_error:" in output
Expand Down
31 changes: 31 additions & 0 deletions tests/test_required_workflow_queue_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -803,6 +803,7 @@ def test_org_queue_sweep_treats_inaccessible_repositories_as_non_fatal() -> None
# The 403 signal is classified as a skipped, non-fatal "unavailable" repo.
assert "ORG_SWEEP_MAX_UNAVAILABLE" in workflow
assert 'grep -qF "Resource not accessible by integration"' in workflow
assert "grep -qF '\"schema_version\": \"pr-review-merge-scheduler/v2\"'" in workflow
assert "unavailable=$((unavailable + 1))" in workflow
assert 'unavailable_repos+=("$repo_full_name")' in workflow
assert "the sweep credential lacks access (HTTP 403" in workflow
Expand All @@ -819,6 +820,36 @@ def test_org_queue_sweep_treats_inaccessible_repositories_as_non_fatal() -> None
assert "ORG_SWEEP_MAX_UNAVAILABLE must be a non-negative integer" in workflow


def test_scheduler_action_errors_propagate_after_structured_summary() -> None:
"""Targeted and organization scans must fail after retaining their summary."""
workflow = workflow_text("pr-review-merge-scheduler.yml")

targeted = workflow.split(" - name: Inspect PR review and merge queue", 1)[1].split(
"\n org-queue-sweep:", 1
)[0]
assert 'python3 scripts/ci/pr_review_merge_scheduler.py "${args[@]}"' in targeted
assert "continue-on-error: true" not in targeted
assert "|| true" not in targeted

org_sweep = workflow.split(" org-queue-sweep:", 1)[1]
assert 'sweep_output="$(python3 scripts/ci/pr_review_merge_scheduler.py "${args[@]}" 2>&1)"' in org_sweep
assert "sweep_rc=$?" in org_sweep
assert 'if [ "$sweep_rc" -ne 0 ]; then' in org_sweep
assert "grep -qF '\"schema_version\": \"pr-review-merge-scheduler/v2\"'" in org_sweep
assert "failures=$((failures + 1))" in org_sweep


def test_scheduler_exit_policy_is_documented() -> None:
policy = (REPO_ROOT / "docs/doctoring/pr-review-merge-scheduler.md").read_text(
encoding="utf-8"
)

assert "action_error" in policy
assert "non-zero" in policy
assert "targeted" in policy
assert "organization sweep" in policy


def test_fix_scheduler_cancels_superseded_cron_runs() -> None:
workflow = workflow_text("pr-review-fix-scheduler.yml")

Expand Down
Loading