319 lines
9.5 KiB
Python
319 lines
9.5 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tarfile
|
|
import tempfile
|
|
import unittest
|
|
|
|
|
|
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
|
|
class ReleaseCliTests(unittest.TestCase):
|
|
def setUp(self) -> None:
|
|
openssl = shutil.which("openssl")
|
|
if openssl is None:
|
|
self.skipTest("OpenSSL is required")
|
|
self.temporary = tempfile.TemporaryDirectory()
|
|
base = Path(self.temporary.name)
|
|
self.repository = base / "working"
|
|
self.remote = base / "remote.git"
|
|
for relative in ("scripts", "ptlinux", "release"):
|
|
(self.repository / relative).mkdir(parents=True, exist_ok=True)
|
|
for relative in (
|
|
"scripts/__init__.py",
|
|
"scripts/build-release.sh",
|
|
"scripts/build_release.py",
|
|
"scripts/release.sh",
|
|
"scripts/release_lib.py",
|
|
"scripts/verify-release.sh",
|
|
"scripts/verify_release.py",
|
|
"ptlinux/update_resolver.py",
|
|
):
|
|
shutil.copy2(PROJECT_ROOT / relative, self.repository / relative)
|
|
(self.repository / "ptlinux/__init__.py").write_text(
|
|
'__version__ = "1.7.4"\n',
|
|
encoding="utf-8",
|
|
)
|
|
(self.repository / "pyproject.toml").write_text(
|
|
'[project]\nname = "release-fixture"\nversion = "1.7.4"\n',
|
|
encoding="utf-8",
|
|
)
|
|
(self.repository / ".gitignore").write_text(
|
|
"dist/\n__pycache__/\n*.pyc\n",
|
|
encoding="utf-8",
|
|
)
|
|
self.private_key = base / "private.pem"
|
|
public_key = self.repository / "ptlinux/launcher-update-public.pem"
|
|
subprocess.run(
|
|
[
|
|
openssl,
|
|
"genpkey",
|
|
"-quiet",
|
|
"-algorithm",
|
|
"RSA",
|
|
"-pkeyopt",
|
|
"rsa_keygen_bits:2048",
|
|
"-out",
|
|
str(self.private_key),
|
|
],
|
|
check=True,
|
|
capture_output=True,
|
|
)
|
|
subprocess.run(
|
|
[
|
|
openssl,
|
|
"pkey",
|
|
"-in",
|
|
str(self.private_key),
|
|
"-pubout",
|
|
"-out",
|
|
str(public_key),
|
|
],
|
|
check=True,
|
|
capture_output=True,
|
|
)
|
|
(self.repository / "release/config.json").write_text(
|
|
json.dumps(
|
|
{
|
|
"application": "PristonTale-EU",
|
|
"archive_layout": "flat",
|
|
"archive_prefix": "PristonTale-EU",
|
|
"channels": ["stable", "prerelease"],
|
|
"config_schema": 1,
|
|
"exclusions": [],
|
|
"legacy_archive_root_prefix": "pristontale-eu-linux-update-v",
|
|
"legacy_minimum_updater_version": "0.5.0",
|
|
"manifest_version": 1,
|
|
"minimum_config_schema": 1,
|
|
"minimum_updater_version": "0.5.0",
|
|
"payload": {
|
|
"files": [
|
|
{
|
|
"mode": "0644",
|
|
"path": "ptlinux/launcher-update-public.pem",
|
|
}
|
|
],
|
|
"python_trees": [{"exclude": [], "source": "ptlinux"}],
|
|
},
|
|
"preflight": [],
|
|
"release_branch": "main",
|
|
"release_status_file": "release/statuses.json",
|
|
"remote": "origin",
|
|
"repository_url": "https://gitea.example.test/owner/project",
|
|
"required_files": [
|
|
"release.json",
|
|
"ptlinux/__init__.py",
|
|
"ptlinux/launcher-update-public.pem",
|
|
],
|
|
"allowed_pem_files": ["ptlinux/launcher-update-public.pem"],
|
|
"version_mirrors": ["pyproject.toml"],
|
|
"version_source": "ptlinux/__init__.py",
|
|
"wrapped_bridge_versions": [],
|
|
},
|
|
indent=2,
|
|
sort_keys=True,
|
|
)
|
|
+ "\n",
|
|
encoding="utf-8",
|
|
)
|
|
self._run(["git", "init", "--bare", str(self.remote)], cwd=base)
|
|
self._git("init", "-b", "main")
|
|
self._git("config", "user.name", "Release Test")
|
|
self._git("config", "user.email", "release-test@example.test")
|
|
self._git("add", ".")
|
|
self._git("commit", "-m", "Initial launcher release")
|
|
self._git("tag", "-a", "v1.7.4", "-m", "Launcher 1.7.4")
|
|
self._git("remote", "add", "origin", str(self.remote))
|
|
self._git("push", "-u", "origin", "main", "v1.7.4")
|
|
(self.repository / "change.txt").write_text("update\n", encoding="utf-8")
|
|
self._git("add", "change.txt")
|
|
self._git("commit", "-m", "Improve updater reliability")
|
|
self._git("push", "origin", "main")
|
|
|
|
def tearDown(self) -> None:
|
|
self.temporary.cleanup()
|
|
|
|
def _run(
|
|
self,
|
|
command: list[str],
|
|
*,
|
|
cwd: Path | None = None,
|
|
environment: dict[str, str] | None = None,
|
|
) -> subprocess.CompletedProcess[str]:
|
|
return subprocess.run(
|
|
command,
|
|
cwd=cwd or self.repository,
|
|
env=environment,
|
|
stdin=subprocess.DEVNULL,
|
|
capture_output=True,
|
|
text=True,
|
|
check=False,
|
|
)
|
|
|
|
def _git(self, *arguments: str) -> str:
|
|
result = self._run(["git", *arguments])
|
|
self.assertEqual(result.returncode, 0, result.stderr)
|
|
return result.stdout.strip()
|
|
|
|
def _release(self, *arguments: str, **environment: str) -> subprocess.CompletedProcess[str]:
|
|
return self._run(
|
|
[str(self.repository / "scripts/release.sh"), *arguments],
|
|
environment={
|
|
**os.environ,
|
|
"PTLINUX_PYTHON": sys.executable,
|
|
"PTLINUX_SIGNING_KEY": str(self.private_key),
|
|
**environment,
|
|
},
|
|
)
|
|
|
|
def test_changelog_modes_preserve_markdown_and_do_not_mutate_git(self) -> None:
|
|
head_before = self._git("rev-parse", "HEAD")
|
|
tags_before = self._git("tag")
|
|
inline = self._release(
|
|
"stable",
|
|
"1.8.0",
|
|
"--description",
|
|
"## Highlights\n\n- Signed channels",
|
|
"--changelog-only",
|
|
)
|
|
self.assertEqual(inline.returncode, 0, inline.stderr)
|
|
notes = (self.repository / "dist/v1.8.0/release-notes.md").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
self.assertLess(notes.index("## Highlights"), notes.index("## Changes"))
|
|
self.assertIn("Improve updater reliability", notes)
|
|
|
|
description = Path(self.temporary.name) / "description.md"
|
|
description.write_text("## Fixes\n\n- Preserved **Markdown**", encoding="utf-8")
|
|
from_file = self._release(
|
|
"stable",
|
|
"1.8.0",
|
|
"--description-file",
|
|
str(description),
|
|
"--no-auto-changelog",
|
|
"--changelog-only",
|
|
)
|
|
self.assertEqual(from_file.returncode, 0, from_file.stderr)
|
|
notes = (self.repository / "dist/v1.8.0/release-notes.md").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
self.assertIn("- Preserved **Markdown**", notes)
|
|
self.assertNotIn("## Changes", notes)
|
|
|
|
editor = Path(self.temporary.name) / "editor"
|
|
editor.write_text(
|
|
'#!/usr/bin/env bash\nprintf "\\nEdited before publication.\\n" >> "$1"\n',
|
|
encoding="utf-8",
|
|
)
|
|
editor.chmod(0o755)
|
|
edited = self._release(
|
|
"stable",
|
|
"1.8.0",
|
|
"--editor",
|
|
"--changelog-only",
|
|
EDITOR=str(editor),
|
|
)
|
|
self.assertEqual(edited.returncode, 0, edited.stderr)
|
|
self.assertIn(
|
|
"Edited before publication.",
|
|
(self.repository / "dist/v1.8.0/release-notes.md").read_text(
|
|
encoding="utf-8"
|
|
),
|
|
)
|
|
self.assertEqual(self._git("rev-parse", "HEAD"), head_before)
|
|
self.assertEqual(self._git("tag"), tags_before)
|
|
|
|
def test_dry_run_then_builds_manual_stable_and_prerelease_packages(self) -> None:
|
|
head_before = self._git("rev-parse", "HEAD")
|
|
tags_before = self._git("tag")
|
|
remote_before = self._git("ls-remote", "origin")
|
|
dry_run = self._release(
|
|
"stable",
|
|
"1.8.0",
|
|
"--description",
|
|
"Stable release",
|
|
"--skip-tests",
|
|
"--dry-run",
|
|
)
|
|
self.assertEqual(dry_run.returncode, 0, dry_run.stderr)
|
|
self.assertIn("Dry run complete", dry_run.stdout)
|
|
self.assertFalse((self.repository / "dist/v1.8.0/manifest.json").exists())
|
|
|
|
stable = self._release(
|
|
"stable",
|
|
"1.8.0",
|
|
"--description",
|
|
"Stable release",
|
|
"--skip-tests",
|
|
)
|
|
self.assertEqual(stable.returncode, 0, stable.stderr)
|
|
stable_output = self.repository / "dist/v1.8.0"
|
|
manifest_path = stable_output / "manifest.json"
|
|
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
|
self.assertEqual(manifest["version"], "1.8.0")
|
|
self.assertEqual(manifest["channel"], "stable")
|
|
self.assertTrue((stable_output / "manifest.json.sig").is_file())
|
|
self.assertEqual(
|
|
manifest_path.read_bytes(),
|
|
(stable_output / "channel/stable.json").read_bytes(),
|
|
)
|
|
archive = stable_output / "PristonTale-EU-1.8.0.tar.gz"
|
|
with tarfile.open(archive, "r:gz") as bundle:
|
|
version_source = bundle.extractfile("ptlinux/__init__.py")
|
|
assert version_source is not None
|
|
self.assertIn(b'__version__ = "1.8.0"', version_source.read())
|
|
self.assertIn("leave the Gitea", stable.stdout)
|
|
|
|
prerelease = self._release(
|
|
"prerelease",
|
|
"1.9.0-beta.1",
|
|
"--description",
|
|
"First beta",
|
|
"--skip-tests",
|
|
)
|
|
self.assertEqual(prerelease.returncode, 0, prerelease.stderr)
|
|
prerelease_output = self.repository / "dist/v1.9.0-beta.1"
|
|
manifest = json.loads(
|
|
(prerelease_output / "manifest.json").read_text(encoding="utf-8")
|
|
)
|
|
self.assertEqual(manifest["version"], "1.9.0-beta.1")
|
|
self.assertEqual(manifest["channel"], "prerelease")
|
|
self.assertTrue(manifest_path.is_file())
|
|
self.assertIn('select the Gitea "Pre-release" checkbox', prerelease.stdout)
|
|
self.assertEqual(self._git("rev-parse", "HEAD"), head_before)
|
|
self.assertEqual(self._git("tag"), tags_before)
|
|
self.assertEqual(self._git("ls-remote", "origin"), remote_before)
|
|
self.assertIn('1.7.4', (self.repository / "ptlinux/__init__.py").read_text())
|
|
self.assertIn('1.7.4', (self.repository / "pyproject.toml").read_text())
|
|
|
|
def test_rejects_a_channel_version_mismatch_before_mutation(self) -> None:
|
|
result = self._release(
|
|
"stable",
|
|
"1.9.0-beta.1",
|
|
"--changelog-only",
|
|
)
|
|
self.assertNotEqual(result.returncode, 0)
|
|
self.assertEqual(self._git("tag", "-l", "v1.9.0-beta.1"), "")
|
|
|
|
def test_build_does_not_depend_on_release_branch_or_clean_worktree(self) -> None:
|
|
self._git("switch", "-c", "release-candidate")
|
|
(self.repository / "uncommitted.txt").write_text("local source\n", encoding="utf-8")
|
|
result = self._release(
|
|
"stable",
|
|
"1.8.0",
|
|
"--skip-tests",
|
|
)
|
|
self.assertEqual(result.returncode, 0, result.stderr)
|
|
self.assertEqual(self._git("tag", "-l", "v1.8.0"), "")
|
|
self.assertTrue((self.repository / "dist/v1.8.0/manifest.json").is_file())
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|