diff --git a/.github/workflows/agent-review-runtime-quality-ci.yml b/.github/workflows/agent-review-runtime-quality-ci.yml index 3680da8778..e8c5a5d664 100644 --- a/.github/workflows/agent-review-runtime-quality-ci.yml +++ b/.github/workflows/agent-review-runtime-quality-ci.yml @@ -88,6 +88,8 @@ on: - ".github/workflows/organization-commercial-readiness-loop.yml" - ".github/workflows/exact-head-coverage-quality-gate.yml" - "scripts/ci/organization_commercial_readiness_loop.py" + - "scripts/ci/organization_commercial_readiness_core.py" + - "scripts/ci/organization_commercial_readiness_ddd_contract.py" - "organization_commercial_readiness_fixtures.py" - "tests/test_organization_commercial_readiness_loop*.py" - "docs/doctoring/organization-commercial-readiness-loop.md" @@ -269,6 +271,8 @@ jobs: .github/workflows/organization-commercial-readiness-loop.yml|\ .github/workflows/exact-head-coverage-quality-gate.yml|\ scripts/ci/organization_commercial_readiness_loop.py|\ + scripts/ci/organization_commercial_readiness_core.py|\ + scripts/ci/organization_commercial_readiness_ddd_contract.py|\ organization_commercial_readiness_fixtures.py|\ tests/test_organization_commercial_readiness_loop*.py|\ docs/doctoring/organization-commercial-readiness-loop.md) @@ -432,6 +436,8 @@ jobs: --fail-under=100 python -m compileall -q \ scripts/ci/organization_commercial_readiness_loop.py \ + scripts/ci/organization_commercial_readiness_core.py \ + scripts/ci/organization_commercial_readiness_ddd_contract.py \ organization_commercial_readiness_fixtures.py \ tests/test_organization_commercial_readiness_loop*.py diff --git a/CHANGELOG.md b/CHANGELOG.md index ca81dcea1d..aa612a4c87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -118,6 +118,7 @@ this file. The format follows Keep a Changelog, and versioned releases follow Semantic Versioning where the repository publishes a release. ## [Unreleased] +- Restore the hourly organization commercial-readiness coordinator when the dedicated maintainer secret is absent by exchanging the protected scheduled job's OIDC identity for a short-lived OpenCode App installation token; retain bounded network calls, token masking, and fail-closed parsing. Require every centrally dispatched product-development entrypoint to accept a machine-checked Domain-Driven Design contract, continuously repairing misleading directory ownership and recording larger bounded-context migrations in `docs/product-technical-gap-baseline.md` without duplicating repository-owned schedules. - **Pin `opencode-review-dispatch.yml` off the starved floating `ubuntu-latest` image.** The 2026-09-01 floating-image fix (see that entry below) pinned `strix.yml`, `opencode-review.yml`, and `noema-review.yml` -- the three required-check diff --git a/docs/doctoring/organization-commercial-readiness-loop.md b/docs/doctoring/organization-commercial-readiness-loop.md index 60675f6df6..832b2420b1 100644 --- a/docs/doctoring/organization-commercial-readiness-loop.md +++ b/docs/doctoring/organization-commercial-readiness-loop.md @@ -50,17 +50,50 @@ The existing organization merge scheduler continues to own review dispatch, bran ## 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: +Product development is dispatched only when a repository has zero open pull requests and exposes one active, manual-only, explicitly marked workflow. The repository owns the human-readable prompt, which may use any language. Eligibility depends on a versioned machine-readable capability set and an executable binding rather than copied English prose. ```yaml # cwl-org-commercial-entrypoint: v1 +# cwl-ddd-architecture-audit: required on: workflow_dispatch: + +concurrency: + group: product-development + +env: + NVIDIA_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }} + CWL_DDD_CONTRACT_VERSION: "1" + CWL_DDD_CONTRACT_CAPABILITIES: >- + aggregate anti_corruption_layer bounded_context context_map + directory_ownership domain_event domain_service entity invariant + minimal_shared_kernel product_gap_baseline repository + subdomain_classification ubiquitous_language value_object + CWL_PRODUCT_AGENT_PROMPT: | + Deliver one buyer-visible increment through the repository-owned product agent. + +jobs: + develop: + steps: + - name: Invoke the repository product agent + run: | + # cwl-ddd-prompt-binding: v1 + product-agent \ + --prompt-env CWL_PRODUCT_AGENT_PROMPT \ + --architecture-contract-env CWL_DDD_CONTRACT_CAPABILITIES ``` -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 root workflow `env` mapping must contain exactly one non-empty `CWL_PRODUCT_AGENT_PROMPT`, one exact version-one capability block, and one version value. The capability set is closed for version one; missing, misspelled, duplicated, or unversioned values fail closed. The prompt and capability environment names must reach the same non-comment shell command under the binding marker. Comments, unrelated YAML, nested or duplicate environment scopes, inert block scalars, shell built-ins, malformed quoting, dangling continuations, and flags split across commands do not satisfy the contract. + +The capability IDs cover the strategic and tactical Domain-Driven Design obligations required by the organization: core/supporting/generic subdomain classification, Bounded Context, Context Map, Ubiquitous Language, Aggregate, Entity, Value Object, Domain Service, Repository, Domain Event, Invariant, Anti-Corruption Layer, minimal Shared Kernel, directory ownership, and product-gap baseline traceability. Human-readable instructions can evolve independently as long as the repository product-agent adapter consumes both bound inputs and implements the declared version. + +Each hourly product increment must identify the owning product responsibility before selecting a repository, then compare the live directory tree, module/package names, API, database objects, tests, and documentation with that responsibility. Misleading directory paths, generic `utils`/`common` dumping grounds that own domain behavior, infrastructure imports inside the domain model, cross-context database access, obsolete product names, or customer-visible implementation boundaries are architecture defects, not cosmetic debt. When one can be corrected safely in the bounded increment, the agent moves the code and updates imports, package manifests, call sites, migrations, tests, ADRs, diagrams, and compatibility adapters in the same pull request. + +The contract does not impose one universal folder template. A move is justified by domain ownership and dependency direction, not by directory aesthetics. Aggregate boundaries remain the smallest consistency boundary; external and legacy systems are isolated behind an Anti-Corruption Layer; the Shared Kernel remains minimal; and cross-context integration uses explicit versioned contracts. If a coherent move exceeds the current pull request's safe scope, the agent must record the exact owner, callers, target context, migration sequence, and acceptance evidence in `docs/product-technical-gap-baseline.md` and select it as the next bounded architecture increment rather than silently leaving the drift unresolved. -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. +This opt-in prevents the central coordinator from guessing that an unrelated manual workflow can safely modify product source. Repositories with an existing hourly or more frequent dedicated writer keep their own lease and are never double-dispatched; those schedules may share the same DDD contract and should adopt it without adding another cron. + +The repository-local entrypoint remains responsible for 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 @@ -76,6 +109,12 @@ Rollback is removal or disabling of `.github/workflows/organization-commercial-r ## APA 7 references +Evans, E. (2004). *Domain-driven design: Tackling complexity in the heart of software*. Addison-Wesley. + +Evans, E. (2015). *Domain-driven design reference: Definitions and pattern summaries*. Domain Language. https://www.domainlanguage.com/ddd/reference/ + +International Organization for Standardization, International Electrotechnical Commission, & Institute of Electrical and Electronics Engineers. (2022). *Software, systems and enterprise—Architecture description* (ISO/IEC/IEEE Standard 42010:2022). https://www.iso.org/standard/74393.html + 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 diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 1cc9e20313..ab970baa09 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -7,6 +7,14 @@ 이 문서는 제품·기술·운영 Gap을 현재 문서와 현재 GitHub 상태에 묶어 두는 기준선이다. 새 작업은 먼저 이 문서의 Gap ID를 PR 설명과 테스트 증거에 연결하고, PR의 정확한 exact HEAD·Checks·리뷰를 다시 수집한 뒤 구현한다. 표의 상태는 작성 시점의 관측값이므로, 병합 판단에는 재사용하지 않는다. 이 인벤토리는 스냅샷이며 merge authorization이 아니다. +## 2026-09-01 시간별 DDD 실행 계약 보강 + +- **관측:** 조직 상용화 루프는 매시 7분 실행되고 기존 전용 writer 예약을 존중하지만, 중앙 제품개발 opt-in은 DDD 및 디렉터리 소유권 감사를 요구하지 않았다. 또한 maintainer secret이 없는 예약 환경에서는 cross-repository dispatch 전에 중단될 수 있었다. +- **Gap `G-DDD-01`:** Bounded Context와 실제 디렉터리·패키지·API·DB 소유권이 어긋나도 시간별 Agent가 이를 필수 결함으로 선택한다는 기계 검증 계약이 없었다. +- **조치:** 수동 제품개발 진입점에 `# cwl-ddd-architecture-audit: required`와 전략·전술 DDD 용어, directory-path repair, `docs/product-technical-gap-baseline.md` 갱신을 요구한다. 기존 전용 예약은 writer lease를 유지해 중복 실행하지 않는다. +- **가용성 조치:** `PR_REVIEW_MERGE_TOKEN`을 우선 사용하되 없으면 protected-default-branch job의 OIDC identity를 short-lived OpenCode App installation token으로 교환한다. repository `GITHUB_TOKEN`, reviewer credential, model provider key는 fallback으로 사용하지 않는다. +- **완료 증거:** exact-head focused policy tests, statement/branch coverage 100%, Python docstring 100%, workflow security checks, independent review, protected merge. 병합 전 상태는 구현 중이며 운영 완료로 간주하지 않는다. + ## 1. 근거와 범위 ### 1.1 우선순위가 높은 근거 diff --git a/organization_commercial_readiness_fixtures.py b/organization_commercial_readiness_fixtures.py index 9d28fc5929..d3bd4bd514 100644 --- a/organization_commercial_readiness_fixtures.py +++ b/organization_commercial_readiness_fixtures.py @@ -74,10 +74,32 @@ def manual_workflow(*, workflow_id: int = 9) -> WorkflowRecord: path=".github/workflows/commercial-product-development.yml", content=( "# cwl-org-commercial-entrypoint: v1\n" + "# cwl-ddd-architecture-audit: required\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" + "env:\n" + " NVIDIA_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }}\n" + " CWL_DDD_CONTRACT_VERSION: \"1\"\n" + " CWL_DDD_CONTRACT_CAPABILITIES: >-\n" + " aggregate anti_corruption_layer bounded_context context_map\n" + " directory_ownership domain_event domain_service entity invariant\n" + " minimal_shared_kernel product_gap_baseline repository\n" + " subdomain_classification ubiquitous_language value_object\n" + " CWL_PRODUCT_AGENT_PROMPT: |\n" + " 제품 책임과 재사용 경계를 먼저 확인하고 구매자가 체감할 한 단위를 개발한다.\n" + "\n" + " 디렉터리, 패키지, API, 데이터베이스, 테스트와 문서의 소유권을 함께 맞춘다.\n" + "jobs:\n" + " develop:\n" + " runs-on: ubuntu-24.04\n" + " steps:\n" + " - name: Invoke the repository product agent\n" + " run: |\n" + " # cwl-ddd-prompt-binding: v1\n" + " python scripts/automation/commercial_product_development.py \\\n" + " --prompt-env CWL_PRODUCT_AGENT_PROMPT \\\n" + " --architecture-contract-env CWL_DDD_CONTRACT_CAPABILITIES\n" ), ) diff --git a/scripts/ci/organization_commercial_readiness_core.py b/scripts/ci/organization_commercial_readiness_core.py new file mode 100644 index 0000000000..20b6b9fde5 --- /dev/null +++ b/scripts/ci/organization_commercial_readiness_core.py @@ -0,0 +1,892 @@ +#!/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" +DDD_ENTRYPOINT_MARKER = "# cwl-ddd-architecture-audit: required" +DDD_CONTRACT_TERMS = ( + "Domain-Driven Design", + "core, supporting, and generic subdomains", + "Bounded Context", + "Context Map", + "Ubiquitous Language", + "Aggregate", + "Entity", + "Value Object", + "Domain Service", + "Repository", + "Domain Event", + "Invariant", + "Anti-Corruption Layer", + "Shared Kernel", + "directory paths", + "docs/product-technical-gap-baseline.md", +) +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*:") +MAX_WORKFLOW_RECORDS_PER_REPOSITORY = 1_000 +MAX_WORKFLOW_SOURCES_PER_REPOSITORY = 100 +MAX_WORKFLOW_SOURCE_BYTES_PER_FILE = 1_048_576 +MAX_WORKFLOW_SOURCE_BYTES_PER_REPOSITORY = 10 * 1_048_576 +SAFE_DIAGNOSTIC_METHODS = frozenset( + {"DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"} +) + + +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: + """Initialize one authenticated GitHub credential with a bounded timeout.""" + 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 _redact_credential(self, value: str) -> str: + """Remove the exact GitHub credential before any diagnostic truncation.""" + return value.replace(self._token, "[REDACTED]") + + 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() + safe_method = ( + normalized_method + if normalized_method in SAFE_DIAGNOSTIC_METHODS + else "[REDACTED_METHOD]" + ) + safe_path = self._redact_credential(path) + 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 = self._redact_credential(raw)[-900:] + raise GitHubError( + f"GitHub API {safe_method} {safe_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 {safe_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 a fail-closed, memory-bounded workflow and writer-source inventory.""" + workflows: list[WorkflowRecord] = [] + source_count = 0 + source_bytes = 0 + page = 1 + while True: + result = self.request( + f"/repos/{repository}/actions/workflows?per_page=100&page={page}" + ) + batch = list((result or {}).get("workflows") or []) + if len(workflows) + len(batch) > MAX_WORKFLOW_RECORDS_PER_REPOSITORY: + raise GitHubError( + f"repository {repository} exceeded workflow metadata limit of " + f"{MAX_WORKFLOW_RECORDS_PER_REPOSITORY}" + ) + 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) + ): + source_count += 1 + if source_count > MAX_WORKFLOW_SOURCES_PER_REPOSITORY: + raise GitHubError( + f"repository {repository} exceeded workflow source limit of " + f"{MAX_WORKFLOW_SOURCES_PER_REPOSITORY}" + ) + encoded_path = quote(path, safe="/") + try: + source = self.request( + f"/repos/{repository}/contents/{encoded_path}?ref={exact_ref}" + ) + source_size = ( + int(source.get("size") or 0) + if isinstance(source, dict) + else 0 + ) + except (GitHubError, ValueError): + source = None + source_size = 0 + if ( + isinstance(source, dict) + and source.get("type") == "file" + and source_size <= MAX_WORKFLOW_SOURCE_BYTES_PER_FILE + and source.get("encoding") == "base64" + ): + if ( + source_bytes + source_size + > MAX_WORKFLOW_SOURCE_BYTES_PER_REPOSITORY + ): + raise GitHubError( + f"repository {repository} exceeded workflow source byte limit of " + f"{MAX_WORKFLOW_SOURCE_BYTES_PER_REPOSITORY}" + ) + try: + decoded = base64.b64decode( + str(source.get("content") or ""), validate=True + ) + content = decoded.decode("utf-8") + content_sha = str(source.get("sha") or "") + except (ValueError, UnicodeDecodeError): + content = None + content_sha = "" + else: + source_bytes += source_size + 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 has_domain_driven_development_contract(source: str) -> bool: + """Return whether one entrypoint accepts the complete DDD repair contract.""" + return DDD_ENTRYPOINT_MARKER in source and all( + term in source for term in DDD_CONTRACT_TERMS + ) + + +def is_manual_product_entrypoint(workflow: WorkflowRecord) -> bool: + """Return whether a workflow safely opts in to central product development.""" + source = workflow.content + if workflow.state != "active" or source is None: + return False + return all( + ( + ENTRYPOINT_MARKER in source, + has_domain_driven_development_contract(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/scripts/ci/organization_commercial_readiness_ddd_contract.py b/scripts/ci/organization_commercial_readiness_ddd_contract.py new file mode 100644 index 0000000000..2b320b2619 --- /dev/null +++ b/scripts/ci/organization_commercial_readiness_ddd_contract.py @@ -0,0 +1,274 @@ +"""Validate machine-readable DDD contracts in product-development workflows.""" + +from __future__ import annotations + +import re +import shlex +import textwrap +from collections.abc import Iterable + +DDD_ENTRYPOINT_MARKER = "# cwl-ddd-architecture-audit: required" +DDD_PROMPT_BINDING_MARKER = "# cwl-ddd-prompt-binding: v1" +DDD_CONTRACT_VERSION_ENVIRONMENT = "CWL_DDD_CONTRACT_VERSION" +DDD_PROMPT_ENVIRONMENT = "CWL_PRODUCT_AGENT_PROMPT" +DDD_CAPABILITY_ENVIRONMENT = "CWL_DDD_CONTRACT_CAPABILITIES" +DDD_PROMPT_OPTION = "--prompt-env" +DDD_CAPABILITY_OPTION = "--architecture-contract-env" +DDD_CONTRACT_VERSION = "1" +DDD_CONTRACT_CAPABILITIES = frozenset( + { + "aggregate", + "anti_corruption_layer", + "bounded_context", + "context_map", + "directory_ownership", + "domain_event", + "domain_service", + "entity", + "invariant", + "minimal_shared_kernel", + "product_gap_baseline", + "repository", + "subdomain_classification", + "ubiquitous_language", + "value_object", + } +) +DDD_CONTRACT_TERMS = tuple(sorted(DDD_CONTRACT_CAPABILITIES)) +_CAPABILITY_TOKEN_RE = re.compile(r"[a-z][a-z0-9_]*") +_BLOCK_HEADER_TEMPLATE = r"^(?P *){key}: *[>|][+-]? *$" +_VERSION_RE = re.compile( + rf"^{DDD_CONTRACT_VERSION_ENVIRONMENT}: *(?:" + r'"(?P[0-9]+)"|' + r"'(?P[0-9]+)'|" + r"(?P[0-9]+)) *$" +) +_COMMAND_OPERATORS = frozenset({";", "&&", "||", "&", "|"}) +_NON_AGENT_EXECUTABLES = frozenset( + {":", "[", "echo", "export", "false", "printf", "test", "true"} +) +_YAML_MAPPING_KEY_RE = re.compile( + r'^(?:"(?P[^"]+)"|\'(?P[^\']+)\'|(?P[A-Za-z0-9_.-]+))' + r":\s*(?:#.*)?$" +) +_HEREDOC_RE = re.compile(r"<<-?\s*['\"]?(?P[A-Za-z_][A-Za-z0-9_]*)") + + +def _top_level_mapping_bodies(source: str, key: str) -> tuple[str, ...]: + """Return bodies of top-level YAML mappings with an exact key.""" + lines = source.splitlines() + bodies: list[str] = [] + for index, line in enumerate(lines): + if line != f"{key}:": + continue + body: list[str] = [] + for candidate in lines[index + 1 :]: + if candidate.strip() and not candidate.startswith(" "): + break + body.append(candidate) + bodies.append(textwrap.dedent("\n".join(body))) + return tuple(bodies) + + +def _block_scalars(source: str, key: str) -> tuple[str, ...]: + """Return YAML literal or folded block scalar bodies for an exact key.""" + header = re.compile(_BLOCK_HEADER_TEMPLATE.format(key=re.escape(key))) + lines = source.splitlines() + blocks: list[str] = [] + for index, line in enumerate(lines): + match = header.fullmatch(line) + if match is None: + continue + base_indent = len(match.group("indent")) + body: list[str] = [] + for candidate in lines[index + 1 :]: + if candidate.strip(): + candidate_indent = len(candidate) - len(candidate.lstrip(" ")) + if candidate_indent <= base_indent: + break + body.append(candidate) + blocks.append(textwrap.dedent("\n".join(body)).strip("\n")) + return tuple(blocks) + + +def _step_run_blocks(source: str) -> tuple[str, ...]: + """Return block ``run`` values structurally nested below job steps.""" + lines = source.splitlines() + candidates = _block_scalars(source, "run") + accepted: list[str] = [] + candidate_index = 0 + header = re.compile(_BLOCK_HEADER_TEMPLATE.format(key="run")) + for index, line in enumerate(lines): + match = header.fullmatch(line) + if match is None: + continue + block = candidates[candidate_index] + candidate_index += 1 + run_indent = len(match.group("indent")) + ancestors: list[tuple[int, str]] = [] + ceiling = run_indent + for previous in reversed(lines[:index]): + if not previous.strip(): + continue + indent = len(previous) - len(previous.lstrip(" ")) + if indent < ceiling: + ancestors.append((indent, previous.strip())) + ceiling = indent + if indent == 0: + break + for ancestor_index, steps in enumerate(ancestors): + remaining = ancestors[ancestor_index + 1 :] + if len(remaining) < 2: + break + job, jobs = remaining[:2] + steps_key = _yaml_mapping_key(steps[1]) + job_key = _yaml_mapping_key(job[1]) + jobs_key = _yaml_mapping_key(jobs[1]) + if ( + steps_key == "steps" + and job_key is not None + and jobs[0] == 0 + and jobs_key == "jobs" + ): + accepted.append(block) + break + return tuple(accepted) + + +def _yaml_mapping_key(line: str) -> str | None: + """Return a simple YAML mapping key while allowing quotes and comments.""" + match = _YAML_MAPPING_KEY_RE.fullmatch(line) + if match is None: + return None + return next(value for value in match.groups() if value is not None) + + +def _reachable_shell(block: str) -> str: + """Remove heredoc bodies and conditional regions from a shell block.""" + reachable: list[str] = [] + heredoc_delimiter: str | None = None + control_depth = 0 + for line in block.splitlines(): + stripped = line.strip() + if heredoc_delimiter is not None: + if stripped == heredoc_delimiter: + heredoc_delimiter = None + continue + if control_depth: + if re.match(r"^(?:if|case|while|until)\b", stripped): + control_depth += 1 + if re.match(r"^(?:fi|esac|done)\b", stripped): + control_depth -= 1 + continue + if re.match(r"^(?:if|case|while|until)\b", stripped): + control_depth = 1 + continue + if match := _HEREDOC_RE.search(stripped): + heredoc_delimiter = match.group("delimiter") + continue + reachable.append(line) + return "\n".join(reachable) + + +def _contract_version(environment: str) -> str | None: + """Return the unique scalar contract version from a root environment body.""" + matches = [ + next(value for value in match.groups() if value is not None) + for line in environment.splitlines() + if (match := _VERSION_RE.fullmatch(line)) is not None + ] + return matches[0] if len(matches) == 1 else None + + +def _shell_segments(block: str) -> Iterable[tuple[str, ...]]: + """Yield non-comment shell command segments with continuations joined.""" + commands: list[str] = [] + fragments: list[str] = [] + for line in block.splitlines(): + fragment = line.strip() + if not fragment or fragment.startswith("#"): + continue + continued = fragment.endswith("\\") + fragments.append(fragment[:-1].rstrip() if continued else fragment) + if not continued: + commands.append(" ".join(fragments)) + fragments = [] + if fragments: + return + for command in commands: + lexer = shlex.shlex(command, posix=True, punctuation_chars=";&|") + lexer.whitespace_split = True + lexer.commenters = "#" + try: + tokens = tuple(lexer) + except ValueError: + continue + segment: list[str] = [] + for token in tokens: + if token in _COMMAND_OPERATORS: + if segment: + yield tuple(segment) + segment = [] + else: + segment.append(token) + if segment: + yield tuple(segment) + + +def _option_value(tokens: tuple[str, ...], option: str) -> str | None: + """Return one unique shell option value from a command segment.""" + values: list[str] = [] + for index, token in enumerate(tokens): + if token == option and index + 1 < len(tokens): + values.append(tokens[index + 1]) + elif token.startswith(f"{option}="): + values.append(token.split("=", 1)[1]) + return values[0] if len(values) == 1 else None + + +def _executable(tokens: tuple[str, ...]) -> str | None: + """Return the executable after optional environment assignments.""" + for token in tokens: + if token == "env" or ("=" in token and not token.startswith("--")): + continue + executable = token.rsplit("/", 1)[-1] + return None if executable.startswith("-") else executable + return None + + +def _has_bound_agent_invocation(source: str) -> bool: + """Return whether one nontrivial command receives both contract inputs.""" + for run_block in _step_run_blocks(source): + if DDD_PROMPT_BINDING_MARKER not in run_block: + continue + for tokens in _shell_segments(_reachable_shell(run_block)): + executable = _executable(tokens) + if executable is None or executable in _NON_AGENT_EXECUTABLES: + continue + if ( + _option_value(tokens, DDD_PROMPT_OPTION) == DDD_PROMPT_ENVIRONMENT + and _option_value(tokens, DDD_CAPABILITY_OPTION) + == DDD_CAPABILITY_ENVIRONMENT + ): + return True + return False + + +def has_domain_driven_development_contract(source: str) -> bool: + """Return whether a workflow binds a scoped versioned DDD contract.""" + if DDD_ENTRYPOINT_MARKER not in source: + return False + environments = _top_level_mapping_bodies(source, "env") + if len(environments) != 1: + return False + environment = environments[0] + if _contract_version(environment) != DDD_CONTRACT_VERSION: + return False + prompts = _block_scalars(environment, DDD_PROMPT_ENVIRONMENT) + capabilities = _block_scalars(environment, DDD_CAPABILITY_ENVIRONMENT) + if len(prompts) != 1 or not prompts[0].strip() or len(capabilities) != 1: + return False + declared = frozenset(_CAPABILITY_TOKEN_RE.findall(capabilities[0])) + if declared != DDD_CONTRACT_CAPABILITIES: + return False + return _has_bound_agent_invocation(source) diff --git a/scripts/ci/organization_commercial_readiness_loop.py b/scripts/ci/organization_commercial_readiness_loop.py index 9657bd2d4d..eef3391540 100644 --- a/scripts/ci/organization_commercial_readiness_loop.py +++ b/scripts/ci/organization_commercial_readiness_loop.py @@ -1,865 +1,60 @@ #!/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. -""" +"""Compatibility entrypoint for the organization commercial-readiness core.""" from __future__ import annotations -import argparse -import base64 -import dataclasses -import enum -import hashlib -import json -import os -import re -import subprocess +import importlib.util 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*:") -MAX_WORKFLOW_RECORDS_PER_REPOSITORY = 1_000 -MAX_WORKFLOW_SOURCES_PER_REPOSITORY = 100 -MAX_WORKFLOW_SOURCE_BYTES_PER_FILE = 1_048_576 -MAX_WORKFLOW_SOURCE_BYTES_PER_REPOSITORY = 10 * 1_048_576 -SAFE_DIAGNOSTIC_METHODS = frozenset( - {"DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"} -) - - -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: - """Initialize one authenticated GitHub credential with a bounded timeout.""" - if not token: - raise GitHubError("GH_TOKEN is required for organization coordination") - self._token = token - self._timeout_seconds = timeout_seconds +from types import ModuleType - @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) +_MODULE_DIRECTORY = Path(__file__).resolve().parent +_CORE_MODULE_NAME = "_cwl_organization_commercial_readiness_core" +_DDD_MODULE_NAME = "_cwl_organization_commercial_readiness_ddd_contract" - def _redact_credential(self, value: str) -> str: - """Remove the exact GitHub credential before any diagnostic truncation.""" - return value.replace(self._token, "[REDACTED]") - 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() - safe_method = ( - normalized_method - if normalized_method in SAFE_DIAGNOSTIC_METHODS - else "[REDACTED_METHOD]" - ) - safe_path = self._redact_credential(path) - 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 = self._redact_credential(raw)[-900:] - raise GitHubError( - f"GitHub API {safe_method} {safe_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 {safe_path}" - ) from exc +def _load_sibling(module_name: str, filename: str) -> ModuleType: + """Load one trusted sibling module under a stable private module name.""" + existing = sys.modules.get(module_name) + if existing is not None: + return existing + path = _MODULE_DIRECTORY / filename + spec = importlib.util.spec_from_file_location(module_name, path) + module = importlib.util.module_from_spec(spec) # type: ignore[arg-type] + sys.modules[module_name] = module + spec.loader.exec_module(module) # type: ignore[union-attr] + return module - 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 a fail-closed, memory-bounded workflow and writer-source inventory.""" - workflows: list[WorkflowRecord] = [] - source_count = 0 - source_bytes = 0 - page = 1 - while True: - result = self.request( - f"/repos/{repository}/actions/workflows?per_page=100&page={page}" - ) - batch = list((result or {}).get("workflows") or []) - if len(workflows) + len(batch) > MAX_WORKFLOW_RECORDS_PER_REPOSITORY: - raise GitHubError( - f"repository {repository} exceeded workflow metadata limit of " - f"{MAX_WORKFLOW_RECORDS_PER_REPOSITORY}" - ) - 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) - ): - source_count += 1 - if source_count > MAX_WORKFLOW_SOURCES_PER_REPOSITORY: - raise GitHubError( - f"repository {repository} exceeded workflow source limit of " - f"{MAX_WORKFLOW_SOURCES_PER_REPOSITORY}" - ) - encoded_path = quote(path, safe="/") - try: - source = self.request( - f"/repos/{repository}/contents/{encoded_path}?ref={exact_ref}" - ) - source_size = ( - int(source.get("size") or 0) - if isinstance(source, dict) - else 0 - ) - except (GitHubError, ValueError): - source = None - source_size = 0 - if ( - isinstance(source, dict) - and source.get("type") == "file" - and source_size <= MAX_WORKFLOW_SOURCE_BYTES_PER_FILE - and source.get("encoding") == "base64" - ): - if ( - source_bytes + source_size - > MAX_WORKFLOW_SOURCE_BYTES_PER_REPOSITORY - ): - raise GitHubError( - f"repository {repository} exceeded workflow source byte limit of " - f"{MAX_WORKFLOW_SOURCE_BYTES_PER_REPOSITORY}" - ) - try: - decoded = base64.b64decode( - str(source.get("content") or ""), validate=True - ) - content = decoded.decode("utf-8") - content_sha = str(source.get("sha") or "") - except (ValueError, UnicodeDecodeError): - content = None - content_sha = "" - else: - source_bytes += source_size - 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 +_core = _load_sibling( + _CORE_MODULE_NAME, "organization_commercial_readiness_core.py" +) +_ddd = _load_sibling( + _DDD_MODULE_NAME, "organization_commercial_readiness_ddd_contract.py" +) -if __name__ == "__main__": # pragma: no cover - exercised through main() - raise SystemExit(main()) +_core.has_domain_driven_development_contract = ( + _ddd.has_domain_driven_development_contract +) +_core.DDD_CONTRACT_TERMS = _ddd.DDD_CONTRACT_TERMS +_core.DDD_CONTRACT_CAPABILITIES = _ddd.DDD_CONTRACT_CAPABILITIES + +if __name__ == "__main__": + raise SystemExit(_core.main()) + +# The public import intentionally aliases the core object so monkeypatches reach +# the globals used by its functions. Preserve the facade's import identity on +# that object as well: standard module runners consult ``__spec__`` and its +# loader after import, and the core's private identity cannot load this public +# module name. +# First import wins: a later import of this facade under another name (the +# bare sibling fallback used by direct ``python scripts/ci/...`` execution) +# must not restamp the shared core with that name, or the entry still cached +# under the first name would carry a loader that cannot load it. +_core_spec = getattr(_core, "__spec__", None) +if _core_spec is None or _core_spec.name == _CORE_MODULE_NAME: + _core.__name__ = __name__ + _core.__package__ = __package__ + _core.__loader__ = __loader__ + _core.__spec__ = __spec__ +sys.modules[__name__] = _core diff --git a/scripts/ci/pr_review_autofix_context.py b/scripts/ci/pr_review_autofix_context.py old mode 100755 new mode 100644 diff --git a/scripts/ci/pr_review_fix_scheduler.py b/scripts/ci/pr_review_fix_scheduler.py old mode 100755 new mode 100644 diff --git a/tests/test_organization_commercial_readiness_loop_credential_contract.py b/tests/test_organization_commercial_readiness_loop_credential_contract.py index 3225d5832a..9f28ed932b 100644 --- a/tests/test_organization_commercial_readiness_loop_credential_contract.py +++ b/tests/test_organization_commercial_readiness_loop_credential_contract.py @@ -10,12 +10,33 @@ def test_central_schedule_has_no_branch_selected_or_reviewer_credential_path() -> None: - """The fleet coordinator must be schedule-only and use maintainer authority.""" + """The fleet coordinator uses schedule-bound maintainer or App 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 "id-token: write" in source + assert "OIDC_AUDIENCE: opencode-github-action" in source + assert '"${OPENCODE_API_BASE_URL}/exchange_github_app_token"' in source assert "persist-credentials: false" in source assert "OPENCODE_APPROVE_TOKEN" not in source + assert "|| github.token" not in source assert "DRY_RUN" not in source assert "inputs.dry_run" not in source + + +def test_opencode_exchange_fails_closed_and_masks_both_tokens() -> None: + """Malformed exchanges remain bounded and never expose either token.""" + source = WORKFLOW_PATH.read_text(encoding="utf-8") + coordinate = source.split( + " - name: Coordinate one bounded fleet pass\n", maxsplit=1 + )[1] + + assert 'if [ -z "${GH_TOKEN:-}" ]; then' in coordinate + assert 'export GH_TOKEN="$app_token"' in coordinate + assert "OIDC token response was malformed or empty" in coordinate + assert "app token response was malformed or empty" in coordinate + assert 'echo "::add-mask::$oidc_token"' in coordinate + assert 'echo "::add-mask::$app_token"' in coordinate + assert coordinate.count("--connect-timeout 10") == 2 + assert coordinate.count("--max-time 30") == 2 diff --git a/tests/test_organization_commercial_readiness_loop_ddd_binding.py b/tests/test_organization_commercial_readiness_loop_ddd_binding.py new file mode 100644 index 0000000000..8cebe0f603 --- /dev/null +++ b/tests/test_organization_commercial_readiness_loop_ddd_binding.py @@ -0,0 +1,328 @@ +"""Regression tests for executable DDD product-entrypoint binding.""" + +from __future__ import annotations + +import runpy +import sys +from pathlib import Path + +import pytest + +from organization_commercial_readiness_fixtures import manual_workflow, workflow +from scripts.ci import organization_commercial_readiness_ddd_contract as contract +from scripts.ci import organization_commercial_readiness_loop as coordinator + + +def _source() -> str: + """Return the canonical machine-bound multilingual workflow fixture.""" + source = manual_workflow().content + assert source is not None + return source + + +def _replace_command(source: str, replacement: str) -> str: + """Replace the canonical multiline product-agent command once.""" + original = ( + "python scripts/automation/commercial_product_development.py \\\n" + " --prompt-env CWL_PRODUCT_AGENT_PROMPT \\\n" + " --architecture-contract-env CWL_DDD_CONTRACT_CAPABILITIES" + ) + assert original in source + return source.replace(original, replacement, 1) + + +def test_accepts_multilingual_prompt_option_forms_and_environment_prefix() -> None: + """Eligibility depends on bound capabilities, not copied English prose.""" + source = _source() + assert "Domain-Driven Design" not in source + assert coordinator.has_domain_driven_development_contract(source) + assert coordinator.is_manual_product_entrypoint(manual_workflow()) + + equals_options = source.replace( + "--prompt-env CWL_PRODUCT_AGENT_PROMPT", + "--prompt-env=CWL_PRODUCT_AGENT_PROMPT", + ).replace( + "--architecture-contract-env CWL_DDD_CONTRACT_CAPABILITIES", + "--architecture-contract-env=CWL_DDD_CONTRACT_CAPABILITIES", + ) + assert coordinator.has_domain_driven_development_contract(equals_options) + + prefixed = source.replace( + "python scripts/automation/commercial_product_development.py", + "env MODE=bounded python scripts/automation/commercial_product_development.py", + 1, + ) + assert coordinator.has_domain_driven_development_contract(prefixed) + + single_quoted = source.replace( + 'CWL_DDD_CONTRACT_VERSION: "1"', "CWL_DDD_CONTRACT_VERSION: '1'" + ) + plain = source.replace( + 'CWL_DDD_CONTRACT_VERSION: "1"', "CWL_DDD_CONTRACT_VERSION: 1" + ) + assert coordinator.has_domain_driven_development_contract(single_quoted) + assert coordinator.has_domain_driven_development_contract(plain) + + +def test_rejects_missing_extra_or_version_drift() -> None: + """Version one accepts exactly the approved strategic and tactical set.""" + source = _source() + for index, capability in enumerate(sorted(contract.DDD_CONTRACT_CAPABILITIES)): + assert not coordinator.has_domain_driven_development_contract( + source.replace(capability, f"omitted_{index}", 1) + ) + assert not coordinator.has_domain_driven_development_contract( + source.replace( + " value_object\n", " value_object unexpected_capability\n", 1 + ) + ) + for replacement in ('2', '"2"', "'2'", "invalid", '"1\''): + assert not coordinator.has_domain_driven_development_contract( + source.replace( + 'CWL_DDD_CONTRACT_VERSION: "1"', + f"CWL_DDD_CONTRACT_VERSION: {replacement}", + 1, + ) + ) + assert not coordinator.has_domain_driven_development_contract( + source.replace(' CWL_DDD_CONTRACT_VERSION: "1"\n', "", 1) + ) + assert not coordinator.has_domain_driven_development_contract( + source.replace( + ' CWL_DDD_CONTRACT_VERSION: "1"\n', + ' CWL_DDD_CONTRACT_VERSION: "1"\n' + " CWL_DDD_CONTRACT_VERSION: 1\n", + 1, + ) + ) + assert not coordinator.has_domain_driven_development_contract( + source.replace("# cwl-ddd-architecture-audit: required\n", "", 1) + ) + + +def test_rejects_unscoped_duplicate_or_empty_environment_values() -> None: + """Only one root environment may own one prompt and capability block.""" + source = _source() + invalid = [ + source.replace("env:\n", "metadata:\n", 1), + source + "\nenv:\n OTHER_VALUE: present\n", + source.replace( + "CWL_PRODUCT_AGENT_PROMPT: |", "UNUSED_PROMPT: |", 1 + ), + source.replace( + " CWL_PRODUCT_AGENT_PROMPT: |\n", + " CWL_PRODUCT_AGENT_PROMPT: |\n" + " duplicate\n" + " CWL_PRODUCT_AGENT_PROMPT: |\n", + 1, + ), + source.replace( + "CWL_DDD_CONTRACT_CAPABILITIES: >-", "UNUSED_CAPABILITIES: >-", 1 + ), + source.replace( + " CWL_DDD_CONTRACT_CAPABILITIES: >-\n", + " CWL_DDD_CONTRACT_CAPABILITIES: >-\n" + " bounded_context\n" + " CWL_DDD_CONTRACT_CAPABILITIES: >-\n", + 1, + ), + source.replace( + " 제품 책임과 재사용 경계를 먼저 확인하고 구매자가 체감할 한 단위를 개발한다.\n", + "", + 1, + ).replace( + " 디렉터리, 패키지, API, 데이터베이스, 테스트와 문서의 소유권을 함께 맞춘다.\n", + "", + 1, + ), + ] + for candidate in invalid: + assert not coordinator.has_domain_driven_development_contract(candidate) + + +def test_rejects_comments_unused_prose_wrong_bindings_and_non_agents() -> None: + """Comments and inert YAML cannot impersonate an executable agent contract.""" + source = _source() + comments_only = ( + "# cwl-ddd-architecture-audit: required\n" + + "\n".join( + f"# {item}" for item in sorted(contract.DDD_CONTRACT_CAPABILITIES) + ) + + "\n# cwl-ddd-prompt-binding: v1\n" + + "# --prompt-env CWL_PRODUCT_AGENT_PROMPT\n" + + "# --architecture-contract-env CWL_DDD_CONTRACT_CAPABILITIES\n" + ) + assert not coordinator.has_domain_driven_development_contract(comments_only) + assert not coordinator.has_domain_driven_development_contract( + source.replace(" CWL_PRODUCT_AGENT_PROMPT: |", " NOTES: |", 1) + ) + for old, new in ( + ("# cwl-ddd-prompt-binding: v1", "# unbound"), + ("--prompt-env CWL_PRODUCT_AGENT_PROMPT", "--prompt-env OTHER_PROMPT"), + ( + "--architecture-contract-env CWL_DDD_CONTRACT_CAPABILITIES", + "--architecture-contract-env OTHER_CAPABILITIES", + ), + ( + "--prompt-env CWL_PRODUCT_AGENT_PROMPT", + "--prompt-env CWL_PRODUCT_AGENT_PROMPT " + "--prompt-env CWL_PRODUCT_AGENT_PROMPT", + ), + ): + assert not coordinator.has_domain_driven_development_contract( + source.replace(old, new, 1) + ) + for executable in (":", "[", "echo", "export", "false", "printf", "test", "true"): + assert not coordinator.has_domain_driven_development_contract( + source.replace( + "python scripts/automation/commercial_product_development.py", + executable, + 1, + ) + ) + assert not coordinator.has_domain_driven_development_contract( + _replace_command( + source, + "--prompt-env CWL_PRODUCT_AGENT_PROMPT " + "--architecture-contract-env CWL_DDD_CONTRACT_CAPABILITIES", + ) + ) + + +def test_rejects_split_malformed_and_dangling_commands() -> None: + """Both environment names must reach one well-formed command segment.""" + source = _source() + for operator in (";", "&&", "||", "|", "&"): + assert not coordinator.has_domain_driven_development_contract( + _replace_command( + source, + "product-agent --prompt-env CWL_PRODUCT_AGENT_PROMPT " + f"{operator} product-agent " + "--architecture-contract-env CWL_DDD_CONTRACT_CAPABILITIES", + ) + ) + assert not coordinator.has_domain_driven_development_contract( + _replace_command(source, "; product-agent --prompt-env CWL_PRODUCT_AGENT_PROMPT") + ) + assert not coordinator.has_domain_driven_development_contract( + source.replace( + "python scripts/automation/commercial_product_development.py", + 'product-agent "unterminated', + 1, + ) + ) + assert not coordinator.has_domain_driven_development_contract( + source.replace( + " --architecture-contract-env CWL_DDD_CONTRACT_CAPABILITIES\n", + "", + 1, + ) + ) + assert not coordinator.has_domain_driven_development_contract( + source.replace( + " --architecture-contract-env CWL_DDD_CONTRACT_CAPABILITIES\n", + " --architecture-contract-env\n", + 1, + ) + ) + + +def test_rejects_marker_and_flags_distributed_across_run_blocks() -> None: + """A marker in one step cannot authorize flags executed by another step.""" + source = _source() + bound = ( + " run: |\n" + " # cwl-ddd-prompt-binding: v1\n" + " python scripts/automation/commercial_product_development.py \\\n" + " --prompt-env CWL_PRODUCT_AGENT_PROMPT \\\n" + " --architecture-contract-env CWL_DDD_CONTRACT_CAPABILITIES\n" + ) + split = ( + " run: |\n" + " # cwl-ddd-prompt-binding: v1\n" + " product-agent --prompt-env CWL_PRODUCT_AGENT_PROMPT\n" + " - run: |\n" + " product-agent --architecture-contract-env CWL_DDD_CONTRACT_CAPABILITIES\n" + ) + assert bound in source + assert not coordinator.has_domain_driven_development_contract( + source.replace(bound, split, 1) + ) + + +def test_rejects_non_step_and_unreachable_agent_bindings() -> None: + """Only directly reachable job-step commands may bind the contract.""" + source = _source() + run_block = next(iter(contract._step_run_blocks(source))) + for prefix in ("env:\n", "metadata:\n"): + inert = source.replace("jobs:\n", f"{prefix} run: |\n" + "\n".join( + f" {line}" for line in run_block.splitlines() + ) + "\njobs:\n", 1) + inert = inert.replace(" run: |", " notes: |", 1) + assert not coordinator.has_domain_driven_development_contract(inert) + + heredoc = _replace_command( + source, + "cat <<'INERT'\n" + " product-agent --prompt-env CWL_PRODUCT_AGENT_PROMPT " + "--architecture-contract-env CWL_DDD_CONTRACT_CAPABILITIES\n" + " INERT", + ) + assert not coordinator.has_domain_driven_development_contract(heredoc) + + skipped = _replace_command( + source, + "if false; then\n" + " product-agent --prompt-env CWL_PRODUCT_AGENT_PROMPT " + "--architecture-contract-env CWL_DDD_CONTRACT_CAPABILITIES\n" + " fi", + ) + assert not coordinator.has_domain_driven_development_contract(skipped) + + +def test_accepts_valid_yaml_keys_and_unrelated_shell_structures() -> None: + """Valid YAML spellings and unrelated shell regions retain eligibility.""" + source = _source() + quoted = source.replace("jobs:\n", '"jobs": # root jobs\n', 1).replace( + " develop:\n", " 'develop': # product job\n", 1 + ).replace(" steps:\n", ' "steps": # executable list\n', 1) + assert coordinator.has_domain_driven_development_contract(quoted) + + direct = ( + "product-agent --prompt-env CWL_PRODUCT_AGENT_PROMPT " + "--architecture-contract-env CWL_DDD_CONTRACT_CAPABILITIES" + ) + for unrelated in ( + "cat <<'NOTE'\n inert prose\n NOTE", + "if false; then\n echo skipped\n fi", + "if false; then\n while false; do\n echo skipped\n done\n fi", + ): + assert coordinator.has_domain_driven_development_contract( + _replace_command(source, f"{unrelated}\n {direct}") + ) + assert coordinator.has_domain_driven_development_contract( + _replace_command(source, f"{direct}\n {unrelated}") + ) + + +def test_private_command_edges_and_compatibility_script_mode( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Cover assignment-only, trailing-operator, and direct-script boundaries.""" + assert contract._executable(("env", "MODE=bounded")) is None + assert list(contract._shell_segments("product-agent ;")) == [("product-agent",)] + assert contract._step_run_blocks("run: |\n product-agent") == () + assert contract._step_run_blocks("jobs:\n\n run: |\n product-agent") == () + + path = Path(coordinator.__file__) + monkeypatch.setattr(sys, "argv", [str(path), "--organization", "invalid/name"]) + with pytest.raises(SystemExit) as raised: + runpy.run_path(str(path), run_name="__main__") + assert raised.value.code == 2 + + +def test_manual_entrypoint_still_rejects_non_contract_workflows() -> None: + """The compatibility facade keeps the original fail-closed API surface.""" + assert not coordinator.is_manual_product_entrypoint( + workflow(content="# cwl-org-commercial-entrypoint: v1\n") + ) diff --git a/tests/test_organization_commercial_readiness_loop_facade.py b/tests/test_organization_commercial_readiness_loop_facade.py new file mode 100644 index 0000000000..95acdc5841 --- /dev/null +++ b/tests/test_organization_commercial_readiness_loop_facade.py @@ -0,0 +1,56 @@ +"""Regression tests for the commercial-readiness compatibility facade.""" + +from __future__ import annotations + +import pickle +import runpy +import sys +from pathlib import Path + +import pytest + +from scripts.ci import organization_commercial_readiness_ddd_contract as contract +from scripts.ci import organization_commercial_readiness_loop as coordinator + + +def test_facade_monkeypatches_reach_core_function_globals( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Existing tests and callers patch the same module used by core functions.""" + sentinel = object() + monkeypatch.setattr(coordinator, "build_plan", sentinel) + assert coordinator.run_once.__globals__["build_plan"] is sentinel + + +def test_facade_direct_script_mode_delegates_to_core( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """The stable script path retains the original argument-validation behavior.""" + path = Path(contract.__file__).with_name( + "organization_commercial_readiness_loop.py" + ) + monkeypatch.setattr(sys, "argv", [str(path), "--organization", "invalid/name"]) + with pytest.raises(SystemExit) as raised: + runpy.run_path(str(path), run_name="__main__") + assert raised.value.code == 2 + + +def test_imported_facade_remains_executable_by_public_module_name( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Importing the facade must not corrupt its public module identity.""" + monkeypatch.setattr( + sys, + "argv", + [coordinator.__file__, "--organization", "invalid/name"], + ) + with pytest.warns(RuntimeWarning, match="found in sys.modules"): + with pytest.raises(SystemExit) as raised: + runpy.run_module( + "scripts.ci.organization_commercial_readiness_loop", + run_name="__main__", + ) + assert raised.value.code == 2 + assert pickle.loads(pickle.dumps(coordinator.ActionKind.REVIEW_REPAIR)) is ( + coordinator.ActionKind.REVIEW_REPAIR + ) diff --git a/tests/test_organization_commercial_readiness_loop_policy.py b/tests/test_organization_commercial_readiness_loop_policy.py index ec206499bc..16b56c88fb 100644 --- a/tests/test_organization_commercial_readiness_loop_policy.py +++ b/tests/test_organization_commercial_readiness_loop_policy.py @@ -12,6 +12,7 @@ from scripts.ci.organization_commercial_readiness_loop import ( ActionKind, ActionResult, + DDD_CONTRACT_TERMS, RunRecord, RunReport, build_plan, @@ -21,6 +22,7 @@ is_manual_product_entrypoint, repository_is_eligible, ) +from scripts.ci import organization_commercial_readiness_core as coordinator_core ROOT = Path(__file__).resolve().parents[1] @@ -47,18 +49,38 @@ def test_static_and_live_writer_lease_policy() -> None: 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.""" +def test_core_fallback_ddd_marker_contract() -> None: + """The standalone core fallback retains positive and negative coverage.""" + assert not coordinator_core.has_domain_driven_development_contract("") + source = "\n".join( + ( + coordinator_core.DDD_ENTRYPOINT_MARKER, + *coordinator_core.DDD_CONTRACT_TERMS, + ) + ) + assert coordinator_core.has_domain_driven_development_contract(source) + + +def test_product_entrypoint_requires_manual_nvidia_and_ddd_opt_in() -> None: + """Product dispatch requires a manual credential-isolated DDD contract.""" 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 ( + mutations = [ (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( + "# cwl-ddd-architecture-audit: required\n", "" + ), (safe.content or "").replace("concurrency:\n", ""), - ): + ] + mutations.extend( + (safe.content or "").replace(term, f"missing-{index}", 1) + for index, term in enumerate(DDD_CONTRACT_TERMS) + ) + for changed in mutations: assert not is_manual_product_entrypoint(workflow(content=changed)) @@ -162,6 +184,9 @@ def test_workflow_and_doctoring_contracts() -> None: 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 'export GH_TOKEN="$app_token"' in workflow_source + assert "id-token: write" in workflow_source + assert "OIDC_AUDIENCE: opencode-github-action" 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 @@ -180,5 +205,7 @@ def test_workflow_and_doctoring_contracts() -> None: assert "github.event.pull_request.head.sha" in quality_gate assert "disabled workflow does not hold a lease" in doctoring assert "manual-only, explicitly marked" in doctoring + assert "# cwl-ddd-architecture-audit: required" in doctoring + assert "Misleading directory paths" 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_pr_review_fix_scheduler.py b/tests/test_pr_review_fix_scheduler.py index 6b9bd91e0c..241e6505c2 100644 --- a/tests/test_pr_review_fix_scheduler.py +++ b/tests/test_pr_review_fix_scheduler.py @@ -1391,6 +1391,16 @@ def test_fix_inspect_skip_wait_and_error_paths(monkeypatch): """Inspect and queue logic report skip, wait, dispatch-limit, and errors.""" args = fix.parse_args(["--repo", "owner/repo", "--base-branch", "main"]) assert fix.inspect_pr("owner/repo", make_pr(isDraft=True), args) == ("skip", ("draft PR",)) + assert fix.inspect_pr( + "owner/repo", + make_pr(isDraft=True, mergeStateStatus="DIRTY"), + args, + ) == ("skip", ("draft PR",)) + assert fix.inspect_pr( + "owner/repo", + make_pr(mergeStateStatus="DIRTY"), + args, + ) == ("skip", ("merge conflict is not authorized for repair",)) assert fix.inspect_pr("owner/repo", make_pr(baseRefName="develop"), args)[1][0].startswith("base branch") wildcard_args = fix.parse_args(["--repo", "owner/repo", "--base-branch", "*"]) monkeypatch.setattr(fix, "needs_autofix", lambda pr: (False, ())) diff --git a/tests/test_pr_review_merge_scheduler.py b/tests/test_pr_review_merge_scheduler.py index 2cbda7f85b..817a9e3b01 100644 --- a/tests/test_pr_review_merge_scheduler.py +++ b/tests/test_pr_review_merge_scheduler.py @@ -3823,6 +3823,34 @@ def test_current_head_approval_cleans_previous_head_change_gate_before_merge(): ) +def test_fetch_workflow_names_by_check_suite_rest_paginates_and_maps(monkeypatch): + """REST fallback preserves suite workflow identity across full pages.""" + first_page = [ + {"check_suite_id": index, "name": f"workflow-{index}"} + for index in range(99) + ] + first_page.append({"check_suite_id": None, "name": ""}) + responses = iter([{"workflow_runs": first_page}, {"workflow_runs": []}]) + monkeypatch.setattr(sched, "gh_api_json", lambda endpoint: next(responses)) + + names = sched.fetch_workflow_names_by_check_suite_rest("owner/repo", "a" * 40) + + assert names[0] == "workflow-0" + assert names[98] == "workflow-98" + + +def test_fetch_workflow_names_by_check_suite_rest_propagates_read_error(monkeypatch): + """Unexpected Actions read errors fail closed instead of hiding identity.""" + monkeypatch.setattr( + sched, + "gh_api_json", + lambda endpoint: (_ for _ in ()).throw(RuntimeError("network exploded")), + ) + + with pytest.raises(RuntimeError, match="network exploded"): + sched.fetch_workflow_names_by_check_suite_rest("owner/repo", "a" * 40) + + def test_failed_status_checks_uses_latest_check_run_for_same_workflow_name(): pr = make_pr( statusCheckRollup={