Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions askcc/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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:
Expand Down
18 changes: 18 additions & 0 deletions askcc/functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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.

Expand Down
4 changes: 4 additions & 0 deletions askcc/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down
96 changes: 96 additions & 0 deletions tests/test_askcc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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"

Expand Down
Loading