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:
|
steps:
|
||||||
- uses: https://git.jc2009.com/admin/actions-checkout@v4
|
- 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
|
- name: Install build tools
|
||||||
run: python3.12 -m pip install --upgrade build twine --break-system-packages
|
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. 发版流程(维护者)
|
## 5. 发版流程(维护者)
|
||||||
|
|
||||||
### main 分支自动预发布
|
### 发布规则
|
||||||
|
|
||||||
每次推送到 `main` 会触发 `.github/workflows/publish.yml`,自动构建并发布 **post 预发布版本**(仅供 CI 流水线内部使用,**不是**正式版本线)。
|
- jiangchang-platform-kit **只发布正式版本**。
|
||||||
|
- 每次需要发布公共能力时,先 bump `pyproject.toml` 的 `version`(例如 `1.0.10` → `1.0.11`)。
|
||||||
正式版本线使用 **1.0.x** semver,通过 git tag 发布。
|
- 推送 `main` 后,`.github/workflows/publish.yml` 会构建并上传该正式版本到 Gitea PyPI。
|
||||||
|
- 发布包版本必须等于 `pyproject.toml` 中的 `version`;若该版本已存在,上传会失败,需继续 bump 版本号后再推送。
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git push origin main
|
git push origin main
|
||||||
```
|
```
|
||||||
|
|
||||||
测试人员升级安装(始终安装当前正式 tag 对应的版本,例如 `1.0.9`):
|
升级安装:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python -m pip install --upgrade \
|
python -m pip install --upgrade \
|
||||||
@@ -161,15 +162,6 @@ python -m pip install --upgrade \
|
|||||||
jiangchang-platform-kit
|
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
|
```bash
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ __all__ = [
|
|||||||
"LaunchError",
|
"LaunchError",
|
||||||
"GatewayDownError",
|
"GatewayDownError",
|
||||||
]
|
]
|
||||||
__version__ = "1.0.9"
|
__version__ = "1.0.11"
|
||||||
|
|
||||||
|
|
||||||
def __getattr__(name: str):
|
def __getattr__(name: str):
|
||||||
|
|||||||
@@ -71,9 +71,23 @@ def _resolve_data_root(env: Mapping[str, str]) -> str:
|
|||||||
return platform_default_data_root()
|
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] = []
|
parts: list[int] = []
|
||||||
for piece in version.strip().split("."):
|
for piece in text.split("."):
|
||||||
digits = ""
|
digits = ""
|
||||||
for ch in piece:
|
for ch in piece:
|
||||||
if ch.isdigit():
|
if ch.isdigit():
|
||||||
@@ -82,11 +96,15 @@ def _parse_version(version: str) -> tuple[int, ...]:
|
|||||||
break
|
break
|
||||||
if digits:
|
if digits:
|
||||||
parts.append(int(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:
|
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)
|
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}",
|
f"record_video_enabled: {diagnostics.record_video_enabled}",
|
||||||
]
|
]
|
||||||
for issue in diagnostics.issues:
|
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
|
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
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -8,14 +8,23 @@ import re
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
_REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
_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"
|
_REPO_ROOT, ".github", "workflows", "reusable-release-skill.yaml"
|
||||||
)
|
)
|
||||||
|
_PUBLISH_WORKFLOW_PATH = os.path.join(
|
||||||
|
_REPO_ROOT, ".github", "workflows", "publish.yml"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="module")
|
@pytest.fixture(scope="module")
|
||||||
def workflow_text() -> str:
|
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()
|
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['\"]\)",
|
r"os\.path\.join\(PACKAGE,\s*['\"]\.env['\"]\)",
|
||||||
workflow_text,
|
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.post23", "1.0.10") is True
|
||||||
assert version_ge("1.0.10", "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.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:
|
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.has_fatal_issues is False
|
||||||
assert diag.issue_codes() == ["ffmpeg_unavailable"]
|
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