diff --git a/.github/workflows/monthly_publish.yml b/.github/workflows/monthly_publish.yml index 15551d9..1907199 100644 --- a/.github/workflows/monthly_publish.yml +++ b/.github/workflows/monthly_publish.yml @@ -199,11 +199,14 @@ jobs: CryptoCodexAuditBridge permission-contents: write - - name: Trigger Self-hosted Codex Monthly Review - if: success() && env.PUBLISH_ENABLED != 'false' && env.SELFHOSTED_CODEX_REVIEW_ENABLED != 'false' + - name: Trigger Monthly Review Automation + if: success() && env.PUBLISH_ENABLED != 'false' env: APP_TOKEN: ${{ steps.codex_review_app_token.outputs.token }} + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} CODEX_AUDIT_DISPATCH_TOKEN: ${{ secrets.CODEX_AUDIT_DISPATCH_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_REPOSITORY: ${{ github.repository }} GITHUB_REF_NAME: ${{ github.ref_name }} ISSUE_NUMBER: ${{ steps.review_issue.outputs.issue_number }} @@ -211,94 +214,111 @@ jobs: TARGET_REPOSITORY: ${{ env.SELFHOSTED_CODEX_REVIEW_REPOSITORY }} REVIEW_MODE: ${{ env.SELFHOSTED_CODEX_REVIEW_MODE }} AUTO_MERGE: ${{ env.SELFHOSTED_CODEX_REVIEW_AUTO_MERGE }} + CODEX_REVIEW_ENABLED: ${{ env.SELFHOSTED_CODEX_REVIEW_ENABLED }} + LEGACY_API_REVIEW_ENABLED: ${{ env.LEGACY_AI_REVIEW_ENABLED }} run: | set -euo pipefail python - <<'PY' import json import os import re + import urllib.error import urllib.request - token = os.environ.get("APP_TOKEN", "").strip() or os.environ.get("CODEX_AUDIT_DISPATCH_TOKEN", "").strip() - if not token: - raise RuntimeError( - "Self-hosted Codex review dispatch requires either a GitHub App token " - "or CODEX_AUDIT_DISPATCH_TOKEN" + def enabled(name: str) -> bool: + return os.environ.get(name, "").strip().lower() == "true" + + def dispatch(token: str, url: str, payload: dict) -> int: + request = urllib.request.Request( + url, + data=json.dumps(payload).encode("utf-8"), + method="POST", + headers={ + "Authorization": f"Bearer {token}", + "Accept": "application/vnd.github+json", + "Content-Type": "application/json", + "X-GitHub-Api-Version": "2022-11-28", + "User-Agent": "crypto-leader-rotation-monthly-publish", + }, ) - target_repository = os.environ["TARGET_REPOSITORY"].strip() - if not re.fullmatch(r"[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+", target_repository): - raise RuntimeError(f"Invalid self-hosted review repository: {target_repository!r}") - mode = os.environ["REVIEW_MODE"].strip() or "review_and_fix" - if mode not in {"review_only", "review_and_fix"}: - raise RuntimeError(f"Unsupported self-hosted review mode: {mode}") + with urllib.request.urlopen(request) as response: + return response.status - payload = { - "event_type": "monthly-review-created", - "client_payload": { - "source_repo": os.environ["GITHUB_REPOSITORY"], - "source_ref": os.environ["GITHUB_REF_NAME"], - "issue_number": os.environ["ISSUE_NUMBER"], - "issue_url": os.environ["ISSUE_URL"], - "mode": mode, - "auto_merge": os.environ["AUTO_MERGE"].strip().lower() == "true", - }, - } - request = urllib.request.Request( - f"https://api.github.com/repos/{target_repository}/dispatches", - data=json.dumps(payload).encode("utf-8"), - method="POST", - headers={ - "Authorization": f"Bearer {token}", - "Accept": "application/vnd.github+json", - "Content-Type": "application/json", - "X-GitHub-Api-Version": "2022-11-28", - "User-Agent": "crypto-leader-rotation-monthly-publish", - }, - ) - with urllib.request.urlopen(request) as response: - if response.status not in (201, 204): - raise RuntimeError(f"Unexpected dispatch status: {response.status}") - print( - f"Dispatched self-hosted Codex review for issue #{os.environ['ISSUE_NUMBER']} " - f"to {target_repository}" - ) - PY + def dispatch_codex() -> None: + token = os.environ.get("APP_TOKEN", "").strip() or os.environ.get("CODEX_AUDIT_DISPATCH_TOKEN", "").strip() + if not token: + raise RuntimeError( + "Codex review dispatch requires either a GitHub App token or CODEX_AUDIT_DISPATCH_TOKEN" + ) + target_repository = os.environ["TARGET_REPOSITORY"].strip() + if not re.fullmatch(r"[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+", target_repository): + raise RuntimeError(f"Invalid Codex review repository: {target_repository!r}") + mode = os.environ["REVIEW_MODE"].strip() or "review_and_fix" + if mode not in {"review_only", "review_and_fix"}: + raise RuntimeError(f"Unsupported Codex review mode: {mode}") + payload = { + "event_type": "monthly-review-created", + "client_payload": { + "source_repo": os.environ["GITHUB_REPOSITORY"], + "source_ref": os.environ["GITHUB_REF_NAME"], + "issue_number": os.environ["ISSUE_NUMBER"], + "issue_url": os.environ["ISSUE_URL"], + "mode": mode, + "auto_merge": os.environ["AUTO_MERGE"].strip().lower() == "true", + }, + } + status = dispatch(token, f"https://api.github.com/repos/{target_repository}/dispatches", payload) + if status not in (201, 204): + raise RuntimeError(f"Unexpected Codex dispatch status: {status}") + print( + f"Dispatched CryptoCodexAuditBridge review for issue #{os.environ['ISSUE_NUMBER']} " + f"to {target_repository}" + ) - - name: Trigger Legacy AI Monthly Review - if: success() && env.PUBLISH_ENABLED != 'false' && env.LEGACY_AI_REVIEW_ENABLED == 'true' - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GITHUB_REPOSITORY: ${{ github.repository }} - GITHUB_REF_NAME: ${{ github.ref_name }} - ISSUE_NUMBER: ${{ steps.review_issue.outputs.issue_number }} - run: | - set -euo pipefail - python - <<'PY' - import json - import os - import urllib.request + def dispatch_legacy_api() -> None: + if not os.environ.get("ANTHROPIC_API_KEY", "").strip(): + raise RuntimeError("Legacy API review fallback is enabled but ANTHROPIC_API_KEY is not configured") + if not os.environ.get("OPENAI_API_KEY", "").strip(): + raise RuntimeError("Legacy API review fallback is enabled but OPENAI_API_KEY is not configured") + repository = os.environ["GITHUB_REPOSITORY"] + ref_name = os.environ["GITHUB_REF_NAME"] + issue_number = os.environ["ISSUE_NUMBER"] + payload = {"ref": ref_name, "inputs": {"issue_number": issue_number}} + status = dispatch( + os.environ["GITHUB_TOKEN"], + f"https://api.github.com/repos/{repository}/actions/workflows/ai_review.yml/dispatches", + payload, + ) + if status not in (201, 204): + raise RuntimeError(f"Unexpected legacy AI dispatch status: {status}") + print(f"Dispatched legacy API AI monthly review for issue #{issue_number} on ref {ref_name}") - token = os.environ["GITHUB_TOKEN"] - repository = os.environ["GITHUB_REPOSITORY"] - ref_name = os.environ["GITHUB_REF_NAME"] - issue_number = os.environ["ISSUE_NUMBER"] - payload = {"ref": ref_name, "inputs": {"issue_number": issue_number}} - request = urllib.request.Request( - f"https://api.github.com/repos/{repository}/actions/workflows/ai_review.yml/dispatches", - data=json.dumps(payload).encode("utf-8"), - method="POST", - headers={ - "Authorization": f"Bearer {token}", - "Accept": "application/vnd.github+json", - "Content-Type": "application/json", - "X-GitHub-Api-Version": "2022-11-28", - "User-Agent": "crypto-leader-rotation-monthly-publish", - }, + codex_error = "" + if enabled("CODEX_REVIEW_ENABLED"): + try: + dispatch_codex() + raise SystemExit(0) + except (RuntimeError, urllib.error.URLError, urllib.error.HTTPError) as exc: + codex_error = str(exc) + print(f"Codex monthly review dispatch failed: {codex_error}") + else: + codex_error = "Codex monthly review is disabled" + print(codex_error) + + if enabled("LEGACY_API_REVIEW_ENABLED"): + try: + dispatch_legacy_api() + raise SystemExit(0) + except (RuntimeError, urllib.error.URLError, urllib.error.HTTPError) as exc: + raise RuntimeError( + f"Codex monthly review failed ({codex_error}); legacy API review fallback also failed: {exc}" + ) from exc + + raise RuntimeError( + "Codex monthly review failed or was disabled, and legacy API fallback is disabled. " + "Set LEGACY_AI_REVIEW_ENABLED=true and configure ANTHROPIC_API_KEY plus OPENAI_API_KEY " + f"to enable the API fallback. Codex failure: {codex_error}" ) - with urllib.request.urlopen(request) as response: - if response.status not in (201, 204): - raise RuntimeError(f"Unexpected dispatch status: {response.status}") - print(f"Triggered ai_review.yml for issue #{issue_number} on ref {ref_name}") PY - name: Write Release Heartbeat diff --git a/README.md b/README.md index 97933da..f7b7563 100644 --- a/README.md +++ b/README.md @@ -533,7 +533,9 @@ Behavior: ## Automated AI Monthly Review -After the monthly report bundle is assembled, the workflow automatically creates a GitHub Issue containing the full `ai_review_input.md` content. A separate workflow (`ai_review.yml`) listens for issues labeled `monthly-review` and triggers Claude Code Action (Anthropic API, Sonnet model) to analyze the report. +After the monthly report bundle is assembled, the workflow creates a GitHub Issue containing the full `ai_review_input.md` content. The default automated review route dispatches `QuantStrategyLab/CryptoCodexAuditBridge`, which runs Codex on a self-hosted VPS runner. Codex reads the monthly issue, posts the audit result back to the issue, and opens a PR directly for safe low-risk fixes. + +The legacy API-based dual AI review remains available as a compatibility fallback. Set `LEGACY_AI_REVIEW_ENABLED=true` and configure both `ANTHROPIC_API_KEY` and `OPENAI_API_KEY` to allow the monthly workflow to dispatch `ai_review.yml` if the Codex bridge dispatch fails. When Codex dispatch fails and the legacy API fallback is not enabled or not configured, the monthly publish workflow fails loudly instead of silently skipping review. The AI review covers: @@ -541,23 +543,26 @@ The AI review covers: - **Anomaly detection**: flags unexpected warnings, stale artifacts, validation failures, or suspicious ranking scores - **Downstream impact**: notes implications for BinancePlatform (the downstream execution engine), including pool changes and degradation risk - **Operator action items**: summarizes the checklist and adds any AI-identified follow-up items -- **Code improvements**: structured review output feeds the monthly optimization planner; concrete low-risk `auto-pr-safe` tasks for `CryptoSnapshotPipelines` are queued to the self-hosted ccbot/Codex runner with `codex-bridge`, while sensitive selector changes remain manual-review work +- **Code improvements**: Codex can open focused PRs directly for low-risk reporting, validation, workflow, test, or documentation defects; sensitive selector changes remain manual-review work -All analysis is posted in both English and Chinese. +Review output is posted back to the monthly issue. The legacy API workflow still renders bilingual output when enabled. -### Required GitHub Secret +### Optional Legacy API Fallback Secrets - `ANTHROPIC_API_KEY`: Anthropic API key for Claude Code Action - `OPENAI_API_KEY`: OpenAI API key for the secondary monthly review +The default production configuration does not need these API secrets because it uses `CryptoCodexAuditBridge`. Configure them only if you want the legacy dual AI fallback. + Setup: ```bash +gh variable set LEGACY_AI_REVIEW_ENABLED --body true gh secret set ANTHROPIC_API_KEY --body "sk-ant-..." gh secret set OPENAI_API_KEY --body "sk-..." ``` -The AI review workflow runs on `ubuntu-latest` (no self-hosted runner required) and costs approximately $0.01-0.05 per monthly run. Code remediation is a separate phase: repo-scoped low-risk tasks are created as GitHub issues, and safe `CryptoSnapshotPipelines` tasks are handed to the VPS ccbot/Codex bridge instead of GitHub-hosted Claude Action. +The legacy AI review workflow runs on `ubuntu-latest` (no self-hosted runner required) and costs approximately $0.01-0.05 per monthly run. It is retained for open-source compatibility and emergency fallback, not as the normal production path. ### Codex Remediation and Auto-Merge Gate @@ -1018,7 +1023,7 @@ Practical review file selection: Automated AI handoff: -The workflow now automatically creates a GitHub Issue with the `monthly-review` label, which triggers Claude Code Action to analyze the report. See the "Automated AI Monthly Review" section for details. +The workflow automatically creates a GitHub Issue with the `monthly-review` label, then dispatches `CryptoCodexAuditBridge`. If the Codex bridge dispatch fails and `LEGACY_AI_REVIEW_ENABLED=true` with both API secrets configured, the workflow falls back to the legacy dual AI `ai_review.yml`; otherwise it fails loudly. See the "Automated AI Monthly Review" section for details. Manual AI handoff (fallback): diff --git a/docs/operator_runbook.md b/docs/operator_runbook.md index e6052c8..d8dd020 100644 --- a/docs/operator_runbook.md +++ b/docs/operator_runbook.md @@ -76,7 +76,11 @@ Boundary rules: ## Monthly Codex Remediation -The monthly optimization planner may create repo-scoped follow-up issues after AI review. For `CryptoSnapshotPipelines`, low-risk non-experiment tasks marked `[auto-pr-safe]` are queued to the self-hosted VPS ccbot/Codex runner with the `codex-bridge` label. GitHub-hosted Claude Action remains only a manual-dispatch fallback; normal automated code remediation should run through ccbot/Codex. +The monthly publish workflow creates a `monthly-review` issue, then dispatches `CryptoCodexAuditBridge` as the primary automated review and remediation path. Codex posts its audit result back to the issue and may open a focused PR directly for low-risk reporting, validation, workflow, test, or documentation fixes. + +The legacy Claude/OpenAI dual review workflow remains only as a compatibility fallback. Enable it with `LEGACY_AI_REVIEW_ENABLED=true` and configure both `ANTHROPIC_API_KEY` and `OPENAI_API_KEY`. If Codex dispatch fails and the legacy API fallback is disabled or missing credentials, the monthly publish workflow fails loudly. + +The monthly optimization planner may still create repo-scoped follow-up issues after AI review. For `CryptoSnapshotPipelines`, low-risk non-experiment tasks marked `[auto-pr-safe]` are queued to the self-hosted VPS ccbot/Codex runner with the `codex-bridge` label. GitHub-hosted Claude Action remains only a manual-dispatch fallback; normal automated code remediation should run through ccbot/Codex. Codex remediation PRs must use branch `codex/monthly-optimization-issue-`, include `` in the PR body, and start as draft. The auto-merge workflow only merges after CI passes, the PR is ready for review, `auto-merge-ok` is present, task-level auto-merge eligibility is recorded, and changed files stay outside guarded selector/config paths. diff --git a/tests/test_monthly_publish_workflow_config.py b/tests/test_monthly_publish_workflow_config.py index 642ad92..5e46d3d 100644 --- a/tests/test_monthly_publish_workflow_config.py +++ b/tests/test_monthly_publish_workflow_config.py @@ -44,10 +44,16 @@ def test_monthly_review_issue_creation_does_not_require_gh_cli(self) -> None: self.assertIn("CryptoCodexAuditBridge", workflow) self.assertIn("permission-contents: write", workflow) self.assertIn("APP_TOKEN", workflow) + self.assertIn("Trigger Monthly Review Automation", workflow) self.assertIn("CODEX_AUDIT_DISPATCH_TOKEN", workflow) + self.assertIn("ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}", workflow) + self.assertIn("OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}", workflow) + self.assertIn("Codex monthly review dispatch failed", workflow) + self.assertIn("legacy API review fallback", workflow) + self.assertIn("legacy API fallback is disabled", workflow) self.assertIn("monthly-review-created", workflow) self.assertIn("/repos/{target_repository}/dispatches", workflow) - self.assertIn("LEGACY_AI_REVIEW_ENABLED == 'true'", workflow) + self.assertIn("LEGACY_API_REVIEW_ENABLED", workflow) self.assertIn("/actions/workflows/ai_review.yml/dispatches", workflow) def test_ai_review_workflow_supports_dispatch_and_comment_posting(self) -> None: