Require both platform artifacts for releases
All checks were successful
Release builds / Linux x64 artifact (push) Successful in 2m17s
Release builds / Windows x64 artifact (push) Successful in 1m19s
Release builds / Publish tagged Gitea release (push) Has been skipped
Core validation / Linux core gate (push) Successful in 1m17s

This commit is contained in:
gamer147
2026-08-03 22:35:43 -04:00
parent 4799b48b9c
commit d657c63e57
7 changed files with 473 additions and 129 deletions

View File

@@ -2,17 +2,21 @@
from __future__ import annotations
import hashlib
import io
import json
import tarfile
import tempfile
import unittest
import zipfile
from pathlib import Path
from typing import Any
import publish_gitea_release
import verify_windows_native
TARGET = "0123456789abcdef0123456789abcdef01234567"
TAG = "v0.1.0"
TAG = "v0.2.0"
class FakeApi:
@@ -48,46 +52,127 @@ def matching_release() -> dict[str, Any]:
"name": f"OpenMaidEngine Himegari {TAG}",
"draft": False,
"prerelease": False,
"html_url": "https://gitea.invalid/releases/tag/v0.1.0",
"html_url": "https://gitea.invalid/releases/tag/v0.2.0",
}
def create_assets(root: Path) -> list[Path]:
archive = root / "OpenMaidEngine-Himegari-linux-x64.tar.gz"
archive.write_bytes(b"synthetic archive")
checksum = root / "OpenMaidEngine-Himegari-linux-x64.tar.gz.sha256"
checksum.write_text(
f"{hashlib.sha256(archive.read_bytes()).hexdigest()} {archive.name}\n",
encoding="ascii",
)
build_info = root / "BUILD-INFO.json"
build_info.write_text(json.dumps({
def json_bytes(value: dict[str, Any]) -> bytes:
return (json.dumps(value, indent=2, sort_keys=True) + "\n").encode()
def build_info(rid: str) -> bytes:
return json_bytes({
"schema_version": 1,
"source_commit": TARGET,
"source_dirty": False,
"target": "linux-x64",
}), encoding="utf-8")
ledger = root / "SHA256SUMS"
ledger.write_text(f"{'0' * 64} Himegari.x86_64\n", encoding="ascii")
smoke = root / "package-smoke.log"
smoke.write_text("PACKAGE SMOKE OK: opcodes=548 ffmpeg-abi=3\n", encoding="utf-8")
return [archive, checksum, build_info, ledger, smoke]
"target": rid,
})
def windows_verification() -> bytes:
return json_bytes({
"schema_version": 1,
"target": "win-x64",
"executable_machine": "AMD64",
"native": {
"schema_version": 1,
"target": "win-x64",
"bundle": "data_Himegari_windows_x86_64",
"machines": {
name: "AMD64"
for name in (verify_windows_native.SHIM, *verify_windows_native.RUNTIME_DLLS)
},
"shim_exports": sorted(verify_windows_native.REQUIRED_EXPORTS),
"shim_imports": [*verify_windows_native.RUNTIME_DLLS, "KERNEL32.dll"],
},
})
def add_tar_bytes(archive: tarfile.TarFile, name: str, data: bytes) -> None:
info = tarfile.TarInfo(name)
info.size = len(data)
archive.addfile(info, io.BytesIO(data))
def write_checksum(directory: Path, archive: Path) -> None:
digest = hashlib.sha256(archive.read_bytes()).hexdigest()
(directory / f"{archive.name}.sha256").write_text(
f"{digest} {archive.name}\n", encoding="ascii"
)
def create_artifacts(root: Path) -> tuple[Path, Path, Path]:
linux = root / "linux"
windows = root / "windows"
prepared = root / "prepared"
linux.mkdir()
windows.mkdir()
linux_info = build_info("linux-x64")
linux_ledger = f"{'0' * 64} Himegari.x86_64\n".encode()
linux_archive = linux / publish_gitea_release.LINUX_ARCHIVE
with tarfile.open(linux_archive, "w:gz") as archive:
add_tar_bytes(
archive,
f"{publish_gitea_release.LINUX_PACKAGE_ROOT}/BUILD-INFO.json",
linux_info,
)
add_tar_bytes(
archive,
f"{publish_gitea_release.LINUX_PACKAGE_ROOT}/SHA256SUMS",
linux_ledger,
)
(linux / "BUILD-INFO.json").write_bytes(linux_info)
(linux / "SHA256SUMS").write_bytes(linux_ledger)
(linux / "package-smoke.log").write_text(
publish_gitea_release.SMOKE_MARKER + "\n", encoding="utf-8"
)
write_checksum(linux, linux_archive)
windows_info = build_info("win-x64")
windows_ledger = f"{'1' * 64} Himegari.exe\n".encode()
verification = windows_verification()
windows_archive = windows / publish_gitea_release.WINDOWS_ARCHIVE
with zipfile.ZipFile(windows_archive, "w", compression=zipfile.ZIP_DEFLATED) as archive:
root_name = publish_gitea_release.WINDOWS_PACKAGE_ROOT
archive.writestr(f"{root_name}/BUILD-INFO.json", windows_info)
archive.writestr(f"{root_name}/SHA256SUMS", windows_ledger)
archive.writestr(f"{root_name}/WINDOWS-VERIFICATION.json", verification)
(windows / "BUILD-INFO.json").write_bytes(windows_info)
(windows / "SHA256SUMS").write_bytes(windows_ledger)
(windows / "WINDOWS-VERIFICATION.json").write_bytes(verification)
write_checksum(windows, windows_archive)
return linux, windows, prepared
class PublishGiteaReleaseTests(unittest.TestCase):
def test_creates_release_and_uploads_verified_asset_set(self) -> None:
def test_creates_release_and_uploads_three_verified_assets(self) -> None:
with tempfile.TemporaryDirectory() as temporary:
assets = create_assets(Path(temporary))
linux, windows, prepared = create_artifacts(Path(temporary))
api = FakeApi()
result = publish_gitea_release.promote_release(api, api.upload, TAG, TARGET, assets)
result = publish_gitea_release.promote_release(
api, api.upload, TAG, TARGET, linux, windows, prepared
)
self.assertEqual(17, result["id"])
self.assertEqual(TAG, api.created_payload["tag_name"])
self.assertEqual(TARGET, api.created_payload["target_commitish"])
self.assertEqual({asset.name for asset in assets}, {asset["name"] for asset in api.assets})
self.assertIn("Windows archive", api.created_payload["body"])
self.assertEqual(
publish_gitea_release.EXPECTED_RELEASE_ASSETS,
{asset["name"] for asset in api.assets},
)
checksums = (prepared / publish_gitea_release.RELEASE_CHECKSUMS).read_text(
encoding="ascii"
)
self.assertIn(publish_gitea_release.LINUX_ARCHIVE, checksums)
self.assertIn(publish_gitea_release.WINDOWS_ARCHIVE, checksums)
def test_retry_keeps_matching_assets_and_uploads_only_missing(self) -> None:
with tempfile.TemporaryDirectory() as temporary:
assets = create_assets(Path(temporary))
linux, windows, prepared = create_artifacts(Path(temporary))
assets = publish_gitea_release.prepare_release_assets(
TAG, TARGET, linux, windows, prepared
)
api = FakeApi(matching_release())
api.assets.append({"name": assets[0].name, "size": assets[0].stat().st_size})
uploaded: list[str] = []
@@ -96,40 +181,68 @@ class PublishGiteaReleaseTests(unittest.TestCase):
uploaded.append(asset.name)
return api.upload(release_id, asset)
publish_gitea_release.promote_release(api, upload, TAG, TARGET, assets)
publish_gitea_release.promote_release(
api, upload, TAG, TARGET, linux, windows, prepared
)
self.assertNotIn(assets[0].name, uploaded)
self.assertEqual(len(assets) - 1, len(uploaded))
self.assertEqual(2, len(uploaded))
self.assertIsNone(api.created_payload)
def test_refuses_mismatched_release_and_asset_collision(self) -> None:
with tempfile.TemporaryDirectory() as temporary:
assets = create_assets(Path(temporary))
linux, windows, prepared = create_artifacts(Path(temporary))
wrong_release = matching_release()
wrong_release["target_commitish"] = "f" * 40
api = FakeApi(wrong_release)
with self.assertRaisesRegex(ValueError, "does not match"):
publish_gitea_release.promote_release(api, api.upload, TAG, TARGET, assets)
publish_gitea_release.promote_release(
api, api.upload, TAG, TARGET, linux, windows, prepared
)
api = FakeApi(matching_release())
api.assets.append({"name": assets[0].name, "size": 999})
api.assets.append({"name": publish_gitea_release.LINUX_ARCHIVE, "size": 999})
with self.assertRaisesRegex(ValueError, "will not be overwritten"):
publish_gitea_release.promote_release(api, api.upload, TAG, TARGET, assets)
publish_gitea_release.promote_release(
api, api.upload, TAG, TARGET, linux, windows, prepared
)
def test_refuses_mismatched_build_evidence(self) -> None:
api = FakeApi(matching_release())
api.assets.append({"name": "stale.txt", "size": 1})
with self.assertRaisesRegex(ValueError, "unexpected assets"):
publish_gitea_release.promote_release(
api, api.upload, TAG, TARGET, linux, windows, prepared
)
def test_refuses_mismatched_archive_commit_and_structural_evidence(self) -> None:
with tempfile.TemporaryDirectory() as temporary:
assets = create_assets(Path(temporary))
by_name = {asset.name: asset for asset in assets}
by_name["OpenMaidEngine-Himegari-linux-x64.tar.gz"].write_bytes(b"changed")
linux, windows, prepared = create_artifacts(Path(temporary))
(linux / publish_gitea_release.LINUX_ARCHIVE).write_bytes(b"changed")
with self.assertRaisesRegex(ValueError, "external SHA-256"):
publish_gitea_release.validate_inputs(TAG, TARGET, assets)
publish_gitea_release.prepare_release_assets(
TAG, TARGET, linux, windows, prepared
)
assets = create_assets(Path(temporary))
by_name = {asset.name: asset for asset in assets}
build_info = json.loads(by_name["BUILD-INFO.json"].read_text(encoding="utf-8"))
build_info["source_commit"] = "f" * 40
by_name["BUILD-INFO.json"].write_text(json.dumps(build_info), encoding="utf-8")
with tempfile.TemporaryDirectory() as temporary:
linux, windows, prepared = create_artifacts(Path(temporary))
info = json.loads((windows / "BUILD-INFO.json").read_text(encoding="utf-8"))
info["source_commit"] = "f" * 40
(windows / "BUILD-INFO.json").write_bytes(json_bytes(info))
with self.assertRaisesRegex(ValueError, "does not match"):
publish_gitea_release.validate_inputs(TAG, TARGET, assets)
publish_gitea_release.prepare_release_assets(
TAG, TARGET, linux, windows, prepared
)
with tempfile.TemporaryDirectory() as temporary:
linux, windows, prepared = create_artifacts(Path(temporary))
report = json.loads(
(windows / "WINDOWS-VERIFICATION.json").read_text(encoding="utf-8")
)
report["executable_machine"] = "I386"
(windows / "WINDOWS-VERIFICATION.json").write_bytes(json_bytes(report))
with self.assertRaisesRegex(ValueError, "not accepted"):
publish_gitea_release.prepare_release_assets(
TAG, TARGET, linux, windows, prepared
)
if __name__ == "__main__":