diff --git a/.github/workflows/organization-commercial-readiness-loop-quality-ci.yml b/.github/workflows/organization-commercial-readiness-loop-quality-ci.yml new file mode 100644 index 000000000..50729db47 --- /dev/null +++ b/.github/workflows/organization-commercial-readiness-loop-quality-ci.yml @@ -0,0 +1,72 @@ +name: Organization Commercial Readiness Loop Quality CI + +on: + pull_request: + branches: [main] + paths: + - ".github/workflows/organization-commercial-readiness-loop.yml" + - ".github/workflows/organization-commercial-readiness-loop-quality-ci.yml" + - "scripts/ci/organization_commercial_readiness_loop.py" + - "organization_commercial_readiness_fixtures.py" + - "tests/test_organization_commercial_readiness_loop*.py" + - "docs/doctoring/organization-commercial-readiness-loop.md" + - "CHANGELOG.md" + +permissions: + contents: read + +concurrency: + group: organization-commercial-readiness-loop-quality-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + exact-head-policy: + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - name: Checkout exact source revision + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + ref: ${{ github.event.pull_request.head.sha }} + persist-credentials: false + + - name: Set up Python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "3.14" + + - name: Install exact hash-verified quality dependencies + env: + PIP_DISABLE_PIP_VERSION_CHECK: "1" + PIP_NO_INPUT: "1" + shell: bash --noprofile --norc -e -o pipefail {0} + run: | + cat >"${RUNNER_TEMP}/organization-loop-quality-requirements.txt" <<'EOF' + coverage==7.15.2 --hash=sha256:b9a6367e4aff723e8ee8190836836124284e8fcd4265e307c844010cfa074f3f + iniconfig==2.1.0 --hash=sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760 + packaging==26.2 --hash=sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e + pluggy==1.6.0 --hash=sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746 + pygments==2.20.0 --hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176 + pytest==9.1.1 --hash=sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c + EOF + python -m pip install \ + --only-binary=:all: \ + --require-hashes \ + -r "${RUNNER_TEMP}/organization-loop-quality-requirements.txt" + + - name: Prove exact-head policy and full branch coverage + shell: bash --noprofile --norc -e -o pipefail {0} + run: | + test "$(git rev-parse HEAD)" = "${{ github.event.pull_request.head.sha }}" + python -m coverage run \ + --branch \ + -m pytest --import-mode=importlib tests/test_organization_commercial_readiness_loop*.py -q + python -m coverage report \ + --include='scripts/ci/organization_commercial_readiness_loop.py' \ + --show-missing \ + --fail-under=100 + python -m compileall -q \ + scripts/ci/organization_commercial_readiness_loop.py \ + organization_commercial_readiness_fixtures.py \ + tests/test_organization_commercial_readiness_loop*.py + git diff --exit-code diff --git a/.github/workflows/organization-commercial-readiness-loop.yml b/.github/workflows/organization-commercial-readiness-loop.yml new file mode 100644 index 000000000..521495617 --- /dev/null +++ b/.github/workflows/organization-commercial-readiness-loop.yml @@ -0,0 +1,81 @@ +name: Organization Commercial Readiness Loop + +on: + schedule: + - cron: "7 * * * *" + +concurrency: + group: organization-commercial-readiness-loop + cancel-in-progress: false + +permissions: + contents: read + +jobs: + coordinate: + if: >- + github.repository == 'ContextualWisdomLab/.github' && + github.ref == format('refs/heads/{0}', github.event.repository.default_branch) + runs-on: ubuntu-24.04 + timeout-minutes: 25 + env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" + ORGANIZATION: ContextualWisdomLab + ROTATION_SEED: ${{ github.run_number }} + MAX_REPOSITORIES: "200" + MAX_REVIEW_DISPATCHES: "1" + MAX_DEVELOPMENT_DISPATCHES: "1" + steps: + - name: Harden runner + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.13.2 + with: + egress-policy: block + allowed-endpoints: >- + api.github.com:443 + github.com:443 + objects.githubusercontent.com:443 + release-assets.githubusercontent.com:443 + results-receiver.actions.githubusercontent.com:443 + *.actions.githubusercontent.com:443 + *.blob.core.windows.net:443 + + - name: Checkout exact trusted coordinator source + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + ref: ${{ github.sha }} + persist-credentials: false + + - name: Set up Python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "3.14" + + - name: Coordinate one bounded fleet pass + env: + GH_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN }} + 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 + fi + echo "::add-mask::$GH_TOKEN" + test "$(git rev-parse HEAD)" = "${GITHUB_SHA}" + + python scripts/ci/organization_commercial_readiness_loop.py \ + --organization "$ORGANIZATION" \ + --rotation-seed "$ROTATION_SEED" \ + --max-repositories "$MAX_REPOSITORIES" \ + --max-review-dispatches "$MAX_REVIEW_DISPATCHES" \ + --max-development-dispatches "$MAX_DEVELOPMENT_DISPATCHES" \ + --json-output "$RUNNER_TEMP/organization-commercial-readiness-loop.json" + python -m json.tool "$RUNNER_TEMP/organization-commercial-readiness-loop.json" >/dev/null + + - name: Preserve the exact fleet receipt + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: organization-commercial-readiness-${{ github.run_id }}-${{ github.run_attempt }} + path: ${{ runner.temp }}/organization-commercial-readiness-loop.json + if-no-files-found: error + retention-days: 3 diff --git a/CHANGELOG.md b/CHANGELOG.md index bf30091dd..32af94836 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ Semantic Versioning where the repository publishes a release. ### Added +- Added an hourly organization commercial-readiness coordinator that discovers writable repositories, honors enabled dedicated writer leases and fully paginated live writer runs, refetches exact repository/workflow/run/PR state before dispatch, rotates bounded review-repair and opt-in NVIDIA OpenCode product-development targets, fails nonzero on fleet-wide inspection or dispatch outages, retains three-day JSON receipts, and keeps the existing 15-minute merge scheduler authoritative. - Added a trusted pull-request comment router for `@cwl-noema-review` and review-only `@opencode-agent` dispatches, with an organization sweep, exact-head receipts, repository allowlisting, fixed runners, immutable checkout pins, and a permanent 100% statement/branch/docstring quality gate. - Added exact-base `uv.lock` materialization that reconstructs standalone nested projects with a checksum-pinned official `uv` exporter, isolated frozen/offline execution, strict exact-pin and SHA-256 output validation, and complete Python 3.10/3.14 quality evidence. diff --git a/docs/doctoring/organization-commercial-readiness-loop.md b/docs/doctoring/organization-commercial-readiness-loop.md new file mode 100644 index 000000000..76ef1fce5 --- /dev/null +++ b/docs/doctoring/organization-commercial-readiness-loop.md @@ -0,0 +1,69 @@ +# Organization commercial-readiness coordinator + +## Decision + +ContextualWisdomLab uses one organization-central hourly coordinator for repositories that do not already have an enabled dedicated commercial, maintenance, review-repair, or product-development writer. The coordinator complements rather than duplicates the existing 15-minute organization merge scheduler. + +The coordinator may dispatch at most one review-repair workflow and one product-development workflow per hour. These may target different repositories, so review or check latency in one repository does not stop useful work in another. The coordinator never approves, merges, releases, edits source, or interprets a failed check as success by itself. + +## Why this is realistic + +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. + +## Dynamic repository-writer lease + +An active workflow with a scheduled high-signal commercial/development/maintenance/review-repair identity owns the repository writer lease. A queued, in-progress, waiting, pending, or requested run with the same identity also owns a live lease. The organization coordinator skips that repository for the entire pass. + +A disabled workflow does not hold a lease. A manual-only workflow does not hold a lease unless it is already running. If an active high-signal workflow exists but its source cannot be read, the coordinator fails closed and treats the repository as leased. The organization-required merge scheduler is explicitly excluded from this classification because it is a governance gate rather than a product-code writer. + +The coordinator lists workflow metadata for every repository but fetches exact workflow source only for identities that can plausibly be a repository writer. This keeps API use proportional to writer candidates rather than every ordinary CI, packaging, or security workflow. Active-run and pull-request inventories remain fully paginated, including writers beyond the first 100 queued or running executions. + +Before every dispatch, the coordinator refetches the exact default-branch SHA, active workflow identities and source blobs, active runs, and open pull-request heads, bases, draft states, and update timestamps. Any change invalidates the predecessor snapshot. A newly appearing writer causes `skipped_writer_lease`; any other movement causes `skipped_state_changed`. + +## Review-repair boundary + +A repository with at least one non-draft pull request targeting its default branch may receive one `pr-review-fix-scheduler` repository dispatch. Draft and stacked pull requests are not treated as generic repair targets because the coordinator cannot safely infer their dependency order. The established central scheduler and autofix worker remain responsible for thread classification, current-head checks, path bounds, credential isolation, and whether a repair is actually warranted. + +The existing organization merge scheduler continues to own review dispatch, branch updates, exact-head approval evaluation, direct or automatic merge, and branch-protection compliance. The hourly coordinator does not create a second merge implementation. + +## Product-development boundary + +Product development is dispatched only when a repository has zero open pull requests and exposes one active, manual-only, explicitly marked workflow: + +```yaml +# cwl-org-commercial-entrypoint: v1 +on: + workflow_dispatch: +``` + +The entrypoint must contain an explicit `concurrency` contract, use `NVIDIA_NIM_API_KEY`, omit `COPILOT_GITHUB_TOKEN`, have no schedule of its own, and carry a commercial/product-development identity. This opt-in prevents the central coordinator from guessing that an unrelated manual workflow can safely modify product source. Repositories with an existing schedule keep their own lease and are never double-dispatched. + +The repository-local entrypoint remains responsible for its own bounded editable paths, tests, 100% production statement and branch coverage, public docstrings, package and security verification, exact-head publication, and pull-request creation. A missing compliant entrypoint is a deliberate no-op, not permission to inject a generic writer into that repository. + +## Failure, evidence, and operations + +The schedule runs at minute 7 rather than minute 0 to reduce exposure to the documented start-of-hour GitHub Actions load spike. The central workflow has no `workflow_dispatch` entrypoint, so branch-selected coordinator source cannot be executed; scheduled execution occurs only from protected default `main`. Local operators may use the script's `--dry-run` mode from a reviewed checkout without adding a central manual workflow entrypoint. + +Organization, workflow, active-run, and pull-request inventories are paginated. One inaccessible repository is recorded as an inspection error while other independently safe repositories continue. A run fails nonzero when every selected repository inspection fails or when every planned dispatch fails; partial, independently contained failures remain visible without discarding successful work. + +Each run writes one deterministic JSON receipt and the same bounded evidence to the GitHub Actions job summary. The JSON is uploaded through the immutable, SHA-pinned artifact action with a three-day retention period. Artifact upload receives no maintainer or model credential. The receipt proves only coordinator observations and downstream dispatch acceptance; it is not merge, release, or product-quality evidence. + +No queued, pending, skipped-required, cancelled, absent, stale-head, predecessor-head, synthetic-merge-only, or failed check is converted to passing evidence. The coordinator's successful dispatch means only that exact state was revalidated and a bounded downstream workflow was accepted by GitHub. + +Rollback is removal or disabling of `.github/workflows/organization-commercial-readiness-loop.yml`. Repository-local dedicated loops and the existing 15-minute merge scheduler remain independently operational. + +## APA 7 references + +GitHub. (n.d.). *Automatic token authentication*. GitHub Docs. Retrieved August 8, 2026, from https://docs.github.com/en/actions/security-for-github-actions/security-guides/automatic-token-authentication + +GitHub. (n.d.). *Events that trigger workflows*. GitHub Docs. Retrieved August 8, 2026, from https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows + +GitHub. (n.d.). *REST API endpoints for artifacts*. GitHub Docs. Retrieved August 8, 2026, from https://docs.github.com/en/rest/actions/artifacts + +GitHub. (n.d.). *REST API endpoints for workflows*. GitHub Docs. Retrieved August 8, 2026, from https://docs.github.com/en/rest/actions/workflows + +GitHub. (n.d.). *REST API endpoints for workflow runs*. GitHub Docs. Retrieved August 8, 2026, from https://docs.github.com/en/rest/actions/workflow-runs + +National Institute of Standards and Technology. (2022). *Secure software development framework (SSDF) version 1.1: Recommendations for mitigating the risk of software vulnerabilities* (NIST Special Publication 800-218). https://doi.org/10.6028/NIST.SP.800-218 diff --git a/organization_commercial_readiness_fixtures.py b/organization_commercial_readiness_fixtures.py new file mode 100644 index 000000000..d86596196 --- /dev/null +++ b/organization_commercial_readiness_fixtures.py @@ -0,0 +1,128 @@ +"""Test fixtures for the organization commercial-readiness coordinator.""" + +from __future__ import annotations + +from typing import Any + +from scripts.ci.organization_commercial_readiness_loop import ( + GitHubError, + PullRequestRecord, + RepositorySnapshot, + RunRecord, + WorkflowRecord, +) + + +def workflow( + *, + workflow_id: int = 1, + name: str = "Hourly Product Development", + path: str = ".github/workflows/hourly-product-development.yml", + state: str = "active", + content: str | None = None, +) -> WorkflowRecord: + """Build one workflow record.""" + return WorkflowRecord(workflow_id, name, path, state, f"sha-{workflow_id}", content) + + +def pull( + number: int, + *, + draft: bool = False, + base_ref: str = "main", + head_sha: str | None = None, + updated_at: str = "2026-08-08T00:00:00Z", +) -> PullRequestRecord: + """Build one pull-request record.""" + return PullRequestRecord( + number, draft, base_ref, head_sha or f"{number:040x}", updated_at + ) + + +def snapshot( + repository: str, + *, + default_branch: str = "main", + default_sha: str = "a" * 40, + workflows: tuple[WorkflowRecord, ...] = (), + runs: tuple[RunRecord, ...] = (), + pulls: tuple[PullRequestRecord, ...] = (), +) -> RepositorySnapshot: + """Build one repository snapshot.""" + return RepositorySnapshot( + repository, default_branch, default_sha, workflows, runs, pulls + ) + + +def repository_payload(name: str) -> dict[str, Any]: + """Return one eligible repository response.""" + return { + "full_name": f"ContextualWisdomLab/{name}", + "default_branch": "main", + "archived": False, + "disabled": False, + "fork": False, + "permissions": {"maintain": True}, + } + + +def manual_workflow(*, workflow_id: int = 9) -> WorkflowRecord: + """Return one safe organization-dispatch product entrypoint.""" + return workflow( + workflow_id=workflow_id, + name="Commercial Product Development", + path=".github/workflows/commercial-product-development.yml", + content=( + "# cwl-org-commercial-entrypoint: v1\n" + "on:\n workflow_dispatch:\n" + "concurrency:\n group: product-development\n" + "permissions:\n contents: write\n" + "NVIDIA_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }}\n" + ), + ) + + +class FakeClient: + """Deterministic GitHub boundary.""" + + def __init__( + self, + repositories: list[dict[str, Any]], + snapshots: dict[str, list[RepositorySnapshot | Exception]], + ) -> None: + self.repositories = repositories + self.snapshots = snapshots + self.dispatched_repairs: list[tuple[str, str]] = [] + self.dispatched_products: list[tuple[str, int, str]] = [] + + def list_repositories(self, organization: str) -> list[dict[str, Any]]: + """Return configured repositories.""" + assert organization == "ContextualWisdomLab" + return self.repositories + + def snapshot(self, repository: str, default_branch: str) -> RepositorySnapshot: + """Return or raise the next configured snapshot value.""" + value = self.snapshots[repository].pop(0) + if isinstance(value, Exception): + raise value + assert value.default_branch == default_branch + return value + + def dispatch_review_repair(self, repository: str, base_branch: str) -> None: + """Record one repair dispatch.""" + self.dispatched_repairs.append((repository, base_branch)) + + def dispatch_product_workflow( + self, repository: str, workflow_id: int, default_branch: str + ) -> None: + """Record one product dispatch.""" + self.dispatched_products.append((repository, workflow_id, default_branch)) + + +class FailingDispatchClient(FakeClient): + """Reject review dispatches for failure-path tests.""" + + def dispatch_review_repair(self, repository: str, base_branch: str) -> None: + """Raise a bounded API failure.""" + del repository, base_branch + raise GitHubError("dispatch rejected") diff --git a/scripts/ci/organization_commercial_readiness_loop.py b/scripts/ci/organization_commercial_readiness_loop.py new file mode 100644 index 000000000..14eeffeab --- /dev/null +++ b/scripts/ci/organization_commercial_readiness_loop.py @@ -0,0 +1,811 @@ +#!/usr/bin/env python3 +"""Coordinate bounded commercial-readiness work across an organization. + +The coordinator deliberately does not implement code review, branch repair, or +product development itself. It discovers repositories that do not already have +an active writer, revalidates their exact live state immediately before a +mutation, and dispatches at most one central review-repair run and one +repository-local product-development run per invocation. +""" + +from __future__ import annotations + +import argparse +import base64 +import dataclasses +import enum +import hashlib +import json +import os +import re +import subprocess +import sys +from pathlib import Path +from typing import Any, Callable, Iterable, Mapping, Sequence +from urllib.parse import quote + + +DEFAULT_ORGANIZATION = "ContextualWisdomLab" +ORGANIZATION_RE = re.compile(r"^[A-Za-z0-9_.-]+$") +ENTRYPOINT_MARKER = "# cwl-org-commercial-entrypoint: v1" +CENTRAL_REPOSITORY = f"{DEFAULT_ORGANIZATION}/.github" +CENTRAL_REPAIR_EVENT = "pr-review-fix-scheduler" +ACTIVE_RUN_STATES = frozenset({"queued", "in_progress", "waiting", "pending", "requested"}) +WRITER_SIGNAL_RE = re.compile( + r"(?:hourly|commercial|product[ _-]*development|autonomous|readiness|" + r"maintenance|review[ _-]*repair|review[ _-]*fix|maintainer|pr[ _-]*disposition)", + re.IGNORECASE, +) +MERGE_SCHEDULER_RE = re.compile( + r"(?:required[ _-]*pr[ _-]*review[ _-]*merge[ _-]*scheduler|" + r"pr-review-merge-scheduler)", + re.IGNORECASE, +) +SCHEDULE_RE = re.compile(r"(?m)^\s*schedule\s*:") +WORKFLOW_DISPATCH_RE = re.compile(r"(?m)^\s*workflow_dispatch\s*:") + + +class GitHubError(RuntimeError): + """Represent a bounded GitHub API or authentication failure.""" + + +class SnapshotChanged(RuntimeError): + """Signal that a repository moved while one snapshot was materialized.""" + + +class ActionKind(str, enum.Enum): + """Supported coordinator mutation classes.""" + + REVIEW_REPAIR = "review_repair" + PRODUCT_DEVELOPMENT = "product_development" + + +@dataclasses.dataclass(frozen=True) +class WorkflowRecord: + """Describe one repository workflow and its exact inspected source.""" + + workflow_id: int + name: str + path: str + state: str + content_sha: str + content: str | None + + +@dataclasses.dataclass(frozen=True) +class RunRecord: + """Describe one workflow run that may hold a live writer lease.""" + + run_id: int + name: str + path: str + status: str + head_sha: str + + +@dataclasses.dataclass(frozen=True) +class PullRequestRecord: + """Describe the exact pull-request fields used by the selection policy.""" + + number: int + draft: bool + base_ref: str + head_sha: str + updated_at: str + + +@dataclasses.dataclass(frozen=True) +class RepositorySnapshot: + """Bind repository selection evidence to one stable default-branch state.""" + + full_name: str + default_branch: str + default_sha: str + workflows: tuple[WorkflowRecord, ...] + active_runs: tuple[RunRecord, ...] + open_pulls: tuple[PullRequestRecord, ...] + + @property + def fingerprint(self) -> str: + """Return a deterministic digest independent of API result ordering.""" + payload = { + "full_name": self.full_name, + "default_branch": self.default_branch, + "default_sha": self.default_sha, + "workflows": sorted( + ( + item.workflow_id, + item.name, + item.path, + item.state, + item.content_sha, + ) + for item in self.workflows + ), + "active_runs": sorted( + (item.run_id, item.name, item.path, item.status, item.head_sha) + for item in self.active_runs + ), + "open_pulls": sorted( + ( + item.number, + item.draft, + item.base_ref, + item.head_sha, + item.updated_at, + ) + for item in self.open_pulls + ), + } + canonical = json.dumps(payload, sort_keys=True, separators=(",", ":")) + return hashlib.sha256(canonical.encode("utf-8")).hexdigest() + + +@dataclasses.dataclass(frozen=True) +class PlanItem: + """Describe one bounded mutation selected from an initial snapshot.""" + + kind: ActionKind + repository: str + default_branch: str + expected_fingerprint: str + workflow_id: int | None = None + + +@dataclasses.dataclass(frozen=True) +class ActionResult: + """Record the outcome of one revalidated coordinator action.""" + + kind: ActionKind + repository: str + status: str + detail: str + + +@dataclasses.dataclass(frozen=True) +class RunReport: + """Provide machine-readable and operator-readable evidence for one run.""" + + organization: str + inspected_repositories: int + leased_repositories: tuple[str, ...] + inspection_errors: tuple[tuple[str, str], ...] + actions: tuple[ActionResult, ...] + dry_run: bool + + def to_dict(self) -> dict[str, Any]: + """Return a JSON-serializable representation of this report.""" + return { + "organization": self.organization, + "inspected_repositories": self.inspected_repositories, + "leased_repositories": list(self.leased_repositories), + "inspection_errors": [ + {"repository": repository, "error": error} + for repository, error in self.inspection_errors + ], + "actions": [ + { + "kind": action.kind.value, + "repository": action.repository, + "status": action.status, + "detail": action.detail, + } + for action in self.actions + ], + "dry_run": self.dry_run, + } + + def to_json(self) -> str: + """Serialize this report as stable UTF-8 JSON text.""" + return json.dumps(self.to_dict(), ensure_ascii=False, indent=2, sort_keys=True) + + def to_markdown(self) -> str: + """Render a concise GitHub Actions job summary.""" + lines = [ + "# Organization commercial-readiness coordinator", + "", + f"- Organization: `{self.organization}`", + f"- Repositories inspected: **{self.inspected_repositories}**", + f"- Repositories leased to dedicated writers: **{len(self.leased_repositories)}**", + f"- Inspection errors: **{len(self.inspection_errors)}**", + f"- Dry run: **{'yes' if self.dry_run else 'no'}**", + "", + "## Actions", + "", + "| Kind | Repository | Status | Detail |", + "|---|---|---|---|", + ] + if self.actions: + for action in self.actions: + detail = action.detail.replace("|", "\\|").replace("\n", " ") + lines.append( + f"| `{action.kind.value}` | `{action.repository}` | " + f"`{action.status}` | {detail} |" + ) + else: + lines.append("| — | — | `no_action` | No safe target was selected. |") + if self.inspection_errors: + lines.extend(["", "## Inspection errors", ""]) + for repository, error in self.inspection_errors: + lines.append(f"- `{repository}`: {error}") + return "\n".join(lines) + "\n" + + +class GitHubClient: + """Use the GitHub CLI as an authenticated, bounded REST transport.""" + + def __init__(self, token: str, *, timeout_seconds: int = 60) -> None: + if not token: + raise GitHubError("GH_TOKEN is required for organization coordination") + self._token = token + self._timeout_seconds = timeout_seconds + + @classmethod + def from_environment(cls, environ: Mapping[str, str] | None = None) -> GitHubClient: + """Build a client without accepting the repository-scoped GITHUB_TOKEN.""" + values = os.environ if environ is None else environ + token = str(values.get("GH_TOKEN") or "").strip() + if not token: + raise GitHubError("GH_TOKEN is required; no GITHUB_TOKEN fallback is permitted") + return cls(token) + + def request( + self, + path: str, + *, + method: str = "GET", + payload: Any = None, + ) -> Any: + """Call one GitHub REST endpoint and decode a bounded JSON response.""" + normalized_method = method.upper() + args = ["gh", "api"] + if normalized_method != "GET": + args.extend(["--method", normalized_method]) + args.append(path) + input_text: str | None = None + if payload is not None: + args.extend(["--input", "-"]) + input_text = json.dumps(payload, separators=(",", ":")) + try: + completed = subprocess.run( + args, + input=input_text, + capture_output=True, + text=True, + timeout=self._timeout_seconds, + env={**os.environ, "GH_TOKEN": self._token}, + check=False, + ) + except (OSError, subprocess.TimeoutExpired) as exc: + raise GitHubError(f"GitHub API transport failed: {type(exc).__name__}") from exc + if completed.returncode != 0: + raw = (completed.stderr or completed.stdout or "GitHub API request failed").strip() + bounded = raw[-900:].replace(self._token, "[REDACTED]") + raise GitHubError(f"GitHub API {normalized_method} {path} failed: {bounded}") + text = completed.stdout.strip() + if not text: + return None + try: + return json.loads(text) + except json.JSONDecodeError as exc: + raise GitHubError(f"GitHub API returned invalid JSON for {path}") from exc + + def list_repositories(self, organization: str) -> list[dict[str, Any]]: + """Return every repository visible to the coordinator installation.""" + repositories: list[dict[str, Any]] = [] + page = 1 + while True: + result = self.request( + f"/orgs/{organization}/repos?type=all&sort=full_name&per_page=100&page={page}" + ) + batch = list(result or []) + repositories.extend(batch) + if len(batch) < 100: + return repositories + page += 1 + + def default_branch_sha(self, repository: str, default_branch: str) -> str: + """Resolve one exact commit for the repository default branch.""" + branch_ref = quote(default_branch, safe="") + result = self.request(f"/repos/{repository}/commits/{branch_ref}") + sha = str((result or {}).get("sha") or "") + if not re.fullmatch(r"[0-9a-fA-F]{40}", sha): + raise GitHubError(f"repository {repository} returned an invalid default-branch SHA") + return sha.lower() + + def list_workflows(self, repository: str, exact_ref: str) -> tuple[WorkflowRecord, ...]: + """Return workflow metadata and exact source only for writer candidates.""" + workflows: list[WorkflowRecord] = [] + page = 1 + while True: + result = self.request( + f"/repos/{repository}/actions/workflows?per_page=100&page={page}" + ) + batch = list((result or {}).get("workflows") or []) + for raw in batch: + workflow_id = int(raw.get("id") or 0) + path = str(raw.get("path") or "") + name = str(raw.get("name") or path) + state = str(raw.get("state") or "unknown") + content: str | None = None + content_sha = "" + if ( + path + and not path.startswith("dynamic/") + and _writer_signal(name, path) + ): + encoded_path = quote(path, safe="/") + try: + source = self.request( + f"/repos/{repository}/contents/{encoded_path}?ref={exact_ref}" + ) + if ( + isinstance(source, dict) + and source.get("type") == "file" + and int(source.get("size") or 0) <= 1_048_576 + and source.get("encoding") == "base64" + ): + decoded = base64.b64decode( + str(source.get("content") or ""), validate=True + ) + content = decoded.decode("utf-8") + content_sha = str(source.get("sha") or "") + except (GitHubError, ValueError, UnicodeDecodeError): + content = None + content_sha = "" + workflows.append( + WorkflowRecord( + workflow_id=workflow_id, + name=name, + path=path, + state=state, + content_sha=content_sha, + content=content, + ) + ) + if len(batch) < 100: + return tuple(workflows) + page += 1 + + def list_active_runs(self, repository: str) -> tuple[RunRecord, ...]: + """Return all queued and running workflow evidence for writer lease detection.""" + records: list[RunRecord] = [] + for status in ("queued", "in_progress", "waiting", "pending", "requested"): + page = 1 + while True: + result = self.request( + f"/repos/{repository}/actions/runs?status={status}&per_page=100&page={page}" + ) + batch = list((result or {}).get("workflow_runs") or []) + for raw in batch: + records.append( + RunRecord( + run_id=int(raw.get("id") or 0), + name=str(raw.get("name") or ""), + path=str(raw.get("path") or ""), + status=str(raw.get("status") or status), + head_sha=str(raw.get("head_sha") or ""), + ) + ) + if len(batch) < 100: + break + page += 1 + return tuple(records) + + def list_open_pulls(self, repository: str) -> tuple[PullRequestRecord, ...]: + """Return all open pull requests with exact stack and head identity.""" + records: list[PullRequestRecord] = [] + page = 1 + while True: + result = self.request( + f"/repos/{repository}/pulls?state=open&per_page=100&page={page}" + ) + batch = list(result or []) + for raw in batch: + records.append( + PullRequestRecord( + number=int(raw.get("number") or 0), + draft=bool(raw.get("draft")), + base_ref=str((raw.get("base") or {}).get("ref") or ""), + head_sha=str((raw.get("head") or {}).get("sha") or ""), + updated_at=str(raw.get("updated_at") or ""), + ) + ) + if len(batch) < 100: + return tuple(records) + page += 1 + + def snapshot(self, repository: str, default_branch: str) -> RepositorySnapshot: + """Materialize one snapshot and reject concurrent default-branch movement.""" + before = self.default_branch_sha(repository, default_branch) + workflows = self.list_workflows(repository, before) + runs = self.list_active_runs(repository) + pulls = self.list_open_pulls(repository) + after = self.default_branch_sha(repository, default_branch) + if before != after: + raise SnapshotChanged( + f"default branch moved while inspecting {repository}: {before} -> {after}" + ) + return RepositorySnapshot( + full_name=repository, + default_branch=default_branch, + default_sha=before, + workflows=workflows, + active_runs=runs, + open_pulls=pulls, + ) + + def dispatch_review_repair(self, repository: str, base_branch: str) -> None: + """Ask the established central scheduler for one bounded repair attempt.""" + self.request( + f"/repos/{CENTRAL_REPOSITORY}/dispatches", + method="POST", + payload={ + "event_type": CENTRAL_REPAIR_EVENT, + "client_payload": { + "target_repository": repository, + "base_branch": base_branch, + "max_prs": "50", + "max_dispatches": "1", + "retry_hours": "1", + "dry_run": False, + }, + }, + ) + + def dispatch_product_workflow( + self, repository: str, workflow_id: int, default_branch: str + ) -> None: + """Dispatch an explicitly opted-in repository-local development entrypoint.""" + self.request( + f"/repos/{repository}/actions/workflows/{workflow_id}/dispatches", + method="POST", + payload={"ref": default_branch}, + ) + + +def _writer_signal(name: str, path: str) -> bool: + """Return whether workflow identity indicates a repository writer.""" + identity = f"{name}\n{path}" + return bool(WRITER_SIGNAL_RE.search(identity)) and not bool( + MERGE_SCHEDULER_RE.search(identity) + ) + + +def is_dedicated_writer_workflow(workflow: WorkflowRecord) -> bool: + """Return whether an active scheduled workflow owns the repository writer lease.""" + if workflow.state != "active" or not _writer_signal(workflow.name, workflow.path): + return False + if workflow.content is None: + return True + return bool(SCHEDULE_RE.search(workflow.content)) + + +def is_live_writer_run(run: RunRecord) -> bool: + """Return whether a queued or running high-signal workflow owns a live lease.""" + return run.status in ACTIVE_RUN_STATES and _writer_signal(run.name, run.path) + + +def is_manual_product_entrypoint(workflow: WorkflowRecord) -> bool: + """Return whether a workflow explicitly opts in to central product dispatch.""" + source = workflow.content + if workflow.state != "active" or source is None: + return False + return all( + ( + ENTRYPOINT_MARKER in source, + bool(WORKFLOW_DISPATCH_RE.search(source)), + not bool(SCHEDULE_RE.search(source)), + "NVIDIA_NIM_API_KEY" in source, + "COPILOT_GITHUB_TOKEN" not in source, + "concurrency:" in source, + _writer_signal(workflow.name, workflow.path), + ) + ) + + +def repository_is_eligible(repository: Mapping[str, Any], organization: str) -> bool: + """Return whether one owned repository can participate in organization coordination.""" + full_name = str(repository.get("full_name") or "") + permissions = repository.get("permissions") or {} + write_capable = any(bool(permissions.get(key)) for key in ("push", "maintain", "admin")) + return all( + ( + full_name.startswith(f"{organization}/"), + full_name != f"{organization}/.github", + not bool(repository.get("archived")), + not bool(repository.get("disabled")), + not bool(repository.get("fork")), + bool(repository.get("default_branch")), + write_capable, + ) + ) + + +def choose_rotating(items: Sequence[Any], seed: int, limit: int) -> tuple[Any, ...]: + """Choose a bounded cyclic window so later repositories are not starved.""" + if not items or limit <= 0: + return () + count = min(limit, len(items)) + start = seed % len(items) + return tuple(items[(start + offset) % len(items)] for offset in range(count)) + + +def _has_writer_lease(snapshot: RepositorySnapshot) -> bool: + """Return whether static or live evidence assigns this repository elsewhere.""" + return any(is_dedicated_writer_workflow(item) for item in snapshot.workflows) or any( + is_live_writer_run(item) for item in snapshot.active_runs + ) + + +def _eligible_review_snapshot(snapshot: RepositorySnapshot) -> bool: + """Return whether generic review repair is safe for at least one direct PR.""" + return any( + not pull.draft and pull.base_ref == snapshot.default_branch + for pull in snapshot.open_pulls + ) + + +def _manual_product_workflow(snapshot: RepositorySnapshot) -> WorkflowRecord | None: + """Return the first deterministic opted-in manual development entrypoint.""" + matches = sorted( + (item for item in snapshot.workflows if is_manual_product_entrypoint(item)), + key=lambda item: (item.path, item.workflow_id), + ) + return matches[0] if matches else None + + +def build_plan( + snapshots: Iterable[RepositorySnapshot], + *, + rotation_seed: int, + max_review_dispatches: int = 1, + max_development_dispatches: int = 1, +) -> tuple[PlanItem, ...]: + """Select independent bounded review and product targets from exact snapshots.""" + usable = tuple( + sorted( + ( + item + for item in snapshots + if item.full_name != CENTRAL_REPOSITORY and not _has_writer_lease(item) + ), + key=lambda item: item.full_name, + ) + ) + review_candidates = tuple(item for item in usable if _eligible_review_snapshot(item)) + development_candidates = tuple( + (item, workflow) + for item in usable + if not item.open_pulls + for workflow in (_manual_product_workflow(item),) + if workflow is not None + ) + plan: list[PlanItem] = [] + for item in choose_rotating(review_candidates, rotation_seed, max_review_dispatches): + plan.append( + PlanItem( + kind=ActionKind.REVIEW_REPAIR, + repository=item.full_name, + default_branch=item.default_branch, + expected_fingerprint=item.fingerprint, + ) + ) + for item, workflow in choose_rotating( + development_candidates, rotation_seed, max_development_dispatches + ): + plan.append( + PlanItem( + kind=ActionKind.PRODUCT_DEVELOPMENT, + repository=item.full_name, + default_branch=item.default_branch, + expected_fingerprint=item.fingerprint, + workflow_id=workflow.workflow_id, + ) + ) + return tuple(plan) + + +def _bounded_error(exc: BaseException) -> str: + """Return a stable, bounded error description without stack or credential data.""" + text = f"{type(exc).__name__}: {exc}".replace("\n", " ") + return text[:1000] + + +def run_once( + client: Any, + *, + organization: str, + rotation_seed: int, + max_repositories: int = 200, + max_review_dispatches: int = 1, + max_development_dispatches: int = 1, + dry_run: bool = False, +) -> RunReport: + """Inspect the organization, revalidate targets, and dispatch bounded work.""" + if organization != DEFAULT_ORGANIZATION: + raise GitHubError( + f"organization must be {DEFAULT_ORGANIZATION}; foreign control planes are not supported" + ) + raw_repositories = client.list_repositories(organization) + eligible = sorted( + ( + item + for item in raw_repositories + if repository_is_eligible(item, organization) + ), + key=lambda item: str(item.get("full_name") or ""), + ) + selected_repositories = choose_rotating(eligible, rotation_seed, max_repositories) + snapshots: list[RepositorySnapshot] = [] + errors: list[tuple[str, str]] = [] + leased: list[str] = [] + for repository in selected_repositories: + full_name = str(repository["full_name"]) + default_branch = str(repository["default_branch"]) + try: + current = client.snapshot(full_name, default_branch) + except (GitHubError, SnapshotChanged) as exc: + errors.append((full_name, _bounded_error(exc))) + continue + snapshots.append(current) + if _has_writer_lease(current): + leased.append(full_name) + plan = build_plan( + snapshots, + rotation_seed=rotation_seed, + max_review_dispatches=max_review_dispatches, + max_development_dispatches=max_development_dispatches, + ) + actions: list[ActionResult] = [] + for item in plan: + try: + live = client.snapshot(item.repository, item.default_branch) + except (GitHubError, SnapshotChanged) as exc: + actions.append( + ActionResult( + kind=item.kind, + repository=item.repository, + status="skipped_refetch_error", + detail=_bounded_error(exc), + ) + ) + continue + if _has_writer_lease(live): + actions.append( + ActionResult( + kind=item.kind, + repository=item.repository, + status="skipped_writer_lease", + detail="a dedicated or live writer appeared before dispatch", + ) + ) + continue + if live.fingerprint != item.expected_fingerprint: + actions.append( + ActionResult( + kind=item.kind, + repository=item.repository, + status="skipped_state_changed", + detail="repository, workflow, run, or pull-request state moved before dispatch", + ) + ) + continue + if dry_run: + actions.append( + ActionResult( + kind=item.kind, + repository=item.repository, + status="dry_run", + detail="exact state revalidated; mutation intentionally suppressed", + ) + ) + continue + try: + if item.kind is ActionKind.REVIEW_REPAIR: + client.dispatch_review_repair(item.repository, item.default_branch) + else: + if item.workflow_id is None: + raise GitHubError("product-development plan omitted workflow identity") + client.dispatch_product_workflow( + item.repository, item.workflow_id, item.default_branch + ) + except GitHubError as exc: + actions.append( + ActionResult( + kind=item.kind, + repository=item.repository, + status="dispatch_failed", + detail=_bounded_error(exc), + ) + ) + else: + actions.append( + ActionResult( + kind=item.kind, + repository=item.repository, + status="dispatched", + detail="exact state revalidated and bounded workflow dispatched", + ) + ) + return RunReport( + organization=organization, + inspected_repositories=len(snapshots), + leased_repositories=tuple(sorted(leased)), + inspection_errors=tuple(errors), + actions=tuple(actions), + dry_run=dry_run, + ) + + +def _non_negative_int(value: str) -> int: + """Parse one non-negative integer command-line bound.""" + parsed = int(value) + if parsed < 0: + raise argparse.ArgumentTypeError("value must be zero or greater") + return parsed + + +def _parser() -> argparse.ArgumentParser: + """Build the command-line parser used by workflow and local dry runs.""" + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--organization", default=DEFAULT_ORGANIZATION) + parser.add_argument("--rotation-seed", type=int, default=0) + parser.add_argument("--max-repositories", type=_non_negative_int, default=200) + parser.add_argument("--max-review-dispatches", type=_non_negative_int, default=1) + parser.add_argument("--max-development-dispatches", type=_non_negative_int, default=1) + parser.add_argument("--dry-run", action="store_true") + parser.add_argument("--json-output", type=Path) + return parser + + +def main( + argv: Sequence[str] | None = None, + *, + client_factory: Callable[[], Any] | None = None, +) -> int: + """Run the coordinator CLI and persist auditable receipts.""" + parser = _parser() + try: + args = parser.parse_args(argv) + except SystemExit: + return 2 + if not ORGANIZATION_RE.fullmatch(args.organization): + print("invalid organization", file=sys.stderr) + return 2 + factory = client_factory or GitHubClient.from_environment + try: + client = factory() + report = run_once( + client, + organization=args.organization, + rotation_seed=args.rotation_seed, + max_repositories=args.max_repositories, + max_review_dispatches=args.max_review_dispatches, + max_development_dispatches=args.max_development_dispatches, + dry_run=args.dry_run, + ) + except (GitHubError, SnapshotChanged, ValueError) as exc: + print(_bounded_error(exc), file=sys.stderr) + return 2 + text = report.to_json() + "\n" + if args.json_output is not None: + args.json_output.parent.mkdir(parents=True, exist_ok=True) + args.json_output.write_text(text, encoding="utf-8") + else: + sys.stdout.write(text) + summary_path = os.environ.get("GITHUB_STEP_SUMMARY") + if summary_path: + with Path(summary_path).open("a", encoding="utf-8") as handle: + handle.write(report.to_markdown()) + all_selected_inspections_failed = ( + report.inspected_repositories == 0 and bool(report.inspection_errors) + ) + all_planned_dispatches_failed = bool(report.actions) and all( + action.status == "dispatch_failed" for action in report.actions + ) + return 1 if all_selected_inspections_failed or all_planned_dispatches_failed else 0 + + +if __name__ == "__main__": # pragma: no cover - exercised through main() + raise SystemExit(main()) diff --git a/tests/test_organization_commercial_readiness_loop_coordinator.py b/tests/test_organization_commercial_readiness_loop_coordinator.py new file mode 100644 index 000000000..0bd601d26 --- /dev/null +++ b/tests/test_organization_commercial_readiness_loop_coordinator.py @@ -0,0 +1,187 @@ +from __future__ import annotations + +import json +from pathlib import Path + +import pytest + +from organization_commercial_readiness_fixtures import ( + FailingDispatchClient, + FakeClient, + manual_workflow, + pull, + repository_payload, + snapshot, + workflow, +) +from scripts.ci.organization_commercial_readiness_loop import ( + ActionKind, + GitHubError, + PlanItem, + SnapshotChanged, + main, + run_once, +) + + +def test_run_dispatches_one_repair_and_one_independent_product() -> None: + """Unchanged exact state authorizes one bounded action of each class.""" + review = snapshot("ContextualWisdomLab/review", pulls=(pull(1),)) + product = snapshot( + "ContextualWisdomLab/product", workflows=(manual_workflow(workflow_id=17),) + ) + client = FakeClient( + [repository_payload("review"), repository_payload("product")], + {review.full_name: [review, review], product.full_name: [product, product]}, + ) + report = run_once(client, organization="ContextualWisdomLab", rotation_seed=0) + assert client.dispatched_repairs == [(review.full_name, "main")] + assert client.dispatched_products == [(product.full_name, 17, "main")] + assert [action.status for action in report.actions] == ["dispatched", "dispatched"] + assert json.loads(report.to_json())["inspected_repositories"] == 2 + + +def test_drift_new_lease_and_refetch_error_skip_only_the_target() -> None: + """Pre-dispatch movement invalidates selection without reusing old evidence.""" + review = snapshot("ContextualWisdomLab/review", pulls=(pull(1),)) + moved = snapshot( + review.full_name, default_sha="b" * 40, pulls=(pull(1, head_sha="c" * 40),) + ) + product = snapshot("ContextualWisdomLab/product", workflows=(manual_workflow(),)) + newly_leased = snapshot( + product.full_name, + workflows=( + manual_workflow(), + workflow( + workflow_id=8, + content='on:\n schedule:\n - cron: "9 * * * *"\n', + ), + ), + ) + broken = snapshot("ContextualWisdomLab/broken", pulls=(pull(2),)) + client = FakeClient( + [ + repository_payload("review"), + repository_payload("product"), + repository_payload("broken"), + ], + { + review.full_name: [review, moved], + product.full_name: [product, newly_leased], + broken.full_name: [broken, SnapshotChanged("moved")], + }, + ) + report = run_once( + client, + organization="ContextualWisdomLab", + rotation_seed=0, + max_review_dispatches=2, + ) + assert [item.status for item in report.actions] == [ + "skipped_refetch_error", + "skipped_state_changed", + "skipped_writer_lease", + ] + + +def test_initial_errors_leases_and_dry_run_are_reported() -> None: + """An inaccessible repo is contained; initial leases and dry-run stay explicit.""" + leased = snapshot( + "ContextualWisdomLab/leased", + workflows=(workflow(content='on:\n schedule:\n - cron: "7 * * * *"\n'),), + ) + review = snapshot("ContextualWisdomLab/review", pulls=(pull(1),)) + client = FakeClient( + [ + repository_payload("broken"), + repository_payload("leased"), + repository_payload("review"), + ], + { + "ContextualWisdomLab/broken": [GitHubError("forbidden")], + leased.full_name: [leased], + review.full_name: [review, review], + }, + ) + report = run_once( + client, + organization="ContextualWisdomLab", + rotation_seed=0, + dry_run=True, + ) + assert report.inspection_errors == ( + ("ContextualWisdomLab/broken", "GitHubError: forbidden"), + ) + assert report.leased_repositories == (leased.full_name,) + assert report.actions[0].status == "dry_run" + assert not client.dispatched_repairs + + +def test_dispatch_failures_and_invalid_internal_product_plan( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """API rejection and an impossible product plan both fail closed per action.""" + review = snapshot("ContextualWisdomLab/review", pulls=(pull(1),)) + failing = FailingDispatchClient( + [repository_payload("review")], {review.full_name: [review, review]} + ) + assert run_once( + failing, organization="ContextualWisdomLab", rotation_seed=0 + ).actions[0].status == "dispatch_failed" + + product = snapshot("ContextualWisdomLab/product") + invalid = PlanItem( + ActionKind.PRODUCT_DEVELOPMENT, + product.full_name, + "main", + product.fingerprint, + None, + ) + monkeypatch.setattr( + "scripts.ci.organization_commercial_readiness_loop.build_plan", + lambda *_args, **_kwargs: (invalid,), + ) + client = FakeClient( + [repository_payload("product")], {product.full_name: [product, product]} + ) + assert run_once( + client, organization="ContextualWisdomLab", rotation_seed=0 + ).actions[0].status == "dispatch_failed" + + +def test_main_writes_file_summary_stdout_and_failure_paths( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + capsys: pytest.CaptureFixture[str], +) -> None: + """CLI output and invalid configuration have deterministic exit behavior.""" + review = snapshot("ContextualWisdomLab/review", pulls=(pull(1),)) + client = FakeClient( + [repository_payload("review")], {review.full_name: [review, review]} + ) + output, summary = tmp_path / "report.json", tmp_path / "summary.md" + monkeypatch.setenv("GITHUB_STEP_SUMMARY", str(summary)) + assert main( + ["--rotation-seed", "2", "--json-output", str(output)], + client_factory=lambda: client, + ) == 0 + assert json.loads(output.read_text())["actions"][0]["status"] == "dispatched" + assert "ContextualWisdomLab/review" in summary.read_text() + + empty = FakeClient([], {}) + monkeypatch.delenv("GITHUB_STEP_SUMMARY") + assert main( + ["--max-repositories", "0", "--max-review-dispatches", "0"], + client_factory=lambda: empty, + ) == 0 + assert '"inspected_repositories": 0' in capsys.readouterr().out + + assert main( + ["--organization", "bad organization"], client_factory=lambda: empty + ) == 2 + assert "invalid organization" in capsys.readouterr().err + assert main( + [], client_factory=lambda: (_ for _ in ()).throw(GitHubError("auth")) + ) == 2 + assert "GitHubError: auth" in capsys.readouterr().err + assert main(["--max-repositories", "-1"], client_factory=lambda: empty) == 2 diff --git a/tests/test_organization_commercial_readiness_loop_credential_contract.py b/tests/test_organization_commercial_readiness_loop_credential_contract.py new file mode 100644 index 000000000..3225d5832 --- /dev/null +++ b/tests/test_organization_commercial_readiness_loop_credential_contract.py @@ -0,0 +1,21 @@ +from pathlib import Path + + +WORKFLOW_PATH = ( + Path(__file__).resolve().parents[1] + / ".github" + / "workflows" + / "organization-commercial-readiness-loop.yml" +) + + +def test_central_schedule_has_no_branch_selected_or_reviewer_credential_path() -> None: + """The fleet coordinator must be schedule-only and use maintainer authority.""" + source = WORKFLOW_PATH.read_text(encoding="utf-8") + + assert "workflow_dispatch:" not in source + assert "GH_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN }}" in source + assert "persist-credentials: false" in source + assert "OPENCODE_APPROVE_TOKEN" not in source + assert "DRY_RUN" not in source + assert "inputs.dry_run" not in source diff --git a/tests/test_organization_commercial_readiness_loop_github.py b/tests/test_organization_commercial_readiness_loop_github.py new file mode 100644 index 000000000..aa4bfa576 --- /dev/null +++ b/tests/test_organization_commercial_readiness_loop_github.py @@ -0,0 +1,226 @@ +from __future__ import annotations + +import base64 +from typing import Any + +import pytest + +from organization_commercial_readiness_fixtures import repository_payload +from scripts.ci.organization_commercial_readiness_loop import ( + GitHubClient, + GitHubError, + SnapshotChanged, +) + + +def test_client_requires_explicit_token_and_decodes_requests( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Organization access never falls back and JSON/empty/error responses stay distinct.""" + with pytest.raises(GitHubError, match="GH_TOKEN"): + GitHubClient("") + with pytest.raises(GitHubError, match="GH_TOKEN"): + GitHubClient.from_environment({}) + assert isinstance(GitHubClient.from_environment({"GH_TOKEN": " token "}), GitHubClient) + monkeypatch.setenv("GH_TOKEN", "live") + assert isinstance(GitHubClient.from_environment(), GitHubClient) + + class Completed: + def __init__(self, code: int, out: str = "", err: str = "") -> None: + self.returncode, self.stdout, self.stderr = code, out, err + + responses = [Completed(0, '{"ok":true}'), Completed(0), Completed(1, err="x" * 2000)] + calls: list[list[str]] = [] + + def fake_run(args: list[str], **kwargs: Any) -> Completed: + calls.append(args) + assert kwargs["env"]["GH_TOKEN"] == "token" # noqa: S105 + return responses.pop(0) + + monkeypatch.setattr("subprocess.run", fake_run) + client = GitHubClient("token") + assert client.request("/ok") == {"ok": True} + assert client.request("/empty", method="POST", payload={"a": 1}) is None + with pytest.raises(GitHubError) as error: + client.request("/fail") + assert len(str(error.value)) < 1200 + assert calls[1][:4] == ["gh", "api", "--method", "POST"] + + +def test_client_transport_and_invalid_json_fail_closed( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Transport and JSON failures never become empty successful evidence.""" + client = GitHubClient("secret") + monkeypatch.setattr( + "subprocess.run", + lambda *_args, **_kwargs: (_ for _ in ()).throw(OSError("network")), + ) + with pytest.raises(GitHubError, match="transport failed"): + client.request("/transport") + + class Completed: + returncode, stdout, stderr = 0, "not-json", "" + + monkeypatch.setattr("subprocess.run", lambda *_args, **_kwargs: Completed()) + with pytest.raises(GitHubError, match="invalid JSON"): + client.request("/invalid") + + +def test_repository_pagination_and_default_sha_validation( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Fleet discovery spans pages and exact default evidence is mandatory.""" + client = GitHubClient("token") + pages = [ + [repository_payload(f"repo-{index}") for index in range(100)], + [repository_payload("last")], + ] + monkeypatch.setattr(client, "request", lambda _path: pages.pop(0)) + assert len(client.list_repositories("ContextualWisdomLab")) == 101 + monkeypatch.setattr(client, "request", lambda _path: {"sha": "bad"}) + with pytest.raises(GitHubError, match="invalid default-branch SHA"): + client.default_branch_sha("ContextualWisdomLab/example", "release/v1") + + +def test_workflow_source_materialization_and_pagination( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Exact workflow source is decoded while unsafe source shapes remain unreadable.""" + client = GitHubClient("token") + page = [ + { + "id": index + 1, + "name": "Hourly Product Development", + "path": "" if index == 0 else "dynamic/x" if index == 1 else f".github/workflows/{index}.yml", + "state": "active", + } + for index in range(100) + ] + workflow_calls = 0 + + def fake(path: str, *, method: str = "GET", payload: Any = None) -> Any: + nonlocal workflow_calls + del method, payload + if "actions/workflows" in path: + workflow_calls += 1 + return {"workflows": page if workflow_calls == 1 else []} + if "/contents/" in path: + index = int(path.split("/")[-1].split(".")[0]) + if index == 2: + data = b"on:\n workflow_dispatch:\n" + return { + "type": "file", + "size": len(data), + "sha": "good", + "encoding": "base64", + "content": base64.b64encode(data).decode(), + } + if index == 8: + raise GitHubError("forbidden") + variants: list[Any] = [ + None, + {"type": "dir", "size": 0, "encoding": "base64"}, + {"type": "file", "size": 1_048_577, "encoding": "base64"}, + {"type": "file", "size": 1, "encoding": "utf-8"}, + {"type": "file", "size": 1, "encoding": "base64", "content": "%%%"}, + {"type": "file", "size": 1, "encoding": "base64", "content": "/w=="}, + ] + return variants[(index - 3) % len(variants)] + raise AssertionError(path) + + monkeypatch.setattr(client, "request", fake) + records = client.list_workflows("ContextualWisdomLab/example", "a" * 40) + assert len(records) == 100 and workflow_calls == 2 + assert records[2].content_sha == "good" + assert sum(item.content is not None for item in records) == 1 + + +def test_run_and_pull_inventories_cover_live_fields_and_pages( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Live-run and pull inventories preserve exact identity across pages.""" + client = GitHubClient("token") + pull_calls = 0 + + def fake(path: str, *, method: str = "GET", payload: Any = None) -> Any: + nonlocal pull_calls + del method, payload + if "actions/runs" in path: + status = path.split("status=")[1].split("&")[0] + page = int(path.split("&page=")[1].split("&")[0]) + if page > 1: + return {"workflow_runs": []} + return { + "workflow_runs": [{ + "id": len(status), + "name": "Hourly Product Development", + "path": ".github/workflows/hourly-product-development.yml", + "status": "" if status == "queued" else status, + "head_sha": "a" * 40, + }] + } + if "/pulls?" in path: + pull_calls += 1 + size = 100 if pull_calls == 1 else 1 + return [{ + "number": index + 1, + "draft": False, + "base": {"ref": "main"}, + "head": {"sha": f"{index + 1:040x}"}, + "updated_at": "2026-08-08T00:00:00Z", + } for index in range(size)] + raise AssertionError(path) + + monkeypatch.setattr(client, "request", fake) + runs = client.list_active_runs("ContextualWisdomLab/example") + assert len(runs) == 5 and runs[0].status == "queued" + assert len(client.list_open_pulls("ContextualWisdomLab/example")) == 101 + + +def test_snapshot_movement_and_dispatch_payloads(monkeypatch: pytest.MonkeyPatch) -> None: + """Snapshots reject movement and dispatches retain the reviewed bounded payloads.""" + client = GitHubClient("token") + shas = iter(("a" * 40, "b" * 40)) + monkeypatch.setattr(client, "default_branch_sha", lambda _repo, _branch: next(shas)) + monkeypatch.setattr(client, "list_workflows", lambda _repo, _ref: ()) + monkeypatch.setattr(client, "list_active_runs", lambda _repo: ()) + monkeypatch.setattr(client, "list_open_pulls", lambda _repo: ()) + with pytest.raises(SnapshotChanged): + client.snapshot("ContextualWisdomLab/example", "main") + + calls: list[tuple[str, str, Any]] = [] + + def capture(path: str, *, method: str = "GET", payload: Any = None) -> None: + calls.append((path, method, payload)) + + monkeypatch.setattr(client, "request", capture) + client.dispatch_review_repair("ContextualWisdomLab/example", "develop") + client.dispatch_product_workflow("ContextualWisdomLab/example", 91, "develop") + assert calls[0][2]["client_payload"] == { + "target_repository": "ContextualWisdomLab/example", + "base_branch": "develop", + "max_prs": "50", + "max_dispatches": "1", + "retry_hours": "1", + "dry_run": False, + } + assert calls[1][2] == {"ref": "develop"} + + +def test_complete_snapshot_materialization(monkeypatch: pytest.MonkeyPatch) -> None: + """One stable default head yields workflows, runs, and pull records together.""" + client = GitHubClient("token") + monkeypatch.setattr(client, "default_branch_sha", lambda _repo, _branch: "a" * 40) + monkeypatch.setattr(client, "list_workflows", lambda _repo, _ref: ()) + monkeypatch.setattr(client, "list_active_runs", lambda _repo: ()) + monkeypatch.setattr(client, "list_open_pulls", lambda _repo: ()) + result = client.snapshot("ContextualWisdomLab/example", "main") + assert result.default_sha == "a" * 40 + + +def test_default_branch_sha_normalizes_valid_hex(monkeypatch: pytest.MonkeyPatch) -> None: + """Valid exact branch identity is normalized before fingerprinting.""" + client = GitHubClient("token") + monkeypatch.setattr(client, "request", lambda _path: {"sha": "A" * 40}) + assert client.default_branch_sha("ContextualWisdomLab/example", "main") == "a" * 40 diff --git a/tests/test_organization_commercial_readiness_loop_import_contract.py b/tests/test_organization_commercial_readiness_loop_import_contract.py new file mode 100644 index 000000000..43c3c71ac --- /dev/null +++ b/tests/test_organization_commercial_readiness_loop_import_contract.py @@ -0,0 +1,20 @@ +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parents[1] +QUALITY_WORKFLOW = ( + REPO_ROOT + / ".github" + / "workflows" + / "organization-commercial-readiness-loop-quality-ci.yml" +) + + +def test_quality_gate_uses_import_stable_test_support() -> None: + """Hosted and complete-suite collection must resolve the same helper module.""" + source = QUALITY_WORKFLOW.read_text(encoding="utf-8") + + assert "--import-mode=importlib" in source + assert '"organization_commercial_readiness_fixtures.py"' in source + assert "tests/organization_commercial_readiness_fixtures.py" not in source + assert "--include='scripts/ci/organization_commercial_readiness_loop.py' \\\n -m pytest" not in source diff --git a/tests/test_organization_commercial_readiness_loop_operational_failures.py b/tests/test_organization_commercial_readiness_loop_operational_failures.py new file mode 100644 index 000000000..ae70d088d --- /dev/null +++ b/tests/test_organization_commercial_readiness_loop_operational_failures.py @@ -0,0 +1,39 @@ +from __future__ import annotations + +import pytest + +from organization_commercial_readiness_fixtures import ( + FailingDispatchClient, + FakeClient, + pull, + repository_payload, + snapshot, +) +from scripts.ci.organization_commercial_readiness_loop import GitHubError, main + + +def test_cli_fails_when_every_selected_repository_inspection_fails( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """A fleet-wide inspection outage must make the scheduled job non-green.""" + monkeypatch.delenv("GITHUB_STEP_SUMMARY", raising=False) + client = FakeClient( + [repository_payload("broken")], + {"ContextualWisdomLab/broken": [GitHubError("forbidden")]}, + ) + + assert main([], client_factory=lambda: client) == 1 + + +def test_cli_fails_when_every_planned_dispatch_fails( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """A run that cannot start any selected work must make the job non-green.""" + monkeypatch.delenv("GITHUB_STEP_SUMMARY", raising=False) + review = snapshot("ContextualWisdomLab/review", pulls=(pull(1),)) + client = FailingDispatchClient( + [repository_payload("review")], + {review.full_name: [review, review]}, + ) + + assert main([], client_factory=lambda: client) == 1 diff --git a/tests/test_organization_commercial_readiness_loop_organization_scope.py b/tests/test_organization_commercial_readiness_loop_organization_scope.py new file mode 100644 index 000000000..5b20bfe5e --- /dev/null +++ b/tests/test_organization_commercial_readiness_loop_organization_scope.py @@ -0,0 +1,23 @@ +from __future__ import annotations + +import pytest + +from organization_commercial_readiness_fixtures import FakeClient +from scripts.ci.organization_commercial_readiness_loop import GitHubError, main, run_once + + +def test_runtime_rejects_a_foreign_organization_before_inventory() -> None: + """A variable org must never dispatch through the fixed CWL control plane.""" + client = FakeClient([], {}) + + with pytest.raises(GitHubError, match="ContextualWisdomLab"): + run_once(client, organization="OtherOrganization", rotation_seed=0) + + +def test_cli_rejects_a_well_formed_foreign_organization() -> None: + """A syntactically valid foreign org is still outside this scheduler's scope.""" + client = FakeClient([], {}) + + assert main( + ["--organization", "OtherOrganization"], client_factory=lambda: client + ) == 2 diff --git a/tests/test_organization_commercial_readiness_loop_policy.py b/tests/test_organization_commercial_readiness_loop_policy.py new file mode 100644 index 000000000..920f8072f --- /dev/null +++ b/tests/test_organization_commercial_readiness_loop_policy.py @@ -0,0 +1,177 @@ +from __future__ import annotations + +from pathlib import Path +from typing import Any + +from organization_commercial_readiness_fixtures import ( + manual_workflow, + pull, + snapshot, + workflow, +) +from scripts.ci.organization_commercial_readiness_loop import ( + ActionKind, + ActionResult, + RunRecord, + RunReport, + build_plan, + choose_rotating, + is_dedicated_writer_workflow, + is_live_writer_run, + is_manual_product_entrypoint, + repository_is_eligible, +) + +ROOT = Path(__file__).resolve().parents[1] + + +def test_static_and_live_writer_lease_policy() -> None: + """Only active high-signal writers, including unreadable ones, hold leases.""" + scheduled = workflow(content='on:\n schedule:\n - cron: "1 * * * *"\n') + disabled = workflow(state="disabled_manually", content=scheduled.content) + manual = workflow(content="on:\n workflow_dispatch:\n") + merge = workflow( + name="Required PR Review Merge Scheduler", + path=".github/workflows/pr-review-merge-scheduler.yml", + content='on:\n schedule:\n - cron: "*/15 * * * *"\n', + ) + assert is_dedicated_writer_workflow(scheduled) + assert is_dedicated_writer_workflow(workflow(content=None)) + assert not is_dedicated_writer_workflow(disabled) + assert not is_dedicated_writer_workflow(manual) + assert not is_dedicated_writer_workflow(merge) + + active = RunRecord(1, scheduled.name, scheduled.path, "in_progress", "a" * 40) + complete = RunRecord(2, scheduled.name, scheduled.path, "completed", "b" * 40) + assert is_live_writer_run(active) + assert not is_live_writer_run(complete) + + +def test_product_entrypoint_requires_manual_nvidia_opt_in() -> None: + """Product dispatch requires a marked, unscheduled, credential-isolated workflow.""" + safe = manual_workflow() + assert is_manual_product_entrypoint(safe) + assert not is_manual_product_entrypoint(workflow(state="disabled_manually", content="x")) + assert not is_manual_product_entrypoint(workflow(content=None)) + for changed in ( + (safe.content or "") + 'schedule:\n - cron: "1 * * * *"\n', + (safe.content or "") + "COPILOT_GITHUB_TOKEN: forbidden\n", + (safe.content or "").replace("# cwl-org-commercial-entrypoint: v1\n", ""), + (safe.content or "").replace("concurrency:\n", ""), + ): + assert not is_manual_product_entrypoint(workflow(content=changed)) + + +def test_repository_eligibility_is_owned_and_write_capable() -> None: + """Archived, forked, disabled, foreign, central, and read-only repos are excluded.""" + base: dict[str, Any] = { + "full_name": "ContextualWisdomLab/example", + "default_branch": "main", + "archived": False, + "disabled": False, + "fork": False, + "permissions": {"push": True}, + } + assert repository_is_eligible(base, "ContextualWisdomLab") + variants = ( + {**base, "archived": True}, + {**base, "disabled": True}, + {**base, "fork": True}, + {**base, "default_branch": None}, + {**base, "full_name": "Other/example"}, + {**base, "full_name": "ContextualWisdomLab/.github"}, + {**base, "permissions": {"pull": True}}, + ) + assert all(not repository_is_eligible(item, "ContextualWisdomLab") for item in variants) + + +def test_rotation_and_plan_are_bounded_and_dependency_safe() -> None: + """Review and development rotate independently without drafts, stacks, or leases.""" + assert choose_rotating(("a", "b", "c"), 1, 2) == ("b", "c") + assert choose_rotating(("a", "b", "c"), 2, 4) == ("c", "a", "b") + assert choose_rotating((), 1, 1) == () + assert choose_rotating(("a",), 1, 0) == () + + records = ( + snapshot("ContextualWisdomLab/review-a", pulls=(pull(1),)), + snapshot("ContextualWisdomLab/review-b", pulls=(pull(2),)), + snapshot("ContextualWisdomLab/product", workflows=(manual_workflow(),)), + snapshot("ContextualWisdomLab/draft", pulls=(pull(3, draft=True),)), + snapshot("ContextualWisdomLab/stack", pulls=(pull(4, base_ref="feature/base"),)), + snapshot( + "ContextualWisdomLab/leased", + workflows=(workflow(content='on:\n schedule:\n - cron: "1 * * * *"\n'),), + pulls=(pull(5),), + ), + ) + plan = build_plan(records, rotation_seed=1) + assert [(item.kind, item.repository) for item in plan] == [ + (ActionKind.REVIEW_REPAIR, "ContextualWisdomLab/review-b"), + (ActionKind.PRODUCT_DEVELOPMENT, "ContextualWisdomLab/product"), + ] + assert plan[1].workflow_id == 9 + + +def test_snapshot_fingerprint_ignores_api_order_only() -> None: + """Reordered workflow and PR lists retain one exact-state fingerprint.""" + a = snapshot( + "ContextualWisdomLab/example", + workflows=(workflow(workflow_id=2), workflow(workflow_id=1)), + pulls=(pull(2), pull(1)), + ) + b = snapshot( + "ContextualWisdomLab/example", + workflows=(workflow(workflow_id=1), workflow(workflow_id=2)), + pulls=(pull(1), pull(2)), + ) + assert a.fingerprint == b.fingerprint + + +def test_report_formats_actions_empty_state_and_errors() -> None: + """JSON and Markdown receipts preserve bounded action and failure evidence.""" + report = RunReport( + "ContextualWisdomLab", + 1, + ("ContextualWisdomLab/leased",), + (("ContextualWisdomLab/broken", "error|detail\nnext"),), + (ActionResult(ActionKind.REVIEW_REPAIR, "ContextualWisdomLab/a", "dry_run", "a|b"),), + True, + ) + assert '"dry_run": true' in report.to_json() + assert "a\\|b" in report.to_markdown() + empty = RunReport("ContextualWisdomLab", 0, (), (), (), False) + assert "No safe target" in empty.to_markdown() + + +def test_workflow_and_doctoring_contracts() -> None: + """Permanent files retain cadence, token, coverage, and realistic-scope controls.""" + workflow_source = ( + ROOT / ".github/workflows/organization-commercial-readiness-loop.yml" + ).read_text() + quality = ( + ROOT + / ".github/workflows/organization-commercial-readiness-loop-quality-ci.yml" + ).read_text() + doctoring = ( + ROOT / "docs/doctoring/organization-commercial-readiness-loop.md" + ).read_text() + assert 'cron: "7 * * * *"' in workflow_source + assert "cancel-in-progress: false" in workflow_source + assert 'MAX_REVIEW_DISPATCHES: "1"' in workflow_source + assert 'MAX_DEVELOPMENT_DISPATCHES: "1"' in workflow_source + assert "GH_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN }}" in workflow_source + assert "OPENCODE_APPROVE_TOKEN" not in workflow_source + assert "workflow_dispatch:" not in workflow_source + assert "|| github.token" not in workflow_source + assert "NVIDIA_NIM_API_KEY" not in workflow_source + assert "COPILOT_GITHUB_TOKEN" not in workflow_source + assert "github.run_number" in workflow_source + assert "persist-credentials: false" in workflow_source + assert "--branch" in quality and "--fail-under=100" in quality + assert "--import-mode=importlib" in quality + assert "organization_commercial_readiness_fixtures.py" in quality + assert "github.event.pull_request.head.sha" in quality + assert "disabled workflow does not hold a lease" in doctoring + assert "manual-only, explicitly marked" in doctoring + assert "does not make every repository directly writable" in doctoring + assert "GITHUB_TOKEN" in doctoring and "APA 7" in doctoring diff --git a/tests/test_organization_commercial_readiness_loop_receipt_contract.py b/tests/test_organization_commercial_readiness_loop_receipt_contract.py new file mode 100644 index 000000000..ce0956bba --- /dev/null +++ b/tests/test_organization_commercial_readiness_loop_receipt_contract.py @@ -0,0 +1,45 @@ +from pathlib import Path + +from organization_commercial_readiness_fixtures import manual_workflow, workflow +from scripts.ci.organization_commercial_readiness_loop import ( + is_manual_product_entrypoint, +) + + +WORKFLOW_PATH = ( + Path(__file__).resolve().parents[1] + / ".github" + / "workflows" + / "organization-commercial-readiness-loop.yml" +) + + +def test_product_entrypoint_rejects_missing_model_key_or_manual_trigger() -> None: + """Both the NVIDIA model boundary and manual opt-in trigger are mandatory.""" + safe = manual_workflow() + without_nvidia = (safe.content or "").replace( + "NVIDIA_NIM_API_KEY", "OTHER_API_KEY" + ) + without_dispatch = (safe.content or "").replace( + "on:\n workflow_dispatch:\n", "on:\n push:\n" + ) + + assert not is_manual_product_entrypoint(workflow(content=without_nvidia)) + assert not is_manual_product_entrypoint(workflow(content=without_dispatch)) + + +def test_json_receipt_is_retained_as_an_immutable_short_lived_artifact() -> None: + """The machine-readable fleet receipt must outlive ephemeral runner storage.""" + source = WORKFLOW_PATH.read_text(encoding="utf-8") + + assert ( + "uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a" + in source + ) + assert "name: organization-commercial-readiness-${{ github.run_id }}-${{ github.run_attempt }}" in source + assert "path: ${{ runner.temp }}/organization-commercial-readiness-loop.json" in source + assert "if-no-files-found: error" in source + assert "retention-days: 3" in source + assert "results-receiver.actions.githubusercontent.com:443" in source + assert "*.actions.githubusercontent.com:443" in source + assert "*.blob.core.windows.net:443" in source diff --git a/tests/test_organization_commercial_readiness_loop_run_pagination.py b/tests/test_organization_commercial_readiness_loop_run_pagination.py new file mode 100644 index 000000000..fc16ea669 --- /dev/null +++ b/tests/test_organization_commercial_readiness_loop_run_pagination.py @@ -0,0 +1,55 @@ +from __future__ import annotations + +from typing import Any + +import pytest + +from scripts.ci.organization_commercial_readiness_loop import GitHubClient + + +def test_active_writer_inventory_paginates_every_status( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """A writer beyond the first 100 active runs must still hold the lease.""" + client = GitHubClient("token") + requested_paths: list[str] = [] + + def fake(path: str, *, method: str = "GET", payload: Any = None) -> Any: + del method, payload + requested_paths.append(path) + status = path.split("status=")[1].split("&")[0] + page = int(path.rsplit("page=", maxsplit=1)[1]) + if status == "queued" and page == 1: + return { + "workflow_runs": [ + { + "id": index + 1, + "name": "Ordinary CI", + "path": ".github/workflows/ci.yml", + "status": "queued", + "head_sha": "a" * 40, + } + for index in range(100) + ] + } + if status == "queued" and page == 2: + return { + "workflow_runs": [ + { + "id": 101, + "name": "Hourly Product Development", + "path": ".github/workflows/hourly-product-development.yml", + "status": "queued", + "head_sha": "b" * 40, + } + ] + } + return {"workflow_runs": []} + + monkeypatch.setattr(client, "request", fake) + + records = client.list_active_runs("ContextualWisdomLab/example") + + assert len(records) == 101 + assert records[-1].name == "Hourly Product Development" + assert any("status=queued&per_page=100&page=2" in path for path in requested_paths) diff --git a/tests/test_organization_commercial_readiness_loop_secret_scope.py b/tests/test_organization_commercial_readiness_loop_secret_scope.py new file mode 100644 index 000000000..b47c2cadc --- /dev/null +++ b/tests/test_organization_commercial_readiness_loop_secret_scope.py @@ -0,0 +1,21 @@ +from pathlib import Path + + +WORKFLOW_PATH = ( + Path(__file__).resolve().parents[1] + / ".github" + / "workflows" + / "organization-commercial-readiness-loop.yml" +) + + +def test_maintainer_token_is_scoped_only_to_the_dispatch_step() -> None: + """Third-party setup actions must never receive the cross-repository token.""" + source = WORKFLOW_PATH.read_text(encoding="utf-8") + before_dispatch, dispatch_step = source.split( + " - name: Coordinate one bounded fleet pass\n", maxsplit=1 + ) + + 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 diff --git a/tests/test_organization_commercial_readiness_loop_workflow_source_scope.py b/tests/test_organization_commercial_readiness_loop_workflow_source_scope.py new file mode 100644 index 000000000..2cd3386ad --- /dev/null +++ b/tests/test_organization_commercial_readiness_loop_workflow_source_scope.py @@ -0,0 +1,57 @@ +from __future__ import annotations + +import base64 +from typing import Any + +import pytest + +from scripts.ci.organization_commercial_readiness_loop import GitHubClient + + +def test_workflow_source_fetch_is_limited_to_writer_candidates( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Ordinary CI workflows must not consume one contents request each.""" + client = GitHubClient("token") + content_paths: list[str] = [] + + def fake(path: str, *, method: str = "GET", payload: Any = None) -> Any: + del method, payload + if "actions/workflows" in path: + return { + "workflows": [ + { + "id": 1, + "name": "Ordinary CI", + "path": ".github/workflows/ci.yml", + "state": "active", + }, + { + "id": 2, + "name": "Hourly Product Development", + "path": ".github/workflows/hourly-product-development.yml", + "state": "active", + }, + ] + } + if "/contents/" in path: + content_paths.append(path) + data = b'on:\n schedule:\n - cron: "7 * * * *"\n' + return { + "type": "file", + "size": len(data), + "sha": "source-sha", + "encoding": "base64", + "content": base64.b64encode(data).decode(), + } + raise AssertionError(path) + + monkeypatch.setattr(client, "request", fake) + + records = client.list_workflows("ContextualWisdomLab/example", "a" * 40) + + assert records[0].content is None + assert records[0].content_sha == "" + assert records[1].content is not None + assert len(content_paths) == 1 + assert "hourly-product-development.yml" in content_paths[0]