diff --git a/.github/workflows/organization-commercial-readiness-loop.yml b/.github/workflows/organization-commercial-readiness-loop.yml index 521495617a..52b3beb229 100644 --- a/.github/workflows/organization-commercial-readiness-loop.yml +++ b/.github/workflows/organization-commercial-readiness-loop.yml @@ -56,8 +56,43 @@ jobs: shell: bash --noprofile --norc -e -o pipefail {0} run: | if [ -z "${GH_TOKEN:-}" ]; then - echo "::error::PR_REVIEW_MERGE_TOKEN is required; neither the reviewer credential nor repository-scoped GITHUB_TOKEN is accepted." - exit 1 + echo "::warning::PR_REVIEW_MERGE_TOKEN is not provisioned; recording a bounded no-op until the maintainer credential is configured." + python - "$RUNNER_TEMP/organization-commercial-readiness-loop.json" <<'PY' + from __future__ import annotations + + import json + import os + import sys + from pathlib import Path + + output_path = Path(sys.argv[1]) + output_path.parent.resolve(strict=True) + if output_path.is_symlink(): + raise RuntimeError("credential-unavailable receipt must not be a symlink") + rendered = ( + json.dumps( + { + "status": "skipped_credential_unavailable", + "reason": "PR_REVIEW_MERGE_TOKEN is not provisioned", + "next_action": "Provision PR_REVIEW_MERGE_TOKEN for the next hourly pass.", + }, + indent=2, + ) + + "\n" + ) + descriptor = os.open( + output_path, + os.O_WRONLY | os.O_CREAT | os.O_TRUNC | os.O_NOFOLLOW, + 0o600, + ) + with os.fdopen(descriptor, "w", encoding="utf-8") as handle: + handle.write(rendered) + PY + { + echo "## Hourly coordinator: credential unavailable" + echo "No cross-repository dispatch was attempted. Provision PR_REVIEW_MERGE_TOKEN, then the next hourly pass can resume bounded work." + } >>"$GITHUB_STEP_SUMMARY" + exit 0 fi echo "::add-mask::$GH_TOKEN" test "$(git rev-parse HEAD)" = "${GITHUB_SHA}" diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e43e6aa6a..999db19ee2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -460,6 +460,7 @@ Semantic Versioning where the repository publishes a release. ### Changed +- Record a bounded, auditable hourly no-op when the cross-repository maintainer credential is not provisioned, so missing configuration does not erase the receipt or fail the scheduler before the next-action guidance is visible. - Emit completed repository pull-list requests as they finish in the five-minute agent-mention sweep, while retaining the four-worker ceiling, rotation, and exact-name dispatch ledger, so one slow repository cannot hide ready sibling diff --git a/docs/doctoring/organization-commercial-readiness-loop.md b/docs/doctoring/organization-commercial-readiness-loop.md index 76ef1fce5a..db4b3307e6 100644 --- a/docs/doctoring/organization-commercial-readiness-loop.md +++ b/docs/doctoring/organization-commercial-readiness-loop.md @@ -10,7 +10,7 @@ The coordinator may dispatch at most one review-repair workflow and one product- A single workflow cannot safely write every repository merely because it runs in the organization `.github` repository. GitHub's default `GITHUB_TOKEN` is scoped to the repository containing the workflow; cross-repository Actions dispatch therefore requires an explicitly provisioned user or GitHub App credential with the required repository and Actions permissions. This control does not make every repository directly writable. It only considers repositories the live API reports as organization-owned, non-fork, enabled, non-archived, default-branch-bearing, and writable by the authenticated installation. -The central job therefore refuses both repository-scoped and reviewer-scoped token fallbacks. It requires the maintainer-scoped `PR_REVIEW_MERGE_TOKEN`; `OPENCODE_APPROVE_TOKEN` remains isolated to the reviewer credential chain and `GITHUB_TOKEN` is not accepted for cross-repository coordination. The maintainer token is exposed only to the final dispatch shell step, not checkout, setup, artifact upload, or other third-party actions. The coordinator itself receives neither `NVIDIA_NIM_API_KEY` nor `COPILOT_GITHUB_TOKEN`. Model credentials remain inside separately reviewed repository-local or central workers. +The central job therefore refuses both repository-scoped and reviewer-scoped token fallbacks. When the maintainer-scoped `PR_REVIEW_MERGE_TOKEN` is absent, the scheduled pass records an explicit `skipped_credential_unavailable` receipt and exits without attempting a dispatch; the next action is to provision that secret. When present, it remains exposed only to the final dispatch shell step. `OPENCODE_APPROVE_TOKEN` remains isolated to the reviewer credential chain and `GITHUB_TOKEN` is not accepted for cross-repository coordination. The coordinator itself receives neither `NVIDIA_NIM_API_KEY` nor `COPILOT_GITHUB_TOKEN`. Model credentials remain inside separately reviewed repository-local or central workers. ## Dynamic repository-writer lease diff --git a/tests/test_organization_commercial_readiness_loop_secret_scope.py b/tests/test_organization_commercial_readiness_loop_secret_scope.py index b47c2cadc2..de520ab860 100644 --- a/tests/test_organization_commercial_readiness_loop_secret_scope.py +++ b/tests/test_organization_commercial_readiness_loop_secret_scope.py @@ -1,3 +1,5 @@ +"""Verify that the hourly coordinator keeps cross-repository credentials scoped.""" + from pathlib import Path @@ -19,3 +21,13 @@ def test_maintainer_token_is_scoped_only_to_the_dispatch_step() -> None: assert "PR_REVIEW_MERGE_TOKEN" not in before_dispatch assert "GH_TOKEN:" not in before_dispatch assert "env:\n GH_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN }}" in dispatch_step + + +def test_missing_maintainer_token_is_an_auditable_noop() -> None: + """Absent cross-repository authority must not create a failing empty receipt.""" + source = WORKFLOW_PATH.read_text(encoding="utf-8") + + assert '"status": "skipped_credential_unavailable"' in source + assert "organization-commercial-readiness-loop.json" in source + assert "Provision PR_REVIEW_MERGE_TOKEN" in source + assert "No cross-repository dispatch was attempted" in source