fix: address release workflow review feedback

This commit is contained in:
Soulter
2026-06-19 15:29:48 +08:00
parent a2567a202e
commit 51bb487346
3 changed files with 226 additions and 39 deletions

View File

@@ -3,6 +3,75 @@
from pathlib import Path
def _read_quoted_value(value: str, field_name: str) -> tuple[str, str]:
"""Read one quoted TOML string value and return its tail.
Args:
value: Raw value text that starts with a quoted string.
field_name: Field name used in error messages.
Returns:
A tuple containing the unquoted string and the remaining text.
Raises:
ValueError: The value is not a supported quoted string.
"""
value = value.strip()
if len(value) < 2 or value[0] not in ("'", '"'):
raise ValueError(f"Unsupported {field_name} value")
quote = value[0]
end_index = value.find(quote, 1)
if end_index == -1:
raise ValueError(f"Unterminated {field_name} string")
result = value[1:end_index]
if not result:
raise ValueError(f"Empty {field_name} value")
return result, value[end_index + 1 :].strip()
def _read_dependency_array(raw_value: str) -> list[str]:
"""Read a simple inline TOML string array.
Args:
raw_value: Raw dependency array text, including the surrounding brackets.
Returns:
Parsed dependency strings.
Raises:
ValueError: The array is missing brackets or contains unsupported entries.
"""
value = raw_value.strip()
if not value.startswith("["):
raise ValueError("Unsupported project.dependencies value")
dependencies = []
value = value[1:].strip()
while value:
if value.startswith("]"):
tail = value[1:].strip()
if tail and not tail.startswith("#"):
raise ValueError("Unsupported content after project.dependencies")
return dependencies
dependency, tail = _read_quoted_value(value, "project.dependencies entry")
dependencies.append(dependency)
if tail.startswith(","):
value = tail[1:].strip()
continue
if tail.startswith("]"):
value = tail
continue
if tail:
raise ValueError("Unsupported content after project.dependencies entry")
raise ValueError("Unterminated project.dependencies array")
raise ValueError("Unterminated project.dependencies array")
def read_pyproject_project_version(pyproject_path: Path) -> str:
"""Read the project version from a pyproject.toml file.
@@ -35,22 +104,9 @@ def read_pyproject_project_version(pyproject_path: Path) -> str:
if not separator:
raise ValueError("Missing value separator for project.version")
value = raw_value.strip()
if len(value) < 2 or value[0] not in ("'", '"'):
raise ValueError("Unsupported project.version value")
quote = value[0]
end_index = value.find(quote, 1)
if end_index == -1:
raise ValueError("Unterminated project.version string")
tail = value[end_index + 1 :].strip()
version, tail = _read_quoted_value(raw_value, "project.version")
if tail and not tail.startswith("#"):
raise ValueError("Unsupported content after project.version")
version = value[1:end_index]
if not version:
raise ValueError("Empty project.version value")
return version
raise ValueError("Missing project.version")
@@ -85,24 +141,21 @@ def read_pyproject_project_dependencies(pyproject_path: Path) -> list[str]:
raise ValueError("Unsupported content after project.dependencies")
return dependencies
value = line.rstrip(",").strip()
if len(value) < 2 or value[0] not in ("'", '"'):
raise ValueError("Unsupported project.dependencies entry")
quote = value[0]
end_index = value.find(quote, 1)
if end_index == -1:
raise ValueError("Unterminated project.dependencies entry")
tail = value[end_index + 1 :].strip()
dependency, tail = _read_quoted_value(
line,
"project.dependencies entry",
)
if tail.startswith(","):
tail = tail[1:].strip()
if tail.startswith("]"):
tail = tail[1:].strip()
dependencies.append(dependency)
if tail and not tail.startswith("#"):
raise ValueError("Unsupported content after project.dependencies")
return dependencies
if tail and not tail.startswith("#"):
raise ValueError("Unsupported content after project.dependencies entry")
dependency = value[1:end_index]
if not dependency:
raise ValueError("Empty project.dependencies entry")
dependencies.append(dependency)
continue
@@ -116,9 +169,15 @@ def read_pyproject_project_dependencies(pyproject_path: Path) -> list[str]:
key, separator, raw_value = line.partition("=")
if key.strip() != "dependencies":
continue
if not separator or raw_value.strip() != "[":
if not separator:
raise ValueError("Unsupported project.dependencies value")
in_dependencies_array = True
raw_value = raw_value.strip()
if raw_value == "[" or raw_value.startswith("[ #"):
in_dependencies_array = True
continue
if raw_value.startswith("["):
return _read_dependency_array(raw_value)
raise ValueError("Unsupported project.dependencies value")
if in_dependencies_array:
raise ValueError("Unterminated project.dependencies array")

View File

@@ -121,15 +121,10 @@ def latest_tag() -> str:
Returns:
The latest tag name, or an empty string when the repository has no tags.
"""
result = subprocess.run(
["git", "describe", "--tags", "--abbrev=0"],
cwd=REPO_ROOT,
capture_output=True,
text=True,
)
if result.returncode != 0:
try:
return git(["describe", "--tags", "--abbrev=0"], capture_output=True)
except ReleaseError:
return ""
return result.stdout.strip()
def release_commits(tag: str) -> list[str]:
@@ -174,9 +169,15 @@ def update_pyproject_version(version: str) -> Path:
if stripped.startswith("[") and stripped.endswith("]"):
in_project_section = stripped == "[project]"
continue
if not in_project_section or not stripped.startswith("version"):
if not in_project_section:
continue
key, separator, _raw_value = stripped.partition("=")
if key.strip() != "version":
continue
if not separator:
raise ReleaseError("Unsupported pyproject.toml project.version format")
match = re.match(
r"^(\s*version\s*=\s*)([\"'])(.*?)(\2)(\s*(?:#.*)?)(\n?)$",
line,
@@ -209,6 +210,7 @@ def write_changelog(version: str, commits: list[str]) -> Path:
if changelog_path.exists():
raise ReleaseError(f"Changelog already exists: {changelog_path}")
changelog_path.parent.mkdir(parents=True, exist_ok=True)
entries = [f"- {commit}" for commit in commits] or ["- "]
changelog_path.write_text(
"\n".join(
@@ -270,7 +272,7 @@ def run_validation(args: argparse.Namespace) -> None:
run_command(["pnpm", "generate:api"], cwd=REPO_ROOT / "dashboard")
if not args.skip_checks:
run_command(["uv", "run", "ruff", "format", "."])
run_command(["uv", "run", "ruff", "format", "--check", "."])
run_command(["uv", "run", "ruff", "check", "."])
if args.dashboard_build:

View File

@@ -27,6 +27,64 @@ def test_read_pyproject_project_version_reads_project_section(tmp_path: Path) ->
assert read_pyproject_project_version(pyproject_path) == "1.2.3-beta.4"
@pytest.mark.parametrize(
("version_line", "expected"),
[
('version = "1.2.3"', "1.2.3"),
("version='1.2.3-beta.4'", "1.2.3-beta.4"),
(' version = "1.2.3-rc.1" ', "1.2.3-rc.1"),
],
)
def test_read_pyproject_project_version_accepts_simple_variants(
tmp_path: Path,
version_line: str,
expected: str,
) -> None:
pyproject_path = tmp_path / "pyproject.toml"
pyproject_path.write_text(
"\n".join(
[
"[project]",
'name = "AstrBot"',
version_line,
]
),
encoding="utf-8",
)
assert read_pyproject_project_version(pyproject_path) == expected
@pytest.mark.parametrize(
("version_line", "message"),
[
("version", "Missing value separator for project.version"),
('version = "1.2.3', "Unterminated project.version string"),
('version = "1.2.3" extra', "Unsupported content after project.version"),
('version = ""', "Empty project.version value"),
],
)
def test_read_pyproject_project_version_rejects_invalid_values(
tmp_path: Path,
version_line: str,
message: str,
) -> None:
pyproject_path = tmp_path / "pyproject.toml"
pyproject_path.write_text(
"\n".join(
[
"[project]",
'name = "AstrBot"',
version_line,
]
),
encoding="utf-8",
)
with pytest.raises(ValueError, match=message):
read_pyproject_project_version(pyproject_path)
def test_read_pyproject_project_dependencies_reads_multiline_array(
tmp_path: Path,
) -> None:
@@ -50,6 +108,74 @@ def test_read_pyproject_project_dependencies_reads_multiline_array(
]
@pytest.mark.parametrize(
("dependency_line", "expected"),
[
("dependencies = []", []),
('dependencies = ["aiohttp>=3.11.18"]', ["aiohttp>=3.11.18"]),
(
'dependencies = ["psutil>=5.8.0,<7.2.0", "httpx[socks]>=0.28.1"]',
["psutil>=5.8.0,<7.2.0", "httpx[socks]>=0.28.1"],
),
],
)
def test_read_pyproject_project_dependencies_accepts_inline_arrays(
tmp_path: Path,
dependency_line: str,
expected: list[str],
) -> None:
pyproject_path = tmp_path / "pyproject.toml"
pyproject_path.write_text(
"\n".join(
[
"[project]",
dependency_line,
]
),
encoding="utf-8",
)
assert read_pyproject_project_dependencies(pyproject_path) == expected
@pytest.mark.parametrize(
("project_lines", "message"),
[
(["[project]", 'name = "AstrBot"'], "Missing project.dependencies"),
(
["[project]", "dependencies = ["],
"Unterminated project.dependencies array",
),
(
["[project]", 'dependencies = "aiohttp>=3.11.18"'],
"Unsupported project.dependencies value",
),
(
["[project]", "dependencies = [", " aiohttp>=3.11.18,", "]"],
"Unsupported project.dependencies entry value",
),
(
["[project]", "dependencies = [", ' "aiohttp>=3.11.18" extra', "]"],
"Unsupported content after project.dependencies entry",
),
(
["[project]", "dependencies = [", ' ""', "]"],
"Empty project.dependencies entry value",
),
],
)
def test_read_pyproject_project_dependencies_rejects_invalid_values(
tmp_path: Path,
project_lines: list[str],
message: str,
) -> None:
pyproject_path = tmp_path / "pyproject.toml"
pyproject_path.write_text("\n".join(project_lines), encoding="utf-8")
with pytest.raises(ValueError, match=message):
read_pyproject_project_dependencies(pyproject_path)
def test_read_pyproject_project_version_raises_when_missing(tmp_path: Path) -> None:
pyproject_path = tmp_path / "pyproject.toml"
pyproject_path.write_text('[project]\nname = "AstrBot"\n', encoding="utf-8")