Skip to content
Closed
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
47 changes: 47 additions & 0 deletions .github/workflows/hourly-pr-maintenance.yml
Original file line number Diff line number Diff line change
@@ -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
56 changes: 43 additions & 13 deletions docs/workflow/pr-review-merge-scheduler.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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.
Original file line number Diff line number Diff line change
@@ -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
Loading