diff --git a/.github/workflows/auto_merge_optimization_pr.yml b/.github/workflows/auto_merge_optimization_pr.yml deleted file mode 100644 index 18d4620..0000000 --- a/.github/workflows/auto_merge_optimization_pr.yml +++ /dev/null @@ -1,94 +0,0 @@ -name: Auto Merge Optimization PR - -"on": - workflow_run: - workflows: ["CI"] - types: [completed] - -jobs: - auto-merge: - if: github.event.workflow_run.conclusion == 'success' && startsWith(github.event.workflow_run.head_branch, 'automation/monthly-optimization-issue-') - runs-on: ubuntu-latest - permissions: - contents: write - pull-requests: write - - steps: - - name: Checkout - uses: actions/checkout@v6 - - - name: Resolve automation PR - id: pr - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - mkdir -p data/output/auto_merge - BRANCH_NAME="${{ github.event.workflow_run.head_branch }}" - PR_NUMBER=$(gh pr list --state open --head "${BRANCH_NAME}" --json number --jq '.[0].number // empty') - if [ -z "${PR_NUMBER}" ]; then - echo "No open automation PR found for ${BRANCH_NAME}." >> "$GITHUB_STEP_SUMMARY" - exit 0 - fi - echo "pr_number=${PR_NUMBER}" >> "$GITHUB_OUTPUT" - - - name: Evaluate merge eligibility - id: merge_guard - if: steps.pr.outputs.pr_number != '' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - gh pr view "${{ steps.pr.outputs.pr_number }}" --json number,isDraft,body,url,files > data/output/auto_merge/pr.json - python3 - <<'PY' - import json - import os - from pathlib import Path - - from scripts.prepare_auto_optimization_pr import evaluate_changed_files - - pr = json.loads(Path("data/output/auto_merge/pr.json").read_text(encoding="utf-8")) - body = pr.get("body") or "" - changed_files = [item.get("path", "") for item in pr.get("files", [])] - guard = evaluate_changed_files(changed_files) - has_marker = "", - "## Summary", - "This PR was generated from a monthly optimization task issue.", - "It only targets low-risk items explicitly marked `[auto-pr-safe]`.", - "", - "## Merge Policy", - f"- Task-level auto-merge eligible: `{'yes' if payload['task_level_auto_merge_allowed'] else 'no'}`", - "- High-risk guardrails remain active for selector logic, live execution, and shared strategy code paths.", - "", - "## Auto-selected tasks", - ] - for action in payload["safe_actions"]: - lines.append(f"- {action['title']}: {action.get('summary', 'No summary provided.')}") - if payload["draft_only_actions"]: - lines.extend(["", "## Draft-only tasks"]) - for action in payload["draft_only_actions"]: - lines.append(f"- {action['title']}: {action.get('auto_merge_blocker', 'guarded_task')}") - lines.extend(["", f"Refs #{payload['issue_number']}"]) - return "\n".join(lines).strip() + "\n" - - -def parse_args() -> argparse.Namespace: - parser = argparse.ArgumentParser(description="Prepare metadata for auto-generated optimization PRs.") - parser.add_argument("--issue-context-file", required=True, type=Path) - parser.add_argument("--output-dir", required=True, type=Path) - return parser.parse_args() - - -def main() -> int: - args = parse_args() - issue_context = json.loads(args.issue_context_file.read_text(encoding="utf-8")) - payload = build_payload(issue_context) - args.output_dir.mkdir(parents=True, exist_ok=True) - payload_file = args.output_dir / "payload.json" - task_summary_file = args.output_dir / "task_summary.md" - pr_body_file = args.output_dir / "pr_body.md" - payload_file.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") - task_summary_file.write_text(render_task_summary(payload), encoding="utf-8") - pr_body_file.write_text(render_pr_body(payload), encoding="utf-8") - print(f"should_run={'true' if payload['should_run'] else 'false'}") - print(f"issue_number={payload['issue_number']}") - print(f"branch_name={payload['branch_name']}") - print(f"commit_message={payload['commit_message']}") - print(f"pr_title={payload['pr_title']}") - print(f"safe_task_count={payload['safe_task_count']}") - print(f"auto_merge_candidate_count={payload['auto_merge_candidate_count']}") - print(f"task_level_auto_merge_allowed={'true' if payload['task_level_auto_merge_allowed'] else 'false'}") - print(f"payload_file={payload_file}") - print(f"task_summary_file={task_summary_file}") - print(f"pr_body_file={pr_body_file}") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/tests/test_auto_optimization_pr_workflow_config.py b/tests/test_auto_optimization_pr_workflow_config.py deleted file mode 100644 index 2bf0525..0000000 --- a/tests/test_auto_optimization_pr_workflow_config.py +++ /dev/null @@ -1,60 +0,0 @@ -from __future__ import annotations - -import unittest -from pathlib import Path - - -PROJECT_ROOT = Path(__file__).resolve().parents[1] -AUTO_WORKFLOW = PROJECT_ROOT / ".github" / "workflows" / "auto_optimization_pr.yml" -MERGE_WORKFLOW = PROJECT_ROOT / ".github" / "workflows" / "auto_merge_optimization_pr.yml" -CI_WORKFLOW = PROJECT_ROOT / ".github" / "workflows" / "ci.yml" - - -class AutoOptimizationPrWorkflowConfigTests(unittest.TestCase): - def test_auto_optimization_workflow_handles_monthly_task_issues(self) -> None: - workflow = AUTO_WORKFLOW.read_text(encoding="utf-8") - - self.assertIn("issues:", workflow) - self.assertIn("monthly-optimization-task", workflow) - self.assertIn("workflow_dispatch:", workflow) - self.assertIn("issue_number:", workflow) - self.assertIn("actions: write", workflow) - self.assertIn("contents: write", workflow) - self.assertIn("pull-requests: write", workflow) - self.assertIn("ANTHROPIC_API_KEY", workflow) - self.assertIn("prepare_auto_optimization_pr.py", workflow) - self.assertIn("anthropics/claude-code-action@v1", workflow) - self.assertIn("Export selected task summary", workflow) - self.assertIn("claude_args: --max-turns 8", workflow) - self.assertIn("timeout-minutes: 15", workflow) - self.assertIn("steps.selected_tasks.outputs.task_summary", workflow) - self.assertIn("Do not use Bash in this workflow.", workflow) - self.assertIn("The workflow will run CI after the PR is created.", workflow) - self.assertIn("Evaluate merge guardrails", workflow) - self.assertIn("task_level_auto_merge_allowed", workflow) - self.assertIn("gh pr ready", workflow) - self.assertIn("steps.merge_guard.outputs.merge_ready", workflow) - self.assertIn("gh workflow run ci.yml", workflow) - self.assertIn("fetch-depth: 0", workflow) - self.assertIn('FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"', workflow) - self.assertIn("You are working inside CryptoStrategies, the shared strategy-logic repository.", workflow) - self.assertIn("Never edit files under src/ in this automation step.", workflow) - - def test_auto_merge_workflow_waits_for_ci_and_merges_only_safe_ready_prs(self) -> None: - workflow = MERGE_WORKFLOW.read_text(encoding="utf-8") - - self.assertIn("workflow_run:", workflow) - self.assertIn('workflows: ["CI"]', workflow) - self.assertIn("automation/monthly-optimization-issue-", workflow) - self.assertIn("gh pr view", workflow) - self.assertIn("evaluate_changed_files", workflow) - self.assertIn("Task-level auto-merge eligible: `yes`", workflow) - self.assertIn("gh pr merge", workflow) - - def test_ci_workflow_supports_manual_dispatch(self) -> None: - workflow = CI_WORKFLOW.read_text(encoding="utf-8") - self.assertIn("workflow_dispatch:", workflow) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_prepare_auto_optimization_pr.py b/tests/test_prepare_auto_optimization_pr.py deleted file mode 100644 index 7b5f1ce..0000000 --- a/tests/test_prepare_auto_optimization_pr.py +++ /dev/null @@ -1,108 +0,0 @@ -from __future__ import annotations - -import tempfile -import unittest -from pathlib import Path - -from scripts.prepare_auto_optimization_pr import build_payload, evaluate_changed_files, parse_actions, render_pr_body - - -class PrepareAutoOptimizationPrTests(unittest.TestCase): - def setUp(self) -> None: - self.issue_context = { - "number": 15, - "title": "Monthly Optimization Tasks · Sandbox", - "body": """# Monthly Optimization Tasks · Sandbox - -## Actions -- [ ] `high` Investigate an upstream issue - - Summary: Manual follow-up only. - - Source: [Sandbox #0](https://example.com/issues/0) -- [ ] `low` Add a short README note [auto-pr-safe] - - Summary: Document a small operator-facing behavior. - - Source: [Sandbox #1](https://example.com/issues/1) -- [ ] `low` Add a boundary tracker [auto-pr-safe, experiment-only] - - Summary: Track near-cutoff symbols monthly. - - Source: [Sandbox #2](https://example.com/issues/2) -""", - } - - def test_parse_actions_preserves_risk_flags_and_source(self) -> None: - actions = parse_actions(self.issue_context["body"]) - - self.assertEqual(len(actions), 3) - self.assertEqual(actions[0]["risk_level"], "high") - self.assertEqual(actions[1]["flags"], ["auto-pr-safe"]) - self.assertEqual(actions[2]["flags"], ["auto-pr-safe", "experiment-only"]) - self.assertEqual(actions[2]["source_label"], "Sandbox #2") - - def test_build_payload_selects_readme_note_as_auto_merge_candidate(self) -> None: - with tempfile.TemporaryDirectory() as temp_dir: - repo_root = Path(temp_dir) / "CryptoStrategies" - repo_root.mkdir() - payload = build_payload(self.issue_context, repo_root=repo_root) - - self.assertTrue(payload["should_run"]) - self.assertEqual(payload["safe_task_count"], 1) - self.assertEqual(payload["auto_merge_candidate_count"], 1) - self.assertEqual(payload["draft_only_task_count"], 0) - self.assertTrue(payload["task_level_auto_merge_allowed"]) - self.assertEqual(payload["branch_name"], "automation/monthly-optimization-issue-15") - self.assertEqual(payload["safe_actions"][0]["title"], "Add a short README note") - - def test_build_payload_keeps_shared_strategy_logic_tasks_as_draft_only(self) -> None: - issue_context = { - "number": 18, - "title": "Monthly Optimization Tasks · Sandbox", - "body": """# Monthly Optimization Tasks · Sandbox - -## Actions -- [ ] `low` Document strategy signal thresholds [auto-pr-safe] - - Summary: Document the strategy threshold handling for shared strategy modules. - - Source: [Sandbox #3](https://example.com/issues/3) -""", - } - with tempfile.TemporaryDirectory() as temp_dir: - repo_root = Path(temp_dir) / "CryptoStrategies" - repo_root.mkdir() - payload = build_payload(issue_context, repo_root=repo_root) - - self.assertTrue(payload["should_run"]) - self.assertEqual(payload["auto_merge_candidate_count"], 0) - self.assertEqual(payload["draft_only_task_count"], 1) - self.assertFalse(payload["task_level_auto_merge_allowed"]) - self.assertIn("guarded_keyword:strategy", payload["draft_only_actions"][0]["auto_merge_blocker"]) - - def test_evaluate_changed_files_blocks_shared_strategy_paths(self) -> None: - with tempfile.TemporaryDirectory() as temp_dir: - repo_root = Path(temp_dir) - allowed = evaluate_changed_files(["README.md", "tests/test_catalog.py"], repo_root=repo_root) - repo_root = Path('/Users/lisiyi/Projects/CryptoStrategies') - blocked = evaluate_changed_files(["src/crypto_strategies/catalog.py", "README.md"], repo_root=repo_root) - - self.assertTrue(allowed["allowed"]) - self.assertFalse(blocked["allowed"]) - self.assertEqual(blocked["blocked_files"], ["src/crypto_strategies/catalog.py"]) - - def test_evaluate_changed_files_blocks_binance_runtime_boundary_paths(self) -> None: - with tempfile.TemporaryDirectory() as temp_dir: - repo_root = Path(temp_dir) / "BinancePlatform" - repo_root.mkdir() - blocked = evaluate_changed_files(["strategy_runtime.py", "decision_mapper.py"], repo_root=repo_root) - - self.assertFalse(blocked["allowed"]) - self.assertEqual(blocked["blocked_files"], ["strategy_runtime.py", "decision_mapper.py"]) - - def test_render_pr_body_contains_merge_policy_and_issue_reference(self) -> None: - with tempfile.TemporaryDirectory() as temp_dir: - payload = build_payload(self.issue_context, repo_root=Path(temp_dir)) - body = render_pr_body(payload) - - self.assertIn("", body) - self.assertIn("Task-level auto-merge eligible: `yes`", body) - self.assertIn("Add a short README note", body) - self.assertIn("Refs #15", body) - - -if __name__ == "__main__": - unittest.main()