From f72f3dba92e6064cd75f5fa358e550fd7bbb8aca Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 3 Aug 2026 12:11:03 +0900 Subject: [PATCH 1/7] test(workflow): specify hourly PR maintenance loop --- .../test_hourly_pr_maintenance_workflow.py | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 services/analysis-engine/tests/test_hourly_pr_maintenance_workflow.py 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..217e50c96 --- /dev/null +++ b/services/analysis-engine/tests/test_hourly_pr_maintenance_workflow.py @@ -0,0 +1,62 @@ +"""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 the pinned central workflow.""" + 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 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_keeps_least_privilege_local_permissions() -> None: + """The caller itself stays read-only and contains no copied merge logic.""" + workflow = _workflow_text() + + assert "permissions:\n contents: read" in workflow + assert "pull-requests: write" not in workflow + assert "contents: write" not in workflow + assert "runs-on:" not in workflow + assert "gh pr merge" not in workflow From 120272d2fed52b14af3f35a204dceb3d5538d9dc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 3 Aug 2026 12:12:11 +0900 Subject: [PATCH 2/7] test(workflow): require delegated scheduler permissions --- .../test_hourly_pr_maintenance_workflow.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/services/analysis-engine/tests/test_hourly_pr_maintenance_workflow.py b/services/analysis-engine/tests/test_hourly_pr_maintenance_workflow.py index 217e50c96..ff742f4c5 100644 --- a/services/analysis-engine/tests/test_hourly_pr_maintenance_workflow.py +++ b/services/analysis-engine/tests/test_hourly_pr_maintenance_workflow.py @@ -51,12 +51,21 @@ def test_hourly_pr_maintenance_calls_central_merge_scheduler() -> None: assert "project_flow: git-flow" in workflow -def test_hourly_pr_maintenance_keeps_least_privilege_local_permissions() -> None: - """The caller itself stays read-only and contains no copied merge logic.""" +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() - assert "permissions:\n contents: read" in workflow - assert "pull-requests: write" not in workflow - assert "contents: write" not in workflow + 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 From e9b22ed49bd19d72e16da2de69a5de901c26359b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 3 Aug 2026 12:12:28 +0900 Subject: [PATCH 3/7] ci: schedule hourly central PR maintenance --- .github/workflows/hourly-pr-maintenance.yml | 47 +++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 .github/workflows/hourly-pr-maintenance.yml diff --git a/.github/workflows/hourly-pr-maintenance.yml b/.github/workflows/hourly-pr-maintenance.yml new file mode 100644 index 000000000..c576ed0bd --- /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: main + 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 From a1c8dd03a6a5f16d736d89fd5725a1c7e7bb74c4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 3 Aug 2026 12:13:51 +0900 Subject: [PATCH 4/7] docs(workflow): document hourly PR maintenance caller --- docs/workflow/pr-review-merge-scheduler.md | 51 ++++++++++++++++------ 1 file changed, 38 insertions(+), 13 deletions(-) diff --git a/docs/workflow/pr-review-merge-scheduler.md b/docs/workflow/pr-review-merge-scheduler.md index adcb9baeb..c33bb8778 100644 --- a/docs/workflow/pr-review-merge-scheduler.md +++ b/docs/workflow/pr-review-merge-scheduler.md @@ -13,8 +13,31 @@ 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. + +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 +57,26 @@ 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, or hiding conflicts behind automation. +- Mitigations: full-length central reusable-workflow SHA pins, 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, 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. From 8301ad4951329b5efe79f471f979083ce5e7cc67 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 3 Aug 2026 12:57:27 +0900 Subject: [PATCH 5/7] test(ci): require immutable review-fix scheduler source --- .../tests/test_hourly_pr_maintenance_workflow.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/services/analysis-engine/tests/test_hourly_pr_maintenance_workflow.py b/services/analysis-engine/tests/test_hourly_pr_maintenance_workflow.py index ff742f4c5..d0558d50d 100644 --- a/services/analysis-engine/tests/test_hourly_pr_maintenance_workflow.py +++ b/services/analysis-engine/tests/test_hourly_pr_maintenance_workflow.py @@ -17,7 +17,7 @@ def _workflow_text() -> str: def test_hourly_pr_maintenance_calls_central_review_fix_scheduler() -> None: - """The hourly loop delegates review fixes to the pinned central workflow.""" + """The hourly loop delegates review fixes to one immutable central revision.""" workflow = _workflow_text() assert 'cron: "17 * * * *"' in workflow @@ -29,6 +29,8 @@ def test_hourly_pr_maintenance_calls_central_review_fix_scheduler() -> None: 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 From e3c30deff005a9ebd8cc9f62ce28ff3a9fb76a41 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 3 Aug 2026 12:57:46 +0900 Subject: [PATCH 6/7] fix(ci): pin hourly review-fix scheduler implementation --- .github/workflows/hourly-pr-maintenance.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/hourly-pr-maintenance.yml b/.github/workflows/hourly-pr-maintenance.yml index c576ed0bd..f18e36e12 100644 --- a/.github/workflows/hourly-pr-maintenance.yml +++ b/.github/workflows/hourly-pr-maintenance.yml @@ -27,7 +27,7 @@ jobs: max_prs: "50" max_dispatches: "3" retry_hours: "1" - canonical_ref: main + canonical_ref: "5983b41ace75040c1d81818171ca7d0f3653254e" secrets: inherit merge-queue: From 166e5a1a54371cb7ff905e251a5b446f7785f7dd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 3 Aug 2026 12:58:24 +0900 Subject: [PATCH 7/7] docs(ci): record immutable hourly scheduler source --- docs/workflow/pr-review-merge-scheduler.md | 25 +++++++++++++--------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/docs/workflow/pr-review-merge-scheduler.md b/docs/workflow/pr-review-merge-scheduler.md index c33bb8778..34370eadc 100644 --- a/docs/workflow/pr-review-merge-scheduler.md +++ b/docs/workflow/pr-review-merge-scheduler.md @@ -30,6 +30,10 @@ The caller performs two ordered phases: 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 @@ -69,14 +73,15 @@ active only after this caller is reviewed and merged into `develop`. - 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, 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, or hiding conflicts behind automation. -- Mitigations: full-length central reusable-workflow SHA pins, 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. +- 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: hourly cron and manual dispatch, central workflow SHA pins, 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. +- 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.