From dc1e673c8daa9c6cc4e31daa4bba7a3c4ec074fb Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Mon, 27 Jul 2026 21:05:30 +0800 Subject: [PATCH] Remove retired AIAudit readiness coupling Co-Authored-By: Codex --- scripts/check_codex_auto_merge_readiness.py | 184 +--------- scripts/plan_codex_auto_merge_enablement.py | 103 +----- .../test_check_codex_auto_merge_readiness.py | 331 ++---------------- .../test_plan_codex_auto_merge_enablement.py | 68 +--- 4 files changed, 48 insertions(+), 638 deletions(-) diff --git a/scripts/check_codex_auto_merge_readiness.py b/scripts/check_codex_auto_merge_readiness.py index 3e2c0de..1529ee5 100644 --- a/scripts/check_codex_auto_merge_readiness.py +++ b/scripts/check_codex_auto_merge_readiness.py @@ -11,12 +11,8 @@ from pathlib import Path from typing import Any - DEFAULT_API_URL = "https://api.github.com" DEFAULT_POLICY_PATH = Path(".github/codex_auto_merge_policy.json") -DEFAULT_AUTO_MERGE_WORKFLOW = Path(".github/workflows/auto_merge_codex_pr.yml") -DEFAULT_CODEX_FEEDBACK_WORKFLOW = Path(".github/workflows/codex_pr_feedback.yml") -DEFAULT_MONTHLY_REVIEW_WORKFLOW = Path(".github/workflows/monthly_review.yml") DEFAULT_TIMEOUT_SECONDS = 30 DEFAULT_REQUIRED_STATUS_CHECKS = ("test",) DEFAULT_HUMAN_REVIEW_LABEL = "human-review-required" @@ -213,157 +209,6 @@ def load_policy_labels(policy_path: Path) -> dict[str, str]: return {"auto_merge_label": label, "human_review_label": human_review_label} -def validate_auto_merge_workflow(workflow_path: Path) -> list[str]: - errors: list[str] = [] - try: - workflow = workflow_path.read_text(encoding="utf-8") - except OSError: - return [f"auto-merge workflow is missing: {workflow_path}"] - required_snippets = { - "CI workflow_run trigger": 'workflows: ["CI"]', - "CI success guard": "github.event.workflow_run.conclusion == 'success'", - "Codex branch guard": "codex/monthly-review-issue-", - "repository variable hard switch": "vars.CODEX_AUDIT_AUTO_MERGE", - "strict true allowlist": '["true","True","TRUE"]', - "workflow-run same-repository guard": "github.event.workflow_run.head_repository.full_name == github.repository", - "configurable required status checks": "CODEX_AUDIT_REQUIRED_STATUS_CHECKS", - "required status checks argument": "--required-status-checks", - "explicit gh repository binding": '--repo "${{ github.repository }}"', - "same-repository PR resolution": "headRepository.nameWithOwner", - "PR additions for changed-line guard": "additions", - "PR deletions for changed-line guard": "deletions", - "PR review decision guard": "reviewDecision", - "paginated PR file metadata fetch": "pulls/${{ steps.pr.outputs.pr_number }}/files?per_page=100", - "PR file status guard": '"status": item.get("status", "")', - "PR previous filename capture": '"previous_filename": item.get("previous_filename", "")', - "source merge guard script": "scripts/evaluate_codex_pr_merge.py", - "same-repository guard": "--require-same-repository", - "merge-time readiness check": "Check guarded auto-merge readiness before merge", - "merge-time readiness step id": "id: merge_readiness", - "merge-time readiness soft-fail": "continue-on-error: true", - "merge-time readiness script": "scripts/check_codex_auto_merge_readiness.py", - "merge-time readiness hard true": "--auto-merge true", - "merge-time readiness failure comment": "Comment merge-time readiness failure", - "merge-time readiness failure decision": "readiness_decision.json", - "merge-time readiness failure comment artifact": "readiness_guard_decision_comment.md", - "merge-time readiness failure reason": "merge_readiness_failed", - "merge-time readiness failure hard stop": "Fail on merge-time readiness failure", - "merge gated by readiness outcome": "steps.merge_readiness.outcome == 'success'", - "optional readiness token fallback": "CODEX_AUDIT_READINESS_TOKEN || secrets.GITHUB_TOKEN", - "content write permission": "contents: write", - "issue write permission": "issues: write", - "pull request write permission": "pull-requests: write", - "auto-merge guard decision comment": "Comment auto-merge guard decision", - "auto-merge guard decision comment script": "scripts/post_codex_auto_merge_decision_comment.py", - "auto-merge guard decision comment artifact": "guard_decision_comment.md", - "auto-merge guard decision label hygiene": "--sync-labels", - "merge command": "gh pr merge", - "head SHA race guard": "--match-head-commit", - "workflow run head SHA": "github.event.workflow_run.head_sha", - "auto-merge diagnostic artifact upload": "Upload Codex auto-merge diagnostics", - "auto-merge diagnostic artifact always uploads": "if: always()", - "auto-merge diagnostic artifact action": "actions/upload-artifact@v7", - "auto-merge diagnostic artifact name": "codex-auto-merge-", - "auto-merge diagnostic artifact path": "data/output/codex_auto_merge/", - "auto-merge diagnostic artifact missing-file warning": "if-no-files-found: warn", - } - for label, snippet in required_snippets.items(): - if snippet not in workflow: - errors.append(f"auto-merge workflow missing {label}") - return errors - - -def validate_codex_feedback_workflow(workflow_path: Path) -> list[str]: - errors: list[str] = [] - try: - workflow = workflow_path.read_text(encoding="utf-8") - except OSError: - return [f"Codex feedback workflow is missing: {workflow_path}"] - required_snippets = { - "CI failure workflow_run trigger": "workflow_run:", - "review feedback trigger": "pull_request_review:", - "CI same-repository guard": "github.event.workflow_run.head_repository.full_name == github.repository", - "review same-repository guard": "github.event.pull_request.head.repo.full_name == github.repository", - "Codex branch guard": "codex/monthly-review-issue-", - "explicit PR repository binding": 'gh pr list --repo "${GITHUB_REPOSITORY}"', - "same-repository PR filter": ".headRepository.nameWithOwner", - "explicit issue repository binding": 'gh issue comment "${issue_number}" --repo "${GITHUB_REPOSITORY}"', - "feedback retry limit": "CODEX_AUDIT_MAX_FEEDBACK_ROUNDS", - "feedback retry limit default": "vars.CODEX_AUDIT_MAX_FEEDBACK_ROUNDS || '3'", - "feedback retry limit fallback": "configured_max_rounds = 3", - "feedback retry limit clamp": "max_rounds = min(max(configured_max_rounds, 1), 10)", - "feedback stale auto-merge label lookup": "auto_merge_label", - "feedback stale auto-merge policy label validation": "load_policy_labels(DEFAULT_POLICY_PATH)", - "feedback stale auto-merge label cleanup skip": "Skipping stale guarded auto-merge label cleanup", - "feedback stale auto-merge label cleanup conditional": 'if [ -n "${guard_label}" ]; then', - "feedback stale auto-merge label cleanup": '--remove-label "${guard_label}"', - "feedback stale auto-merge label cleanup log": "Removed stale guarded auto-merge label", - "paginated feedback comment fetch": "gh api --paginate --slurp", - "feedback comment pages artifact": "comment_pages.json", - "feedback comments page size": "/comments?per_page=100", - "retry limit handoff": "--remove-label codex-bridge", - "dispatch hard switch": "env.CODEX_AUDIT_ENABLED", - "Bridge dispatch command": "gh workflow run codex_audit.yml", - "Bridge repository input": "--repo \"${TARGET_REPOSITORY}\"", - "source issue input": "--field issue_number=\"${ISSUE_NUMBER}\"", - "monthly task input": '--field task="monthly_snapshot_audit"', - "guarded auto-merge input": "--field auto_merge=\"${auto_merge}\"", - "feedback auto-merge readiness check": "scripts/check_codex_auto_merge_readiness.py", - "feedback optional readiness token fallback": "CODEX_AUDIT_READINESS_TOKEN || secrets.GITHUB_TOKEN", - "feedback configurable required status checks": "CODEX_AUDIT_REQUIRED_STATUS_CHECKS", - "feedback required status checks argument": "--required-status-checks", - "feedback auto-merge readiness soft-fail": "continue-on-error: true", - "feedback auto-merge readiness outcome gate": "AUTO_MERGE_READINESS_OUTCOME", - "feedback auto-merge downgrade": "dispatching Codex feedback retry with auto_merge=false", - "GitHub App token fallback": "CODEX_AUDIT_DISPATCH_TOKEN", - "GitHub App action permission": "permission-actions: write", - "dispatch output gate": "steps.feedback.outputs.dispatch_feedback == 'true'", - "feedback diagnostic artifact upload": "Upload Codex feedback diagnostics", - "feedback diagnostic artifact always uploads": "if: always()", - "feedback diagnostic artifact action": "actions/upload-artifact@v7", - "CI feedback diagnostic artifact": "codex-pr-feedback-ci-", - "review feedback diagnostic artifact": "codex-pr-feedback-review-", - "feedback diagnostic artifact path": "data/output/codex_feedback/", - "feedback diagnostic artifact missing-file warning": "if-no-files-found: warn", - } - for label, snippet in required_snippets.items(): - if snippet not in workflow: - errors.append(f"Codex feedback workflow missing {label}") - return errors - - -def validate_monthly_review_workflow(workflow_path: Path) -> list[str]: - errors: list[str] = [] - try: - workflow = workflow_path.read_text(encoding="utf-8") - except OSError: - return [f"Monthly review workflow is missing: {workflow_path}"] - required_snippets = { - "Bridge dispatch workflow": "actions/workflows/codex_audit.yml/dispatches", - "guarded label sync step": "Ensure guarded auto-merge labels", - "guarded label sync script": "scripts/sync_codex_auto_merge_labels.py", - "guarded label sync artifact": "codex_auto_merge_label_sync.md", - "preflight label sync input": "--label-sync-file data/output/monthly_report_bundle/codex_auto_merge_label_sync.md", - "readiness check": "scripts/check_codex_auto_merge_readiness.py", - "optional readiness token fallback": "CODEX_AUDIT_READINESS_TOKEN || secrets.GITHUB_TOKEN", - "configurable required status checks": "CODEX_AUDIT_REQUIRED_STATUS_CHECKS", - "required status checks argument": "--required-status-checks", - "readiness step id": "id: auto_merge_readiness", - "readiness soft-fail": "continue-on-error: true", - "auto-merge requested input": "AUTO_MERGE_REQUESTED", - "readiness outcome gate": "AUTO_MERGE_READINESS_OUTCOME", - "auto-merge downgrade": "dispatching Codex audit with auto_merge=false", - "guarded auto-merge dispatch field": '"auto_merge": str(auto_merge).lower()', - "monthly task input": '"task": "monthly_snapshot_audit"', - "diagnostic bundle always uploads": "if: always()", - "diagnostic bundle month fallback": "steps.bundle.outputs.report_month || 'unknown'", - "diagnostic bundle missing-file warning": "if-no-files-found: warn", - } - for label, snippet in required_snippets.items(): - if snippet not in workflow: - errors.append(f"Monthly review workflow missing {label}") - return errors - def _protection_status_check_contexts(protection: dict[str, Any]) -> set[str]: required_status_checks = protection.get("required_status_checks") @@ -607,9 +452,6 @@ def evaluate_readiness( branch: str, token: str, policy_path: Path = DEFAULT_POLICY_PATH, - workflow_path: Path = DEFAULT_AUTO_MERGE_WORKFLOW, - feedback_workflow_path: Path = DEFAULT_CODEX_FEEDBACK_WORKFLOW, - monthly_workflow_path: Path = DEFAULT_MONTHLY_REVIEW_WORKFLOW, api_url: str = DEFAULT_API_URL, required_status_checks: tuple[str, ...] = DEFAULT_REQUIRED_STATUS_CHECKS, ) -> dict[str, Any]: @@ -633,7 +475,7 @@ def evaluate_readiness( except ReadinessError as exc: required_status_check_errors.append(str(exc)) errors.extend(required_status_check_errors) - required_status_checks = tuple() + required_status_checks = () try: labels = load_policy_labels(policy_path) @@ -647,24 +489,6 @@ def evaluate_readiness( checks.append(f"Loaded auto-merge policy label `{label}`.") checks.append(f"Loaded high-risk human-review label `{human_review_label}`.") - workflow_errors = validate_auto_merge_workflow(workflow_path) - if workflow_errors: - errors.extend(workflow_errors) - else: - checks.append("Auto-merge workflow contains required CI, branch, guard, and merge checks.") - - feedback_workflow_errors = validate_codex_feedback_workflow(feedback_workflow_path) - if feedback_workflow_errors: - errors.extend(feedback_workflow_errors) - else: - checks.append("Codex feedback workflow contains same-repository retry, limit, and Bridge dispatch checks.") - - monthly_workflow_errors = validate_monthly_review_workflow(monthly_workflow_path) - if monthly_workflow_errors: - errors.extend(monthly_workflow_errors) - else: - checks.append("Monthly review workflow contains readiness-gated Bridge dispatch checks.") - if not token.strip(): errors.append("GITHUB_TOKEN is required when CODEX_AUDIT_AUTO_MERGE=true") elif label and human_review_label and not required_status_check_errors: @@ -720,9 +544,6 @@ def parse_args() -> argparse.Namespace: parser.add_argument("--branch", required=True) parser.add_argument("--auto-merge", default=os.environ.get("CODEX_AUDIT_AUTO_MERGE", "false")) parser.add_argument("--policy-file", type=Path, default=DEFAULT_POLICY_PATH) - parser.add_argument("--workflow-file", type=Path, default=DEFAULT_AUTO_MERGE_WORKFLOW) - parser.add_argument("--feedback-workflow-file", type=Path, default=DEFAULT_CODEX_FEEDBACK_WORKFLOW) - parser.add_argument("--monthly-workflow-file", type=Path, default=DEFAULT_MONTHLY_REVIEW_WORKFLOW) parser.add_argument("--api-url", default=DEFAULT_API_URL) parser.add_argument("--required-status-check", action="append") parser.add_argument( @@ -742,9 +563,6 @@ def main() -> int: branch=args.branch, token=os.environ.get("GITHUB_TOKEN", ""), policy_path=args.policy_file, - workflow_path=args.workflow_file, - feedback_workflow_path=args.feedback_workflow_file, - monthly_workflow_path=args.monthly_workflow_file, api_url=args.api_url, required_status_checks=parse_required_status_check_args( args.required_status_check, diff --git a/scripts/plan_codex_auto_merge_enablement.py b/scripts/plan_codex_auto_merge_enablement.py index 8efcdab..4791479 100644 --- a/scripts/plan_codex_auto_merge_enablement.py +++ b/scripts/plan_codex_auto_merge_enablement.py @@ -14,7 +14,6 @@ from scripts.check_codex_auto_merge_readiness import ( DEFAULT_API_URL, - DEFAULT_AUTO_MERGE_WORKFLOW, DEFAULT_POLICY_PATH, GitHubApiError, ReadinessError, @@ -37,8 +36,6 @@ "Codex remediation PR requires human review before merge", ), ) -AUTO_MERGE_VARIABLE = "CODEX_AUDIT_AUTO_MERGE" - def branch_protection_payload(required_status_checks: tuple[str, ...]) -> dict[str, Any]: required_status_checks = validate_required_status_checks(required_status_checks) @@ -232,33 +229,6 @@ def discover_branch_protection_status_checks( return discovered, warnings -def discover_repository_variable( - *, - api_url: str, - repo: str, - token: str, - name: str = AUTO_MERGE_VARIABLE, -) -> tuple[str | None, list[str]]: - if not token.strip(): - return None, [f"GITHUB_TOKEN is not set; skipped {name} repository variable discovery."] - - api_url = api_url.rstrip("/") - encoded_name = urllib.parse.quote(name, safe="") - try: - payload = github_request("GET", f"{api_url}/repos/{repo}/actions/variables/{encoded_name}", token) - except GitHubApiError as exc: - if exc.status_code == 404: - return None, [f"Repository variable {name} is not set."] - return None, [f"Could not read repository variable {name}: HTTP {exc.status_code}"] - if not isinstance(payload, dict): - return None, [f"Could not read repository variable {name}: invalid response"] - - value = payload.get("value") - if not isinstance(value, str): - return None, [f"Repository variable {name} has an invalid value."] - return value, [] - - def render_enablement_plan( *, repo: str, @@ -268,14 +238,11 @@ def render_enablement_plan( discovered_check_contexts: list[str] | None = None, discovered_branch_protection_status_checks: list[str] | None = None, discovered_ruleset_status_checks: list[str] | None = None, - auto_merge_variable_value: str | None = None, discovery_warnings: list[str] | None = None, protection_discovery_warnings: list[str] | None = None, ruleset_discovery_warnings: list[str] | None = None, - variable_discovery_warnings: list[str] | None = None, ) -> str: branch_command = render_branch_protection_command(repo, branch, required_status_checks) - label_commands = render_label_commands(repo) required_checks = ", ".join(f"`{item}`" for item in required_status_checks) discovered_check_contexts = discovered_check_contexts or [] discovered_branch_protection_status_checks = discovered_branch_protection_status_checks or [] @@ -283,21 +250,7 @@ def render_enablement_plan( discovery_warnings = discovery_warnings or [] protection_discovery_warnings = protection_discovery_warnings or [] ruleset_discovery_warnings = ruleset_discovery_warnings or [] - variable_discovery_warnings = variable_discovery_warnings or [] expected_missing = sorted(set(required_status_checks) - set(discovered_check_contexts)) if discovered_check_contexts else [] - variable_section_lines = [ - "## Current guarded auto-merge variable", - "", - ] - if auto_merge_variable_value is None: - variable_section_lines.append(f"- `{AUTO_MERGE_VARIABLE}`: unknown or not set") - else: - variable_section_lines.append(f"- `{AUTO_MERGE_VARIABLE}`: `{auto_merge_variable_value}`") - variable_section_lines.append("- The source auto-merge workflow runs only when this value is `true`, `True`, or `TRUE`.") - if variable_discovery_warnings: - variable_section_lines.extend(["", "### Variable discovery warnings"]) - variable_section_lines.extend(f"- {item}" for item in variable_discovery_warnings) - variable_section = "\n".join(variable_section_lines).strip() + "\n\n" discovered_section = "" if discovered_check_contexts or discovery_warnings: discovered_section_lines = ["## Discovered check contexts", ""] @@ -331,61 +284,32 @@ def render_enablement_plan( ruleset_section_lines.extend(f"- {item}" for item in ruleset_discovery_warnings) ruleset_section = "\n".join(ruleset_section_lines).strip() + "\n\n" preflight_section = ( - "## Enablement preflight checklist\n\n" + "## Readiness assessment checklist\n\n" "- [ ] Readiness reports `Ready: yes` after labels and branch protection or rulesets are configured.\n" - f"- [ ] `{AUTO_MERGE_VARIABLE}` is still `false` or unset before the final enablement step.\n" f"- [ ] Required status checks are configured as strict/up-to-date checks: {required_checks}.\n" "- [ ] Any existing branch protection or ruleset settings were reviewed before applying the generated `PUT` command.\n" - "- [ ] The first guarded Codex PR after enablement will be monitored, and the rollback command below is ready.\n\n" + "- [ ] Keep automatic merge disabled; this report does not authorize workflow or repository-setting changes.\n\n" ) return ( "# Codex guarded auto-merge enablement plan\n\n" "This plan is read-only. It does not modify GitHub repository settings.\n\n" "## Current readiness\n\n" f"{render_summary(readiness)}\n" - f"{variable_section}" f"{discovered_section}" f"{protection_section}" f"{ruleset_section}" f"{preflight_section}" - "## Manual enablement steps\n\n" - "1. Confirm that source CI check contexts, merge guard, and feedback retry workflow are stable, and both `auto-merge-ok` and " - "`human-review-required` labels exist. This plan expects " + "## Read-only readiness guidance\n\n" + "1. Confirm that source CI check contexts and both `auto-merge-ok` and " + "`human-review-required` labels exist. This report expects " f"{required_checks}.\n" - " Compare the expected checks with the discovered contexts, branch protection checks, and ruleset checks above before applying branch protection.\n" - "2. Create or update the labels used by the unattended and human-review paths:\n\n" - "```bash\n" - f"{label_commands}\n" - "```\n\n" - "3. Apply minimal branch protection or an equivalent active repository ruleset that requires the CI status check before merge.\n" - " The command below uses GitHub's branch protection `PUT` API. If branch protection already exists, " - "review and merge the generated payload with the existing rules before running it so review, " - "restriction, or admin-enforcement settings are not unintentionally overwritten:\n\n" + " Compare the expected checks with the discovered contexts, branch protection checks, and ruleset checks above.\n" + "2. If branch protection needs a separate human-approved change, use the generated payload only after reviewing the existing settings; " + "it must not unintentionally overwrite review, restriction, or admin-enforcement settings.\n\n" "```bash\n" f"{branch_command}\n" "```\n\n" - "4. Verify readiness again before enabling dispatch-time auto-merge requests:\n\n" - " If GitHub Actions' default token cannot read branch protection or label settings in this repository, " - "configure `CODEX_AUDIT_READINESS_TOKEN` as a source-repository secret for readiness checks only.\n\n" - "```bash\n" - "GITHUB_TOKEN=\"$(gh auth token)\" .venv/bin/python scripts/check_codex_auto_merge_readiness.py \\\n" - f" --repo {repo} \\\n" - f" --branch {branch} \\\n" - " --auto-merge true \\\n" - + "".join(f" --required-status-check {check} \\\n" for check in required_status_checks) - + " --summary-file data/output/codex_auto_merge_readiness.md\n" - "```\n\n" - "5. Only after readiness reports `Ready: yes`, enable guarded requests from monthly review:\n\n" - "```bash\n" - "gh variable set CODEX_AUDIT_AUTO_MERGE --repo " - f"{repo} --body true\n" - "```\n\n" - "## Rollback\n\n" - "```bash\n" - "gh variable set CODEX_AUDIT_AUTO_MERGE --repo " - f"{repo} --body false\n" - "```\n\n" - "High-risk PRs remain blocked by the source merge guard even after enablement.\n" + "3. Automatic merge remains disabled. This report does not instruct a workflow enablement, variable change, or merge action.\n" ) @@ -400,7 +324,6 @@ def parse_args() -> argparse.Namespace: help="Comma- or newline-separated required status check contexts. Repeated --required-status-check is still supported.", ) parser.add_argument("--policy-file", type=Path, default=DEFAULT_POLICY_PATH) - parser.add_argument("--workflow-file", type=Path, default=DEFAULT_AUTO_MERGE_WORKFLOW) parser.add_argument("--api-url", default=DEFAULT_API_URL) parser.add_argument("--output-file", type=Path) return parser.parse_args() @@ -423,7 +346,6 @@ def main() -> int: branch=args.branch, token=token, policy_path=args.policy_file, - workflow_path=args.workflow_file, api_url=args.api_url, required_status_checks=required_status_checks, ) @@ -445,11 +367,6 @@ def main() -> int: branch=args.branch, token=token, ) - auto_merge_variable_value, variable_discovery_warnings = discover_repository_variable( - api_url=args.api_url, - repo=args.repo, - token=token, - ) plan = render_enablement_plan( repo=args.repo, branch=args.branch, @@ -458,11 +375,9 @@ def main() -> int: discovered_check_contexts=discovered_contexts, discovered_branch_protection_status_checks=discovered_protection_checks, discovered_ruleset_status_checks=discovered_ruleset_checks, - auto_merge_variable_value=auto_merge_variable_value, discovery_warnings=discovery_warnings, protection_discovery_warnings=protection_discovery_warnings, ruleset_discovery_warnings=ruleset_discovery_warnings, - variable_discovery_warnings=variable_discovery_warnings, ) if args.output_file: args.output_file.parent.mkdir(parents=True, exist_ok=True) diff --git a/tests/test_check_codex_auto_merge_readiness.py b/tests/test_check_codex_auto_merge_readiness.py index 607e2a0..0784d70 100644 --- a/tests/test_check_codex_auto_merge_readiness.py +++ b/tests/test_check_codex_auto_merge_readiness.py @@ -10,9 +10,6 @@ evaluate_readiness, parse_required_status_check_args, render_summary, - validate_auto_merge_workflow, - validate_codex_feedback_workflow, - validate_monthly_review_workflow, validate_required_status_checks, ) @@ -58,105 +55,6 @@ def write_policy( ) -def write_workflow(path: Path) -> None: - path.write_text( - """ -name: Auto Merge Codex Remediation PR -on: - workflow_run: - workflows: ["CI"] - types: [completed] -jobs: - auto-merge: - if: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.head_repository.full_name == github.repository && startsWith(github.event.workflow_run.head_branch, 'codex/monthly-review-issue-') && contains(fromJSON('["true","True","TRUE"]'), vars.CODEX_AUDIT_AUTO_MERGE) - permissions: - contents: write - issues: write - pull-requests: write - steps: - - run: | - gh pr list --repo "${{ github.repository }}" --json number,headRepository,isCrossRepository --jq 'headRepository.nameWithOwner' - gh pr view 12 --repo "${{ github.repository }}" --json files,changedFiles,additions,deletions,reviewDecision - gh api --paginate --slurp "/repos/${{ github.repository }}/pulls/${{ steps.pr.outputs.pr_number }}/files?per_page=100" > data/output/codex_auto_merge/pr_files_pages.json - python3 - <<'PY' - item = {} - files = [{"status": item.get("status", ""), "previous_filename": item.get("previous_filename", "")}] - PY - python3 scripts/evaluate_codex_pr_merge.py --require-same-repository - - name: Comment auto-merge guard decision - run: python3 scripts/post_codex_auto_merge_decision_comment.py --repo "${{ github.repository }}" --pr-json data/output/codex_auto_merge/pr.json --decision-json data/output/codex_auto_merge/decision.json --output-file data/output/codex_auto_merge/guard_decision_comment.md --sync-labels - - name: Check guarded auto-merge readiness before merge - id: merge_readiness - continue-on-error: true - env: - GITHUB_TOKEN: ${{ secrets.CODEX_AUDIT_READINESS_TOKEN || secrets.GITHUB_TOKEN }} - CODEX_AUDIT_REQUIRED_STATUS_CHECKS: ${{ vars.CODEX_AUDIT_REQUIRED_STATUS_CHECKS || 'test' }} - run: python3 scripts/check_codex_auto_merge_readiness.py --repo "${{ github.repository }}" --branch main --auto-merge true --required-status-checks "${CODEX_AUDIT_REQUIRED_STATUS_CHECKS}" - - name: Comment merge-time readiness failure - run: python3 scripts/post_codex_auto_merge_decision_comment.py --repo "${{ github.repository }}" --pr-json data/output/codex_auto_merge/pr.json --decision-json data/output/codex_auto_merge/readiness_decision.json --output-file data/output/codex_auto_merge/readiness_guard_decision_comment.md --sync-labels # merge_readiness_failed - - name: Fail on merge-time readiness failure - run: exit 1 - - run: | - if [ "${{ steps.merge_readiness.outcome == 'success' }}" = "true" ]; then gh pr merge 12 --repo "${{ github.repository }}" --rebase --delete-branch --match-head-commit "${{ github.event.workflow_run.head_sha }}"; fi - - name: Upload Codex auto-merge diagnostics - if: always() - uses: actions/upload-artifact@v7 - with: - name: codex-auto-merge-${{ github.run_id }} - path: data/output/codex_auto_merge/ - if-no-files-found: warn -""", - encoding="utf-8", - ) - - -def test_validate_auto_merge_workflow_requires_changed_line_fields(tmp_path: Path) -> None: - workflow_path = tmp_path / "auto_merge.yml" - write_workflow(workflow_path) - workflow_path.write_text( - workflow_path.read_text(encoding="utf-8").replace(",additions,deletions", ""), - encoding="utf-8", - ) - - errors = validate_auto_merge_workflow(workflow_path) - - assert "auto-merge workflow missing PR additions for changed-line guard" in errors - assert "auto-merge workflow missing PR deletions for changed-line guard" in errors - - -def test_validate_auto_merge_workflow_requires_review_decision(tmp_path: Path) -> None: - workflow_path = tmp_path / "auto_merge.yml" - write_workflow(workflow_path) - workflow_path.write_text( - workflow_path.read_text(encoding="utf-8").replace(",reviewDecision", ""), - encoding="utf-8", - ) - - errors = validate_auto_merge_workflow(workflow_path) - - assert "auto-merge workflow missing PR review decision guard" in errors - - - -def test_validate_auto_merge_workflow_requires_paginated_file_status_metadata(tmp_path: Path) -> None: - workflow_path = tmp_path / "auto_merge.yml" - write_workflow(workflow_path) - workflow_path.write_text( - workflow_path.read_text(encoding="utf-8") - .replace( - 'gh api --paginate --slurp "/repos/${{ github.repository }}/pulls/${{ steps.pr.outputs.pr_number }}/files?per_page=100" > data/output/codex_auto_merge/pr_files_pages.json\n', - "", - ) - .replace('"status": item.get("status", ""), ', "") - .replace('"previous_filename": item.get("previous_filename", "")', ""), - encoding="utf-8", - ) - - errors = validate_auto_merge_workflow(workflow_path) - - assert "auto-merge workflow missing paginated PR file metadata fetch" in errors - assert "auto-merge workflow missing PR file status guard" in errors - assert "auto-merge workflow missing PR previous filename capture" in errors def test_evaluate_readiness_skips_when_auto_merge_is_false(tmp_path: Path) -> None: decision = evaluate_readiness( @@ -165,7 +63,6 @@ def test_evaluate_readiness_skips_when_auto_merge_is_false(tmp_path: Path) -> No branch="main", token="", policy_path=tmp_path / "missing-policy.json", - workflow_path=tmp_path / "missing-workflow.yml", ) assert decision["ready"] @@ -175,14 +72,12 @@ def test_evaluate_readiness_skips_when_auto_merge_is_false(tmp_path: Path) -> No def test_evaluate_readiness_passes_with_label_and_protected_branch(tmp_path: Path, monkeypatch) -> None: policy_path = tmp_path / "policy.json" - workflow_path = tmp_path / "auto_merge.yml" write_policy(policy_path) - write_workflow(workflow_path) def fake_github_request(method: str, url: str, token: str, payload=None, *, timeout=30): assert method == "GET" assert token == "token" - if url.endswith("/labels/auto-merge-ok") or url.endswith("/labels/human-review-required"): + if url.endswith(("/labels/auto-merge-ok", "/labels/human-review-required")): return {"name": url.rsplit("/", 1)[-1]} if url.endswith("/branches/main/protection"): return {"required_status_checks": {"strict": True, "contexts": ["test"]}} @@ -196,7 +91,6 @@ def fake_github_request(method: str, url: str, token: str, payload=None, *, time branch="main", token="token", policy_path=policy_path, - workflow_path=workflow_path, ) assert decision["ready"] @@ -205,20 +99,13 @@ def fake_github_request(method: str, url: str, token: str, payload=None, *, time assert decision["human_review_label"] == "human-review-required" assert decision["errors"] == [] assert "Remote labels exist and branch protection or rulesets require status checks: test." in decision["checks"] - assert ( - "Codex feedback workflow contains same-repository retry, limit, and Bridge dispatch checks." - in decision["checks"] - ) - assert "Monthly review workflow contains readiness-gated Bridge dispatch checks." in decision["checks"] def test_evaluate_readiness_fails_when_policy_allows_control_plane_exact_path( tmp_path: Path, monkeypatch ) -> None: policy_path = tmp_path / "policy.json" - workflow_path = tmp_path / "auto_merge.yml" write_policy(policy_path, medium_exact=["scripts/evaluate_codex_pr_merge.py"]) - write_workflow(workflow_path) def fake_github_request(method: str, url: str, token: str, payload=None, *, timeout=30): raise AssertionError("remote readiness should not run when local policy guardrail fails") @@ -231,7 +118,6 @@ def fake_github_request(method: str, url: str, token: str, payload=None, *, time branch="main", token="token", policy_path=policy_path, - workflow_path=workflow_path, ) assert not decision["ready"] @@ -242,9 +128,7 @@ def fake_github_request(method: str, url: str, token: str, payload=None, *, time def test_evaluate_readiness_fails_when_policy_allows_control_plane_prefix(tmp_path: Path, monkeypatch) -> None: policy_path = tmp_path / "policy.json" - workflow_path = tmp_path / "auto_merge.yml" write_policy(policy_path, low_prefixes=[".github/"]) - write_workflow(workflow_path) def fake_github_request(method: str, url: str, token: str, payload=None, *, timeout=30): raise AssertionError("remote readiness should not run when local policy guardrail fails") @@ -257,21 +141,18 @@ def fake_github_request(method: str, url: str, token: str, payload=None, *, time branch="main", token="token", policy_path=policy_path, - workflow_path=workflow_path, ) assert not decision["ready"] assert decision["errors"] == [ - "auto-merge policy must keep control-plane paths high-risk: " - ".github/codex_auto_merge_policy.json, .github/workflows/*" + ("auto-merge policy must keep control-plane paths high-risk: " + ".github/codex_auto_merge_policy.json, .github/workflows/*") ] def test_evaluate_readiness_fails_when_label_is_missing(tmp_path: Path, monkeypatch) -> None: policy_path = tmp_path / "policy.json" - workflow_path = tmp_path / "auto_merge.yml" write_policy(policy_path) - write_workflow(workflow_path) def fake_github_request(method: str, url: str, token: str, payload=None, *, timeout=30): if url.endswith("/labels/auto-merge-ok"): @@ -290,7 +171,6 @@ def fake_github_request(method: str, url: str, token: str, payload=None, *, time branch="main", token="token", policy_path=policy_path, - workflow_path=workflow_path, ) assert not decision["ready"] @@ -300,9 +180,7 @@ def fake_github_request(method: str, url: str, token: str, payload=None, *, time def test_evaluate_readiness_fails_when_human_review_label_is_missing(tmp_path: Path, monkeypatch) -> None: policy_path = tmp_path / "policy.json" - workflow_path = tmp_path / "auto_merge.yml" write_policy(policy_path) - write_workflow(workflow_path) def fake_github_request(method: str, url: str, token: str, payload=None, *, timeout=30): if url.endswith("/labels/auto-merge-ok"): @@ -321,7 +199,6 @@ def fake_github_request(method: str, url: str, token: str, payload=None, *, time branch="main", token="token", policy_path=policy_path, - workflow_path=workflow_path, ) assert not decision["ready"] @@ -331,9 +208,7 @@ def fake_github_request(method: str, url: str, token: str, payload=None, *, time def test_evaluate_readiness_fails_when_policy_labels_match(tmp_path: Path, monkeypatch) -> None: policy_path = tmp_path / "policy.json" - workflow_path = tmp_path / "auto_merge.yml" write_policy(policy_path, human_review_label="auto-merge-ok") - write_workflow(workflow_path) def fake_github_request(method: str, url: str, token: str, payload=None, *, timeout=30): raise AssertionError("remote readiness should not run when policy labels collide") @@ -346,7 +221,6 @@ def fake_github_request(method: str, url: str, token: str, payload=None, *, time branch="main", token="token", policy_path=policy_path, - workflow_path=workflow_path, ) assert not decision["ready"] @@ -357,12 +231,10 @@ def test_evaluate_readiness_accepts_branch_endpoint_fallback_when_protection_det tmp_path: Path, monkeypatch ) -> None: policy_path = tmp_path / "policy.json" - workflow_path = tmp_path / "auto_merge.yml" write_policy(policy_path) - write_workflow(workflow_path) def fake_github_request(method: str, url: str, token: str, payload=None, *, timeout=30): - if url.endswith("/labels/auto-merge-ok") or url.endswith("/labels/human-review-required"): + if url.endswith(("/labels/auto-merge-ok", "/labels/human-review-required")): return {"name": url.rsplit("/", 1)[-1]} if url.endswith("/branches/main/protection"): raise GitHubApiError(method, url, 403, '{"message":"Resource not accessible by integration"}') @@ -386,7 +258,6 @@ def fake_github_request(method: str, url: str, token: str, payload=None, *, time branch="main", token="token", policy_path=policy_path, - workflow_path=workflow_path, ) assert decision["ready"] @@ -397,12 +268,10 @@ def test_evaluate_readiness_rejects_branch_endpoint_fallback_without_required_ch tmp_path: Path, monkeypatch ) -> None: policy_path = tmp_path / "policy.json" - workflow_path = tmp_path / "auto_merge.yml" write_policy(policy_path) - write_workflow(workflow_path) def fake_github_request(method: str, url: str, token: str, payload=None, *, timeout=30): - if url.endswith("/labels/auto-merge-ok") or url.endswith("/labels/human-review-required"): + if url.endswith(("/labels/auto-merge-ok", "/labels/human-review-required")): return {"name": url.rsplit("/", 1)[-1]} if url.endswith("/branches/main/protection"): raise GitHubApiError(method, url, 403, '{"message":"Resource not accessible by integration"}') @@ -423,7 +292,6 @@ def fake_github_request(method: str, url: str, token: str, payload=None, *, time branch="main", token="token", policy_path=policy_path, - workflow_path=workflow_path, ) assert not decision["ready"] @@ -432,12 +300,10 @@ def fake_github_request(method: str, url: str, token: str, payload=None, *, time def test_evaluate_readiness_fails_when_branch_is_not_protected(tmp_path: Path, monkeypatch) -> None: policy_path = tmp_path / "policy.json" - workflow_path = tmp_path / "auto_merge.yml" write_policy(policy_path) - write_workflow(workflow_path) def fake_github_request(method: str, url: str, token: str, payload=None, *, timeout=30): - if url.endswith("/labels/auto-merge-ok") or url.endswith("/labels/human-review-required"): + if url.endswith(("/labels/auto-merge-ok", "/labels/human-review-required")): return {"name": url.rsplit("/", 1)[-1]} if url.endswith("/branches/main/protection"): raise GitHubApiError(method, url, 404, '{"message":"Branch not protected"}') @@ -453,7 +319,6 @@ def fake_github_request(method: str, url: str, token: str, payload=None, *, time branch="main", token="token", policy_path=policy_path, - workflow_path=workflow_path, ) assert not decision["ready"] @@ -464,12 +329,10 @@ def test_evaluate_readiness_accepts_strict_required_status_check_ruleset( tmp_path: Path, monkeypatch ) -> None: policy_path = tmp_path / "policy.json" - workflow_path = tmp_path / "auto_merge.yml" write_policy(policy_path) - write_workflow(workflow_path) def fake_github_request(method: str, url: str, token: str, payload=None, *, timeout=30): - if url.endswith("/labels/auto-merge-ok") or url.endswith("/labels/human-review-required"): + if url.endswith(("/labels/auto-merge-ok", "/labels/human-review-required")): return {"name": url.rsplit("/", 1)[-1]} if url.endswith("/branches/main/protection"): raise GitHubApiError(method, url, 404, '{"message":"Branch not protected"}') @@ -493,7 +356,6 @@ def fake_github_request(method: str, url: str, token: str, payload=None, *, time branch="main", token="token", policy_path=policy_path, - workflow_path=workflow_path, ) assert decision["ready"] @@ -504,12 +366,10 @@ def test_evaluate_readiness_rejects_non_strict_required_status_check_ruleset( tmp_path: Path, monkeypatch ) -> None: policy_path = tmp_path / "policy.json" - workflow_path = tmp_path / "auto_merge.yml" write_policy(policy_path) - write_workflow(workflow_path) def fake_github_request(method: str, url: str, token: str, payload=None, *, timeout=30): - if url.endswith("/labels/auto-merge-ok") or url.endswith("/labels/human-review-required"): + if url.endswith(("/labels/auto-merge-ok", "/labels/human-review-required")): return {"name": url.rsplit("/", 1)[-1]} if url.endswith("/branches/main/protection"): raise GitHubApiError(method, url, 404, '{"message":"Branch not protected"}') @@ -533,7 +393,6 @@ def fake_github_request(method: str, url: str, token: str, payload=None, *, time branch="main", token="token", policy_path=policy_path, - workflow_path=workflow_path, ) assert not decision["ready"] @@ -544,12 +403,10 @@ def test_evaluate_readiness_accepts_ruleset_when_legacy_protection_lacks_require tmp_path: Path, monkeypatch ) -> None: policy_path = tmp_path / "policy.json" - workflow_path = tmp_path / "auto_merge.yml" write_policy(policy_path) - write_workflow(workflow_path) def fake_github_request(method: str, url: str, token: str, payload=None, *, timeout=30): - if url.endswith("/labels/auto-merge-ok") or url.endswith("/labels/human-review-required"): + if url.endswith(("/labels/auto-merge-ok", "/labels/human-review-required")): return {"name": url.rsplit("/", 1)[-1]} if url.endswith("/branches/main/protection"): return {"required_status_checks": {"strict": True, "contexts": ["lint"]}} @@ -573,7 +430,6 @@ def fake_github_request(method: str, url: str, token: str, payload=None, *, time branch="main", token="token", policy_path=policy_path, - workflow_path=workflow_path, ) assert decision["ready"] @@ -582,12 +438,10 @@ def fake_github_request(method: str, url: str, token: str, payload=None, *, time def test_evaluate_readiness_fails_when_required_status_check_is_missing(tmp_path: Path, monkeypatch) -> None: policy_path = tmp_path / "policy.json" - workflow_path = tmp_path / "auto_merge.yml" write_policy(policy_path) - write_workflow(workflow_path) def fake_github_request(method: str, url: str, token: str, payload=None, *, timeout=30): - if url.endswith("/labels/auto-merge-ok") or url.endswith("/labels/human-review-required"): + if url.endswith(("/labels/auto-merge-ok", "/labels/human-review-required")): return {"name": url.rsplit("/", 1)[-1]} if url.endswith("/branches/main/protection"): return {"required_status_checks": {"strict": True, "contexts": ["lint"]}} @@ -603,7 +457,6 @@ def fake_github_request(method: str, url: str, token: str, payload=None, *, time branch="main", token="token", policy_path=policy_path, - workflow_path=workflow_path, ) assert not decision["ready"] @@ -649,9 +502,7 @@ def test_evaluate_readiness_fails_closed_for_blank_required_status_checks( tmp_path: Path, monkeypatch ) -> None: policy_path = tmp_path / "policy.json" - workflow_path = tmp_path / "auto_merge.yml" write_policy(policy_path) - write_workflow(workflow_path) def fake_github_request(method: str, url: str, token: str, payload=None, *, timeout=30): raise AssertionError("remote readiness should not run with invalid required status checks") @@ -664,7 +515,6 @@ def fake_github_request(method: str, url: str, token: str, payload=None, *, time branch="main", token="token", policy_path=policy_path, - workflow_path=workflow_path, required_status_checks=("",), ) @@ -676,12 +526,10 @@ def fake_github_request(method: str, url: str, token: str, payload=None, *, time def test_evaluate_readiness_fails_when_status_checks_are_not_strict(tmp_path: Path, monkeypatch) -> None: policy_path = tmp_path / "policy.json" - workflow_path = tmp_path / "auto_merge.yml" write_policy(policy_path) - write_workflow(workflow_path) def fake_github_request(method: str, url: str, token: str, payload=None, *, timeout=30): - if url.endswith("/labels/auto-merge-ok") or url.endswith("/labels/human-review-required"): + if url.endswith(("/labels/auto-merge-ok", "/labels/human-review-required")): return {"name": url.rsplit("/", 1)[-1]} if url.endswith("/branches/main/protection"): return {"required_status_checks": {"strict": False, "contexts": ["test"]}} @@ -697,7 +545,6 @@ def fake_github_request(method: str, url: str, token: str, payload=None, *, time branch="main", token="token", policy_path=policy_path, - workflow_path=workflow_path, ) assert not decision["ready"] @@ -706,9 +553,7 @@ def fake_github_request(method: str, url: str, token: str, payload=None, *, time def test_evaluate_readiness_reports_network_errors_without_traceback(tmp_path: Path, monkeypatch) -> None: policy_path = tmp_path / "policy.json" - workflow_path = tmp_path / "auto_merge.yml" write_policy(policy_path) - write_workflow(workflow_path) def fake_github_request(method: str, url: str, token: str, payload=None, *, timeout=30): raise GitHubApiError(method, url, 0, "certificate verify failed") @@ -721,7 +566,6 @@ def fake_github_request(method: str, url: str, token: str, payload=None, *, time branch="main", token="token", policy_path=policy_path, - workflow_path=workflow_path, ) assert not decision["ready"] @@ -732,150 +576,21 @@ def fake_github_request(method: str, url: str, token: str, payload=None, *, time ] -def test_evaluate_readiness_fails_when_auto_merge_workflow_lacks_guard(tmp_path: Path, monkeypatch) -> None: - policy_path = tmp_path / "policy.json" - workflow_path = tmp_path / "auto_merge.yml" - write_policy(policy_path) - workflow_path.write_text("name: unsafe\n", encoding="utf-8") - def fake_github_request(method: str, url: str, token: str, payload=None, *, timeout=30): - if url.endswith("/labels/auto-merge-ok") or url.endswith("/labels/human-review-required"): - return {"name": url.rsplit("/", 1)[-1]} - if url.endswith("/branches/main/protection"): - return {"required_status_checks": {"strict": True, "contexts": ["test"]}} - raise AssertionError(url) +def test_readiness_has_no_retired_workflow_parameters() -> None: + import inspect - monkeypatch.setattr(readiness, "github_request", fake_github_request) + parameters = inspect.signature(evaluate_readiness).parameters + assert "workflow_path" not in parameters + assert "feedback_workflow_path" not in parameters + assert "monthly_workflow_path" not in parameters - decision = evaluate_readiness( - auto_merge=True, - repo="QuantStrategyLab/UsEquitySnapshotPipelines", - branch="main", - token="token", - policy_path=policy_path, - workflow_path=workflow_path, - ) - assert not decision["ready"] - assert "auto-merge workflow missing source merge guard script" in decision["errors"] - assert "auto-merge workflow missing same-repository guard" in decision["errors"] - assert "auto-merge workflow missing auto-merge guard decision comment" in decision["errors"] - assert "auto-merge workflow missing auto-merge guard decision comment script" in decision["errors"] - assert "auto-merge workflow missing auto-merge guard decision label hygiene" in decision["errors"] - assert "auto-merge workflow missing merge-time readiness failure comment" in decision["errors"] - assert "auto-merge workflow missing merge-time readiness failure decision" in decision["errors"] - assert "auto-merge workflow missing merge-time readiness failure hard stop" in decision["errors"] - assert "auto-merge workflow missing merge gated by readiness outcome" in decision["errors"] - assert "auto-merge workflow missing auto-merge diagnostic artifact upload" in decision["errors"] - assert "auto-merge workflow missing auto-merge diagnostic artifact always uploads" in decision["errors"] - assert "auto-merge workflow missing auto-merge diagnostic artifact path" in decision["errors"] - - -def test_validate_codex_feedback_workflow_requires_retry_dispatch_guards(tmp_path: Path) -> None: - workflow_path = tmp_path / "codex_pr_feedback.yml" - workflow_path.write_text("name: unsafe\n", encoding="utf-8") - - errors = validate_codex_feedback_workflow(workflow_path) - - assert "Codex feedback workflow missing CI same-repository guard" in errors - assert "Codex feedback workflow missing review same-repository guard" in errors - assert "Codex feedback workflow missing Bridge dispatch command" in errors - assert "Codex feedback workflow missing feedback retry limit" in errors - assert "Codex feedback workflow missing feedback stale auto-merge label lookup" in errors - assert "Codex feedback workflow missing feedback stale auto-merge policy label validation" in errors - assert "Codex feedback workflow missing feedback stale auto-merge label cleanup skip" in errors - assert "Codex feedback workflow missing feedback stale auto-merge label cleanup conditional" in errors - assert "Codex feedback workflow missing feedback stale auto-merge label cleanup" in errors - assert "Codex feedback workflow missing paginated feedback comment fetch" in errors - assert "Codex feedback workflow missing feedback comment pages artifact" in errors - assert "Codex feedback workflow missing feedback diagnostic artifact upload" in errors - assert "Codex feedback workflow missing feedback diagnostic artifact always uploads" in errors - assert "Codex feedback workflow missing CI feedback diagnostic artifact" in errors - assert "Codex feedback workflow missing review feedback diagnostic artifact" in errors - - -def test_validate_monthly_review_workflow_requires_readiness_gated_dispatch(tmp_path: Path) -> None: - workflow_path = tmp_path / "monthly_review.yml" - workflow_path.write_text("name: unsafe\n", encoding="utf-8") - - errors = validate_monthly_review_workflow(workflow_path) - - assert "Monthly review workflow missing Bridge dispatch workflow" in errors - assert "Monthly review workflow missing guarded label sync step" in errors - assert "Monthly review workflow missing guarded label sync script" in errors - assert "Monthly review workflow missing guarded label sync artifact" in errors - assert "Monthly review workflow missing preflight label sync input" in errors - assert "Monthly review workflow missing readiness check" in errors - assert "Monthly review workflow missing readiness soft-fail" in errors - assert "Monthly review workflow missing readiness outcome gate" in errors - assert "Monthly review workflow missing auto-merge downgrade" in errors - assert "Monthly review workflow missing diagnostic bundle always uploads" in errors - assert "Monthly review workflow missing diagnostic bundle missing-file warning" in errors - - -def test_evaluate_readiness_fails_when_codex_feedback_workflow_lacks_retry_guard( - tmp_path: Path, monkeypatch -) -> None: - policy_path = tmp_path / "policy.json" - workflow_path = tmp_path / "auto_merge.yml" - feedback_workflow_path = tmp_path / "codex_pr_feedback.yml" - write_policy(policy_path) - write_workflow(workflow_path) - feedback_workflow_path.write_text("name: unsafe\n", encoding="utf-8") +def test_cli_has_no_retired_workflow_options(monkeypatch) -> None: + monkeypatch.setattr("sys.argv", ["readiness", "--repo", "org/repo", "--branch", "main"]) - def fake_github_request(method: str, url: str, token: str, payload=None, *, timeout=30): - if url.endswith("/labels/auto-merge-ok") or url.endswith("/labels/human-review-required"): - return {"name": url.rsplit("/", 1)[-1]} - if url.endswith("/branches/main/protection"): - return {"required_status_checks": {"strict": True, "contexts": ["test"]}} - raise AssertionError(url) + args = readiness.parse_args() - monkeypatch.setattr(readiness, "github_request", fake_github_request) - - decision = evaluate_readiness( - auto_merge=True, - repo="QuantStrategyLab/UsEquitySnapshotPipelines", - branch="main", - token="token", - policy_path=policy_path, - workflow_path=workflow_path, - feedback_workflow_path=feedback_workflow_path, - ) - - assert not decision["ready"] - assert "Codex feedback workflow missing Bridge dispatch command" in decision["errors"] - assert "Codex feedback workflow missing CI same-repository guard" in decision["errors"] - - -def test_evaluate_readiness_fails_when_monthly_review_workflow_lacks_readiness_gate( - tmp_path: Path, monkeypatch -) -> None: - policy_path = tmp_path / "policy.json" - workflow_path = tmp_path / "auto_merge.yml" - monthly_workflow_path = tmp_path / "monthly_review.yml" - write_policy(policy_path) - write_workflow(workflow_path) - monthly_workflow_path.write_text("name: unsafe\n", encoding="utf-8") - - def fake_github_request(method: str, url: str, token: str, payload=None, *, timeout=30): - if url.endswith("/labels/auto-merge-ok") or url.endswith("/labels/human-review-required"): - return {"name": url.rsplit("/", 1)[-1]} - if url.endswith("/branches/main/protection"): - return {"required_status_checks": {"strict": True, "contexts": ["test"]}} - raise AssertionError(url) - - monkeypatch.setattr(readiness, "github_request", fake_github_request) - - decision = evaluate_readiness( - auto_merge=True, - repo="QuantStrategyLab/UsEquitySnapshotPipelines", - branch="main", - token="token", - policy_path=policy_path, - workflow_path=workflow_path, - monthly_workflow_path=monthly_workflow_path, - ) - - assert not decision["ready"] - assert "Monthly review workflow missing readiness check" in decision["errors"] - assert "Monthly review workflow missing auto-merge downgrade" in decision["errors"] + assert not hasattr(args, "workflow_file") + assert not hasattr(args, "feedback_workflow_file") + assert not hasattr(args, "monthly_workflow_file") diff --git a/tests/test_plan_codex_auto_merge_enablement.py b/tests/test_plan_codex_auto_merge_enablement.py index 2a3e92d..4251ef1 100644 --- a/tests/test_plan_codex_auto_merge_enablement.py +++ b/tests/test_plan_codex_auto_merge_enablement.py @@ -7,7 +7,6 @@ discover_branch_protection_status_checks, discover_branch_ruleset_status_checks, discover_check_contexts, - discover_repository_variable, render_branch_protection_command, render_enablement_plan, render_label_commands, @@ -60,7 +59,7 @@ def test_render_label_commands_are_copyable_and_idempotent() -> None: assert "Codex remediation PR requires human review before merge" in commands -def test_render_enablement_plan_includes_readiness_enable_and_rollback_commands() -> None: +def test_render_enablement_plan_reports_readiness_without_enablement_commands() -> None: plan = render_enablement_plan( repo="QuantStrategyLab/UsEquitySnapshotPipelines", branch="main", @@ -79,20 +78,14 @@ def test_render_enablement_plan_includes_readiness_enable_and_rollback_commands( discovered_check_contexts=["test", "monthly-review"], discovered_branch_protection_status_checks=["test (strict)"], discovered_ruleset_status_checks=["test (strict)", "lint (non-strict)"], - auto_merge_variable_value="false", discovery_warnings=["Could not read commit statuses for abc123: HTTP 403"], protection_discovery_warnings=["No other branch protection checks found."], ruleset_discovery_warnings=["No other active branch rulesets found."], ) assert "This plan is read-only" in plan - assert "## Current guarded auto-merge variable" in plan - assert "- `CODEX_AUDIT_AUTO_MERGE`: `false`" in plan - assert "runs only when this value is `true`, `True`, or `TRUE`" in plan assert "Human-review label: `human-review-required`" in plan - assert "feedback retry workflow are stable" in plan assert "both `auto-merge-ok` and `human-review-required` labels exist" in plan - assert "gh label create human-review-required --repo QuantStrategyLab/UsEquitySnapshotPipelines" in plan assert "branch protection is not enabled for main" in plan assert "## Discovered check contexts" in plan assert "- `test`" in plan @@ -104,19 +97,14 @@ def test_render_enablement_plan_includes_readiness_enable_and_rollback_commands( assert "## Discovered branch protection required checks" in plan assert "No other branch protection checks found." in plan assert "Compare the expected checks with the discovered contexts, branch protection checks, and ruleset checks" in plan - assert "## Enablement preflight checklist" in plan + assert "## Readiness assessment checklist" in plan assert "- [ ] Readiness reports `Ready: yes`" in plan - assert "- [ ] `CODEX_AUDIT_AUTO_MERGE` is still `false` or unset before the final enablement step." in plan assert "- [ ] Required status checks are configured as strict/up-to-date checks: `test`." in plan assert "- [ ] Any existing branch protection or ruleset settings were reviewed" in plan - assert "- [ ] The first guarded Codex PR after enablement will be monitored" in plan - assert "GitHub's branch protection `PUT` API" in plan - assert "not unintentionally overwritten" in plan - assert "CODEX_AUDIT_READINESS_TOKEN" in plan - assert "scripts/check_codex_auto_merge_readiness.py" in plan - assert "--required-status-check test" in plan - assert "gh variable set CODEX_AUDIT_AUTO_MERGE --repo QuantStrategyLab/UsEquitySnapshotPipelines --body true" in plan - assert "gh variable set CODEX_AUDIT_AUTO_MERGE --repo QuantStrategyLab/UsEquitySnapshotPipelines --body false" in plan + assert "not unintentionally overwrite review, restriction, or admin-enforcement settings" in plan + assert "Automatic merge remains disabled." in plan + assert "CODEX_AUDIT_AUTO_MERGE" not in plan + assert "gh variable set" not in plan def test_render_enablement_plan_warns_when_expected_check_is_not_discovered() -> None: @@ -258,41 +246,6 @@ def fake_github_request(method: str, url: str, token: str, payload=None, *, time assert warnings == ["No branch protection found for main."] -def test_discover_repository_variable_reads_current_value(monkeypatch) -> None: - def fake_github_request(method: str, url: str, token: str, payload=None, *, timeout=30): - assert method == "GET" - assert token == "token" - assert url.endswith("/actions/variables/CODEX_AUDIT_AUTO_MERGE") - return {"name": "CODEX_AUDIT_AUTO_MERGE", "value": "false"} - - monkeypatch.setattr(planner, "github_request", fake_github_request) - - value, warnings = discover_repository_variable( - api_url="https://api.github.com", - repo="QuantStrategyLab/UsEquitySnapshotPipelines", - token="token", - ) - - assert value == "false" - assert warnings == [] - - -def test_discover_repository_variable_reports_missing_variable(monkeypatch) -> None: - def fake_github_request(method: str, url: str, token: str, payload=None, *, timeout=30): - raise GitHubApiError(method, url, 404, "Not Found") - - monkeypatch.setattr(planner, "github_request", fake_github_request) - - value, warnings = discover_repository_variable( - api_url="https://api.github.com", - repo="QuantStrategyLab/UsEquitySnapshotPipelines", - token="token", - ) - - assert value is None - assert warnings == ["Repository variable CODEX_AUDIT_AUTO_MERGE is not set."] - - def test_discover_check_contexts_reports_branch_errors(monkeypatch) -> None: def fake_github_request(method: str, url: str, token: str, payload=None, *, timeout=30): raise GitHubApiError(method, url, 403, "Forbidden") @@ -308,3 +261,12 @@ def fake_github_request(method: str, url: str, token: str, payload=None, *, time assert contexts == [] assert warnings == ["Could not read branch main for check context discovery: HTTP 403"] + + +def test_planner_has_no_retired_workflow_or_auto_merge_enablement_cli(monkeypatch) -> None: + monkeypatch.setattr("sys.argv", ["planner", "--repo", "org/repo", "--branch", "main"]) + + args = planner.parse_args() + + assert not hasattr(args, "workflow_file") + assert not hasattr(args, "auto_merge")