ci: publish formal version from pyproject.toml on main push
All checks were successful
Publish Python Package to Gitea / publish (push) Successful in 25s
All checks were successful
Publish Python Package to Gitea / publish (push) Successful in 25s
Remove .postN CI versioning, fix runtime version_ge for legacy post releases, and use ASCII issue separators in health output. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
7
.github/workflows/publish.yml
vendored
7
.github/workflows/publish.yml
vendored
@@ -22,13 +22,6 @@ jobs:
|
||||
steps:
|
||||
- uses: https://git.jc2009.com/admin/actions-checkout@v4
|
||||
|
||||
- name: Resolve and patch package version
|
||||
env:
|
||||
GITHUB_REF: ${{ github.ref }}
|
||||
GITHUB_RUN_NUMBER: ${{ github.run_number }}
|
||||
GITHUB_RUN_ID: ${{ github.run_id }}
|
||||
run: python3.12 tools/ci_set_package_version.py
|
||||
|
||||
- name: Install build tools
|
||||
run: python3.12 -m pip install --upgrade build twine --break-system-packages
|
||||
|
||||
|
||||
20
README.md
20
README.md
@@ -142,17 +142,18 @@ run_screencast(
|
||||
|
||||
## 5. 发版流程(维护者)
|
||||
|
||||
### main 分支自动预发布
|
||||
### 发布规则
|
||||
|
||||
每次推送到 `main` 会触发 `.github/workflows/publish.yml`,自动构建并发布 **post 预发布版本**(仅供 CI 流水线内部使用,**不是**正式版本线)。
|
||||
|
||||
正式版本线使用 **1.0.x** semver,通过 git tag 发布。
|
||||
- jiangchang-platform-kit **只发布正式版本**。
|
||||
- 每次需要发布公共能力时,先 bump `pyproject.toml` 的 `version`(例如 `1.0.10` → `1.0.11`)。
|
||||
- 推送 `main` 后,`.github/workflows/publish.yml` 会构建并上传该正式版本到 Gitea PyPI。
|
||||
- 发布包版本必须等于 `pyproject.toml` 中的 `version`;若该版本已存在,上传会失败,需继续 bump 版本号后再推送。
|
||||
|
||||
```bash
|
||||
git push origin main
|
||||
```
|
||||
|
||||
测试人员升级安装(始终安装当前正式 tag 对应的版本,例如 `1.0.9`):
|
||||
升级安装:
|
||||
|
||||
```bash
|
||||
python -m pip install --upgrade \
|
||||
@@ -161,15 +162,6 @@ python -m pip install --upgrade \
|
||||
jiangchang-platform-kit
|
||||
```
|
||||
|
||||
### 正式版本(tag)
|
||||
|
||||
打 tag 并推送后,发布 **去掉 `v` 前缀** 的正式版本号(例如 `v1.0.9` → PyPI 包 `1.0.9`):
|
||||
|
||||
```bash
|
||||
git tag v1.0.9
|
||||
git push origin v1.0.9
|
||||
```
|
||||
|
||||
### 安装后验证
|
||||
|
||||
```bash
|
||||
|
||||
@@ -23,7 +23,7 @@ __all__ = [
|
||||
"LaunchError",
|
||||
"GatewayDownError",
|
||||
]
|
||||
__version__ = "1.0.9"
|
||||
__version__ = "1.0.11"
|
||||
|
||||
|
||||
def __getattr__(name: str):
|
||||
|
||||
@@ -71,9 +71,23 @@ def _resolve_data_root(env: Mapping[str, str]) -> str:
|
||||
return platform_default_data_root()
|
||||
|
||||
|
||||
def _parse_version(version: str) -> tuple[int, ...]:
|
||||
def _parse_version(version: str) -> tuple[int, int, int, int]:
|
||||
"""Parse major.minor.patch[.postN] into (major, minor, patch, post)."""
|
||||
text = version.strip()
|
||||
post = 0
|
||||
if ".post" in text:
|
||||
base, _, post_part = text.partition(".post")
|
||||
post_digits = ""
|
||||
for ch in post_part:
|
||||
if ch.isdigit():
|
||||
post_digits += ch
|
||||
else:
|
||||
break
|
||||
post = int(post_digits) if post_digits else 0
|
||||
text = base
|
||||
|
||||
parts: list[int] = []
|
||||
for piece in version.strip().split("."):
|
||||
for piece in text.split("."):
|
||||
digits = ""
|
||||
for ch in piece:
|
||||
if ch.isdigit():
|
||||
@@ -82,11 +96,15 @@ def _parse_version(version: str) -> tuple[int, ...]:
|
||||
break
|
||||
if digits:
|
||||
parts.append(int(digits))
|
||||
return tuple(parts)
|
||||
|
||||
while len(parts) < 3:
|
||||
parts.append(0)
|
||||
|
||||
return parts[0], parts[1], parts[2], post
|
||||
|
||||
|
||||
def version_ge(installed: str, required: str) -> bool:
|
||||
"""Compare PEP 440-like versions without external deps (post release suffix ignored)."""
|
||||
"""Compare semver-like versions without external deps (supports legacy .postN)."""
|
||||
return _parse_version(installed) >= _parse_version(required)
|
||||
|
||||
|
||||
@@ -281,7 +299,7 @@ def format_runtime_health_lines(diagnostics: RuntimeDiagnostics) -> list[str]:
|
||||
f"record_video_enabled: {diagnostics.record_video_enabled}",
|
||||
]
|
||||
for issue in diagnostics.issues:
|
||||
lines.append(f"runtime_issue[{issue.severity}]: {issue.code} — {issue.message}")
|
||||
lines.append(f"runtime_issue[{issue.severity}]: {issue.code} - {issue.message}")
|
||||
return lines
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Text-level checks that reusable-release-skill packages .env.example at package root."""
|
||||
"""Text-level checks for release-related GitHub Actions workflows."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -8,14 +8,23 @@ import re
|
||||
import pytest
|
||||
|
||||
_REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
||||
_WORKFLOW_PATH = os.path.join(
|
||||
_SKILL_WORKFLOW_PATH = os.path.join(
|
||||
_REPO_ROOT, ".github", "workflows", "reusable-release-skill.yaml"
|
||||
)
|
||||
_PUBLISH_WORKFLOW_PATH = os.path.join(
|
||||
_REPO_ROOT, ".github", "workflows", "publish.yml"
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def workflow_text() -> str:
|
||||
with open(_WORKFLOW_PATH, encoding="utf-8") as f:
|
||||
with open(_SKILL_WORKFLOW_PATH, encoding="utf-8") as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def publish_workflow_text() -> str:
|
||||
with open(_PUBLISH_WORKFLOW_PATH, encoding="utf-8") as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
@@ -40,3 +49,18 @@ def test_workflow_does_not_package_dot_env(workflow_text: str) -> None:
|
||||
r"os\.path\.join\(PACKAGE,\s*['\"]\.env['\"]\)",
|
||||
workflow_text,
|
||||
)
|
||||
|
||||
|
||||
def test_publish_workflow_triggers_on_main_only(publish_workflow_text: str) -> None:
|
||||
assert "branches:" in publish_workflow_text
|
||||
assert "- main" in publish_workflow_text
|
||||
assert "tags:" not in publish_workflow_text
|
||||
|
||||
|
||||
def test_publish_workflow_uses_pyproject_version_only(publish_workflow_text: str) -> None:
|
||||
assert "ci_set_package_version" not in publish_workflow_text
|
||||
assert "GITHUB_RUN_NUMBER" not in publish_workflow_text
|
||||
assert "GITHUB_RUN_ID" not in publish_workflow_text
|
||||
assert ".post" not in publish_workflow_text
|
||||
assert "python3.12 -m build" in publish_workflow_text
|
||||
assert "twine upload" in publish_workflow_text
|
||||
|
||||
@@ -25,6 +25,11 @@ def test_version_ge_post_release() -> None:
|
||||
assert version_ge("1.0.10.post23", "1.0.10") is True
|
||||
assert version_ge("1.0.10", "1.0.10") is True
|
||||
assert version_ge("1.0.9.post19", "1.0.10") is False
|
||||
assert version_ge("1.0.10.post23", "1.0.10.post21") is True
|
||||
assert version_ge("1.0.10.post21", "1.0.10.post23") is False
|
||||
assert version_ge("1.0.10", "1.0.10.post1") is False
|
||||
assert version_ge("1.0.11", "1.0.10.post99") is True
|
||||
assert version_ge("1.0.11", "1.0.10") is True
|
||||
|
||||
|
||||
def test_is_jiangchang_skill_core_from_skill_tree(tmp_path: Path) -> None:
|
||||
@@ -311,3 +316,32 @@ def test_runtime_diagnostics_frozen_dataclass() -> None:
|
||||
)
|
||||
assert diag.has_fatal_issues is False
|
||||
assert diag.issue_codes() == ["ffmpeg_unavailable"]
|
||||
|
||||
|
||||
def test_format_runtime_health_lines_issue_separator_ascii() -> None:
|
||||
diag = RuntimeDiagnostics(
|
||||
skill_slug="x",
|
||||
python_executable="python",
|
||||
platform_kit_version="1.0.11",
|
||||
platform_kit_min_version=None,
|
||||
platform_kit_version_ok=None,
|
||||
jiangchang_skill_core_file=None,
|
||||
claw_data_root=None,
|
||||
jiangchang_data_root=None,
|
||||
resolved_data_root="/tmp",
|
||||
media_assets_root="/tmp/media",
|
||||
ffmpeg_available=False,
|
||||
ffmpeg_path=None,
|
||||
background_music_mp3_count=0,
|
||||
background_music_usable_count=0,
|
||||
background_music_issue=None,
|
||||
background_music_sample_path=None,
|
||||
record_video_enabled=False,
|
||||
issues=(RuntimeIssue(code="ffmpeg_unavailable", message="no ffmpeg"),),
|
||||
)
|
||||
lines = format_runtime_health_lines(diag)
|
||||
issue_lines = [line for line in lines if line.startswith("runtime_issue[")]
|
||||
assert len(issue_lines) == 1
|
||||
assert " - " in issue_lines[0]
|
||||
assert "\u2014" not in issue_lines[0]
|
||||
assert "\u2013" not in issue_lines[0]
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
"""Patch pyproject.toml and __init__.py version for CI builds (not committed)."""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
def _read_base_version() -> str:
|
||||
text = (ROOT / "pyproject.toml").read_text(encoding="utf-8")
|
||||
match = re.search(r'^version\s*=\s*"([^"]+)"', text, re.MULTILINE)
|
||||
if not match:
|
||||
raise SystemExit("ERROR: version not found in pyproject.toml")
|
||||
base = match.group(1)
|
||||
return re.sub(r"\.post\d+$", "", base)
|
||||
|
||||
|
||||
def resolve_version() -> str:
|
||||
ref = os.environ.get("GITHUB_REF", "")
|
||||
if ref.startswith("refs/tags/v"):
|
||||
return ref.removeprefix("refs/tags/v")
|
||||
if ref.startswith("refs/tags/"):
|
||||
return ref.removeprefix("refs/tags/")
|
||||
|
||||
run_number = (
|
||||
os.environ.get("GITHUB_RUN_NUMBER")
|
||||
or os.environ.get("GITHUB_RUN_ID")
|
||||
or "0"
|
||||
)
|
||||
base = _read_base_version()
|
||||
return f"{base}.post{run_number}"
|
||||
|
||||
|
||||
def patch_version(version: str) -> None:
|
||||
pyproject = ROOT / "pyproject.toml"
|
||||
py_text = pyproject.read_text(encoding="utf-8")
|
||||
py_text, n = re.subn(
|
||||
r'^version\s*=\s*"[^"]*"',
|
||||
f'version = "{version}"',
|
||||
py_text,
|
||||
count=1,
|
||||
flags=re.MULTILINE,
|
||||
)
|
||||
if n != 1:
|
||||
raise SystemExit("ERROR: failed to patch version in pyproject.toml")
|
||||
pyproject.write_text(py_text, encoding="utf-8")
|
||||
|
||||
init_py = ROOT / "src" / "jiangchang_desktop_sdk" / "__init__.py"
|
||||
init_text = init_py.read_text(encoding="utf-8")
|
||||
init_text, n = re.subn(
|
||||
r'^__version__\s*=\s*"[^"]*"',
|
||||
f'__version__ = "{version}"',
|
||||
init_text,
|
||||
count=1,
|
||||
flags=re.MULTILINE,
|
||||
)
|
||||
if n != 1:
|
||||
raise SystemExit("ERROR: failed to patch __version__ in __init__.py")
|
||||
init_py.write_text(init_text, encoding="utf-8")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
version = resolve_version()
|
||||
print(f"Resolved package version: {version}")
|
||||
patch_version(version)
|
||||
if os.environ.get("GITHUB_OUTPUT"):
|
||||
with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as fh:
|
||||
fh.write(f"package_version={version}\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
main()
|
||||
except SystemExit as exc:
|
||||
print(exc, file=sys.stderr)
|
||||
raise
|
||||
Reference in New Issue
Block a user