Skip to content

Commit aa38229

Browse files
mavamclaude
andauthored
Fix release progress panel truncating failed commands (#7)
* Fix release progress panel truncating failed commands The release progress panel could truncate long commands due to panel width constraints. Now the full failed command is printed below the panel using soft_wrap=True for user reference and retry purposes. Changes: - Add update_command() method to StepTracker to update command strings after initial registration - In publish_release(), update the "publish" step with the full gh command using shlex.join() after it's fully constructed - In _render_release_progress(), print the full failed command below the panel with soft_wrap=True to prevent truncation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add changelog entry for release progress panel fix Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fix release retry command rendering with bracketed args * Remove stale DEVELOPMENT.md package reference --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent f603231 commit aa38229

File tree

4 files changed

+111
-4
lines changed

4 files changed

+111
-4
lines changed
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
title: Fix release progress panel truncating failed commands
3+
type: bugfix
4+
authors:
5+
- mavam
6+
- claude
7+
pr: 7
8+
created: 2026-02-15T10:29:09.765161Z
9+
---
10+
11+
When a release step fails, the full command now prints below the progress panel so you can copy-paste it for manual recovery.
12+
13+
Previously, long commands would get truncated in the release progress panel, making it difficult to reproduce the failure manually. Now when a step fails, the complete command is displayed in full below the panel, giving you what you need to debug and retry the operation.

pyproject.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@ include = [
5252
"/src/**/*",
5353
"/tests",
5454
"/README.md",
55-
"/DEVELOPMENT.md",
5655
"/LICENSE",
5756
"/changelog",
5857
"/pyproject.toml",

src/tenzir_ship/cli/_release.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from __future__ import annotations
44

5+
import shlex
56
import shutil
67
import subprocess
78
from dataclasses import dataclass, field
@@ -13,6 +14,7 @@
1314
import click
1415
from click.core import ParameterSource
1516
from packaging.version import InvalidVersion, Version
17+
from rich.markup import escape
1618
from rich.panel import Panel
1719
from rich.table import Table
1820
from rich.text import Text
@@ -30,6 +32,7 @@
3032
)
3133
from ..utils import (
3234
abort_on_user_interrupt,
35+
console,
3336
create_annotated_git_tag,
3437
create_git_commit,
3538
emit_output,
@@ -109,6 +112,12 @@ def fail(self, name: str) -> None:
109112
if step.name == name:
110113
step.status = StepStatus.FAILED
111114

115+
def update_command(self, name: str, command: str) -> None:
116+
"""Update the command string for a step."""
117+
for step in self.steps:
118+
if step.name == name:
119+
step.command = command
120+
112121

113122
def _render_release_progress(tracker: StepTracker) -> None:
114123
"""Render release progress summary to stderr on failure."""
@@ -120,22 +129,28 @@ def _render_release_progress(tracker: StepTracker) -> None:
120129
for step in tracker.steps:
121130
if step.status == StepStatus.COMPLETED:
122131
icon = "[green]\u2714[/green]"
123-
cmd = f"[dim]{step.command}[/dim]"
132+
cmd = f"[dim]{escape(step.command)}[/dim]"
124133
elif step.status == StepStatus.FAILED:
125134
icon = "[red]\u2718[/red]"
126-
cmd = f"[red]{step.command}[/red]"
135+
cmd = f"[red]{escape(step.command)}[/red]"
127136
elif step.status == StepStatus.SKIPPED:
128137
continue # Don't show skipped steps
129138
else: # PENDING
130139
icon = "[dim]\u25cb[/dim]"
131-
cmd = f"[dim]{step.command}[/dim]"
140+
cmd = f"[dim]{escape(step.command)}[/dim]"
132141
lines.append(f"{icon} {cmd}")
133142

134143
if lines:
135144
content = Text.from_markup("\n".join(lines))
136145
title = f"Release Progress ({progress})"
137146
_print_renderable(Panel(content, title=title, border_style="red"))
138147

148+
for step in tracker.steps:
149+
if step.status == StepStatus.FAILED:
150+
console.print()
151+
console.print("[bold]To retry the failed step, run:[/bold]", highlight=False)
152+
console.print(f" {step.command}", highlight=False, markup=False, soft_wrap=True)
153+
139154

140155
__all__ = [
141156
"create_release",
@@ -671,6 +686,8 @@ def _fail_step_and_raise(step_name: str, exc: Exception) -> NoReturn:
671686
command.append("--latest=false")
672687
confirmation_action = "gh release create"
673688

689+
tracker.update_command("publish", shlex.join(command))
690+
674691
if not assume_yes:
675692
prompt_question = f"Publish {manifest.version} to GitHub repository {config.repository}?"
676693
log_info(prompt_question.lower())

tests/test_cli.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1883,6 +1883,84 @@ def fake_run(
18831883
assert commands[0][:3] == ["/usr/bin/gh", "release", "view"]
18841884

18851885

1886+
def test_release_publish_retry_hint_preserves_bracketed_title(
1887+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
1888+
) -> None:
1889+
runner = CliRunner()
1890+
project_dir = tmp_path / "project"
1891+
project_dir.mkdir()
1892+
1893+
add_entry = runner.invoke(
1894+
cli,
1895+
[
1896+
"--root",
1897+
str(project_dir),
1898+
"add",
1899+
"--title",
1900+
"Retry Hint Test",
1901+
"--type",
1902+
"feature",
1903+
"--description",
1904+
"Ensures retry hint output is copy-paste safe.",
1905+
"--author",
1906+
"codex",
1907+
],
1908+
)
1909+
assert add_entry.exit_code == 0, add_entry.output
1910+
1911+
create_release = runner.invoke(
1912+
cli,
1913+
[
1914+
"--root",
1915+
str(project_dir),
1916+
"release",
1917+
"create",
1918+
"v3.1.0",
1919+
"--title",
1920+
"[LTS] Stable",
1921+
"--yes",
1922+
],
1923+
)
1924+
assert create_release.exit_code == 0, create_release.output
1925+
1926+
config_path = project_dir / "config.yaml"
1927+
config_data = yaml.safe_load(config_path.read_text(encoding="utf-8"))
1928+
config_data["repository"] = "tenzir/example"
1929+
config_path.write_text(yaml.safe_dump(config_data, sort_keys=False), encoding="utf-8")
1930+
1931+
def fake_which(command: str) -> str:
1932+
assert command == "gh"
1933+
return "/usr/bin/gh"
1934+
1935+
def fake_run(
1936+
args: list[str], *, check: bool, stdout: object = None, stderr: object = None
1937+
) -> None:
1938+
if len(args) >= 3 and args[1:3] == ["release", "view"]:
1939+
raise subprocess.CalledProcessError(returncode=1, cmd=args)
1940+
if len(args) >= 3 and args[1:3] == ["release", "create"]:
1941+
raise subprocess.CalledProcessError(returncode=23, cmd=args)
1942+
raise AssertionError(f"Unexpected command: {args}")
1943+
1944+
monkeypatch.setattr("tenzir_ship.cli._release.shutil.which", fake_which)
1945+
monkeypatch.setattr("tenzir_ship.cli._release.subprocess.run", fake_run)
1946+
1947+
publish_result = runner.invoke(
1948+
cli,
1949+
[
1950+
"--root",
1951+
str(project_dir),
1952+
"release",
1953+
"publish",
1954+
"v3.1.0",
1955+
"--yes",
1956+
],
1957+
)
1958+
assert publish_result.exit_code != 0
1959+
plain_output = click.utils.strip_ansi(publish_result.output)
1960+
assert "To retry the failed step, run:" in plain_output
1961+
assert "--title '[LTS] Stable'" in plain_output
1962+
1963+
18861964
def test_release_publish_updates_existing_release(
18871965
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
18881966
) -> None:

0 commit comments

Comments
 (0)