Files
GiteaMailTemplates/.github/scripts/test_track_gitea_release.py
T
KenanZhu 14dc37046d
Release / Validate Templates (push) Canceled after 0s
Release / Package & Release (push) Canceled after 0s
Release / Update Latest Release Documentation (push) Canceled after 0s
refactor: unify markdown release tracking blocks
2026-10-09 12:10:12 +08:00

283 lines
15 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Offline tests for the compatibility tracker."""
import importlib.util
import io
import json
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch
SCRIPT = Path(__file__).with_name("track_gitea_release.py")
SPEC = importlib.util.spec_from_file_location("track_gitea_release", SCRIPT)
TRACKER = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(TRACKER)
RELEASE_URL = "https://gitea.kenanzhu.com/KenanZhu/GiteaMailTemplates/releases/latest"
FIXTURES = {
"COMPATIBILITY.md": """# Gitea Compatibility
<!-- DOC-TAGS: {"TRACKER":["LATEST-VERIFIED","VERSION-MAP","HISTORY"]} -->
| Template Release | Min Gitea | Max Tested Gitea | Status |
|---|---|---|---|
| **v28.0.0** | **28.0.0** | **28.0.0** | [PASS] Active |
<!-- TRACKER:LATEST-VERIFIED -->
> **Latest verified:** Release v28.0.0 passes tests.
<!-- /TRACKER:LATEST-VERIFIED -->
<!-- TRACKER:VERSION-MAP -->
| Gitea version | Template release |
|---|---|
| 28.1.0 | [PENDING] Compatibility verification; no tested template release yet |
| 28.0.0 | **v28.0.0** |
<!-- /TRACKER:VERSION-MAP -->
<!-- TRACKER:HISTORY -->
| Gitea | Release Date | Mail Template Changes | Breaking? |
|---|---|---|---|
| **28.1.0** | 2026-10-06 | [PENDING] Mail-template compatibility verification | TBD |
| **28.0.0** | 2026-09-29 | FormatByteSize | Yes |
| **≤ 1.24.x** | — | Old directory | [UNSUPPORTED] |
<!-- /TRACKER:HISTORY -->
""",
"README.md": f"""# Gitea Mail Templates
<!-- DOC-TAGS: {{"TRACKER":["BADGE","LATEST-TESTED","UPSTREAM"],"RELEASE":["HEADER","SUMMARY"]}} -->
<!-- RELEASE:HEADER -->
> Latest Release: [v28.0.0]({RELEASE_URL})
<!-- /RELEASE:HEADER -->
<!-- TRACKER:BADGE -->
[![Gitea](https://img.shields.io/badge/Gitea-28.1.0%20%5BPENDING%5D%20%7C%2028.0.0%20tested-yellow)](COMPATIBILITY.md)
<!-- /TRACKER:BADGE -->
<!-- TRACKER:LATEST-TESTED -->
- **Latest tested:** Gitea 28.0.0
<!-- /TRACKER:LATEST-TESTED -->
<!-- RELEASE:SUMMARY -->
- **Latest release:** [v28.0.0]({RELEASE_URL})
<!-- /RELEASE:SUMMARY -->
<!-- TRACKER:UPSTREAM -->
- **Upstream Gitea 28.1.0:** [PENDING]
<!-- /TRACKER:UPSTREAM -->
""",
"AGENTS.md": """# Agents
<!-- DOC-TAGS: {"TRACKER":["UPSTREAM"],"RELEASE":["CURRENT"]} -->
<!-- RELEASE:CURRENT -->
- Current template release: **v28.0.0**.
<!-- /RELEASE:CURRENT -->
<!-- TRACKER:UPSTREAM -->
- Latest upstream Gitea release: 28.1.0 [PENDING].
<!-- /TRACKER:UPSTREAM -->
""",
"docs/README.zh-CN.md": f"""# Gitea 邮件模板
<!-- DOC-TAGS: {{"TRACKER":["LATEST-TESTED","UPSTREAM"],"RELEASE":["HEADER","SUMMARY"]}} -->
<!-- RELEASE:HEADER -->
> 最新发布版:[v28.0.0]({RELEASE_URL})
<!-- /RELEASE:HEADER -->
<!-- TRACKER:LATEST-TESTED -->
- **最新测试:** Gitea 28.0.0
<!-- /TRACKER:LATEST-TESTED -->
<!-- RELEASE:SUMMARY -->
- **最新发布版:** [v28.0.0]({RELEASE_URL})
<!-- /RELEASE:SUMMARY -->
<!-- TRACKER:UPSTREAM -->
- **上游 Gitea 28.1.0:** [PENDING]
<!-- /TRACKER:UPSTREAM -->
""",
}
class TrackerTests(unittest.TestCase):
def setUp(self):
self.directory = tempfile.TemporaryDirectory(prefix="tracker-test-", dir=TRACKER.ROOT)
self.addCleanup(self.directory.cleanup)
self.root = Path(self.directory.name)
self.paths = ("COMPATIBILITY.md", "README.md", "AGENTS.md", "docs/README.zh-CN.md")
self.originals = {}
for name in self.paths:
destination = self.root / name
destination.parent.mkdir(parents=True, exist_ok=True)
content = FIXTURES[name]
destination.write_text(content, encoding="utf-8")
self.originals[name] = content
def read(self, name):
return (self.root / name).read_text(encoding="utf-8")
def assert_unchanged(self):
for name, content in self.originals.items():
self.assertEqual(content, self.read(name), name)
def test_new_release_adds_only_pending_rows(self):
changed = TRACKER.apply_release(self.root, "28.2.0", "2026-11-01")
self.assertEqual({self.root / name for name in self.paths}, set(changed))
compatibility = self.read("COMPATIBILITY.md")
self.assertIn("| 28.2.0 | [PENDING] Compatibility verification", compatibility)
self.assertIn("| **28.2.0** | 2026-11-01 | [PENDING]", compatibility)
self.assertIn("| **v28.0.0** | **28.0.0** | **28.0.0** | [PASS] Active |", compatibility)
self.assertIn("Latest verified:** Release v28.0.0", compatibility)
self.assertIn("Gitea-28.2.0%20%5BPENDING%5D%20%7C%2028.0.0%20tested-yellow", self.read("README.md"))
self.assertIn("Gitea 28.2.0:** [PENDING]", self.read("README.md"))
self.assertIn("Gitea 28.2.0:** [PENDING]", self.read("docs/README.zh-CN.md"))
self.assertIn("Gitea release: 28.2.0 [PENDING]", self.read("AGENTS.md"))
self.assertIn("Latest tested:** Gitea 28.0.0", self.read("README.md"))
self.assertIn("最新测试:** Gitea 28.0.0", self.read("docs/README.zh-CN.md"))
self.assertEqual([], TRACKER.apply_release(self.root, "28.2.0", "2026-11-01"))
def test_repository_markers_are_valid(self):
documents, blocks = TRACKER.documents(TRACKER.ROOT)
self.assertTrue({TRACKER.ROOT / name for name in self.paths}.issubset(documents))
self.assertIn(("TRACKER", "HISTORY"), {(block.subject, block.content) for block in blocks})
self.assertIn(("RELEASE", "HEADER"), {(block.subject, block.content) for block in blocks})
TRACKER.latest_tested(documents, blocks)
def test_existing_version_is_idempotent(self):
self.assertEqual([], TRACKER.apply_release(self.root, "28.1.0", "2026-10-06"))
self.assert_unchanged()
def test_partial_update_is_repaired(self):
compatibility = self.root / "COMPATIBILITY.md"
partial = self.read("COMPATIBILITY.md").replace("| 28.1.0 | [PENDING] Compatibility verification; no tested template release yet |\n", "")
compatibility.write_text(partial, encoding="utf-8")
self.assertEqual([compatibility], TRACKER.apply_release(self.root, "28.1.0", "2026-10-06"))
self.assertIn("| 28.1.0 | [PENDING] Compatibility verification", self.read("COMPATIBILITY.md"))
def test_older_version_does_not_rewrite_history(self):
self.assertEqual([], TRACKER.apply_release(self.root, "28.0.0", "2026-09-29"))
self.assert_unchanged()
def test_dry_run_does_not_write(self):
self.assertEqual(4, len(TRACKER.apply_release(self.root, "28.2.0", "2026-11-01", dry_run=True)))
self.assert_unchanged()
def test_missing_marker_fails_without_writing(self):
compatibility = self.root / "COMPATIBILITY.md"
compatibility.write_text(self.read("COMPATIBILITY.md").replace("<!-- TRACKER:HISTORY -->\n", ""), encoding="utf-8")
with self.assertRaises(ValueError):
TRACKER.apply_release(self.root, "28.2.0", "2026-11-01")
self.assertEqual(self.originals["README.md"], self.read("README.md"))
def test_unknown_marker_fails_without_writing(self):
news = self.root / "docs/NEWS.md"
news.write_text('<!-- DOC-TAGS: {"TRACKER":["FUTURE-UNKNOWN"]} -->\n', encoding="utf-8")
with self.assertRaisesRegex(ValueError, "Unknown TRACKER content"):
TRACKER.apply_release(self.root, "28.2.0", "2026-11-01")
self.assert_unchanged()
def test_new_marked_document_is_discovered(self):
news = self.root / "misc/notes/NEWS.md"
news.parent.mkdir(parents=True)
news.write_text('<!-- DOC-TAGS: {"TRACKER":["UPSTREAM"]} -->\n<!-- TRACKER:UPSTREAM -->\nGitea 28.1.0 [PENDING]\n<!-- /TRACKER:UPSTREAM -->\n', encoding="utf-8")
changed = TRACKER.apply_release(self.root, "28.2.0", "2026-11-01")
self.assertIn(news, changed)
self.assertIn("Gitea 28.2.0 [PENDING]", news.read_text(encoding="utf-8"))
def test_duplicate_block_uses_first_in_document_order(self):
english = self.root / "README.md"
duplicate = "<!-- TRACKER:UPSTREAM -->\nGitea 28.1.0 [PENDING]\n<!-- /TRACKER:UPSTREAM -->\n"
english.write_text(self.read("README.md") + duplicate, encoding="utf-8")
TRACKER.apply_release(self.root, "28.2.0", "2026-11-01")
self.assertIn("**Upstream Gitea 28.2.0:** [PENDING]", self.read("README.md"))
self.assertTrue(self.read("README.md").endswith(duplicate))
def test_unclosed_block_rejected_without_writing(self):
english = self.root / "README.md"
english.write_text(self.read("README.md").replace("<!-- /TRACKER:UPSTREAM -->", ""), encoding="utf-8")
with self.assertRaisesRegex(ValueError, "Unclosed tag block"):
TRACKER.apply_release(self.root, "28.2.0", "2026-11-01")
self.assertEqual(self.originals["COMPATIBILITY.md"], self.read("COMPATIBILITY.md"))
def test_undeclared_block_rejected(self):
english = self.root / "README.md"
english.write_text(self.read("README.md") + "<!-- RELEASE:CURRENT -->\nv28.0.0\n<!-- /RELEASE:CURRENT -->\n", encoding="utf-8")
with self.assertRaisesRegex(ValueError, "Undeclared RELEASE:CURRENT"):
TRACKER.apply_template_release(self.root, "v28.0.1")
def test_misspelled_subject_rejected(self):
english = self.root / "README.md"
english.write_text(self.read("README.md").replace("<!-- TRACKER:UPSTREAM -->", "<!-- TRACER:UPSTREAM -->"), encoding="utf-8")
with self.assertRaisesRegex(ValueError, "Undeclared TRACER:UPSTREAM"):
TRACKER.apply_release(self.root, "28.2.0", "2026-11-01")
def test_block_without_manifest_rejected(self):
news = self.root / "NEWS.md"
news.write_text("<!-- TRACKER:UPSTREAM -->\nGitea 28.1.0 [PENDING]\n<!-- /TRACKER:UPSTREAM -->\n", encoding="utf-8")
with self.assertRaisesRegex(ValueError, "without DOC-TAGS"):
TRACKER.apply_release(self.root, "28.2.0", "2026-11-01")
def test_mismatched_close_rejected(self):
english = self.root / "README.md"
english.write_text(self.read("README.md").replace("<!-- /RELEASE:SUMMARY -->", "<!-- /RELEASE:HEADER -->"), encoding="utf-8")
with self.assertRaisesRegex(ValueError, "Unmatched closing tag"):
TRACKER.apply_template_release(self.root, "v28.0.1")
def test_disagreeing_tested_versions_fail(self):
chinese = self.root / "docs/README.zh-CN.md"
chinese.write_text(self.read("docs/README.zh-CN.md").replace("**最新测试:** Gitea 28.0.0", "**最新测试:** Gitea 28.1.0"), encoding="utf-8")
with self.assertRaisesRegex(ValueError, "disagree"):
TRACKER.apply_release(self.root, "28.2.0", "2026-11-01")
def test_after_verification_new_version_becomes_pending(self):
english = self.root / "README.md"
chinese = self.root / "docs/README.zh-CN.md"
agents = self.root / "AGENTS.md"
english.write_text(self.read("README.md")
.replace("**Latest tested:** Gitea 28.0.0", "**Latest tested:** Gitea 28.1.0")
.replace("Gitea-28.1.0%20%5BPENDING%5D%20%7C%2028.0.0%20tested-yellow", "Gitea-28.1.0%20%7C%2028.1.0%20tested-blue")
.replace("**Upstream Gitea 28.1.0:** [PENDING]", "**Upstream Gitea 28.1.0:** [PASS]"), encoding="utf-8")
chinese.write_text(self.read("docs/README.zh-CN.md")
.replace("**最新测试:** Gitea 28.0.0", "**最新测试:** Gitea 28.1.0")
.replace("**上游 Gitea 28.1.0:** [PENDING]", "**上游 Gitea 28.1.0:** [PASS]"), encoding="utf-8")
agents.write_text(self.read("AGENTS.md").replace("28.1.0 [PENDING]", "28.1.0 [PASS]"), encoding="utf-8")
TRACKER.apply_release(self.root, "28.2.0", "2026-11-01")
self.assertIn("Gitea-28.2.0%20%5BPENDING%5D%20%7C%2028.1.0%20tested-yellow", self.read("README.md"))
self.assertIn("Gitea 28.2.0:** [PENDING]", self.read("README.md"))
self.assertIn("Latest tested:** Gitea 28.1.0", self.read("README.md"))
self.assertIn("最新测试:** Gitea 28.1.0", self.read("docs/README.zh-CN.md"))
def test_invalid_version_rejected(self):
with self.assertRaises(ValueError):
TRACKER.apply_release(self.root, "28.2.0-rc1", "2026-11-01")
def test_template_release_updates_all_marked_labels(self):
changed = TRACKER.apply_template_release(self.root, "v28.0.1")
self.assertEqual({self.root / name for name in ("README.md", "docs/README.zh-CN.md", "AGENTS.md")}, set(changed))
for name in ("README.md", "docs/README.zh-CN.md"):
self.assertEqual(2, self.read(name).count("[v28.0.1](https://gitea.kenanzhu.com/KenanZhu/GiteaMailTemplates/releases/latest)"))
self.assertNotIn("/releases/tag/v28.0.1", self.read(name))
self.assertIn("Current template release: **v28.0.1**", self.read("AGENTS.md"))
self.assertEqual(self.originals["COMPATIBILITY.md"], self.read("COMPATIBILITY.md"))
self.assertEqual([], TRACKER.apply_template_release(self.root, "v28.0.1"))
self.assertEqual([], TRACKER.apply_template_release(self.root, "v28.0.0"))
def test_template_release_dry_run_does_not_write(self):
self.assertEqual(3, len(TRACKER.apply_template_release(self.root, "v28.0.1", dry_run=True)))
self.assert_unchanged()
def test_template_release_rejects_mismatched_labels(self):
english = self.root / "README.md"
english.write_text(self.read("README.md").replace("Latest Release: [v28.0.0]", "Latest Release: [v28.0.1]"), encoding="utf-8")
with self.assertRaisesRegex(ValueError, "disagree"):
TRACKER.apply_template_release(self.root, "v28.0.2")
def test_template_release_rejects_tagged_link(self):
english = self.root / "README.md"
english.write_text(self.read("README.md").replace("[v28.0.0](https://gitea.kenanzhu.com/KenanZhu/GiteaMailTemplates/releases/latest)", "[v28.0.0](https://gitea.kenanzhu.com/KenanZhu/GiteaMailTemplates/releases/tag/v28.0.0)", 1), encoding="utf-8")
with self.assertRaisesRegex(ValueError, "/releases/latest"):
TRACKER.apply_template_release(self.root, "v28.0.1")
def test_template_release_rejects_invalid_tag(self):
with self.assertRaises(ValueError):
TRACKER.apply_template_release(self.root, "v28.0.1-rc1")
def test_release_api_uses_published_date(self):
payload = {"tag_name": "v28.2.0", "published_at": "2026-11-01T08:15:00Z", "draft": False, "prerelease": False}
with patch.object(TRACKER, "urlopen", return_value=io.BytesIO(json.dumps(payload).encode("utf-8"))) as mocked:
self.assertEqual(("28.2.0", "2026-11-01"), TRACKER.fetch_release("28.2.0"))
self.assertEqual(f"{TRACKER.RELEASES_API}/tags/v28.2.0", mocked.call_args.args[0].full_url)
def test_prerelease_is_rejected(self):
payload = {"tag_name": "v28.2.0", "published_at": "2026-11-01T08:15:00Z", "prerelease": True}
with patch.object(TRACKER, "urlopen", return_value=io.BytesIO(json.dumps(payload).encode("utf-8"))):
with self.assertRaises(ValueError):
TRACKER.fetch_release(None)
if __name__ == "__main__":
unittest.main()