diff --git a/.github/workflows/hourly-pr-maintenance.yml b/.github/workflows/hourly-pr-maintenance.yml new file mode 100644 index 000000000..f18e36e12 --- /dev/null +++ b/.github/workflows/hourly-pr-maintenance.yml @@ -0,0 +1,47 @@ +name: Hourly PR Maintenance + +on: + schedule: + - cron: "17 * * * *" + workflow_dispatch: + +concurrency: + group: bandscope-hourly-pr-maintenance + cancel-in-progress: false + +permissions: + actions: write + checks: read + contents: write + id-token: write + issues: write + pull-requests: write + statuses: read + +jobs: + review-fix: + uses: ContextualWisdomLab/.github/.github/workflows/pr-review-fix-scheduler.yml@5983b41ace75040c1d81818171ca7d0f3653254e + with: + target_repository: ContextualWisdomLab/bandscope + base_branch: "develop" + max_prs: "50" + max_dispatches: "3" + retry_hours: "1" + canonical_ref: "5983b41ace75040c1d81818171ca7d0f3653254e" + secrets: inherit + + merge-queue: + needs: review-fix + uses: ContextualWisdomLab/.github/.github/workflows/pr-review-merge-scheduler.yml@5983b41ace75040c1d81818171ca7d0f3653254e + with: + base_branch: "develop" + max_prs: "100" + trigger_reviews: true + review_dispatch_limit: "3" + branch_update_limit: "3" + enable_auto_merge: true + merge_mode: direct_or_auto + update_branches: true + stale_opencode_minutes: "90" + project_flow: git-flow + secrets: inherit diff --git a/docs/workflow/pr-review-merge-scheduler.md b/docs/workflow/pr-review-merge-scheduler.md index adcb9baeb..34370eadc 100644 --- a/docs/workflow/pr-review-merge-scheduler.md +++ b/docs/workflow/pr-review-merge-scheduler.md @@ -13,8 +13,35 @@ credential, not by a maintainer's local `gh` session. The central scheduler may `PR_REVIEW_MERGE_TOKEN`, `OPENCODE_APPROVE_TOKEN`, an exchanged OpenCode GitHub App token, or the workflow `GITHUB_TOKEN`, depending on which credential can perform the guarded repository mutation. -The local repository may keep product CI, security, release, and build workflows. It must not restore -repo-local copies of `opencode-review.yml`, `pr-review-merge-scheduler.yml`, or their `scripts/ci` helper implementations. +The local repository may keep product CI, security, release, build, and thin reusable-workflow caller +workflows. It must not restore repo-local copies of `opencode-review.yml`, +`pr-review-merge-scheduler.yml`, or their `scripts/ci` helper implementations. + +## Hourly maintenance caller + +`.github/workflows/hourly-pr-maintenance.yml` is a thin, source-pinned caller for the central +workflows. It runs at minute 17 of every hour and can also be dispatched manually. + +The caller performs two ordered phases: + +1. Call the central review-fix scheduler for open pull requests targeting `develop`, with a one-hour + retry window and a bounded maximum of three autofix dispatches per cycle. +2. Call the central review-and-merge scheduler to request missing current-head reviews, refresh up to + three outdated branches, enable normal auto-merge, and merge only after required checks and + independent approval are satisfied. + +Both reusable workflow references use the same full central commit SHA. The review-fix caller also +passes that exact SHA as `canonical_ref`, so its checked-out Python scheduler implementation cannot +drift to mutable `.github@main` after the caller has been reviewed. + +The local workflow contains no review parser, code-fix agent, review dismissal, thread resolution, +branch-update implementation, or merge command. Reusable workflows cannot elevate the caller's +`GITHUB_TOKEN`, so the caller declares only the permission union required by the two reviewed central +workflows. Credentials remain in repository or organization secrets and are inherited without being +copied into BandScope. + +Scheduled workflows execute from the repository default branch. Therefore, the hourly loop becomes +active only after this caller is reviewed and merged into `develop`. ## Behavior @@ -34,24 +61,27 @@ repo-local copies of `opencode-review.yml`, `pr-review-merge-scheduler.yml`, or ## Non-Goals -- It does not generate code fixes. +- It does not generate code fixes locally. - It does not dismiss reviews. - It does not resolve review threads. - It does not use admin merge or ruleset bypass. - It does not weaken required checks, branch protection, or repository rulesets. - It does not require BandScope to carry repo-local OpenCode or scheduler workflow/helper copies. -- It does not move central token permissions into this repository. +- It does not copy central credentials or central mutation implementations into this repository. ## Security Notes - Attack surface: organization required workflows with write access to PR comments, PR branch updates, and normal merges. -- Trust boundary touched: GitHub repository governance, PR review state, status checks, and CodeRabbit review requests. -- Realistic threats: spammed review comments, merging a PR with unresolved conversations, merging without required checks, or hiding conflicts behind automation. -- Mitigations: central required workflow source pinning, idempotent per-head review comment marker, - explicit unresolved-thread check, retry-bounded GitHub API reads, required-check verification - through GitHub, conflict skip, guarded merge with `--match-head-commit`, and no admin bypass path. +- Trust boundary touched: GitHub repository governance, PR review state, status checks, inherited workflow secrets, and CodeRabbit review requests. +- Realistic threats: spammed review comments, merging a PR with unresolved conversations, merging without required checks, widening caller permissions, mutable implementation drift, or hiding conflicts behind automation. +- Mitigations: full-length central reusable-workflow SHA pins, an identical immutable `canonical_ref` + for the review-fix implementation, an exact hourly caller contract test, least-privilege permission + union, idempotent per-head review comment marker, explicit unresolved-thread check, retry-bounded + GitHub API reads, required-check verification through GitHub, conflict skip, guarded merge with + `--match-head-commit`, and no admin bypass path. - Remaining risk: CodeRabbit and GitHub check state can be delayed or stale; the scheduler therefore only advances eligible PRs and leaves code-fix work to agents or maintainers. -- Test points: organization ruleset inheritance, current-head OpenCode approval, unresolved review - thread count, required-check rollup, approved behind PR, approved conflict-free PR, approved dirty PR, - external failed-check classification, provider/runtime failure summary, and Strix evidence lookup - scope diagnostics. +- Test points: hourly cron and manual dispatch, central workflow SHA pins, immutable review-fix + implementation source, delegated permission union, organization ruleset inheritance, current-head + OpenCode approval, unresolved review thread count, required-check rollup, approved behind PR, + approved conflict-free PR, approved dirty PR, external failed-check classification, + provider/runtime failure summary, and Strix evidence lookup scope diagnostics. diff --git a/services/analysis-engine/tests/test_hourly_pr_maintenance_workflow.py b/services/analysis-engine/tests/test_hourly_pr_maintenance_workflow.py new file mode 100644 index 000000000..d0558d50d --- /dev/null +++ b/services/analysis-engine/tests/test_hourly_pr_maintenance_workflow.py @@ -0,0 +1,73 @@ +"""Contract tests for the hourly central PR maintenance caller.""" + +from __future__ import annotations + +from pathlib import Path + + +CENTRAL_WORKFLOW_REVISION = "5983b41ace75040c1d81818171ca7d0f3653254e" + + +def _workflow_text() -> str: + """Return the checked-in hourly PR maintenance workflow text.""" + repo_root = Path(__file__).resolve().parents[3] + return ( + repo_root / ".github" / "workflows" / "hourly-pr-maintenance.yml" + ).read_text(encoding="utf-8") + + +def test_hourly_pr_maintenance_calls_central_review_fix_scheduler() -> None: + """The hourly loop delegates review fixes to one immutable central revision.""" + workflow = _workflow_text() + + assert 'cron: "17 * * * *"' in workflow + assert ( + "uses: ContextualWisdomLab/.github/.github/workflows/" + f"pr-review-fix-scheduler.yml@{CENTRAL_WORKFLOW_REVISION}" + ) in workflow + assert "target_repository: ContextualWisdomLab/bandscope" in workflow + assert 'base_branch: "develop"' in workflow + assert 'retry_hours: "1"' in workflow + assert 'max_dispatches: "3"' in workflow + assert f'canonical_ref: "{CENTRAL_WORKFLOW_REVISION}"' in workflow + assert "canonical_ref: main" not in workflow + assert workflow.count("secrets: inherit") == 2 + + +def test_hourly_pr_maintenance_calls_central_merge_scheduler() -> None: + """The same loop rechecks approvals, checks, branch freshness, and merges.""" + workflow = _workflow_text() + + assert ( + "uses: ContextualWisdomLab/.github/.github/workflows/" + f"pr-review-merge-scheduler.yml@{CENTRAL_WORKFLOW_REVISION}" + ) in workflow + assert "needs: review-fix" in workflow + assert 'base_branch: "develop"' in workflow + assert "trigger_reviews: true" in workflow + assert 'review_dispatch_limit: "3"' in workflow + assert 'branch_update_limit: "3"' in workflow + assert "enable_auto_merge: true" in workflow + assert "merge_mode: direct_or_auto" in workflow + assert "update_branches: true" in workflow + assert "project_flow: git-flow" in workflow + + +def test_hourly_pr_maintenance_grants_only_called_workflow_permissions() -> None: + """The caller grants the exact union needed by both central workflows.""" + workflow = _workflow_text() + + expected_permissions = """permissions: + actions: write + checks: read + contents: write + id-token: write + issues: write + pull-requests: write + statuses: read +""" + assert expected_permissions in workflow + assert "administration: write" not in workflow + assert "security-events: write" not in workflow + assert "runs-on:" not in workflow + assert "gh pr merge" not in workflow