chore: split validation and release workflows and refactor packaging
This commit is contained in:
1 parent
5b06a795e4
commit
5f5ad058a8
9 files changed
+397
-505
No files matched your search
@@ -1,97 +1,196 @@
|
||||
"""Offline artifact/provenance tests independent of generated repository output."""
|
||||
"""Offline packaging tests using a minimal, independently generated repository."""
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
from pathlib import Path
|
||||
import tarfile
|
||||
import tempfile
|
||||
import unittest
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
SPEC = importlib.util.spec_from_file_location("package_release", Path(__file__).with_name("package_release.py"))
|
||||
|
||||
SCRIPT_PATH = Path(__file__).with_name("package_release.py")
|
||||
SPEC = importlib.util.spec_from_file_location("package_release", SCRIPT_PATH)
|
||||
PACKER = importlib.util.module_from_spec(SPEC)
|
||||
SPEC.loader.exec_module(PACKER)
|
||||
|
||||
|
||||
class PackagingTests(unittest.TestCase):
|
||||
VERSION = "v28.0.0"
|
||||
COMMIT = "a" * 40
|
||||
THEME = "demo"
|
||||
LANGUAGE = "en-US"
|
||||
ROOT_DOCUMENTS = (
|
||||
"LICENSE",
|
||||
"AGENTS.md",
|
||||
"README.md",
|
||||
"CONTRIBUTING.md",
|
||||
"COMPATIBILITY.md",
|
||||
"THIRD_PARTY_NOTICES.md",
|
||||
"docs/README.zh-CN.md",
|
||||
"docs/CONTRIBUTING.zh-CN.md",
|
||||
"preview/index.html",
|
||||
"docs/images/README.md",
|
||||
)
|
||||
|
||||
def setUp(self):
|
||||
self.temp = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self.temp.cleanup)
|
||||
self.root = Path(self.temp.name)
|
||||
self.tag = "v28.0.0"
|
||||
self.commit = "a" * 40
|
||||
theme = json.dumps({"name": "demo", "mode": "shared"}).encode()
|
||||
css = b"a { color:blue; }"
|
||||
self.write("themes/demo/theme.json", theme)
|
||||
self.write("themes/demo/theme.css", css)
|
||||
files = {"mail/base/head.tmpl": b"<title>test</title>", "mail/base/footer.tmpl": b"<p>footer</p>"}
|
||||
meta = {"theme": "demo", "mode": "shared", "gitea_tag": self.tag, "gitea_commit": self.commit, "sources": {"theme.json": PACKER.digest(theme), "theme.css": PACKER.digest(css)}, "files": {p: PACKER.digest(data) for p, data in files.items()}}
|
||||
for path, data in files.items():
|
||||
self.write("build/themes/demo/" + path, data)
|
||||
self.write_json("build/themes/demo/build.json", meta)
|
||||
self.write_json("gitea.lock.json", {"tag": self.tag, "commit": self.commit, "files": {"LICENSE": PACKER.digest(b"MIT"), "options/locale/locale_en-US.json": PACKER.digest(b"{}")}})
|
||||
self.write("build/upstream/LICENSE", b"MIT")
|
||||
manifest = {"upstream": self.tag, "upstream_commit": self.commit, "themes": ["demo"], "languages": ["en-US"], "registry": {"activate": {}}}
|
||||
self.write("preview/rendered.js", ("window.__MAIL_PREVIEW__ = " + json.dumps(manifest) + ";").encode())
|
||||
self.write("preview/rendered/en-US.js", ('window.__MAIL_PREVIEW_DATA__["en-US"] = ' + json.dumps({"demo": {"activate": "<html></html>"}}) + ";").encode())
|
||||
for name in ["LICENSE", "AGENTS.md", "README.md", "CONTRIBUTING.md", "COMPATIBILITY.md", "THIRD_PARTY_NOTICES.md", "docs/README.zh-CN.md", "docs/CONTRIBUTING.zh-CN.md", "preview/index.html", "docs/images/README.md"]:
|
||||
temporary_directory = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(temporary_directory.cleanup)
|
||||
self.root = Path(temporary_directory.name)
|
||||
self.output = self.root / "artifacts"
|
||||
self.create_theme()
|
||||
self.create_snapshot()
|
||||
self.create_preview()
|
||||
for name in self.ROOT_DOCUMENTS:
|
||||
self.write(name, b"test")
|
||||
|
||||
# Fixture helpers keep test bodies focused on the behavior being checked.
|
||||
def write(self, name, data):
|
||||
path = self.root / name
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_bytes(data)
|
||||
|
||||
def write_json(self, name, data):
|
||||
self.write(name, json.dumps(data).encode())
|
||||
self.write(name, json.dumps(data).encode("utf-8"))
|
||||
|
||||
def test_packages_declared_outputs_only_and_never_overwrites(self):
|
||||
def create_theme(self):
|
||||
theme_data = json.dumps({"name": self.THEME, "mode": "shared"}).encode("utf-8")
|
||||
css_data = b"a { color:blue; }"
|
||||
self.write("themes/demo/theme.json", theme_data)
|
||||
self.write("themes/demo/theme.css", css_data)
|
||||
|
||||
generated_files = {
|
||||
"mail/base/head.tmpl": b"<title>test</title>",
|
||||
"mail/base/footer.tmpl": b"<p>footer</p>",
|
||||
}
|
||||
metadata = {
|
||||
"theme": self.THEME,
|
||||
"mode": "shared",
|
||||
"gitea_tag": self.VERSION,
|
||||
"gitea_commit": self.COMMIT,
|
||||
"sources": {
|
||||
"theme.json": PACKER.digest(theme_data),
|
||||
"theme.css": PACKER.digest(css_data),
|
||||
},
|
||||
"files": {
|
||||
path: PACKER.digest(data)
|
||||
for path, data in generated_files.items()
|
||||
},
|
||||
}
|
||||
for path, data in generated_files.items():
|
||||
self.write("build/themes/demo/" + path, data)
|
||||
self.write_json("build/themes/demo/build.json", metadata)
|
||||
|
||||
def create_snapshot(self):
|
||||
self.write_json("gitea.lock.json", {
|
||||
"tag": self.VERSION,
|
||||
"commit": self.COMMIT,
|
||||
"files": {
|
||||
"LICENSE": PACKER.digest(b"MIT"),
|
||||
"options/locale/locale_en-US.json": PACKER.digest(b"{}"),
|
||||
},
|
||||
})
|
||||
self.write("build/upstream/LICENSE", b"MIT")
|
||||
|
||||
def create_preview(self):
|
||||
manifest = {
|
||||
"upstream": self.VERSION,
|
||||
"upstream_commit": self.COMMIT,
|
||||
"themes": [self.THEME],
|
||||
"languages": [self.LANGUAGE],
|
||||
"registry": {"activate": {}},
|
||||
}
|
||||
bundle = {self.THEME: {"activate": "<html></html>"}}
|
||||
self.write("preview/rendered.js", (
|
||||
"window.__MAIL_PREVIEW__ = " + json.dumps(manifest) + ";"
|
||||
).encode("utf-8"))
|
||||
self.write("preview/rendered/en-US.js", (
|
||||
'window.__MAIL_PREVIEW_DATA__["en-US"] = ' + json.dumps(bundle) + ";"
|
||||
).encode("utf-8"))
|
||||
|
||||
# Archives: both formats contain the verified bytes, never stale build files.
|
||||
def test_archives_contain_only_verified_outputs(self):
|
||||
self.write("build/themes/stale/mail/stale.tmpl", b"stale")
|
||||
out = self.root / "artifacts"
|
||||
PACKER.package(self.root, self.tag, out)
|
||||
with zipfile.ZipFile(next(out.glob("*.zip"))) as archive:
|
||||
names = archive.namelist()
|
||||
self.assertTrue(any(name.endswith("themes/demo/mail/base/head.tmpl") for name in names))
|
||||
self.assertFalse(any("stale" in name or "/build/" in name for name in names))
|
||||
with self.assertRaises(ValueError):
|
||||
PACKER.package(self.root, self.tag, out)
|
||||
expected_files = PACKER.collect(self.root, self.VERSION)
|
||||
prefix = f"gitea-mail-templates-{self.VERSION}/"
|
||||
expected_members = {
|
||||
prefix + name: data for name, data in expected_files.items()
|
||||
}
|
||||
|
||||
def test_refuses_stale_source_tampered_output_and_wrong_version(self):
|
||||
with self.assertRaises(ValueError):
|
||||
PACKER.package(self.root, self.VERSION, self.output)
|
||||
|
||||
with zipfile.ZipFile(self.output / f"gitea-mail-templates-{self.VERSION}.zip") as archive:
|
||||
zipped = {name: archive.read(name) for name in archive.namelist()}
|
||||
with tarfile.open(self.output / f"gitea-mail-templates-{self.VERSION}.tar.gz") as archive:
|
||||
tarred = {
|
||||
member.name: archive.extractfile(member).read()
|
||||
for member in archive.getmembers()
|
||||
}
|
||||
self.assertTrue(all(member.mode == 0o644 for member in archive.getmembers()))
|
||||
self.assertEqual(expected_members, zipped)
|
||||
self.assertEqual(expected_members, tarred)
|
||||
self.assertIn(prefix + "themes/demo/mail/base/head.tmpl", zipped)
|
||||
self.assertFalse(any("stale" in name or "/build/" in name for name in zipped))
|
||||
|
||||
def test_existing_archives_are_not_overwritten(self):
|
||||
PACKER.package(self.root, self.VERSION, self.output)
|
||||
original_archives = {
|
||||
path.name: path.read_bytes() for path in self.output.iterdir()
|
||||
}
|
||||
|
||||
with self.assertRaisesRegex(ValueError, "Refusing to overwrite"):
|
||||
PACKER.package(self.root, self.VERSION, self.output)
|
||||
|
||||
self.assertEqual(original_archives, {
|
||||
path.name: path.read_bytes() for path in self.output.iterdir()
|
||||
})
|
||||
|
||||
# Validation: release identity, source checksums and generated checksums.
|
||||
def test_wrong_version_is_rejected(self):
|
||||
with self.assertRaisesRegex(ValueError, "must match the locked Gitea tag"):
|
||||
PACKER.collect(self.root, "v28.1.0")
|
||||
|
||||
def test_changed_source_is_rejected(self):
|
||||
self.write("themes/demo/theme.css", b"changed")
|
||||
with self.assertRaisesRegex(ValueError, "Source changed"):
|
||||
PACKER.collect(self.root, self.tag)
|
||||
PACKER.collect(self.root, self.VERSION)
|
||||
|
||||
def test_refuses_missing_language_bundle(self):
|
||||
def test_missing_language_bundle_is_rejected(self):
|
||||
(self.root / "preview/rendered/en-US.js").unlink()
|
||||
with self.assertRaises(ValueError):
|
||||
PACKER.collect(self.root, self.tag)
|
||||
with self.assertRaisesRegex(ValueError, "Expected regular file"):
|
||||
PACKER.collect(self.root, self.VERSION)
|
||||
|
||||
def test_refuses_modified_generated_files(self):
|
||||
def test_modified_generated_files_are_rejected(self):
|
||||
self.write("build/themes/demo/mail/base/head.tmpl", b"modified")
|
||||
with self.assertRaisesRegex(ValueError, "checksum mismatch"):
|
||||
PACKER.collect(self.root, self.tag)
|
||||
PACKER.collect(self.root, self.VERSION)
|
||||
|
||||
def test_includes_nested_documentation_and_release_note_sources(self):
|
||||
# Documentation: recursive inclusion and optional root files.
|
||||
def test_nested_documentation_and_release_notes_are_included(self):
|
||||
documents = {
|
||||
"docs/INDEX.zh-CN.md": "中文索引".encode(),
|
||||
"docs/images/README.zh-CN.md": "截图指南".encode(),
|
||||
"docs/release-notes/v28.0.0.zh-CN.md": "发布说明".encode(),
|
||||
"docs/INDEX.zh-CN.md": "中文索引".encode("utf-8"),
|
||||
"docs/images/README.zh-CN.md": "截图指南".encode("utf-8"),
|
||||
"docs/release-notes/v28.0.0.zh-CN.md": "发布说明".encode("utf-8"),
|
||||
".github/release-notes/v28.0.0.md": b"English release notes",
|
||||
}
|
||||
for name, data in documents.items():
|
||||
self.write(name, data)
|
||||
self.write("docs/private.tmp", b"not documentation")
|
||||
files = PACKER.collect(self.root, self.tag)
|
||||
|
||||
files = PACKER.collect(self.root, self.VERSION)
|
||||
|
||||
for name, data in documents.items():
|
||||
self.assertEqual(files[name], data)
|
||||
with self.subTest(document=name):
|
||||
self.assertEqual(data, files[name])
|
||||
self.assertIn("AGENTS.md", files)
|
||||
self.assertNotIn("docs/private.tmp", files)
|
||||
|
||||
def test_packages_without_removed_optional_root_documents(self):
|
||||
for name in ["AGENTS.md", "THIRD_PARTY_NOTICES.md"]:
|
||||
def test_optional_root_documents_may_be_absent(self):
|
||||
for name in ("AGENTS.md", "THIRD_PARTY_NOTICES.md"):
|
||||
(self.root / name).unlink()
|
||||
files = PACKER.collect(self.root, self.tag)
|
||||
|
||||
files = PACKER.collect(self.root, self.VERSION)
|
||||
|
||||
self.assertNotIn("AGENTS.md", files)
|
||||
self.assertNotIn("THIRD_PARTY_NOTICES.md", files)
|
||||
self.assertIn("GITEA-LICENSE", files)
|
||||
|
||||
Reference in new issue
Block a user