Skip to content

Commit 0653efd

Browse files
committed
wip
1 parent 332afc3 commit 0653efd

File tree

3 files changed

+167
-29
lines changed

3 files changed

+167
-29
lines changed

docs/doc/usage.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,13 +86,17 @@ release-tool publish 9.1.0
8686

8787
# Or explicitly specify a notes file
8888
release-tool publish 9.1.0 -f .release_tool_cache/draft-releases/owner-repo/9.1.0.md
89+
90+
# Associate with a specific GitHub issue/ticket
91+
release-tool publish 9.1.0 --ticket 123
8992
```
9093

9194
This will:
9295
- Auto-find draft release notes (or use specified file)
9396
- Create a git tag `v9.1.0`
9497
- Create a GitHub release with the release notes
9598
- Optionally create a PR with release notes (use `--pr`)
99+
- Optionally associate with a GitHub issue for tracking (use `--ticket`)
96100

97101
#### Testing Before Publishing
98102

@@ -171,6 +175,7 @@ release-tool publish 9.1.0 --prerelease auto # Auto-detect
171175
| `list-releases` | Lists releases from the database with filters |
172176
| `publish <version>` | Creates a GitHub release (auto-finds draft notes) |
173177
| `publish <version> -f <file>` | Creates a GitHub release from a markdown file |
178+
| `publish <version> --ticket <number>` | Associate release with a GitHub issue |
174179
| `publish --list` or `publish -l` | List all available draft releases |
175180
| `publish --dry-run` | Preview publish operation without making changes |
176181
| `publish --debug` | Show detailed debugging information |

src/release_tool/commands/publish.py

Lines changed: 41 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -377,10 +377,11 @@ def _display_draft_releases(draft_files: list[Path], title: str = "Draft Release
377377
@click.option('--prerelease', type=click.Choice(['auto', 'true', 'false'], case_sensitive=False), default=None,
378378
help='Mark as prerelease: auto (detect from version), true, or false (default: from config)')
379379
@click.option('--force', type=click.Choice(['none', 'draft', 'published'], case_sensitive=False), default='none', help='Force overwrite existing release (default: none)')
380+
@click.option('--ticket', type=int, default=None, help='Ticket/issue number to associate with this release')
380381
@click.option('--dry-run', is_flag=True, help='Show what would be published without making changes')
381382
@click.pass_context
382383
def publish(ctx, version: Optional[str], list_drafts: bool, delete_drafts: bool, notes_file: Optional[str], create_release: Optional[bool],
383-
create_pr: Optional[bool], release_mode: Optional[str], prerelease: Optional[str], force: str,
384+
create_pr: Optional[bool], release_mode: Optional[str], prerelease: Optional[str], force: str, ticket: Optional[int],
384385
dry_run: bool):
385386
"""
386387
Publish a release to GitHub.
@@ -914,13 +915,32 @@ def publish(ctx, version: Optional[str], list_drafts: bool, delete_drafts: bool,
914915
# Create release tracking ticket if enabled
915916
ticket_result = None
916917

917-
# If force=draft, try to find existing ticket interactively first if not in DB
918-
if config.output.create_ticket and force == 'draft' and not dry_run:
918+
# If ticket number provided explicitly, use it directly
919+
if config.output.create_ticket and ticket and not dry_run:
920+
try:
921+
issue = github_client.gh.get_repo(issues_repo).get_issue(ticket)
922+
ticket_result = {'number': str(issue.number), 'url': issue.html_url}
923+
console.print(f"[blue]Using provided ticket #{ticket}[/blue]")
924+
# Save association to database
925+
db.save_ticket_association(
926+
repo_full_name=repo_name,
927+
version=version,
928+
ticket_number=ticket,
929+
ticket_url=issue.html_url
930+
)
931+
if debug:
932+
console.print(f"[dim]Saved ticket association to database[/dim]")
933+
except Exception as e:
934+
console.print(f"[yellow]Warning: Could not use ticket #{ticket}: {e}[/yellow]")
935+
ticket_result = None
936+
937+
# If force=draft, try to find existing ticket automatically (non-interactive)
938+
if config.output.create_ticket and force == 'draft' and not dry_run and not ticket_result:
919939
existing_association = db.get_ticket_association(repo_name, version)
920940
if not existing_association:
921-
ticket_result = _find_existing_ticket_interactive(config, github_client, version)
941+
ticket_result = _find_existing_ticket_auto(config, github_client, version, debug)
922942
if ticket_result:
923-
console.print(f"[blue]Reusing existing ticket #{ticket_result['number']}[/blue]")
943+
console.print(f"[blue]Auto-selected open ticket #{ticket_result['number']}[/blue]")
924944
# Save association
925945
db.save_ticket_association(
926946
repo_full_name=repo_name,
@@ -1171,35 +1191,27 @@ def publish(ctx, version: Optional[str], list_drafts: bool, delete_drafts: bool,
11711191
db.close()
11721192

11731193

1174-
def _find_existing_ticket_interactive(config: Config, github_client: GitHubClient, version: str) -> Optional[dict]:
1175-
"""Find existing ticket interactively."""
1194+
def _find_existing_ticket_auto(config: Config, github_client: GitHubClient, version: str, debug: bool = False) -> Optional[dict]:
1195+
"""Find existing ticket automatically (non-interactive, picks first open ticket)."""
11761196
issues_repo = _get_issues_repo(config)
1177-
query = f"repo:{issues_repo} is:issue {version} in:title"
1178-
console.print(f"[cyan]Searching for existing tickets in {issues_repo}...[/cyan]")
1197+
# Search only for OPEN issues
1198+
query = f"repo:{issues_repo} is:issue is:open {version} in:title"
1199+
1200+
if debug:
1201+
console.print(f"[dim]Searching for open tickets matching version {version}...[/dim]")
11791202

1180-
# We need to use the underlying github client to search
1181-
# This is a bit of a hack, but we don't have a search method in GitHubClient
1182-
# Assuming github_client.gh is available
1203+
# Search for open issues matching the version
11831204
issues = list(github_client.gh.search_issues(query)[:5])
11841205

11851206
if not issues:
1186-
console.print("[yellow]No matching tickets found.[/yellow]")
1207+
if debug:
1208+
console.print("[dim]No matching open tickets found.[/dim]")
11871209
return None
1188-
1189-
table = Table(title="Found Tickets")
1190-
table.add_column("#", style="cyan")
1191-
table.add_column("Number", style="green")
1192-
table.add_column("Title", style="white")
1193-
table.add_column("State", style="yellow")
11941210

1195-
for i, issue in enumerate(issues):
1196-
table.add_row(str(i+1), str(issue.number), issue.title, issue.state)
1197-
1198-
console.print(table)
1211+
# Automatically use the first open ticket found
1212+
selected = issues[0]
11991213

1200-
response = input("\nSelect ticket to reuse (1-5) or 'n' to create new: ").strip().lower()
1201-
if response.isdigit() and 1 <= int(response) <= len(issues):
1202-
selected = issues[int(response)-1]
1203-
return {'number': str(selected.number), 'url': selected.html_url}
1204-
1205-
return None
1214+
if debug:
1215+
console.print(f"[dim]Found open ticket #{selected.number}: {selected.title}[/dim]")
1216+
1217+
return {'number': str(selected.number), 'url': selected.html_url}

tests/test_publish.py

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -698,3 +698,124 @@ def test_branch_creation_disabled_by_config(test_config, test_notes_file):
698698
# Should succeed (but will likely fail at GitHub release creation due to missing branch)
699699
# For this test we're just verifying branch creation was skipped
700700
assert result.exit_code == 0
701+
702+
703+
def test_ticket_parameter_associates_with_issue(test_config, test_notes_file):
704+
"""Test that --ticket parameter properly associates release with a GitHub issue."""
705+
runner = CliRunner()
706+
707+
with patch('release_tool.commands.publish.GitHubClient') as mock_gh_client, \
708+
patch('release_tool.commands.publish.GitOperations') as mock_git_ops, \
709+
patch('release_tool.commands.publish.determine_release_branch_strategy') as mock_strategy, \
710+
patch('release_tool.commands.publish.Database') as mock_db_class:
711+
712+
# Mock database
713+
mock_db = MagicMock()
714+
mock_db_class.return_value = mock_db
715+
mock_db.get_repository.return_value = None
716+
mock_db.get_ticket_association.return_value = None # No existing association
717+
718+
# Mock git operations
719+
mock_git_instance = MagicMock()
720+
mock_git_ops.return_value = mock_git_instance
721+
mock_git_instance.get_version_tags.return_value = []
722+
mock_git_instance.tag_exists.return_value = False
723+
mock_git_instance.branch_exists.return_value = True
724+
725+
# Mock strategy
726+
mock_strategy.return_value = ("release/0.0", "main", False)
727+
728+
# Mock GitHub client
729+
mock_gh_instance = MagicMock()
730+
mock_gh_client.return_value = mock_gh_instance
731+
mock_gh_instance.create_release.return_value = "https://github.com/test/repo/releases/tag/v0.0.1"
732+
733+
# Mock the issue retrieval
734+
mock_issue = MagicMock()
735+
mock_issue.number = 123
736+
mock_issue.html_url = "https://github.com/test/repo/issues/123"
737+
mock_gh_instance.gh.get_repo.return_value.get_issue.return_value = mock_issue
738+
739+
# Enable ticket creation and PR creation in config
740+
test_config.output.create_ticket = True
741+
test_config.output.create_pr = True
742+
743+
result = runner.invoke(
744+
publish,
745+
['0.0.1', '-f', str(test_notes_file), '--release', '--pr', '--ticket', '123'],
746+
obj={'config': test_config}
747+
)
748+
749+
assert result.exit_code == 0
750+
751+
# Verify the issue was retrieved (can be called multiple times during PR creation)
752+
mock_gh_instance.gh.get_repo.return_value.get_issue.assert_called_with(123)
753+
754+
# Verify the ticket association was saved to database
755+
assert mock_db.save_ticket_association.called
756+
# Find the call with ticket_number=123
757+
calls = [call for call in mock_db.save_ticket_association.call_args_list
758+
if 'ticket_number' in call[1] and call[1]['ticket_number'] == 123]
759+
assert len(calls) > 0
760+
call_args = calls[0]
761+
assert call_args[1]['version'] == '0.0.1'
762+
assert call_args[1]['ticket_url'] == "https://github.com/test/repo/issues/123"
763+
764+
765+
def test_auto_select_open_ticket_for_draft_release(test_config, test_notes_file):
766+
"""Test that publishing with --force draft auto-selects the first open ticket."""
767+
runner = CliRunner()
768+
769+
with patch('release_tool.commands.publish.GitHubClient') as mock_gh_client, \
770+
patch('release_tool.commands.publish.GitOperations') as mock_git_ops, \
771+
patch('release_tool.commands.publish.determine_release_branch_strategy') as mock_strategy, \
772+
patch('release_tool.commands.publish.Database') as mock_db_class, \
773+
patch('release_tool.commands.publish._find_existing_ticket_auto') as mock_find_ticket:
774+
775+
# Mock database
776+
mock_db = MagicMock()
777+
mock_db_class.return_value = mock_db
778+
mock_db.get_repository.return_value = None
779+
mock_db.get_ticket_association.return_value = None # No existing association
780+
781+
# Mock git operations
782+
mock_git_instance = MagicMock()
783+
mock_git_ops.return_value = mock_git_instance
784+
mock_git_instance.get_version_tags.return_value = []
785+
mock_git_instance.tag_exists.return_value = False
786+
mock_git_instance.branch_exists.return_value = True
787+
788+
# Mock strategy
789+
mock_strategy.return_value = ("release/0.0", "main", False)
790+
791+
# Mock GitHub client
792+
mock_gh_instance = MagicMock()
793+
mock_gh_client.return_value = mock_gh_instance
794+
mock_gh_instance.create_release.return_value = "https://github.com/test/repo/releases/tag/v0.0.1-rc.0"
795+
796+
# Mock automatic ticket finding (returns first open ticket)
797+
mock_find_ticket.return_value = {
798+
'number': '456',
799+
'url': 'https://github.com/test/repo/issues/456'
800+
}
801+
802+
# Enable ticket creation and PR creation in config
803+
test_config.output.create_ticket = True
804+
test_config.output.create_pr = True
805+
806+
result = runner.invoke(
807+
publish,
808+
['0.0.1-rc.0', '-f', str(test_notes_file), '--release', '--pr', '--force', 'draft'],
809+
obj={'config': test_config}
810+
)
811+
812+
assert result.exit_code == 0
813+
814+
# Verify auto-selection was called
815+
mock_find_ticket.assert_called_once()
816+
817+
# Verify the ticket association was saved
818+
mock_db.save_ticket_association.assert_called()
819+
call_args = mock_db.save_ticket_association.call_args
820+
assert call_args[1]['ticket_number'] == 456
821+
assert call_args[1]['version'] == '0.0.1-rc.0'

0 commit comments

Comments
 (0)