mirror of
https://github.com/feigeCode/navop.git
synced 2026-09-22 16:01:30 +00:00
只改 CHANGELOG.md / main/Cargo.toml / Cargo.lock 的发布 PR 此前仍要跑 macOS / Linux / Windows 三平台全量测试(Windows job 约 25 分钟),而改动里 没有任何代码,这些测试没有意义。 - ci.yml 新增 classify job:PR 场景下用 script/release_pr.py 判定是否只包含 发布改动,是则跳过 test 与 windows-rdp-probe,改为就地校验发布元数据 - script/release_pr.py:分类规则为「改动文件全部属于发布白名单」且 「Cargo.toml / Cargo.lock 的差异只有 version = "..." 行」;命中后校验 main/Cargo.toml 与 Cargo.lock 中 main 包版本一致,且该版本的双语 changelog 条目完整(更新内容/修复与优化 + What's New/Fixes and Improvements,含 CNB 镜像行);无法算出 diff 时回退到全量测试 - ci-gate 仍是唯一必需状态检查,分类或校验失败都会拦住合并 - .github/RELEASE.md 记录该快速通道 - windows_rdp_host 契约测试钉住 classify/needs/if 结构与脚本白名单 验证: - python3 -m unittest discover -s script/tests -p "test_release_pr.py" — 16 passed - cargo test -p windows_rdp_host --test contract — 40 passed - ruby -ryaml 解析 ci.yml,核对 classify/test/windows-rdp-probe/ci-gate 的 needs 与 if 表达式 - rustfmt --edition 2024 --check crates/windows_rdp_host/tests/contract.rs
242 lines
7.4 KiB
Python
242 lines
7.4 KiB
Python
from __future__ import annotations
|
|
|
|
import argparse
|
|
import importlib.util
|
|
from pathlib import Path
|
|
import subprocess
|
|
import tempfile
|
|
import unittest
|
|
|
|
|
|
SCRIPT_PATH = Path(__file__).resolve().parents[1] / "release_pr.py"
|
|
SPEC = importlib.util.spec_from_file_location("navop_release_pr", SCRIPT_PATH)
|
|
assert SPEC is not None and SPEC.loader is not None
|
|
release_pr = importlib.util.module_from_spec(SPEC)
|
|
SPEC.loader.exec_module(release_pr)
|
|
|
|
|
|
MANIFEST = """\
|
|
[package]
|
|
name = "main"
|
|
version = "0.0.9"
|
|
publish.workspace = true
|
|
"""
|
|
|
|
LOCK = """\
|
|
# This file is automatically @generated by Cargo.
|
|
|
|
[[package]]
|
|
name = "main"
|
|
version = "0.0.9"
|
|
dependencies = ["one-assets"]
|
|
|
|
[[package]]
|
|
name = "one-assets"
|
|
version = "3.4.5"
|
|
"""
|
|
|
|
CHANGELOG = """\
|
|
# Changelog
|
|
|
|
Navop user-facing release notes.
|
|
|
|
<!-- NAVOP_RELEASES -->
|
|
"""
|
|
|
|
ENTRY = """\
|
|
## [v0.1.0] - 2026-01-01
|
|
|
|
#### 修复与优化
|
|
|
|
- 修复了某个问题。
|
|
|
|
国内下载:如果 GitHub 下载较慢,可从 [CNB 镜像](https://cnb.cool/navop-dev/navop/-/releases/tag/v0.1.0) 下载桌面端安装包
|
|
|
|
---
|
|
|
|
#### Fixes and Improvements
|
|
|
|
- Fixed something.
|
|
"""
|
|
|
|
|
|
def git(cwd: Path, *args: str) -> None:
|
|
completed = subprocess.run(
|
|
["git", *args], cwd=cwd, capture_output=True, text=True, check=False
|
|
)
|
|
assert completed.returncode == 0, completed.stderr
|
|
|
|
|
|
def write(path: Path, content: str) -> None:
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_text(content, encoding="utf-8")
|
|
|
|
|
|
class ClassifyTests(unittest.TestCase):
|
|
def test_release_only_changes_qualify_for_the_fast_path(self) -> None:
|
|
release_only, reason = release_pr.classify(
|
|
["CHANGELOG.md", "main/Cargo.toml", "Cargo.lock"],
|
|
'--- a/main/Cargo.toml\n+++ b/main/Cargo.toml\n-version = "0.0.9"\n'
|
|
'+version = "0.1.0"\n',
|
|
)
|
|
|
|
self.assertTrue(release_only, reason)
|
|
|
|
def test_code_changes_require_the_full_matrix(self) -> None:
|
|
release_only, reason = release_pr.classify(
|
|
["CHANGELOG.md", "crates/terminal/src/wsl_distributions.rs"],
|
|
"",
|
|
)
|
|
|
|
self.assertFalse(release_only)
|
|
self.assertIn("crates/terminal/src/wsl_distributions.rs", reason)
|
|
|
|
def test_dependency_changes_require_the_full_matrix(self) -> None:
|
|
release_only, reason = release_pr.classify(
|
|
["main/Cargo.toml"],
|
|
'--- a/main/Cargo.toml\n+++ b/main/Cargo.toml\n'
|
|
'-version = "0.0.9"\n+version = "0.1.0"\n'
|
|
'+"one-assets = { workspace = true }"\n',
|
|
)
|
|
|
|
self.assertFalse(release_only)
|
|
self.assertIn("beyond the version bump", reason)
|
|
|
|
def test_empty_changes_are_not_a_release_pull_request(self) -> None:
|
|
release_only, reason = release_pr.classify([], "")
|
|
|
|
self.assertFalse(release_only)
|
|
self.assertIn("no file changed", reason)
|
|
|
|
def test_diff_headers_do_not_count_as_changes(self) -> None:
|
|
self.assertTrue(
|
|
release_pr.only_version_lines_changed(
|
|
"diff --git a/Cargo.lock b/Cargo.lock\n"
|
|
"--- a/Cargo.lock\n"
|
|
"+++ b/Cargo.lock\n"
|
|
"-version = \"0.0.9\"\n"
|
|
"+version = \"0.1.0\"\n"
|
|
)
|
|
)
|
|
|
|
|
|
class VersionTests(unittest.TestCase):
|
|
def test_manifest_version_reads_the_package_section(self) -> None:
|
|
self.assertEqual(release_pr.manifest_version(MANIFEST), "0.0.9")
|
|
|
|
def test_lock_version_reads_the_main_package(self) -> None:
|
|
self.assertEqual(release_pr.lock_main_version(LOCK), "0.0.9")
|
|
|
|
def test_missing_manifest_version_is_reported(self) -> None:
|
|
with self.assertRaises(release_pr.ReleasePrError):
|
|
release_pr.manifest_version('[workspace]\nmembers = ["main"]\n')
|
|
|
|
def test_missing_lock_package_is_reported(self) -> None:
|
|
with self.assertRaises(release_pr.ReleasePrError):
|
|
release_pr.lock_main_version('[[package]]\nname = "other"\nversion = "1.0.0"\n')
|
|
|
|
|
|
class ChangelogTests(unittest.TestCase):
|
|
def test_entry_for_the_bumped_version_validates(self) -> None:
|
|
release_pr.validate_changelog_entry(f"{CHANGELOG}\n{ENTRY}", "0.1.0")
|
|
|
|
def test_missing_entry_is_reported(self) -> None:
|
|
with self.assertRaises(release_pr.ReleasePrError):
|
|
release_pr.validate_changelog_entry(CHANGELOG, "0.1.0")
|
|
|
|
|
|
class CheckCommandTests(unittest.TestCase):
|
|
def setUp(self) -> None:
|
|
self._temporary = tempfile.TemporaryDirectory()
|
|
self.addCleanup(self._temporary.cleanup)
|
|
self.root = Path(self._temporary.name)
|
|
git(self.root, "init", "-b", "main")
|
|
git(self.root, "config", "user.email", "release@example.com")
|
|
git(self.root, "config", "user.name", "Release Test")
|
|
write(self.root / "main/Cargo.toml", MANIFEST)
|
|
write(self.root / "Cargo.lock", LOCK)
|
|
write(self.root / "CHANGELOG.md", CHANGELOG)
|
|
git(self.root, "add", ".")
|
|
git(self.root, "commit", "-m", "base")
|
|
|
|
def _release_commit(self) -> None:
|
|
write(
|
|
self.root / "main/Cargo.toml",
|
|
MANIFEST.replace('version = "0.0.9"', 'version = "0.1.0"'),
|
|
)
|
|
write(
|
|
self.root / "Cargo.lock",
|
|
LOCK.replace('version = "0.0.9"', 'version = "0.1.0"'),
|
|
)
|
|
write(self.root / "CHANGELOG.md", f"{CHANGELOG}\n{ENTRY}")
|
|
git(self.root, "commit", "-am", "release")
|
|
|
|
def _check(self, base: str = "HEAD~1") -> None:
|
|
release_pr.command_check(
|
|
argparse.Namespace(
|
|
base=base, head="HEAD", root=self.root, github_output=""
|
|
)
|
|
)
|
|
|
|
def test_release_commit_is_classified_and_validated(self) -> None:
|
|
self._release_commit()
|
|
|
|
self._check()
|
|
|
|
def test_release_commit_publishes_the_release_output(self) -> None:
|
|
self._release_commit()
|
|
output = self.root / "github-output.txt"
|
|
|
|
release_pr.command_check(
|
|
argparse.Namespace(
|
|
base="HEAD~1",
|
|
head="HEAD",
|
|
root=self.root,
|
|
github_output=str(output),
|
|
)
|
|
)
|
|
|
|
self.assertEqual(
|
|
output.read_text(encoding="utf-8"),
|
|
"release_only=true\nversion=0.1.0\n",
|
|
)
|
|
|
|
def test_unavailable_diff_falls_back_to_the_full_matrix(self) -> None:
|
|
output = self.root / "github-output.txt"
|
|
|
|
release_pr.command_check(
|
|
argparse.Namespace(
|
|
base="does-not-exist",
|
|
head="HEAD",
|
|
root=self.root,
|
|
github_output=str(output),
|
|
)
|
|
)
|
|
|
|
self.assertFalse(output.exists(), "fallback must not skip the matrix")
|
|
|
|
def test_version_mismatch_fails_the_release_check(self) -> None:
|
|
write(
|
|
self.root / "main/Cargo.toml",
|
|
MANIFEST.replace('version = "0.0.9"', 'version = "0.1.0"'),
|
|
)
|
|
write(self.root / "CHANGELOG.md", f"{CHANGELOG}\n{ENTRY}")
|
|
git(self.root, "commit", "-am", "release with a stale lock")
|
|
|
|
with self.assertRaises(release_pr.ReleasePrError) as error:
|
|
self._check()
|
|
|
|
self.assertIn("Cargo.lock pins main at", str(error.exception))
|
|
|
|
def test_code_change_is_not_a_release_pull_request(self) -> None:
|
|
self._release_commit()
|
|
write(self.root / "crates/terminal/src/lib.rs", "pub fn placeholder() {}\n")
|
|
git(self.root, "add", ".")
|
|
git(self.root, "commit", "-m", "feature")
|
|
|
|
self._check(base="HEAD~2")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|