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
23 changes: 14 additions & 9 deletions .github/workflows/pr-review-fix-scheduler.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ on:
retry_hours:
description: Minimum hours before redispatching autofix for the same head
required: false
default: "24"
default: "1"
type: string
autofix_workflow:
description: Autofix workflow file to dispatch
Expand All @@ -44,14 +44,16 @@ on:
default: ""
type: string
canonical_ref:
description: Ref of ContextualWisdomLab/.github to use for scheduler code
description: Deprecated compatibility input; accepted and ignored because privileged source is bound to the called workflow SHA
required: false
default: "main"
default: ""
type: string
repository_dispatch:
types: [pr-review-fix-scheduler]
schedule:
- cron: "23 */2 * * *"
# Run away from minute zero, where scheduled GitHub Actions are more likely
# to be delayed, while preserving a bounded one-dispatch-per-run repair loop.
- cron: "23 * * * *"

concurrency:
group: central-pr-review-fix-scheduler-${{ github.event.client_payload.target_repository || inputs.target_repository || vars.PR_REVIEW_FIX_TARGET_REPOSITORY || github.repository }}
Expand Down Expand Up @@ -80,16 +82,19 @@ jobs:
DRY_RUN: ${{ github.event.client_payload.dry_run == true || github.event.client_payload.dry_run == 'true' || inputs.dry_run == true }}
MAX_PRS: ${{ github.event.client_payload.max_prs || inputs.max_prs || '50' }}
MAX_DISPATCHES: ${{ github.event.client_payload.max_dispatches || inputs.max_dispatches || '1' }}
RETRY_HOURS: ${{ github.event.client_payload.retry_hours || inputs.retry_hours || '24' }}
RETRY_HOURS: ${{ github.event.client_payload.retry_hours || inputs.retry_hours || '1' }}
AUTOFIX_WORKFLOW: pr-review-autofix.yml
AUTOFIX_REPOSITORY: ContextualWisdomLab/.github
CANONICAL_REF: main
steps:
- name: Checkout canonical scheduler
- name: Checkout immutable called-workflow source
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
repository: ContextualWisdomLab/.github
ref: ${{ env.CANONICAL_REF }}
# In a reusable workflow the ordinary github context belongs to the
# caller. The job workflow context identifies the called workflow's
# repository and immutable resolved SHA, preventing a caller input or
# mutable branch from selecting the privileged scheduler code.
repository: ${{ job.workflow_repository }}
ref: ${{ job.workflow_sha }}
fetch-depth: 1
persist-credentials: false

Expand Down
45 changes: 45 additions & 0 deletions tests/test_pr_review_fix_hourly_contract.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"""Static contract for the central hourly PR review-fix scheduler."""

from __future__ import annotations

from pathlib import Path


_WORKFLOW = Path(".github/workflows/pr-review-fix-scheduler.yml")


def _workflow_text() -> str:
"""Return the canonical scheduler workflow text."""
return _WORKFLOW.read_text(encoding="utf-8")


def test_review_fix_scheduler_runs_once_each_hour() -> None:
"""The bounded repair dispatcher uses the requested hourly heartbeat."""
text = _workflow_text()

assert 'cron: "23 * * * *"' in text
assert 'cron: "23 */2 * * *"' not in text


def test_review_fix_scheduler_retries_same_head_after_one_hour() -> None:
"""A blocked head can be retried on the next hourly cycle, not a day later."""
text = _workflow_text()

retry_block = text.split("retry_hours:", maxsplit=1)[1].split(
"autofix_workflow:", maxsplit=1
)[0]
assert 'default: "1"' in retry_block
assert "inputs.retry_hours || '1'" in text
assert "inputs.retry_hours || '24'" not in text


def test_review_fix_scheduler_remains_bounded_and_single_flight() -> None:
"""Higher cadence never expands mutation volume or parallel execution."""
text = _workflow_text()

dispatch_block = text.split("max_dispatches:", maxsplit=1)[1].split(
"target_repository:", maxsplit=1
)[0]
assert 'default: "1"' in dispatch_block
assert "cancel-in-progress: true" in text
assert "MAX_DISPATCHES" in text
54 changes: 54 additions & 0 deletions tests/test_pr_review_fix_scheduler_source_pin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
"""Supply-chain contract for the reusable PR-review autofix scheduler."""

from __future__ import annotations

from pathlib import Path


_REPO_ROOT = Path(__file__).resolve().parents[1]
_WORKFLOW = _REPO_ROOT / ".github" / "workflows" / "pr-review-fix-scheduler.yml"


def _workflow_text() -> str:
"""Read the reusable scheduler workflow as UTF-8 text."""
return _WORKFLOW.read_text(encoding="utf-8")


def test_reusable_scheduler_checks_out_the_called_workflow_sha() -> None:
"""Privileged scheduler code comes from the immutable called-workflow revision."""
workflow = _workflow_text()
assert "repository: ${{ job.workflow_repository }}" in workflow
assert "ref: ${{ job.workflow_sha }}" in workflow
assert "persist-credentials: false" in workflow


def test_reusable_scheduler_source_is_not_caller_input_controlled() -> None:
"""No caller-supplied ref or ordinary caller GitHub SHA selects trusted code."""
workflow = _workflow_text()
assert "inputs.canonical_ref" not in workflow
assert "github.event.client_payload.canonical_ref" not in workflow
assert "ref: ${{ env.CANONICAL_REF }}" not in workflow
assert "ref: ${{ github.sha }}" not in workflow


def test_deprecated_canonical_ref_input_is_accepted_but_never_consumed() -> None:
"""Existing callers can upgrade pins without controlling privileged source."""
workflow = _workflow_text()
declaration = workflow.split("canonical_ref:", 1)[1].split(
"repository_dispatch:", 1
)[0]

assert "Deprecated compatibility input" in declaration
assert "ignored" in declaration
assert 'default: ""' in declaration
assert workflow.count("canonical_ref") == 1


def test_reusable_scheduler_retains_least_privilege_and_bounded_dispatch() -> None:
"""Source pinning does not broaden token scope or queue fan-out."""
workflow = _workflow_text()
assert "contents: write" not in workflow
assert "pull-requests: write" not in workflow
assert "MAX_DISPATCHES:" in workflow
assert "RETRY_HOURS:" in workflow
assert "cancel-in-progress: true" in workflow
Loading