diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 9a54036..6431c02 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -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 diff --git a/README.md b/README.md index 7dcb6f6..890bd59 100644 --- a/README.md +++ b/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 diff --git a/src/jiangchang_desktop_sdk/__init__.py b/src/jiangchang_desktop_sdk/__init__.py index e85a637..ef9f258 100644 --- a/src/jiangchang_desktop_sdk/__init__.py +++ b/src/jiangchang_desktop_sdk/__init__.py @@ -23,7 +23,7 @@ __all__ = [ "LaunchError", "GatewayDownError", ] -__version__ = "1.0.9" +__version__ = "1.0.11" def __getattr__(name: str): diff --git a/src/jiangchang_skill_core/runtime_diagnostics.py b/src/jiangchang_skill_core/runtime_diagnostics.py index 620c3fe..4ad32ea 100644 --- a/src/jiangchang_skill_core/runtime_diagnostics.py +++ b/src/jiangchang_skill_core/runtime_diagnostics.py @@ -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 diff --git a/tests/test_release_workflow.py b/tests/test_release_workflow.py index a482838..fc6935a 100644 --- a/tests/test_release_workflow.py +++ b/tests/test_release_workflow.py @@ -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 diff --git a/tests/test_runtime_diagnostics.py b/tests/test_runtime_diagnostics.py index b5d35b9..a46b684 100644 --- a/tests/test_runtime_diagnostics.py +++ b/tests/test_runtime_diagnostics.py @@ -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] diff --git a/tools/ci_set_package_version.py b/tools/ci_set_package_version.py deleted file mode 100644 index 5227547..0000000 --- a/tools/ci_set_package_version.py +++ /dev/null @@ -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