diff --git a/.github/workflows/pr-review-fix-scheduler.yml b/.github/workflows/pr-review-fix-scheduler.yml index cc7875bc..d8b144f7 100644 --- a/.github/workflows/pr-review-fix-scheduler.yml +++ b/.github/workflows/pr-review-fix-scheduler.yml @@ -43,11 +43,6 @@ on: required: false default: "" type: string - canonical_ref: - description: Ref of ContextualWisdomLab/.github to use for scheduler code - required: false - default: "main" - type: string repository_dispatch: types: [pr-review-fix-scheduler] schedule: @@ -83,13 +78,16 @@ jobs: RETRY_HOURS: ${{ github.event.client_payload.retry_hours || inputs.retry_hours || '24' }} AUTOFIX_WORKFLOW: pr-review-autofix.yml AUTOFIX_REPOSITORY: ContextualWisdomLab/.github - CANONICAL_REF: main steps: - name: Checkout canonical scheduler 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, so no caller input or branch + # can select the privileged scheduler implementation. + repository: ${{ job.workflow_repository }} + ref: ${{ job.workflow_sha }} fetch-depth: 1 persist-credentials: false diff --git a/tests/test_pr_review_fix_scheduler_source_pin.py b/tests/test_pr_review_fix_scheduler_source_pin.py new file mode 100644 index 00000000..04388998 --- /dev/null +++ b/tests/test_pr_review_fix_scheduler_source_pin.py @@ -0,0 +1,42 @@ +"""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(): + """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(): + """No caller-supplied ref or ordinary caller GitHub SHA selects trusted code.""" + workflow = _workflow_text() + assert "canonical_ref:" not in workflow + 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_reusable_scheduler_retains_least_privilege_and_bounded_dispatch(): + """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