mirror of
https://github.com/github/spec-kit.git
synced 2026-08-03 06:26:30 +08:00
fix(cli): render the literal [suffix] in --tag help and rejection message (#3800)
Both places a user learns the `specify self upgrade --tag` syntax silently drop
the `[suffix]` token, because Rich parses the literal square brackets as a
markup tag and discards them:
rejected tag -> "Invalid --tag: expected vMAJOR.MINOR.PATCH"
(constant is "Invalid --tag: expected vMAJOR.MINOR.PATCH[suffix]")
--help -> "Pin the target version (vX.Y.Z). Without --tag, ..."
So the CLI implies a bare vX.Y.Z is the ONLY accepted form, when v1.0.0-rc1,
v0.8.0.dev0 and v0.8.0+build.42 are all valid -- and the shipped docs advertise
the suffix in four places (docs/upgrade.md x3, README.md x2).
Escape the rejection message at the PRINT site rather than baking `\[` into
_INVALID_TAG_MESSAGE: the same constant is raised through typer.BadParameter,
which Click renders without Rich, so it must stay plain text. Escape the literal
bracket in the option help, which Typer renders through Rich.
Same literal-bracket class as the existing precedents in workflows/_commands.py
(`\[disabled]`, `\[<type>]`). Static CLI text only -- no validation semantics
change and `_validate_tag` is untouched.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -27,6 +27,7 @@ from pathlib import Path
|
||||
|
||||
import typer
|
||||
from packaging.version import InvalidVersion, Version
|
||||
from rich.markup import escape as _escape_markup
|
||||
|
||||
from ._download_security import MAX_JSON_METADATA_BYTES, read_response_limited
|
||||
from ._console import console
|
||||
@@ -1230,7 +1231,10 @@ def self_upgrade(
|
||||
tag: str | None = typer.Option(
|
||||
None,
|
||||
"--tag",
|
||||
help="Pin the target version (vX.Y.Z[suffix]). Without --tag, the "
|
||||
# Typer renders help through Rich, so escape the literal bracket (\[)
|
||||
# or `[suffix]` is parsed as a style tag and dropped -- `--help` then
|
||||
# advertises only `(vX.Y.Z)`, contradicting docs/upgrade.md and README.
|
||||
help="Pin the target version (vX.Y.Z\\[suffix]). Without --tag, the "
|
||||
"latest stable release is resolved via GitHub Releases.",
|
||||
),
|
||||
) -> None:
|
||||
@@ -1270,7 +1274,14 @@ def self_upgrade(
|
||||
try:
|
||||
tag = _validate_tag(tag)
|
||||
except typer.BadParameter as exc:
|
||||
console.print(str(exc), soft_wrap=True)
|
||||
# Escape at the print site rather than baking `\[` into
|
||||
# _INVALID_TAG_MESSAGE: the message is also raised through
|
||||
# typer.BadParameter, which Click renders without Rich, so the
|
||||
# constant must stay plain text. Unescaped, Rich parses the literal
|
||||
# `[suffix]` as a style tag and drops it, leaving the user with
|
||||
# "expected vMAJOR.MINOR.PATCH" -- implying a bare vX.Y.Z is the only
|
||||
# accepted form when -rc1 / .dev0 / +build.42 are all valid.
|
||||
console.print(_escape_markup(str(exc)), soft_wrap=True)
|
||||
raise typer.Exit(1) from exc
|
||||
|
||||
plan, failure_reason = _build_upgrade_plan(target_tag_override=tag)
|
||||
|
||||
@@ -472,6 +472,26 @@ class TestTagValidation:
|
||||
output = strip_ansi(result.output)
|
||||
assert "Invalid --tag" in output or "expected vMAJOR.MINOR.PATCH" in output
|
||||
|
||||
def test_rejection_message_keeps_the_suffix_token(
|
||||
self, uv_tool_argv0, clean_environ
|
||||
):
|
||||
"""Rich must not swallow the literal `[suffix]`.
|
||||
|
||||
Unescaped it is parsed as a style tag and dropped, so the user is told
|
||||
only "expected vMAJOR.MINOR.PATCH" -- implying a bare vX.Y.Z is the only
|
||||
accepted form, when -rc1 / .dev0 / +build.42 are all valid and are
|
||||
documented as such in docs/upgrade.md and README.md.
|
||||
"""
|
||||
result = runner.invoke(app, ["self", "upgrade", "--tag", "latest"])
|
||||
assert result.exit_code == 1
|
||||
assert "expected vMAJOR.MINOR.PATCH[suffix]" in strip_ansi(result.output)
|
||||
|
||||
def test_tag_option_help_keeps_the_suffix_token(self):
|
||||
"""Typer renders option help through Rich, so `--help` dropped it too."""
|
||||
result = runner.invoke(app, ["self", "upgrade", "--help"])
|
||||
assert result.exit_code == 0
|
||||
assert "[suffix]" in strip_ansi(result.output)
|
||||
|
||||
|
||||
class TestUnknownCurrent:
|
||||
"""'unknown' current version renders literally in notice and success message."""
|
||||
|
||||
Reference in New Issue
Block a user