@pennyfarthing/core 7.8.1 → 7.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -1
- package/pennyfarthing-dist/scripts/core/prime.sh +8 -0
- package/pennyfarthing_scripts/__init__.py +17 -0
- package/pennyfarthing_scripts/__pycache__/__init__.cpython-311.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/config.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/jira.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/jira_epic_creation.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/jira_sync.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/jira_sync_story.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/sprint.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/workflow.cpython-311.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/workflow.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bellmode_hook.py +154 -0
- package/pennyfarthing_scripts/brownfield/__init__.py +35 -0
- package/pennyfarthing_scripts/brownfield/__main__.py +7 -0
- package/pennyfarthing_scripts/brownfield/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/brownfield/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/brownfield/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/brownfield/__pycache__/discover.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/brownfield/cli.py +131 -0
- package/pennyfarthing_scripts/brownfield/discover.py +753 -0
- package/pennyfarthing_scripts/common/__init__.py +49 -0
- package/pennyfarthing_scripts/common/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/common/__pycache__/config.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/common/__pycache__/output.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/common/config.py +65 -0
- package/pennyfarthing_scripts/common/output.py +180 -0
- package/pennyfarthing_scripts/config.py +21 -0
- package/pennyfarthing_scripts/git/__init__.py +29 -0
- package/pennyfarthing_scripts/git/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/git/__pycache__/create_branches.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/git/__pycache__/status_all.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/git/create_branches.py +439 -0
- package/pennyfarthing_scripts/git/status_all.py +310 -0
- package/pennyfarthing_scripts/hooks.py +455 -0
- package/pennyfarthing_scripts/jira/__init__.py +93 -0
- package/pennyfarthing_scripts/jira/__main__.py +10 -0
- package/pennyfarthing_scripts/jira/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/bidirectional.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/claim.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/client.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/compat.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/epic.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/mappings.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/models.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/story.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/sync.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/bidirectional.py +561 -0
- package/pennyfarthing_scripts/jira/claim.py +211 -0
- package/pennyfarthing_scripts/jira/cli.py +150 -0
- package/pennyfarthing_scripts/jira/client.py +613 -0
- package/pennyfarthing_scripts/jira/epic.py +176 -0
- package/pennyfarthing_scripts/jira/story.py +219 -0
- package/pennyfarthing_scripts/jira/sync.py +350 -0
- package/pennyfarthing_scripts/jira_bidirectional_sync.py +37 -0
- package/pennyfarthing_scripts/jira_epic_creation.py +30 -0
- package/pennyfarthing_scripts/jira_sync.py +36 -0
- package/pennyfarthing_scripts/jira_sync_story.py +30 -0
- package/pennyfarthing_scripts/output.py +37 -0
- package/pennyfarthing_scripts/preflight/__init__.py +17 -0
- package/pennyfarthing_scripts/preflight/__main__.py +10 -0
- package/pennyfarthing_scripts/preflight/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/preflight/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/preflight/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/preflight/__pycache__/finish.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/preflight/cli.py +141 -0
- package/pennyfarthing_scripts/preflight/finish.py +382 -0
- package/pennyfarthing_scripts/pretooluse_hook.py +142 -0
- package/pennyfarthing_scripts/prime/__init__.py +38 -0
- package/pennyfarthing_scripts/prime/__main__.py +8 -0
- package/pennyfarthing_scripts/prime/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/__pycache__/loader.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/__pycache__/models.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/__pycache__/persona.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/__pycache__/session.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/__pycache__/workflow.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/cli.py +220 -0
- package/pennyfarthing_scripts/prime/loader.py +239 -0
- package/pennyfarthing_scripts/sprint/__init__.py +66 -0
- package/pennyfarthing_scripts/sprint/__main__.py +10 -0
- package/pennyfarthing_scripts/sprint/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/archive.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/loader.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/status.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/validator.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/work.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/archive.py +108 -0
- package/pennyfarthing_scripts/sprint/cli.py +124 -0
- package/pennyfarthing_scripts/sprint/loader.py +193 -0
- package/pennyfarthing_scripts/sprint/status.py +122 -0
- package/pennyfarthing_scripts/sprint/validator.py +405 -0
- package/pennyfarthing_scripts/sprint/work.py +192 -0
- package/pennyfarthing_scripts/story/__init__.py +67 -0
- package/pennyfarthing_scripts/story/__main__.py +10 -0
- package/pennyfarthing_scripts/story/cli.py +105 -0
- package/pennyfarthing_scripts/story/create.py +167 -0
- package/pennyfarthing_scripts/story/size.py +113 -0
- package/pennyfarthing_scripts/story/template.py +151 -0
- package/pennyfarthing_scripts/swebench.py +216 -0
- package/pennyfarthing_scripts/tests/__init__.py +1 -0
- package/pennyfarthing_scripts/tests/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/conftest.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_brownfield.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_git_utils.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_prime.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_sprint_validator.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/conftest.py +106 -0
- package/pennyfarthing_scripts/tests/test_brownfield.py +842 -0
- package/pennyfarthing_scripts/tests/test_cli_modules.py +245 -0
- package/pennyfarthing_scripts/tests/test_common.py +180 -0
- package/pennyfarthing_scripts/tests/test_git_utils.py +866 -0
- package/pennyfarthing_scripts/tests/test_jira_package.py +334 -0
- package/pennyfarthing_scripts/tests/test_package_structure.py +372 -0
- package/pennyfarthing_scripts/tests/test_prime.py +397 -0
- package/pennyfarthing_scripts/tests/test_sprint_package.py +236 -0
- package/pennyfarthing_scripts/tests/test_sprint_validator.py +675 -0
- package/pennyfarthing_scripts/tests/test_story_package.py +156 -0
- package/pennyfarthing_scripts/welcome_hook.py +157 -0
- package/pennyfarthing_scripts/workflow.py +183 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Create Jira epics from Pennyfarthing sprint YAML definitions.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
python -m pennyfarthing_scripts.jira create epic <epic_id> [options]
|
|
6
|
+
|
|
7
|
+
Options:
|
|
8
|
+
--dry-run Show what would be done without making changes
|
|
9
|
+
|
|
10
|
+
Examples:
|
|
11
|
+
python -m pennyfarthing_scripts.jira create epic epic-63 --dry-run
|
|
12
|
+
python -m pennyfarthing_scripts.jira create epic 63
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import argparse
|
|
16
|
+
import json
|
|
17
|
+
import sys
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
from pennyfarthing_scripts.jira.client import JiraClient, JIRA_PROJECT
|
|
21
|
+
from pennyfarthing_scripts.sprint.loader import find_epic, load_sprint as load_current_sprint
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def parse_args(args: list[str] | None = None) -> argparse.Namespace:
|
|
25
|
+
"""Parse command line arguments.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
args: Command line arguments (defaults to sys.argv[1:])
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
Parsed arguments namespace
|
|
32
|
+
"""
|
|
33
|
+
parser = argparse.ArgumentParser(
|
|
34
|
+
description="Create Jira epic from Pennyfarthing sprint YAML"
|
|
35
|
+
)
|
|
36
|
+
parser.add_argument("epic_id", help="Epic ID (e.g., epic-63 or 63)")
|
|
37
|
+
parser.add_argument("--dry-run", action="store_true", help="Show changes without applying")
|
|
38
|
+
|
|
39
|
+
return parser.parse_args(args)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def build_epic_payload(epic_data: dict[str, Any]) -> dict[str, Any]:
|
|
43
|
+
"""Build Jira API payload for creating an epic.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
epic_data: Epic data from sprint YAML
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
Jira API request payload
|
|
50
|
+
"""
|
|
51
|
+
title = epic_data.get("title", "")
|
|
52
|
+
description = epic_data.get("description", "")
|
|
53
|
+
|
|
54
|
+
# Build ADF (Atlassian Document Format) for description
|
|
55
|
+
description_adf = {
|
|
56
|
+
"type": "doc",
|
|
57
|
+
"version": 1,
|
|
58
|
+
"content": [
|
|
59
|
+
{
|
|
60
|
+
"type": "paragraph",
|
|
61
|
+
"content": [{"type": "text", "text": description}] if description else [],
|
|
62
|
+
}
|
|
63
|
+
],
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
"fields": {
|
|
68
|
+
"project": {"key": JIRA_PROJECT},
|
|
69
|
+
"summary": title,
|
|
70
|
+
"description": description_adf,
|
|
71
|
+
"issuetype": {"name": "Epic"},
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def create_epic(
|
|
77
|
+
title: str,
|
|
78
|
+
description: str = "",
|
|
79
|
+
*,
|
|
80
|
+
dry_run: bool = False,
|
|
81
|
+
) -> dict[str, Any]:
|
|
82
|
+
"""Create a Jira epic.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
title: Epic title/summary
|
|
86
|
+
description: Epic description
|
|
87
|
+
dry_run: If True, show changes without applying
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
Result dict with success, key, error fields
|
|
91
|
+
"""
|
|
92
|
+
epic_data = {"title": title, "description": description}
|
|
93
|
+
payload = build_epic_payload(epic_data)
|
|
94
|
+
|
|
95
|
+
if dry_run:
|
|
96
|
+
return {
|
|
97
|
+
"success": True,
|
|
98
|
+
"dry_run": True,
|
|
99
|
+
"payload": payload,
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
client = JiraClient()
|
|
103
|
+
response = client.create_issue_sync(payload)
|
|
104
|
+
|
|
105
|
+
if response and "key" in response:
|
|
106
|
+
return {
|
|
107
|
+
"success": True,
|
|
108
|
+
"key": response["key"],
|
|
109
|
+
"id": response.get("id"),
|
|
110
|
+
}
|
|
111
|
+
else:
|
|
112
|
+
return {
|
|
113
|
+
"success": False,
|
|
114
|
+
"error": "Failed to create epic",
|
|
115
|
+
"response": response,
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def create_epic_from_yaml(epic_id: str, *, dry_run: bool = False) -> dict[str, Any]:
|
|
120
|
+
"""Create a Jira epic from sprint YAML definition.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
epic_id: Epic ID (e.g., "epic-63" or "63")
|
|
124
|
+
dry_run: If True, show changes without applying
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
Result dict with success, key, error fields
|
|
128
|
+
"""
|
|
129
|
+
sprint_data = load_current_sprint()
|
|
130
|
+
if not sprint_data:
|
|
131
|
+
return {"success": False, "error": "Could not load sprint YAML"}
|
|
132
|
+
|
|
133
|
+
epic = find_epic(sprint_data, epic_id)
|
|
134
|
+
if not epic:
|
|
135
|
+
return {"success": False, "error": f"Epic '{epic_id}' not found in sprint YAML"}
|
|
136
|
+
|
|
137
|
+
# Check if epic already has a Jira key
|
|
138
|
+
if epic.get("jira"):
|
|
139
|
+
return {
|
|
140
|
+
"success": False,
|
|
141
|
+
"error": f"Epic already has Jira key: {epic['jira']}",
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
title = epic.get("title", f"Epic {epic_id}")
|
|
145
|
+
description = epic.get("description", "")
|
|
146
|
+
|
|
147
|
+
return create_epic(title, description, dry_run=dry_run)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def main(args: list[str] | None = None) -> int:
|
|
151
|
+
"""CLI entry point.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
args: Command line arguments (defaults to sys.argv[1:])
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
Exit code (0 for success, 1 for failure)
|
|
158
|
+
"""
|
|
159
|
+
parsed_args = parse_args(args)
|
|
160
|
+
|
|
161
|
+
result = create_epic_from_yaml(parsed_args.epic_id, dry_run=parsed_args.dry_run)
|
|
162
|
+
|
|
163
|
+
if result["success"]:
|
|
164
|
+
if result.get("dry_run"):
|
|
165
|
+
print(f"[DRY-RUN] Would create epic for {parsed_args.epic_id}")
|
|
166
|
+
print(f" Payload: {json.dumps(result.get('payload', {}), indent=2)}")
|
|
167
|
+
else:
|
|
168
|
+
print(f"Created epic: {result.get('key')}")
|
|
169
|
+
return 0
|
|
170
|
+
else:
|
|
171
|
+
print(f"Failed: {result.get('error', 'Unknown error')}", file=sys.stderr)
|
|
172
|
+
return 1
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
if __name__ == "__main__":
|
|
176
|
+
sys.exit(main())
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Sync a single story between Pennyfarthing sprint YAML and Jira.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
python -m pennyfarthing_scripts.jira story <story_key> [options]
|
|
6
|
+
|
|
7
|
+
Options:
|
|
8
|
+
--transition Sync status (transition Jira issue)
|
|
9
|
+
--points Sync story points
|
|
10
|
+
--comment MSG Add comment to issue
|
|
11
|
+
--dry-run Show what would be done without making changes
|
|
12
|
+
|
|
13
|
+
Examples:
|
|
14
|
+
python -m pennyfarthing_scripts.jira story 63-7 --transition
|
|
15
|
+
python -m pennyfarthing_scripts.jira story MSSCI-12401 --points --dry-run
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import argparse
|
|
19
|
+
import sys
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
from pennyfarthing_scripts.jira import client as jira_client
|
|
23
|
+
from pennyfarthing_scripts.sprint.loader import (
|
|
24
|
+
find_epic,
|
|
25
|
+
find_story,
|
|
26
|
+
get_story_by_id,
|
|
27
|
+
load_sprint as load_current_sprint,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def parse_args(args: list[str] | None = None) -> argparse.Namespace:
|
|
32
|
+
"""Parse command line arguments.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
args: Command line arguments (defaults to sys.argv[1:])
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
Parsed arguments namespace
|
|
39
|
+
"""
|
|
40
|
+
parser = argparse.ArgumentParser(
|
|
41
|
+
description="Sync a single story between Pennyfarthing and Jira"
|
|
42
|
+
)
|
|
43
|
+
parser.add_argument("story_key", help="Story ID (e.g., 63-7) or Jira key (MSSCI-12401)")
|
|
44
|
+
parser.add_argument("--transition", action="store_true", help="Sync status")
|
|
45
|
+
parser.add_argument("--points", action="store_true", help="Sync story points")
|
|
46
|
+
parser.add_argument("--comment", type=str, help="Add comment to issue")
|
|
47
|
+
parser.add_argument("--dry-run", action="store_true", help="Show changes without applying")
|
|
48
|
+
|
|
49
|
+
return parser.parse_args(args)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def get_story_from_sprint(story_key: str) -> dict[str, Any] | None:
|
|
53
|
+
"""Find a story in the sprint YAML.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
story_key: Story ID or Jira key
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
Story dict if found, None otherwise
|
|
60
|
+
"""
|
|
61
|
+
# Try direct lookup first
|
|
62
|
+
story = get_story_by_id(story_key)
|
|
63
|
+
if story:
|
|
64
|
+
return story
|
|
65
|
+
|
|
66
|
+
# If it looks like a local ID (e.g., "63-7"), try finding via epic
|
|
67
|
+
if "-" in story_key and not story_key.startswith("MSSCI"):
|
|
68
|
+
sprint_data = load_current_sprint()
|
|
69
|
+
if sprint_data:
|
|
70
|
+
parts = story_key.split("-")
|
|
71
|
+
if len(parts) >= 2:
|
|
72
|
+
epic_num = parts[0]
|
|
73
|
+
epic = find_epic(sprint_data, epic_num)
|
|
74
|
+
if epic:
|
|
75
|
+
return find_story(epic, story_key)
|
|
76
|
+
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def fetch_jira_issue(jira_key: str) -> dict[str, Any] | None:
|
|
81
|
+
"""Fetch issue from Jira.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
jira_key: Jira issue key
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
Issue JSON if found, None otherwise
|
|
88
|
+
"""
|
|
89
|
+
return jira_client.get_issue(jira_key)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def sync_story(
|
|
93
|
+
story_key: str,
|
|
94
|
+
*,
|
|
95
|
+
do_transition: bool = False,
|
|
96
|
+
sync_points: bool = False,
|
|
97
|
+
comment: str | None = None,
|
|
98
|
+
dry_run: bool = False,
|
|
99
|
+
) -> dict[str, Any]:
|
|
100
|
+
"""Sync a story between Pennyfarthing and Jira.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
story_key: Story ID or Jira key
|
|
104
|
+
do_transition: Whether to sync status
|
|
105
|
+
sync_points: Whether to sync story points
|
|
106
|
+
comment: Comment to add (optional)
|
|
107
|
+
dry_run: If True, show changes without applying
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
Result dict with success, actions, error fields
|
|
111
|
+
"""
|
|
112
|
+
# Find story in sprint YAML
|
|
113
|
+
story = get_story_from_sprint(story_key)
|
|
114
|
+
if not story:
|
|
115
|
+
return {
|
|
116
|
+
"success": False,
|
|
117
|
+
"error": f"Story '{story_key}' not found in sprint YAML",
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
jira_key = story.get("jira")
|
|
121
|
+
if not jira_key:
|
|
122
|
+
return {
|
|
123
|
+
"success": False,
|
|
124
|
+
"error": f"Story '{story_key}' has no Jira key",
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
# Fetch current Jira state
|
|
128
|
+
issue = fetch_jira_issue(jira_key)
|
|
129
|
+
if not issue:
|
|
130
|
+
return {
|
|
131
|
+
"success": False,
|
|
132
|
+
"error": f"Could not fetch Jira issue {jira_key}",
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
actions = []
|
|
136
|
+
errors = []
|
|
137
|
+
|
|
138
|
+
# Sync status if requested
|
|
139
|
+
if do_transition:
|
|
140
|
+
current_jira_status = jira_client.get_jira_field(issue, "fields.status.name")
|
|
141
|
+
target_status = jira_client.map_status_to_jira(story.get("status"))
|
|
142
|
+
|
|
143
|
+
if current_jira_status != target_status:
|
|
144
|
+
action = f"transition {jira_key}: {current_jira_status} -> {target_status}"
|
|
145
|
+
if dry_run:
|
|
146
|
+
actions.append(f"[DRY-RUN] {action}")
|
|
147
|
+
else:
|
|
148
|
+
if jira_client.update_issue_status(jira_key, target_status):
|
|
149
|
+
actions.append(action)
|
|
150
|
+
else:
|
|
151
|
+
errors.append(f"Failed to transition {jira_key}")
|
|
152
|
+
else:
|
|
153
|
+
actions.append(f"status already {current_jira_status}")
|
|
154
|
+
|
|
155
|
+
# Sync points if requested
|
|
156
|
+
if sync_points:
|
|
157
|
+
story_points = story.get("points")
|
|
158
|
+
if story_points is not None:
|
|
159
|
+
current_points = jira_client.get_story_points(jira_key, issue)
|
|
160
|
+
if current_points != story_points:
|
|
161
|
+
action = f"sync points {jira_key}: {current_points} -> {story_points}"
|
|
162
|
+
if dry_run:
|
|
163
|
+
actions.append(f"[DRY-RUN] {action}")
|
|
164
|
+
else:
|
|
165
|
+
# Note: point sync requires REST API, not CLI
|
|
166
|
+
actions.append(f"[SKIP] {action} (requires REST API)")
|
|
167
|
+
|
|
168
|
+
# Add comment if provided
|
|
169
|
+
if comment:
|
|
170
|
+
action = f"add comment to {jira_key}"
|
|
171
|
+
if dry_run:
|
|
172
|
+
actions.append(f"[DRY-RUN] {action}")
|
|
173
|
+
else:
|
|
174
|
+
if jira_client.add_comment(jira_key, comment):
|
|
175
|
+
actions.append(action)
|
|
176
|
+
else:
|
|
177
|
+
errors.append(f"Failed to add comment to {jira_key}")
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
"success": len(errors) == 0,
|
|
181
|
+
"story_id": story.get("id"),
|
|
182
|
+
"jira_key": jira_key,
|
|
183
|
+
"actions": actions,
|
|
184
|
+
"errors": errors if errors else None,
|
|
185
|
+
"dry_run": dry_run,
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def main(args: list[str] | None = None) -> int:
|
|
190
|
+
"""CLI entry point.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
args: Command line arguments (defaults to sys.argv[1:])
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
Exit code (0 for success, 1 for failure)
|
|
197
|
+
"""
|
|
198
|
+
parsed_args = parse_args(args)
|
|
199
|
+
|
|
200
|
+
result = sync_story(
|
|
201
|
+
parsed_args.story_key,
|
|
202
|
+
do_transition=parsed_args.transition,
|
|
203
|
+
sync_points=parsed_args.points,
|
|
204
|
+
comment=parsed_args.comment,
|
|
205
|
+
dry_run=parsed_args.dry_run,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
if result["success"]:
|
|
209
|
+
print(f"Synced {result.get('story_id', parsed_args.story_key)}")
|
|
210
|
+
for action in result.get("actions", []):
|
|
211
|
+
print(f" {action}")
|
|
212
|
+
return 0
|
|
213
|
+
else:
|
|
214
|
+
print(f"Failed: {result.get('error', 'Unknown error')}", file=sys.stderr)
|
|
215
|
+
return 1
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
if __name__ == "__main__":
|
|
219
|
+
sys.exit(main())
|