mirror of
https://github.com/feigeCode/navop.git
synced 2026-09-21 16:01:21 +00:00
Merge pull request #237 from feigeCode/ci/release-only-fast-path
ci: 发布 PR 只跑发布元数据校验,跳过全平台测试
This commit is contained in:
@@ -34,6 +34,17 @@ The normal release sequence is:
|
||||
|
||||
The build workflow checks out the requested tag, while the workflow itself runs from `main`. This keeps Cargo input caches and sccache data reusable across tags and repair runs.
|
||||
|
||||
## Release-only pull requests
|
||||
|
||||
A pull request that only carries the `CHANGELOG.md` entry and the version bump does not need the platform matrix. `.github/workflows/ci.yml` classifies every pull request in the `classify` job through `script/release_pr.py check`:
|
||||
|
||||
- The pull request qualifies only when every changed file is `CHANGELOG.md`, `main/Cargo.toml`, or `Cargo.lock`, and the `Cargo.toml` / `Cargo.lock` diff contains nothing but `version = "..."` lines. Any other file, or a dependency change in `main/Cargo.toml`, keeps the full matrix.
|
||||
- When it qualifies, the `test` and `windows-rdp-probe` jobs are skipped and the release metadata is validated instead: `main/Cargo.toml` and the `main` package in `Cargo.lock` must agree, and the bilingual entry for that version must exist, be complete (`更新内容` / `修复与优化` plus `What's New` / `Fixes and Improvements`) and carry the CNB mirror line.
|
||||
- `CI gate` stays the only required status check, and it still fails the pull request when the classification itself fails, so an invalid release entry cannot reach `main`.
|
||||
- If the diff cannot be computed (for example a fork pull request whose head commit was not fetched), the classifier falls back to the full matrix.
|
||||
|
||||
A release pull request keeps the fast path after `main` is merged back into `dev`, because the classifier only looks at the diff against the base branch.
|
||||
|
||||
## Branch model
|
||||
|
||||
- `dev` is the beta development branch. Changes are pushed and validated here before a release.
|
||||
|
||||
@@ -54,9 +54,30 @@ jobs:
|
||||
esac
|
||||
echo "matrix=$matrix" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# 发布 PR 只改 CHANGELOG + 版本号,跑全平台测试没有意义:这一层只做分类,
|
||||
# 并在判定为发布改动时就地校验 changelog 与版本号;其余 PR 照旧跑全量测试。
|
||||
classify:
|
||||
name: Classify changes
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
release_only: ${{ steps.check.outputs.release_only }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Classify and validate release-only changes
|
||||
id: check
|
||||
env:
|
||||
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
run: python3 script/release_pr.py check
|
||||
|
||||
test:
|
||||
name: Test (${{ matrix.platform }}, ${{ matrix.target }})
|
||||
needs: prepare
|
||||
needs: [prepare, classify]
|
||||
if: ${{ needs.classify.outputs.release_only != 'true' }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -144,11 +165,13 @@ jobs:
|
||||
|
||||
windows-rdp-probe:
|
||||
name: Windows RDP probe (${{ matrix.target }})
|
||||
needs: [classify]
|
||||
if: >-
|
||||
${{
|
||||
inputs.platform == '' ||
|
||||
inputs.platform == 'all' ||
|
||||
inputs.platform == 'windows'
|
||||
(inputs.platform == '' ||
|
||||
inputs.platform == 'all' ||
|
||||
inputs.platform == 'windows') &&
|
||||
needs.classify.outputs.release_only != 'true'
|
||||
}}
|
||||
runs-on: windows-2022
|
||||
env:
|
||||
@@ -186,7 +209,7 @@ jobs:
|
||||
|
||||
ci-gate:
|
||||
name: CI gate
|
||||
needs: [prepare, test, windows-rdp-probe]
|
||||
needs: [prepare, classify, test, windows-rdp-probe]
|
||||
if: always()
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
@@ -2802,7 +2802,7 @@ fn build_is_windows_hosted_msvc_only_and_ci_runs_host_tests() {
|
||||
);
|
||||
assert_contains_all(
|
||||
".github/workflows/ci.yml",
|
||||
&["needs: [prepare, test, windows-rdp-probe]"],
|
||||
&["needs: [prepare, classify, test, windows-rdp-probe]"],
|
||||
);
|
||||
assert_contains_all(
|
||||
".github/workflows/release.yml",
|
||||
@@ -2825,6 +2825,54 @@ fn build_is_windows_hosted_msvc_only_and_ci_runs_host_tests() {
|
||||
);
|
||||
}
|
||||
|
||||
// A release pull request only carries the CHANGELOG entry and the version bump,
|
||||
// so it must skip the platform matrix and be validated by script/release_pr.py
|
||||
// instead. `ci-gate` stays the single required check and still fails the merge
|
||||
// when the release metadata is invalid.
|
||||
#[test]
|
||||
fn release_only_pull_requests_skip_the_platform_matrix() {
|
||||
let ci = ".github/workflows/ci.yml";
|
||||
assert_tokens_in_scope(
|
||||
ci,
|
||||
" classify:",
|
||||
" test:",
|
||||
&[
|
||||
"if: ${{ github.event_name == 'pull_request' }}",
|
||||
"release_only: ${{ steps.check.outputs.release_only }}",
|
||||
"- uses: actions/checkout@v7",
|
||||
"fetch-depth: 0",
|
||||
"BASE_SHA: ${{ github.event.pull_request.base.sha }}",
|
||||
"python3 script/release_pr.py check",
|
||||
],
|
||||
);
|
||||
assert_contains_all(
|
||||
ci,
|
||||
&[
|
||||
"needs: [prepare, classify]",
|
||||
"if: ${{ needs.classify.outputs.release_only != 'true' }}",
|
||||
"needs: [classify]",
|
||||
],
|
||||
);
|
||||
// The classifier must keep the release diff allow-list and validate the
|
||||
// bilingual entry plus the version pair itself.
|
||||
assert_contains_all(
|
||||
"script/release_pr.py",
|
||||
&[
|
||||
"ALLOWED_FILES = (\"CHANGELOG.md\", \"main/Cargo.toml\", \"Cargo.lock\")",
|
||||
"def classify(",
|
||||
"def verify_release_metadata(",
|
||||
"def validate_changelog_entry(",
|
||||
],
|
||||
);
|
||||
assert_contains_all(
|
||||
"script/tests/test_release_pr.py",
|
||||
&[
|
||||
"class CheckCommandTests(unittest.TestCase)",
|
||||
"test_code_changes_require_the_full_matrix",
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn active_x_policy_sections_run_explicitly_in_frozen_order() {
|
||||
let policy_source = &format!("{HOST_CRATE}/native/connection_policy.cpp");
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Classify and verify `dev` -> `main` release pull requests.
|
||||
|
||||
A release pull request carries only the bilingual `CHANGELOG.md` entry and the
|
||||
version bump, so it does not need the full platform test matrix. `check`
|
||||
decides whether a pull request qualifies for that fast path; when it does, the
|
||||
release metadata is validated here instead of by a Rust build. `ci-gate` still
|
||||
blocks the merge whenever the entry is missing or malformed.
|
||||
|
||||
Usage (CI):
|
||||
BASE_SHA=<sha> HEAD_SHA=<sha> GITHUB_OUTPUT=<file> \
|
||||
python3 script/release_pr.py check
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import importlib.util
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ALLOWED_FILES = ("CHANGELOG.md", "main/Cargo.toml", "Cargo.lock")
|
||||
VERSION_PATHS = ("main/Cargo.toml", "Cargo.lock")
|
||||
CHANGELOG_PATH = "CHANGELOG.md"
|
||||
MANIFEST_PATH = "main/Cargo.toml"
|
||||
LOCK_PATH = "Cargo.lock"
|
||||
VERSION_LINE_RE = re.compile(r'^[+-][ \t]*version[ \t]*=[ \t]*"')
|
||||
PACKAGE_SECTION_RE = re.compile(r"^\[package\]$")
|
||||
SECTION_RE = re.compile(r"^\[")
|
||||
NAME_RE = re.compile(r'^name[ \t]*=[ \t]*"([^"]+)"')
|
||||
VERSION_RE = re.compile(r'^version[ \t]*=[ \t]*"([^"]+)"')
|
||||
|
||||
|
||||
class ReleasePrError(Exception):
|
||||
"""Raised when a release pull request cannot be classified or is invalid."""
|
||||
|
||||
|
||||
def run_git(arguments: list[str], cwd: Path) -> str:
|
||||
completed = subprocess.run(
|
||||
["git", *arguments],
|
||||
cwd=cwd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
if completed.returncode != 0:
|
||||
raise ReleasePrError(
|
||||
f"git {' '.join(arguments)} failed: {completed.stderr.strip()}"
|
||||
)
|
||||
return completed.stdout
|
||||
|
||||
|
||||
def changed_files(base: str, head: str, cwd: Path) -> list[str]:
|
||||
output = run_git(["diff", "--name-only", base, head], cwd)
|
||||
return [line.strip() for line in output.splitlines() if line.strip()]
|
||||
|
||||
|
||||
def version_paths_diff(base: str, head: str, cwd: Path) -> str:
|
||||
return run_git(["diff", base, head, "--", *VERSION_PATHS], cwd)
|
||||
|
||||
|
||||
def only_version_lines_changed(diff: str) -> bool:
|
||||
"""True when every added/removed line is a `version = "..."` assignment."""
|
||||
|
||||
for line in diff.splitlines():
|
||||
if not line.startswith(("+", "-")):
|
||||
continue
|
||||
if line.startswith(("+++", "---")):
|
||||
continue
|
||||
if VERSION_LINE_RE.match(line) is None:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def classify(files: list[str], diff: str) -> tuple[bool, str]:
|
||||
"""Return whether the pull request is a release-only change and why."""
|
||||
|
||||
foreign = [path for path in files if path not in ALLOWED_FILES]
|
||||
if foreign:
|
||||
return False, f"code or configuration changed: {', '.join(sorted(foreign))}"
|
||||
if not files:
|
||||
return False, "no file changed"
|
||||
if not only_version_lines_changed(diff):
|
||||
return False, "Cargo.toml / Cargo.lock changed beyond the version bump"
|
||||
return True, "only CHANGELOG.md and the version bump changed"
|
||||
|
||||
|
||||
def manifest_version(manifest: str) -> str:
|
||||
inside_package = False
|
||||
for line in manifest.splitlines():
|
||||
if PACKAGE_SECTION_RE.match(line):
|
||||
inside_package = True
|
||||
continue
|
||||
if SECTION_RE.match(line):
|
||||
inside_package = False
|
||||
continue
|
||||
if not inside_package:
|
||||
continue
|
||||
match = VERSION_RE.match(line)
|
||||
if match:
|
||||
return match.group(1)
|
||||
raise ReleasePrError(f"{MANIFEST_PATH} has no [package] version")
|
||||
|
||||
|
||||
def lock_main_version(lock: str) -> str:
|
||||
current_name: str | None = None
|
||||
for line in lock.splitlines():
|
||||
if line == "[[package]]":
|
||||
current_name = None
|
||||
continue
|
||||
name_match = NAME_RE.match(line)
|
||||
if name_match:
|
||||
current_name = name_match.group(1)
|
||||
continue
|
||||
if current_name != "main":
|
||||
continue
|
||||
version_match = VERSION_RE.match(line)
|
||||
if version_match:
|
||||
return version_match.group(1)
|
||||
raise ReleasePrError(f'{LOCK_PATH} has no version for the "main" package')
|
||||
|
||||
|
||||
def load_changelog_module():
|
||||
script_path = Path(__file__).resolve().parent / "changelog.py"
|
||||
spec = importlib.util.spec_from_file_location("navop_changelog", script_path)
|
||||
if spec is None or spec.loader is None:
|
||||
raise ReleasePrError(f"unable to load {script_path}")
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def validate_changelog_entry(changelog: str, version: str) -> None:
|
||||
module = load_changelog_module()
|
||||
tag = f"v{version}"
|
||||
try:
|
||||
notes = module.extract_release_notes(changelog, tag)
|
||||
module.validate_release_notes(notes, require_cnb_line=True)
|
||||
except module.ChangelogError as error:
|
||||
raise ReleasePrError(str(error)) from error
|
||||
|
||||
|
||||
def read(path: Path) -> str:
|
||||
try:
|
||||
return path.read_text(encoding="utf-8")
|
||||
except FileNotFoundError as error:
|
||||
raise ReleasePrError(f"file not found: {path}") from error
|
||||
|
||||
|
||||
def verify_release_metadata(root: Path, changelog: str) -> str:
|
||||
"""Validate the release entry and the version pair; return the version."""
|
||||
|
||||
version = manifest_version(read(root / MANIFEST_PATH))
|
||||
locked = lock_main_version(read(root / LOCK_PATH))
|
||||
if version != locked:
|
||||
raise ReleasePrError(
|
||||
f"{MANIFEST_PATH} is {version} but {LOCK_PATH} pins main at {locked}"
|
||||
)
|
||||
validate_changelog_entry(changelog, version)
|
||||
return version
|
||||
|
||||
|
||||
def write_github_output(path: Path, values: dict[str, str]) -> None:
|
||||
with path.open("a", encoding="utf-8") as handle:
|
||||
for key, value in values.items():
|
||||
handle.write(f"{key}={value}\n")
|
||||
|
||||
|
||||
def command_check(arguments: argparse.Namespace) -> None:
|
||||
base = arguments.base or os.environ.get("BASE_SHA", "")
|
||||
head = arguments.head or os.environ.get("HEAD_SHA", "")
|
||||
if not base or not head:
|
||||
raise ReleasePrError("BASE_SHA and HEAD_SHA are required")
|
||||
|
||||
root = arguments.root.resolve()
|
||||
try:
|
||||
files = changed_files(base, head, root)
|
||||
diff = version_paths_diff(base, head, root)
|
||||
except ReleasePrError as error:
|
||||
# 无法算出差异时(例如只 fetch 了合并引用的外部 PR)宁可跑全量测试,
|
||||
# 也不能把发布快速通道当成默认结论。
|
||||
print(f"::warning::cannot diff {base}..{head}: {error}")
|
||||
print("release-only: false (diff unavailable, running the full matrix)")
|
||||
return
|
||||
|
||||
release_only, reason = classify(files, diff)
|
||||
print(f"changed files: {', '.join(files) if files else '<none>'}")
|
||||
print(f"release-only: {release_only} ({reason})")
|
||||
|
||||
version = ""
|
||||
if release_only:
|
||||
version = verify_release_metadata(root, read(root / CHANGELOG_PATH))
|
||||
print(f"release metadata validated for v{version}")
|
||||
|
||||
output_path = arguments.github_output or os.environ.get("GITHUB_OUTPUT", "")
|
||||
if release_only and output_path:
|
||||
write_github_output(
|
||||
Path(output_path),
|
||||
{"release_only": "true", "version": version},
|
||||
)
|
||||
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
subparsers = parser.add_subparsers(dest="command", required=True)
|
||||
|
||||
check_parser = subparsers.add_parser(
|
||||
"check", help="classify a pull request and validate release-only contents"
|
||||
)
|
||||
check_parser.add_argument("--base", default="")
|
||||
check_parser.add_argument("--head", default="")
|
||||
check_parser.add_argument("--root", type=Path, default=Path.cwd())
|
||||
check_parser.add_argument("--github-output", default="")
|
||||
check_parser.set_defaults(handler=command_check)
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = build_parser()
|
||||
arguments = parser.parse_args()
|
||||
try:
|
||||
arguments.handler(arguments)
|
||||
except ReleasePrError as error:
|
||||
print(f"::error::{error}", file=sys.stderr)
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,241 @@
|
||||
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()
|
||||
Reference in New Issue
Block a user