@pennyfarthing/core 7.8.1 → 7.8.4
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/README.md +1 -1
- package/package.json +2 -1
- package/packages/core/dist/cli/commands/init.d.ts.map +1 -1
- package/packages/core/dist/cli/commands/init.js +7 -6
- package/packages/core/dist/cli/commands/init.js.map +1 -1
- package/packages/core/dist/cli/utils/symlinks.d.ts +7 -0
- package/packages/core/dist/cli/utils/symlinks.d.ts.map +1 -1
- package/packages/core/dist/cli/utils/symlinks.js +25 -0
- package/packages/core/dist/cli/utils/symlinks.js.map +1 -1
- package/pennyfarthing-dist/scripts/core/phase-check-start.sh +1 -1
- package/pennyfarthing-dist/scripts/core/prime.sh +23 -0
- package/pennyfarthing-dist/scripts/core/run.sh +5 -5
- package/pennyfarthing-dist/scripts/git/install-git-hooks.sh +2 -2
- package/pennyfarthing-dist/scripts/git/release.sh +2 -2
- package/pennyfarthing-dist/scripts/health/drift-detection.sh +1 -1
- package/pennyfarthing-dist/scripts/hooks/post-merge.sh +2 -2
- package/pennyfarthing-dist/scripts/hooks/pre-push.sh +2 -2
- package/pennyfarthing-dist/scripts/hooks/session-stop.sh +1 -1
- package/pennyfarthing-dist/scripts/jira/create-jira-epic.sh +1 -1
- package/pennyfarthing-dist/scripts/jira/create-jira-story.sh +1 -1
- package/pennyfarthing-dist/scripts/jira/jira-reconcile.sh +1 -1
- package/pennyfarthing-dist/scripts/lib/common.sh +1 -1
- package/pennyfarthing-dist/scripts/lib/find-root.sh +4 -4
- package/pennyfarthing-dist/scripts/maintenance/sidecar-health.sh +1 -1
- package/pennyfarthing-dist/scripts/misc/add_short_names.py +2 -2
- package/pennyfarthing-dist/scripts/misc/backlog.sh +2 -2
- package/pennyfarthing-dist/scripts/misc/deploy.sh +2 -2
- package/pennyfarthing-dist/scripts/misc/generate-skill-docs.sh +4 -4
- package/pennyfarthing-dist/scripts/misc/log-skill-usage.sh +2 -2
- package/pennyfarthing-dist/scripts/misc/run-ci.sh +1 -1
- package/pennyfarthing-dist/scripts/misc/skill-usage-report.sh +2 -2
- package/pennyfarthing-dist/scripts/sprint/archive-story.sh +1 -1
- package/pennyfarthing-dist/scripts/sprint/available-stories.sh +1 -1
- package/pennyfarthing-dist/scripts/sprint/check-story.sh +1 -1
- package/pennyfarthing-dist/scripts/sprint/get-epic-field.sh +1 -1
- package/pennyfarthing-dist/scripts/sprint/get-story-field.sh +1 -1
- package/pennyfarthing-dist/scripts/sprint/import_epic_to_future.py +2 -2
- package/pennyfarthing-dist/scripts/sprint/list-future.sh +1 -1
- package/pennyfarthing-dist/scripts/sprint/new-sprint.sh +1 -1
- package/pennyfarthing-dist/scripts/sprint/promote-epic.sh +1 -1
- package/pennyfarthing-dist/scripts/sprint/sprint-common.sh +3 -3
- package/pennyfarthing-dist/scripts/sprint/sprint-info.sh +1 -1
- package/pennyfarthing-dist/scripts/sprint/sprint-metrics.sh +2 -2
- package/pennyfarthing-dist/scripts/theme/compute_theme_tiers.py +2 -2
- package/pennyfarthing-dist/scripts/workflow/check.py +2 -2
- package/pennyfarthing-dist/scripts/workflow/finish-story.sh +1 -1
- package/pennyfarthing-dist/scripts/workflow/fix-session-phase.sh +1 -1
- package/pennyfarthing-dist/scripts/workflow/get-workflow-type.py +2 -2
- package/pennyfarthing-dist/scripts/workflow/list-workflows.sh +1 -1
- package/pennyfarthing-dist/scripts/workflow/phase-owner.sh +1 -1
- package/pennyfarthing-dist/scripts/workflow/resume-workflow.sh +1 -1
- package/pennyfarthing-dist/scripts/workflow/show-workflow.sh +1 -1
- package/pennyfarthing-dist/scripts/workflow/start-workflow.sh +1 -1
- package/pennyfarthing-dist/scripts/workflow/workflow-status.sh +1 -1
- 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,350 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Jira sync for Pennyfarthing epics.
|
|
3
|
+
|
|
4
|
+
Syncs epic stories to Jira with async parallel API calls.
|
|
5
|
+
Ported from jira-sync.mjs for Python parallelism.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
python -m pennyfarthing_scripts.jira sync <epic_number> [--dry-run] [--transition] [--points]
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
import asyncio
|
|
15
|
+
import sys
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
from pennyfarthing_scripts.common.config import get_project_root
|
|
20
|
+
from pennyfarthing_scripts.jira.client import (
|
|
21
|
+
JiraClient,
|
|
22
|
+
extract_jira_key,
|
|
23
|
+
get_jira_field,
|
|
24
|
+
map_status_to_jira,
|
|
25
|
+
)
|
|
26
|
+
from pennyfarthing_scripts.common.output import error, info, success, warn
|
|
27
|
+
from pennyfarthing_scripts.sprint.loader import find_epic, load_sprint
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class SyncResult:
|
|
32
|
+
"""Result of syncing a single story."""
|
|
33
|
+
|
|
34
|
+
story_id: str
|
|
35
|
+
success: bool
|
|
36
|
+
skipped: bool
|
|
37
|
+
error: str | None
|
|
38
|
+
actions: list[str] = field(default_factory=list)
|
|
39
|
+
dry_run: bool = False
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# Module-level client for async operations
|
|
43
|
+
_client: JiraClient | None = None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _get_client() -> JiraClient:
|
|
47
|
+
"""Get or create the module's JiraClient instance."""
|
|
48
|
+
global _client
|
|
49
|
+
if _client is None:
|
|
50
|
+
_client = JiraClient()
|
|
51
|
+
return _client
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def format_story_line(story: dict[str, Any]) -> str:
|
|
55
|
+
"""Format a story for display.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
story: Story dict from sprint YAML
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
Formatted string for display
|
|
62
|
+
"""
|
|
63
|
+
story_id = story.get("id", "?")
|
|
64
|
+
title = story.get("title", "Untitled")
|
|
65
|
+
status = story.get("status", "backlog")
|
|
66
|
+
return f"Story {story_id}: {title} [{status}]"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def format_summary(synced: int, skipped: int, errors: int) -> str:
|
|
70
|
+
"""Format sync summary.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
synced: Number of stories synced
|
|
74
|
+
skipped: Number of stories skipped
|
|
75
|
+
errors: Number of errors
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
Formatted summary string
|
|
79
|
+
"""
|
|
80
|
+
return f"Summary: {synced} synced, {skipped} skipped, {errors} errors"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
async def sync_story(
|
|
84
|
+
story: dict[str, Any],
|
|
85
|
+
dry_run: bool = False,
|
|
86
|
+
do_transition: bool = False,
|
|
87
|
+
sync_points: bool = False,
|
|
88
|
+
) -> SyncResult:
|
|
89
|
+
"""Sync a single story to Jira.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
story: Story dict from sprint YAML
|
|
93
|
+
dry_run: If True, don't make changes
|
|
94
|
+
do_transition: If True, transition status
|
|
95
|
+
sync_points: If True, sync story points
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
SyncResult with outcome
|
|
99
|
+
"""
|
|
100
|
+
story_id = story.get("id", "?")
|
|
101
|
+
jira_key = story.get("jira")
|
|
102
|
+
|
|
103
|
+
# Skip if no Jira key
|
|
104
|
+
if not jira_key or jira_key == "null":
|
|
105
|
+
return SyncResult(
|
|
106
|
+
story_id=story_id,
|
|
107
|
+
success=False,
|
|
108
|
+
skipped=True,
|
|
109
|
+
error=None,
|
|
110
|
+
actions=[],
|
|
111
|
+
dry_run=dry_run,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
jira_key = extract_jira_key(jira_key)
|
|
115
|
+
actions: list[str] = []
|
|
116
|
+
|
|
117
|
+
# Dry run mode
|
|
118
|
+
if dry_run:
|
|
119
|
+
if do_transition:
|
|
120
|
+
actions.append("transition")
|
|
121
|
+
if sync_points:
|
|
122
|
+
actions.append("sync_points")
|
|
123
|
+
return SyncResult(
|
|
124
|
+
story_id=story_id,
|
|
125
|
+
success=True,
|
|
126
|
+
skipped=False,
|
|
127
|
+
error=None,
|
|
128
|
+
actions=actions,
|
|
129
|
+
dry_run=True,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# Get client and fetch current Jira state
|
|
133
|
+
client = _get_client()
|
|
134
|
+
issue_json = await client.get_issue_async(jira_key)
|
|
135
|
+
if not issue_json:
|
|
136
|
+
return SyncResult(
|
|
137
|
+
story_id=story_id,
|
|
138
|
+
success=False,
|
|
139
|
+
skipped=False,
|
|
140
|
+
error=f"Could not fetch {jira_key}",
|
|
141
|
+
actions=[],
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
current_status = get_jira_field(issue_json, "fields.status.name", "Unknown")
|
|
145
|
+
target_status = map_status_to_jira(story.get("status"))
|
|
146
|
+
|
|
147
|
+
# Transition if requested and needed
|
|
148
|
+
if do_transition and current_status != target_status:
|
|
149
|
+
result = await client.transition_async(jira_key, target_status)
|
|
150
|
+
if result.get("success"):
|
|
151
|
+
actions.append(f"transitioned: {current_status} -> {target_status}")
|
|
152
|
+
else:
|
|
153
|
+
actions.append(f"transition failed: {result.get('reason')}")
|
|
154
|
+
|
|
155
|
+
# Sync points if requested
|
|
156
|
+
story_points = story.get("points")
|
|
157
|
+
if sync_points and story_points:
|
|
158
|
+
current_points = get_jira_field(issue_json, "fields.customfield_10031")
|
|
159
|
+
current_points_int = int(current_points) if current_points else None
|
|
160
|
+
|
|
161
|
+
result = await client.sync_story_points_async(
|
|
162
|
+
jira_key, int(story_points), current_points_int
|
|
163
|
+
)
|
|
164
|
+
if result.get("success"):
|
|
165
|
+
if result.get("already_synced"):
|
|
166
|
+
actions.append(f"points already synced: {story_points}")
|
|
167
|
+
else:
|
|
168
|
+
actions.append(f"synced points: {story_points}")
|
|
169
|
+
else:
|
|
170
|
+
actions.append(f"points sync failed: {result.get('reason')}")
|
|
171
|
+
|
|
172
|
+
return SyncResult(
|
|
173
|
+
story_id=story_id,
|
|
174
|
+
success=True,
|
|
175
|
+
skipped=False,
|
|
176
|
+
error=None,
|
|
177
|
+
actions=actions,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
async def sync_epic(
|
|
182
|
+
epic: dict[str, Any],
|
|
183
|
+
dry_run: bool = False,
|
|
184
|
+
do_transition: bool = False,
|
|
185
|
+
sync_points: bool = False,
|
|
186
|
+
) -> dict[str, Any]:
|
|
187
|
+
"""Sync all stories in an epic to Jira.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
epic: Epic dict from sprint YAML
|
|
191
|
+
dry_run: If True, don't make changes
|
|
192
|
+
do_transition: If True, transition status
|
|
193
|
+
sync_points: If True, sync story points
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
Summary dict with counts
|
|
197
|
+
"""
|
|
198
|
+
stories = epic.get("stories", [])
|
|
199
|
+
|
|
200
|
+
# Process all stories in parallel
|
|
201
|
+
tasks = [
|
|
202
|
+
sync_story(story, dry_run=dry_run, do_transition=do_transition, sync_points=sync_points)
|
|
203
|
+
for story in stories
|
|
204
|
+
]
|
|
205
|
+
results = await asyncio.gather(*tasks)
|
|
206
|
+
|
|
207
|
+
# Count results
|
|
208
|
+
synced = sum(1 for r in results if r.success and not r.skipped)
|
|
209
|
+
skipped = sum(1 for r in results if r.skipped)
|
|
210
|
+
errors = sum(1 for r in results if not r.success and not r.skipped)
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
"total": len(stories),
|
|
214
|
+
"synced": synced,
|
|
215
|
+
"skipped": skipped,
|
|
216
|
+
"errors": errors,
|
|
217
|
+
"results": results,
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def parse_args(args: list[str] | None = None) -> argparse.Namespace:
|
|
222
|
+
"""Parse command line arguments.
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
args: Arguments to parse (defaults to sys.argv[1:])
|
|
226
|
+
|
|
227
|
+
Returns:
|
|
228
|
+
Parsed arguments namespace
|
|
229
|
+
"""
|
|
230
|
+
parser = argparse.ArgumentParser(
|
|
231
|
+
description="Sync Pennyfarthing epic to Jira",
|
|
232
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
233
|
+
epilog="""
|
|
234
|
+
Examples:
|
|
235
|
+
jira sync 35 Show sync status for epic 35
|
|
236
|
+
jira sync 35 --dry-run Show what would be done
|
|
237
|
+
jira sync 35 --transition Sync status to Jira
|
|
238
|
+
jira sync 35 --transition --points Sync status and story points
|
|
239
|
+
""",
|
|
240
|
+
)
|
|
241
|
+
parser.add_argument("epic", help="Epic number (e.g., '35' or 'epic-35')")
|
|
242
|
+
parser.add_argument(
|
|
243
|
+
"--dry-run", action="store_true", help="Show what would be done without making changes"
|
|
244
|
+
)
|
|
245
|
+
parser.add_argument(
|
|
246
|
+
"--transition", action="store_true", help="Transition Jira issues to match status"
|
|
247
|
+
)
|
|
248
|
+
parser.add_argument(
|
|
249
|
+
"--points", action="store_true", help="Sync story points from Pennyfarthing to Jira"
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
return parser.parse_args(args)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
async def async_main(args: argparse.Namespace) -> int:
|
|
256
|
+
"""Async main entry point.
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
args: Parsed arguments
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
Exit code
|
|
263
|
+
"""
|
|
264
|
+
# Find project root and load sprint
|
|
265
|
+
try:
|
|
266
|
+
project_root = get_project_root()
|
|
267
|
+
except FileNotFoundError as e:
|
|
268
|
+
error(str(e))
|
|
269
|
+
return 1
|
|
270
|
+
|
|
271
|
+
sprint_data = load_sprint(project_root)
|
|
272
|
+
if not sprint_data:
|
|
273
|
+
error("Could not load sprint data")
|
|
274
|
+
return 1
|
|
275
|
+
|
|
276
|
+
# Find epic
|
|
277
|
+
epic = find_epic(sprint_data, args.epic)
|
|
278
|
+
if not epic:
|
|
279
|
+
error(f"Epic {args.epic} not found in sprint file")
|
|
280
|
+
return 1
|
|
281
|
+
|
|
282
|
+
# Display header
|
|
283
|
+
print("", file=sys.stderr)
|
|
284
|
+
info("==========================================")
|
|
285
|
+
info(f"Epic {args.epic}: {epic.get('title', 'Untitled')}")
|
|
286
|
+
if epic.get("jira"):
|
|
287
|
+
info(f"Jira: {epic.get('jira')}")
|
|
288
|
+
info("==========================================")
|
|
289
|
+
print("", file=sys.stderr)
|
|
290
|
+
|
|
291
|
+
if args.dry_run:
|
|
292
|
+
warn("[DRY-RUN MODE] No changes will be made")
|
|
293
|
+
print("", file=sys.stderr)
|
|
294
|
+
|
|
295
|
+
stories = epic.get("stories", [])
|
|
296
|
+
if not stories:
|
|
297
|
+
warn(f"No stories found in epic {args.epic}")
|
|
298
|
+
return 0
|
|
299
|
+
|
|
300
|
+
info(f"Found {len(stories)} stories to process")
|
|
301
|
+
print("", file=sys.stderr)
|
|
302
|
+
|
|
303
|
+
# Sync epic
|
|
304
|
+
result = await sync_epic(
|
|
305
|
+
epic,
|
|
306
|
+
dry_run=args.dry_run,
|
|
307
|
+
do_transition=args.transition,
|
|
308
|
+
sync_points=args.points,
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
# Display results
|
|
312
|
+
for sync_result in result["results"]:
|
|
313
|
+
story = next((s for s in stories if s.get("id") == sync_result.story_id), {})
|
|
314
|
+
print("---", file=sys.stderr)
|
|
315
|
+
info(format_story_line(story))
|
|
316
|
+
if sync_result.skipped:
|
|
317
|
+
warn(" Not synced to Jira - skipping")
|
|
318
|
+
elif sync_result.error:
|
|
319
|
+
error(f" {sync_result.error}")
|
|
320
|
+
elif sync_result.dry_run:
|
|
321
|
+
actions_str = ", ".join(sync_result.actions) if sync_result.actions else "view only"
|
|
322
|
+
warn(f" [DRY-RUN] Would sync: {actions_str}")
|
|
323
|
+
else:
|
|
324
|
+
for action in sync_result.actions:
|
|
325
|
+
success(f" {action}")
|
|
326
|
+
|
|
327
|
+
# Summary
|
|
328
|
+
print("", file=sys.stderr)
|
|
329
|
+
print("==========================================", file=sys.stderr)
|
|
330
|
+
success(format_summary(result["synced"], result["skipped"], result["errors"]))
|
|
331
|
+
print("==========================================", file=sys.stderr)
|
|
332
|
+
|
|
333
|
+
return 1 if result["errors"] > 0 else 0
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def main(args: list[str] | None = None) -> int:
|
|
337
|
+
"""Main entry point.
|
|
338
|
+
|
|
339
|
+
Args:
|
|
340
|
+
args: Command line arguments (defaults to sys.argv[1:])
|
|
341
|
+
|
|
342
|
+
Returns:
|
|
343
|
+
Exit code
|
|
344
|
+
"""
|
|
345
|
+
parsed_args = parse_args(args)
|
|
346
|
+
return asyncio.run(async_main(parsed_args))
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
if __name__ == "__main__":
|
|
350
|
+
sys.exit(main())
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Bidirectional sync between sprint YAML and Jira.
|
|
3
|
+
|
|
4
|
+
This module re-exports from pennyfarthing_scripts.jira.bidirectional for
|
|
5
|
+
backwards compatibility. New code should import from jira.bidirectional directly.
|
|
6
|
+
|
|
7
|
+
Usage: python -m pennyfarthing_scripts.jira_bidirectional_sync [options]
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
# Re-export everything from the new location
|
|
11
|
+
from pennyfarthing_scripts.jira.bidirectional import (
|
|
12
|
+
SyncChange,
|
|
13
|
+
SyncPlan,
|
|
14
|
+
SyncResult,
|
|
15
|
+
async_main,
|
|
16
|
+
execute_sync_plan,
|
|
17
|
+
format_sync_plan,
|
|
18
|
+
generate_sync_plan,
|
|
19
|
+
main,
|
|
20
|
+
parse_cli_args,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"SyncChange",
|
|
25
|
+
"SyncPlan",
|
|
26
|
+
"SyncResult",
|
|
27
|
+
"async_main",
|
|
28
|
+
"execute_sync_plan",
|
|
29
|
+
"format_sync_plan",
|
|
30
|
+
"generate_sync_plan",
|
|
31
|
+
"main",
|
|
32
|
+
"parse_cli_args",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
if __name__ == "__main__":
|
|
36
|
+
import sys
|
|
37
|
+
sys.exit(main())
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Create Jira epics from Pennyfarthing sprint YAML definitions.
|
|
3
|
+
|
|
4
|
+
This module re-exports from pennyfarthing_scripts.jira.epic for
|
|
5
|
+
backwards compatibility. New code should import from jira.epic directly.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
python -m pennyfarthing_scripts.jira_epic_creation <epic_id> [options]
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
# Re-export everything from the new location
|
|
12
|
+
from pennyfarthing_scripts.jira.epic import (
|
|
13
|
+
build_epic_payload,
|
|
14
|
+
create_epic,
|
|
15
|
+
create_epic_from_yaml,
|
|
16
|
+
main,
|
|
17
|
+
parse_args,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"build_epic_payload",
|
|
22
|
+
"create_epic",
|
|
23
|
+
"create_epic_from_yaml",
|
|
24
|
+
"main",
|
|
25
|
+
"parse_args",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
if __name__ == "__main__":
|
|
29
|
+
import sys
|
|
30
|
+
sys.exit(main())
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Jira sync for Pennyfarthing epics.
|
|
3
|
+
|
|
4
|
+
This module re-exports from pennyfarthing_scripts.jira.sync for
|
|
5
|
+
backwards compatibility. New code should import from jira.sync directly.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
python -m pennyfarthing_scripts.jira_sync <epic_number> [--dry-run] [--transition] [--points]
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
# Re-export everything from the new location
|
|
12
|
+
from pennyfarthing_scripts.jira.sync import (
|
|
13
|
+
SyncResult,
|
|
14
|
+
async_main,
|
|
15
|
+
format_story_line,
|
|
16
|
+
format_summary,
|
|
17
|
+
main,
|
|
18
|
+
parse_args,
|
|
19
|
+
sync_epic,
|
|
20
|
+
sync_story,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"SyncResult",
|
|
25
|
+
"async_main",
|
|
26
|
+
"format_story_line",
|
|
27
|
+
"format_summary",
|
|
28
|
+
"main",
|
|
29
|
+
"parse_args",
|
|
30
|
+
"sync_epic",
|
|
31
|
+
"sync_story",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
if __name__ == "__main__":
|
|
35
|
+
import sys
|
|
36
|
+
sys.exit(main())
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Sync a single story between Pennyfarthing sprint YAML and Jira.
|
|
3
|
+
|
|
4
|
+
This module re-exports from pennyfarthing_scripts.jira.story for
|
|
5
|
+
backwards compatibility. New code should import from jira.story directly.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
python -m pennyfarthing_scripts.jira_sync_story <story_key> [options]
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
# Re-export everything from the new location
|
|
12
|
+
from pennyfarthing_scripts.jira.story import (
|
|
13
|
+
fetch_jira_issue,
|
|
14
|
+
get_story_from_sprint,
|
|
15
|
+
main,
|
|
16
|
+
parse_args,
|
|
17
|
+
sync_story,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"fetch_jira_issue",
|
|
22
|
+
"get_story_from_sprint",
|
|
23
|
+
"main",
|
|
24
|
+
"parse_args",
|
|
25
|
+
"sync_story",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
if __name__ == "__main__":
|
|
29
|
+
import sys
|
|
30
|
+
sys.exit(main())
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Console output utilities for Pennyfarthing scripts.
|
|
3
|
+
|
|
4
|
+
This module re-exports from pennyfarthing_scripts.common.output for
|
|
5
|
+
backwards compatibility. New code should import from common.output directly.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
# Re-export everything from the new location
|
|
9
|
+
from pennyfarthing_scripts.common.output import (
|
|
10
|
+
Colors,
|
|
11
|
+
_colorize,
|
|
12
|
+
_supports_color,
|
|
13
|
+
bold,
|
|
14
|
+
debug,
|
|
15
|
+
dim,
|
|
16
|
+
divider,
|
|
17
|
+
error,
|
|
18
|
+
header,
|
|
19
|
+
info,
|
|
20
|
+
success,
|
|
21
|
+
warn,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"Colors",
|
|
26
|
+
"_colorize",
|
|
27
|
+
"_supports_color",
|
|
28
|
+
"bold",
|
|
29
|
+
"debug",
|
|
30
|
+
"dim",
|
|
31
|
+
"divider",
|
|
32
|
+
"error",
|
|
33
|
+
"header",
|
|
34
|
+
"info",
|
|
35
|
+
"success",
|
|
36
|
+
"warn",
|
|
37
|
+
]
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Preflight checks for Pennyfarthing workflows.
|
|
2
|
+
|
|
3
|
+
This package provides async preflight checks that run in parallel
|
|
4
|
+
to validate story completion before finishing.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from pennyfarthing_scripts.preflight.finish import (
|
|
8
|
+
PreflightResult,
|
|
9
|
+
PreflightIssue,
|
|
10
|
+
run_finish_preflight,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"PreflightResult",
|
|
15
|
+
"PreflightIssue",
|
|
16
|
+
"run_finish_preflight",
|
|
17
|
+
]
|
|
Binary file
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Preflight CLI - Fan-out CLI for preflight checks.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
python -m pennyfarthing_scripts.preflight <subcommand> [args]
|
|
6
|
+
|
|
7
|
+
Subcommands:
|
|
8
|
+
finish Run finish preflight checks
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import asyncio
|
|
13
|
+
import json
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
from pennyfarthing_scripts.preflight.finish import run_finish_preflight
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def finish(args: list[str]) -> int:
|
|
21
|
+
"""Run finish preflight checks."""
|
|
22
|
+
parser = argparse.ArgumentParser(
|
|
23
|
+
prog="preflight finish",
|
|
24
|
+
description="Run finish preflight checks in parallel",
|
|
25
|
+
)
|
|
26
|
+
parser.add_argument(
|
|
27
|
+
"story_id",
|
|
28
|
+
help="Story identifier (e.g., 31-10)",
|
|
29
|
+
)
|
|
30
|
+
parser.add_argument(
|
|
31
|
+
"--branch", "-b",
|
|
32
|
+
required=True,
|
|
33
|
+
help="Feature branch name",
|
|
34
|
+
)
|
|
35
|
+
parser.add_argument(
|
|
36
|
+
"--jira", "-j",
|
|
37
|
+
help="Jira issue key (optional, skips Jira checks if absent)",
|
|
38
|
+
)
|
|
39
|
+
parser.add_argument(
|
|
40
|
+
"--repo", "-r",
|
|
41
|
+
help="Repository name for PR lookup (optional)",
|
|
42
|
+
)
|
|
43
|
+
parser.add_argument(
|
|
44
|
+
"--project-root", "-p",
|
|
45
|
+
type=Path,
|
|
46
|
+
help="Project root path (defaults to cwd)",
|
|
47
|
+
)
|
|
48
|
+
parser.add_argument(
|
|
49
|
+
"--format", "-f",
|
|
50
|
+
choices=["json", "yaml"],
|
|
51
|
+
default="json",
|
|
52
|
+
help="Output format (default: json)",
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
parsed = parser.parse_args(args)
|
|
56
|
+
|
|
57
|
+
# Run async preflight
|
|
58
|
+
result = asyncio.run(run_finish_preflight(
|
|
59
|
+
story_id=parsed.story_id,
|
|
60
|
+
branch=parsed.branch,
|
|
61
|
+
jira_key=parsed.jira,
|
|
62
|
+
repo=parsed.repo,
|
|
63
|
+
project_root=parsed.project_root,
|
|
64
|
+
))
|
|
65
|
+
|
|
66
|
+
# Output result
|
|
67
|
+
result_dict = result.to_dict()
|
|
68
|
+
|
|
69
|
+
if parsed.format == "yaml":
|
|
70
|
+
try:
|
|
71
|
+
import yaml
|
|
72
|
+
print(yaml.dump(result_dict, default_flow_style=False, sort_keys=False))
|
|
73
|
+
except ImportError:
|
|
74
|
+
print("YAML output requires PyYAML. Falling back to JSON.", file=sys.stderr)
|
|
75
|
+
print(json.dumps(result_dict, indent=2))
|
|
76
|
+
else:
|
|
77
|
+
print(json.dumps(result_dict, indent=2))
|
|
78
|
+
|
|
79
|
+
# Return non-zero if not ready to finish
|
|
80
|
+
return 0 if result.ready_to_finish else 1
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# Subcommand registry
|
|
84
|
+
SUBCOMMANDS = {
|
|
85
|
+
"finish": finish,
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def cli(args: list[str] | None = None) -> int:
|
|
90
|
+
"""Main CLI entry point."""
|
|
91
|
+
if args is None:
|
|
92
|
+
args = sys.argv[1:]
|
|
93
|
+
|
|
94
|
+
parser = argparse.ArgumentParser(
|
|
95
|
+
prog="preflight",
|
|
96
|
+
description="Preflight checks for Pennyfarthing workflows",
|
|
97
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
98
|
+
epilog="""
|
|
99
|
+
Subcommands:
|
|
100
|
+
finish <story_id> --branch <branch> [--jira <key>]
|
|
101
|
+
Run finish preflight checks in parallel
|
|
102
|
+
|
|
103
|
+
Examples:
|
|
104
|
+
preflight finish 31-10 --branch feat/31-10-feature --jira MSSCI-12345
|
|
105
|
+
preflight finish 63-9 -b feat/63-9-fanout -j MSSCI-12413 -f yaml
|
|
106
|
+
""",
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
parser.add_argument(
|
|
110
|
+
"subcommand",
|
|
111
|
+
nargs="?",
|
|
112
|
+
choices=list(SUBCOMMANDS.keys()),
|
|
113
|
+
help="Subcommand to run",
|
|
114
|
+
)
|
|
115
|
+
parser.add_argument(
|
|
116
|
+
"args",
|
|
117
|
+
nargs=argparse.REMAINDER,
|
|
118
|
+
help="Arguments for subcommand",
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
parsed = parser.parse_args(args)
|
|
122
|
+
|
|
123
|
+
if not parsed.subcommand:
|
|
124
|
+
parser.print_help()
|
|
125
|
+
return 0
|
|
126
|
+
|
|
127
|
+
handler = SUBCOMMANDS.get(parsed.subcommand)
|
|
128
|
+
if handler:
|
|
129
|
+
return handler(parsed.args)
|
|
130
|
+
else:
|
|
131
|
+
print(f"Unknown subcommand: {parsed.subcommand}", file=sys.stderr)
|
|
132
|
+
return 1
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def main(args: list[str] | None = None) -> int:
|
|
136
|
+
"""Alias for cli()."""
|
|
137
|
+
return cli(args)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
if __name__ == "__main__":
|
|
141
|
+
sys.exit(cli())
|