From 5b87328538b761dea624eb921500d000d3efc326 Mon Sep 17 00:00:00 2001 From: shane Date: Mon, 13 Apr 2026 15:16:01 +0900 Subject: [PATCH] :sparkles: Transition issue after successful plan (#70) Add post-plan transition: swaps action:plan -> action:develop and moves project status to ready/todo, mirroring the existing prepare and develop transitions. No-op on failure or missing project board. --- askcc/cli.py | 4 ++ askcc/functions.py | 18 +++++++++ askcc/settings.py | 4 ++ tests/test_askcc.py | 96 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 122 insertions(+) diff --git a/askcc/cli.py b/askcc/cli.py index 47239a4..7d2e145 100644 --- a/askcc/cli.py +++ b/askcc/cli.py @@ -18,6 +18,7 @@ fetch_pr_content, install_skills, load_agent_config, + transition_issue_to_development, transition_issue_to_planning, transition_issue_to_review, validate_issue_labels, @@ -263,6 +264,9 @@ def main() -> None: # noqa: PLR0912, PLR0915, C901 if action == AgentAction.PREPARE and return_code == 0: transition_issue_to_planning(issue_url) + if action == AgentAction.PLAN and return_code == 0: + transition_issue_to_development(issue_url) + if action == AgentAction.DEVELOP and return_code == 0: verify_result = _run_project_verification(cwd) if verify_result.passed: diff --git a/askcc/functions.py b/askcc/functions.py index 980ac42..a5c9872 100644 --- a/askcc/functions.py +++ b/askcc/functions.py @@ -17,7 +17,9 @@ BLOCKING_LABELS, DEVELOP_LABEL, ENABLE_ISSUE_LABEL_PREFIX_VALIDATION, + PLAN_LABEL, PLANNING_STATUS_OPTIONS, + READY_STATUS_OPTIONS, REQUIRED_ISSUE_LABEL_PREFIXES, REVIEW_LABEL, REVIEW_STATUS_OPTIONS, @@ -729,6 +731,22 @@ def transition_issue_to_planning(github_issue_url: str) -> None: ) +def transition_issue_to_development(github_issue_url: str) -> None: + """Transition issue labels and project state after successful planning. + + Swaps action:plan -> action:develop and moves project status to ready/todo. + If action:plan is not present, action:develop is still added (the remove + step warns but does not raise). + All failures are logged as warnings, never raised. + """ + gh = _require_gh_cli() + owner, repo, issue_number = _parse_issue_url(github_issue_url) + repo_nwo = f"{owner}/{repo}" + + _swap_issue_labels(gh, repo_nwo, issue_number, remove=PLAN_LABEL, add=DEVELOP_LABEL) + _transition_project_fields(gh, owner, repo, issue_number, status_options=READY_STATUS_OPTIONS) + + def transition_issue_to_review(github_issue_url: str) -> None: """Transition issue labels and project state after successful PR creation. diff --git a/askcc/settings.py b/askcc/settings.py index 1d2e7ab..b38acf0 100644 --- a/askcc/settings.py +++ b/askcc/settings.py @@ -15,12 +15,16 @@ REQUIRED_ISSUE_LABEL_PREFIXES: tuple[str, ...] = ("action:",) # Label transition +PLAN_LABEL = "action:plan" DEVELOP_LABEL = "action:develop" REVIEW_LABEL = "action:review" # Prepare transition PLANNING_STATUS_OPTIONS: tuple[str, ...] = ("planning",) +# Plan transition (post-plan, ready for development) +READY_STATUS_OPTIONS: tuple[str, ...] = ("ready", "todo") + # Project field transition REVIEW_STATUS_OPTIONS: tuple[str, ...] = ("in-internal-review", "in-review") diff --git a/tests/test_askcc.py b/tests/test_askcc.py index b3bf0ac..f0acb61 100644 --- a/tests/test_askcc.py +++ b/tests/test_askcc.py @@ -35,6 +35,7 @@ install_skills, load_agent_config, load_template, + transition_issue_to_development, transition_issue_to_planning, transition_issue_to_review, validate_issue_readiness, @@ -1201,6 +1202,101 @@ def test_calls_add_label_and_project_transition(self): ) +class TestTransitionIssueToDevelopmentIntegration: + ISSUE_URL = "https://github.com/monkut/askcc-cli/issues/42" + + def test_calls_swap_labels_and_project_transition(self): + with ( + patch("askcc.functions.shutil.which", return_value="/usr/bin/gh"), + patch("askcc.functions._swap_issue_labels") as mock_swap, + patch("askcc.functions._transition_project_fields") as mock_project, + ): + transition_issue_to_development(self.ISSUE_URL) + + mock_swap.assert_called_once_with( + "/usr/bin/gh", + "monkut/askcc-cli", + 42, + remove="action:plan", + add="action:develop", + ) + mock_project.assert_called_once_with( + "/usr/bin/gh", + "monkut", + "askcc-cli", + 42, + status_options=("ready", "todo"), + ) + + def test_handles_no_project_cleanly(self): + empty_items = subprocess.CompletedProcess( + args=[], + returncode=0, + stdout=json.dumps({"data": {"repository": {"issue": {"projectItems": {"nodes": []}}}}}), + stderr="", + ) + ok = subprocess.CompletedProcess(args=[], returncode=0, stdout="", stderr="") + with ( + patch("askcc.functions.shutil.which", return_value="/usr/bin/gh"), + patch("askcc.functions.subprocess.run", side_effect=[ok, ok, empty_items]), + ): + transition_issue_to_development(self.ISSUE_URL) + + +class TestPlanCommand: + ISSUE_URL = "https://github.com/monkut/askcc-cli/issues/1" + + def test_plan_success_triggers_development_transition(self): + with ( + patch("askcc.cli.bootstrap_templates"), + patch("askcc.cli.validate_issue_labels", return_value=[]), + patch("askcc.cli.fetch_github_issue", return_value="issue body"), + patch("askcc.cli.get_runner", return_value=_mock_runner()), + patch("askcc.cli.transition_issue_to_development") as mock_transition, + patch("sys.argv", ["askcc", "plan", "-g", self.ISSUE_URL]), + pytest.raises(SystemExit), + ): + main() + + mock_transition.assert_called_once_with(self.ISSUE_URL) + + def test_plan_success_invokes_project_transition(self): + with ( + patch("askcc.cli.bootstrap_templates"), + patch("askcc.cli.validate_issue_labels", return_value=[]), + patch("askcc.cli.fetch_github_issue", return_value="issue body"), + patch("askcc.cli.get_runner", return_value=_mock_runner()), + patch("askcc.functions.shutil.which", return_value="/usr/bin/gh"), + patch("askcc.functions._swap_issue_labels"), + patch("askcc.functions._transition_project_fields") as mock_project, + patch("sys.argv", ["askcc", "plan", "-g", self.ISSUE_URL]), + pytest.raises(SystemExit), + ): + main() + + mock_project.assert_called_once_with( + "/usr/bin/gh", + "monkut", + "askcc-cli", + 1, + status_options=("ready", "todo"), + ) + + def test_plan_failure_skips_transition(self): + with ( + patch("askcc.cli.bootstrap_templates"), + patch("askcc.cli.validate_issue_labels", return_value=[]), + patch("askcc.cli.fetch_github_issue", return_value="issue body"), + patch("askcc.cli.get_runner", return_value=_mock_runner(1)), + patch("askcc.cli.transition_issue_to_development") as mock_transition, + patch("sys.argv", ["askcc", "plan", "-g", self.ISSUE_URL]), + pytest.raises(SystemExit), + ): + main() + + mock_transition.assert_not_called() + + class TestPrepareCommand: ISSUE_URL = "https://github.com/monkut/askcc-cli/issues/1"