forked from 1b-agent/opencadstudio-upstream
`v2026.40.1` matched neither the weekly pattern nor plain semver (2026 is past the MSI major limit), so a fix release could not be built. Weekly tags now take an optional patch: Cargo 2026.40.1, MSI 26.40.1, display 2026.40.1 (week zero-padded, as build.rs and the update check expect). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
168 lines
8.8 KiB
Python
168 lines
8.8 KiB
Python
"""Run with python3 scripts/test_release.py; uses only local temporary repositories."""
|
|
|
|
from contextlib import redirect_stdout
|
|
from datetime import datetime, timezone
|
|
import io
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import subprocess
|
|
import tempfile
|
|
import textwrap
|
|
import unittest
|
|
from unittest.mock import patch
|
|
|
|
import release
|
|
|
|
|
|
class ReleaseTests(unittest.TestCase):
|
|
def test_web_source_selection(self):
|
|
workflow = (Path(__file__).resolve().parents[1] / ".github/workflows/pages.yml").read_text()
|
|
source = workflow.split(" - name: Resolve release source\n", 1)[1]
|
|
script = textwrap.dedent(source.split(" run: |\n", 1)[1].split("\n - uses:", 1)[0])
|
|
mock_gh = 'gh() { if [ "$1" = release ]; then echo v2026.37; else echo release-sha; fi; }\n'
|
|
cases = (
|
|
({}, "release-sha", None),
|
|
({"RELEASE_TAG": "v2026.37", "RELEASE_COMMIT": "release-sha"}, "release-sha", None),
|
|
({"RELEASE_COMMIT": "wrong-sha"}, None, "Release tag and commit do not match"),
|
|
({"BUILD_MAIN": "true"}, "main-sha", None),
|
|
({"BUILD_MAIN": "true", "GITHUB_REF": "refs/heads/other"}, None, "Web hotfixes must run on main"),
|
|
)
|
|
with tempfile.TemporaryDirectory() as temp:
|
|
output = Path(temp) / "output"
|
|
for overrides, commit, error in cases:
|
|
with self.subTest(overrides=overrides):
|
|
output.write_text("")
|
|
result = subprocess.run(["bash", "-e", "-c", mock_gh + script], text=True, capture_output=True, env={
|
|
**os.environ, "GITHUB_REPOSITORY": "owner/repo", "GITHUB_REF": "refs/heads/main",
|
|
"GITHUB_SHA": "main-sha", "GITHUB_OUTPUT": str(output),
|
|
"RELEASE_TAG": "", "RELEASE_COMMIT": "", "BUILD_MAIN": "false", **overrides,
|
|
})
|
|
if error:
|
|
self.assertNotEqual(result.returncode, 0)
|
|
self.assertIn(error, result.stdout)
|
|
self.assertEqual(output.read_text(), "")
|
|
else:
|
|
self.assertEqual(result.returncode, 0, result.stderr)
|
|
self.assertEqual(output.read_text(), f"tag=v2026.37\ncommit={commit}\n")
|
|
|
|
def test_build_metadata(self):
|
|
with tempfile.TemporaryDirectory() as temp:
|
|
binary = str(Path(temp) / "build-script")
|
|
subprocess.run(["rustc", str(Path(__file__).resolve().parents[1] / "build.rs"), "-o", binary], check=True)
|
|
for cargo in ("0.9.8", "2026.9.0", "2026.35.0", "2026.53.0", "2027.1.0", "2026.40.1", "2026.5.2"):
|
|
result = subprocess.check_output([binary], text=True, env={**os.environ, "CARGO_PKG_VERSION": cargo})
|
|
self.assertIn(f"cargo:rustc-env=OCS_APP_VERSION={release.display_version(cargo)}\n", result)
|
|
|
|
def test_calendar_versions(self):
|
|
self.assertEqual(release.versions("v2026.35"), {
|
|
"version": "2026.35", "cargo": "2026.35.0", "msi": "26.35.0", "tag": "v2026.35",
|
|
})
|
|
self.assertEqual(release.versions("2026.09")["cargo"], "2026.9.0")
|
|
self.assertEqual(release.display_version("2026.9.0"), "2026.09")
|
|
self.assertEqual(release.display_version("0.9.8"), "0.9.8")
|
|
self.assertEqual(release.versions("v2026.40.1"), {
|
|
"version": "2026.40.1", "cargo": "2026.40.1", "msi": "26.40.1", "tag": "v2026.40.1",
|
|
})
|
|
self.assertEqual(release.display_version("2026.5.2"), "2026.05.2")
|
|
self.assertEqual(datetime(2027, 1, 3, tzinfo=timezone.utc).strftime("v%G.%V"), "v2026.53")
|
|
for value in ("2026.00", "2026.54", "2027.53", "2026.9", "2026.35.0", "bad", "0.09.8"):
|
|
with self.assertRaises(ValueError, msg=value):
|
|
release.versions(value)
|
|
|
|
def test_release_commit_push_and_retry(self):
|
|
original_run = release.run
|
|
releases = {"v0.9.8": {"name": "v0.9.8", "body": "Previous notes", "isDraft": False}}
|
|
latest = "v0.9.8"
|
|
fail_create = False
|
|
|
|
def run(*args):
|
|
nonlocal latest, fail_create
|
|
if args[0] != "gh":
|
|
return original_run(*args)
|
|
if args[1:3] == ("release", "list"):
|
|
return json.dumps([{"tagName": tag, "isDraft": False} for tag in releases])
|
|
if args[1:3] == ("release", "create"):
|
|
if fail_create:
|
|
fail_create = False
|
|
raise RuntimeError("Temporary release API failure")
|
|
latest = args[3]
|
|
releases[latest] = {
|
|
"name": args[args.index("--title") + 1],
|
|
"body": Path(args[args.index("--notes-file") + 1]).read_text(encoding="utf-8"),
|
|
"isDraft": False,
|
|
}
|
|
return ""
|
|
self.assertEqual(args[1:3], ("release", "view"))
|
|
if args[3] == "--json":
|
|
return json.dumps({"tagName": latest})
|
|
return json.dumps(releases[args[3]])
|
|
|
|
with tempfile.TemporaryDirectory() as temp:
|
|
temp = Path(temp)
|
|
root = temp / "checkout"
|
|
root.mkdir()
|
|
previous_directory = Path.cwd()
|
|
os.chdir(root)
|
|
try:
|
|
original_run("git", "init", "-b", "main")
|
|
original_run("git", "config", "user.name", "Release test")
|
|
original_run("git", "config", "user.email", "test@example.invalid")
|
|
original_run("git", "init", "--bare", "-b", "main", str(temp / "origin.git"))
|
|
original_run("git", "remote", "add", "origin", str(temp / "origin.git"))
|
|
Path("Cargo.toml").write_text('[package]\nname = "OpenCADStudio"\nversion = "0.9.8"\n')
|
|
Path("Cargo.lock").write_text('version = 4\n[[package]]\nname = "OpenCADStudio"\nversion = "0.9.8"\n')
|
|
original_run("git", "add", ".")
|
|
original_run("git", "commit", "-m", "Initial release")
|
|
original_run("git", "tag", "v0.9.8")
|
|
original_run("git", "push", "origin", "main", "--tags")
|
|
|
|
with patch.object(release, "run", run), patch.object(release, "datetime") as clock, patch.dict(os.environ, {
|
|
"GITHUB_REPOSITORY": "owner/repo", "GITHUB_REF": "refs/heads/main", "GITHUB_OUTPUT": str(temp / "output"),
|
|
}):
|
|
clock.now.return_value = datetime(2026, 8, 30, 12, tzinfo=timezone.utc)
|
|
|
|
def prepare(publish):
|
|
result = io.StringIO()
|
|
with redirect_stdout(result):
|
|
release.prepare(publish)
|
|
return result.getvalue()
|
|
|
|
self.assertIn("ready=false", prepare(True))
|
|
Path("feature").write_text("web release source")
|
|
original_run("git", "add", "feature")
|
|
original_run("git", "commit", "-m", "Add web release synchronization")
|
|
preview = prepare(False)
|
|
self.assertIn("Add web release synchronization", preview)
|
|
self.assertEqual(release.cargo_version(), "0.9.8")
|
|
self.assertEqual(original_run("git", "tag", "--list", "v2026.35"), "")
|
|
|
|
self.assertIn("ready=true", prepare(True))
|
|
sha = original_run("git", "rev-parse", "HEAD")
|
|
self.assertEqual(release.cargo_version(), "2026.35.0")
|
|
self.assertIn('version = "2026.35.0"', Path("Cargo.lock").read_text())
|
|
self.assertEqual(original_run("git", "log", "-1", "--format=%s"), "Release v2026.35")
|
|
self.assertEqual(original_run("git", "rev-parse", "origin/main"), sha)
|
|
self.assertEqual(original_run("git", "rev-parse", "v2026.35^{commit}"), sha)
|
|
self.assertEqual(original_run("git", "status", "--porcelain"), "")
|
|
self.assertIn(f"commit={sha}", prepare(True))
|
|
|
|
clock.now.return_value = datetime(2026, 9, 6, 12, tzinfo=timezone.utc)
|
|
self.assertIn("ready=false", prepare(True))
|
|
Path("feature").write_text("next change")
|
|
original_run("git", "add", "feature")
|
|
original_run("git", "commit", "-m", "Fix release retry")
|
|
fail_create = True
|
|
with self.assertRaises(RuntimeError):
|
|
prepare(True)
|
|
sha = original_run("git", "rev-parse", "HEAD")
|
|
self.assertIn(f"commit={sha}", prepare(True))
|
|
self.assertEqual(original_run("git", "rev-parse", "HEAD"), sha)
|
|
self.assertEqual(releases["v2026.36"]["name"], "2026.36")
|
|
finally:
|
|
os.chdir(previous_directory)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|