Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/release/artifacts.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import zipfile
from pathlib import Path

from common import digest, mod_version, require, staging_digest, summary
from common import digest, release_type, require, staging_digest, summary

GROUP = "dev.anvilcraft.lib"

Expand All @@ -31,7 +31,7 @@ def verify_mod(jar, expected_id, version):
def collect(source, line, sha, version, dest):
actual_sha = subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=source, text=True).strip()
require(actual_sha == sha, f"Expected {sha} for {line}, found {actual_sha}")
require(mod_version((source / "gradle.properties").read_bytes()) == version, "Version drift")
release_type(version)
game = tomllib.loads((source / "gradle/libs.versions.toml").read_text())["versions"]["minecraft"]
modules = json.loads((source / ".github/modules.json").read_text())
main = f"anvillib-neoforge-{line}"
Expand Down
4 changes: 3 additions & 1 deletion .github/release/central.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ def portal(path):


def deploy(tag, line, source, staging):
target = tag_manifest(tag)["targets"][line]
plan = tag_manifest(tag)
target = plan["targets"][line]
sha = target["sha"]
context = f"release/{tag}/{line}/central"
previous = commit_status(sha, context)
Expand All @@ -49,6 +50,7 @@ def deploy(tag, line, source, staging):
# JReleaser 1.23 logs the receipt immediately, before waiting for validation.
# Save it here so a cancelled Gradle process can be resumed by deployment ID.
with subprocess.Popen(["bash", "gradlew", f":anvillib-neoforge-{line}:jreleaserDeploy",
f"-Pmod_version={plan['version']}",
f"-PanvillibCentralStaging={Path(staging).resolve()}",
"--git-root-search", "--no-configuration-cache", "--console=plain"], cwd=source,
env=dict(os.environ, JRELEASER_MAVENCENTRAL_STAGE="UPLOAD"),
Expand Down
10 changes: 4 additions & 6 deletions .github/release/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,7 @@ def release_type(version):
def mod_version(properties):
matches = VERSION_LINE.findall(properties)
require(len(matches) == 1, "Expected one mod_version property")
version = matches[0][1].decode()
release_type(version)
return version
return matches[0][1].decode()


def digest(path, algorithm="sha256"):
Expand Down Expand Up @@ -106,21 +104,21 @@ def artifact(name):


def tag_manifest(tag):
require(re.fullmatch("v" + VERSION, tag), f"Invalid release tag: {tag}")
require(re.fullmatch(VERSION, tag), f"Invalid release tag: {tag}")
ref = github(f"git/ref/tags/{tag}", missing=True)
if ref is None:
return None
require(ref["object"]["type"] == "tag", f"{tag} already exists as a lightweight tag")
obj = github("git/tags/" + ref["object"]["sha"])
manifest = json.loads(obj["message"].strip().split("\n\n", 1)[1])
require(manifest["tag"] == tag and manifest["repo"] == repo()
and manifest["version"] == tag[1:] and re.fullmatch(r"[0-9]+", manifest["run"])
and manifest["version"] == tag and re.fullmatch(r"[0-9]+", manifest["run"])
and set(manifest["targets"]) == set(LINES)
and obj["object"]["sha"] == manifest["targets"][ANCHOR]["sha"], "Conflicting release tag")
for target in manifest["targets"].values():
require(re.fullmatch(r"[0-9]+(?:\.[0-9]+)+", target["game"])
and re.fullmatch(r"[0-9a-f]{40}", target["sha"]), "Invalid Minecraft version or source SHA")
stem = f"anvillib-neoforge-{target['game']}-{tag[1:]}"
stem = f"anvillib-neoforge-{target['game']}-{tag}"
require(target["jar"] == stem + ".jar" and set(target["files"]) ==
{stem + suffix for suffix in (".jar", "-sources.jar", "-javadoc.jar")}
and all(re.fullmatch(r"[0-9a-f]{64}", value) for value in target["files"].values()),
Expand Down
14 changes: 7 additions & 7 deletions .github/release/release.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,29 +15,28 @@

def prepare():
require(os.environ["GITHUB_REF"] == f"refs/heads/dev/{ANCHOR}", "Run Create Multi-Version Releases from dev/26.1")
tag = os.environ.get("RELEASE_TAG", "")
release_type(tag)
plan_file = Path("release-plan.json")
saved = artifact("source-plan")
if saved:
gh("run", "download", os.environ["GITHUB_RUN_ID"], "-n", "source-plan")
plan = json.loads(plan_file.read_text())
require(plan["tag"] == tag, "Input tag differs from the original release plan")
else:
refs = github("git/matching-refs/heads/dev/")
heads = {ref["ref"]: ref["object"]["sha"] for ref in refs}
targets = {}
versions = {}
for line in LINES:
sha = heads[f"refs/heads/dev/{line}"]
if line == ANCHOR:
require(sha == os.environ["GITHUB_SHA"], "Default branch advanced before preflight; dispatch again")
props = contents("gradle.properties", sha)
versions[line] = mod_version(props)
java = re.search(rb"(?m)^java_version=(\d+)\s*$", props).group(1).decode()
# Catch an incomplete rollout before scheduling seven builds.
contents("gradle/scripts/release.gradle", sha)
targets[line] = {"sha": sha, "java": java}
require(len(set(versions.values())) == 1, f"Branches disagree on mod_version: {versions}")
version = versions[ANCHOR]
plan = {"repo": repo(), "tag": "v" + version, "version": version,
plan = {"repo": repo(), "tag": tag, "version": tag,
"run": os.environ["GITHUB_RUN_ID"], "targets": targets}
if os.environ.get("GENERATE_RELEASE_NOTES") == "true":
plan.update(generate_notes(plan, ANCHOR))
Expand Down Expand Up @@ -164,7 +163,8 @@ def bump(tag, line):
if current == next_version:
summary(f"{base} is already at {next_version}.")
return
require(current == version, f"{base} is at {current}; expected {version}")
expected = mod_version(contents("gradle.properties", plan["targets"][line]["sha"]))
require(current == expected, f"{base} is at {current}; expected {expected} from the locked source")
title = f"chore(release): 将版本推进到 {next_version} [skip ci]"
owner = repo().split("/")[0]
prior = github(f"pulls?state=all&base={quote(base, safe='')}&head={quote(owner + ':' + head, safe='')}")
Expand All @@ -178,7 +178,7 @@ def bump(tag, line):
and [f["filename"] for f in commit["files"]] == ["gradle.properties"],
f"Unexpected content on {head}")
parent = contents("gradle.properties", commit["parents"][0]["sha"])
require(mod_version(parent) == version and contents("gradle.properties", ref["object"]["sha"]) ==
require(mod_version(parent) == expected and contents("gradle.properties", ref["object"]["sha"]) ==
VERSION_LINE.sub(lambda m: m[1] + next_version.encode() + m[3], parent), f"Conflicting {head}")
else:
updated = VERSION_LINE.sub(lambda m: m[1] + next_version.encode() + m[3], props)
Expand Down
52 changes: 38 additions & 14 deletions .github/release/test_release.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
import release
from common import LINES, digest, mod_version, staging_digest

TAG = "v2.0.0"
TAG = "2.0.0"
PLAN = {"tag": TAG, "version": "2.0.0", "targets": {
line: {"sha": f"sha-{line}", "game": line, "jar": f"{line}.jar", "files": {f"{line}.jar": line}}
for line in LINES}}
Expand All @@ -28,19 +28,23 @@ def setUp(self):
for module in (release, central, mod_sites):
self.enterContext(patch.object(module, "summary"))

def test_preflight_rejects_a_branch_with_a_different_version(self):
def test_preflight_uses_input_tag_despite_different_development_versions(self):
refs = [{"ref": f"refs/heads/dev/{line}", "object": {"sha": line}} for line in LINES]
def content(path, sha):
if path != "gradle.properties":
return b""
version = "2.0.1" if sha == "26.2" else "2.0.0"
return f"java_version=25\nmod_version={version}\n".encode()
with patch.dict(os.environ, {"GITHUB_REF": "refs/heads/dev/26.1", "GITHUB_SHA": "26.1"}), \
return f"java_version=25\nmod_version=development-{sha}\n".encode()
tag = "3.2.0+pre-release.13"
with tempfile.TemporaryDirectory() as tmp, chdir(tmp), \
patch.dict(os.environ, {"GITHUB_REF": "refs/heads/dev/26.1", "GITHUB_SHA": "26.1",
"RELEASE_TAG": tag, "DRY_RUN": "true", "GENERATE_RELEASE_NOTES": "false"}), \
patch.object(release, "artifact", return_value=None), patch.object(release, "github", return_value=refs), \
patch.object(release, "contents", side_effect=content), patch.object(release, "output") as output:
with self.assertRaisesRegex(ValueError, "Branches disagree"):
release.prepare()
output.assert_not_called()
release.prepare()
plan = json.loads(Path("release-plan.json").read_text())
self.assertEqual((plan["tag"], plan["version"]), (tag, tag))
self.assertEqual(set(plan["targets"]), set(LINES))
self.assertEqual(dict(c.args for c in output.call_args_list)["tag"], tag)

def test_staging_digest_covers_extra_and_modified_files(self):
with tempfile.TemporaryDirectory() as tmp:
Expand Down Expand Up @@ -116,6 +120,26 @@ def upload(_release, _upload, _tag, path):
release.seal()
self.assertEqual((len(tags), len(drafts), len(uploads), len(set(uploads))), (1, 1, 7, 7))

def test_central_passes_the_release_version_to_gradle(self):
receipt = "bce8fb04-e144-4d32-a5b9-7616a564ef19"
version = "2.0.0+pre-release.13"
plan = dict(PLAN, version=version, tag=version,
targets={"26.1": {"sha": "sha-26.1", "staging_sha256": "checksum"}})
credentials = {"JRELEASER_" + key: "test" for key in (
"MAVENCENTRAL_USERNAME", "MAVENCENTRAL_PASSWORD", "GPG_SECRET_KEY", "GPG_PUBLIC_KEY", "GPG_PASSPHRASE")}
with tempfile.TemporaryDirectory() as folder, patch.dict(os.environ, credentials), \
patch.object(central, "tag_manifest", return_value=plan), \
patch.object(central, "commit_status", return_value=None), \
patch.object(central, "staging_digest", return_value="checksum"), \
patch.object(central, "portal", return_value=b'{"deploymentState": "PUBLISHED"}'), \
patch.object(central, "record") as record, patch.object(central.subprocess, "Popen") as gradle:
process = gradle.return_value.__enter__.return_value
process.stdout = [f"uploaded as deployment {receipt}\n"]
process.wait.return_value = 0
central.deploy(version, "26.1", folder, "built/staging")
self.assertIn("-Pmod_version=" + version, gradle.call_args.args[0])
record.assert_called_with("sha-26.1", f"release/{version}/26.1/central", "success", "deployment " + receipt)

def test_central_continues_from_receipt_after_cancellation(self):
receipt = "bce8fb04-e144-4d32-a5b9-7616a564ef19"
statuses = [json.dumps({"deploymentState": state}).encode()
Expand Down Expand Up @@ -163,8 +187,8 @@ def test_uncertain_upload_requires_reconciliation(self):
upload.assert_not_called()

def test_bump_preserves_bytes_and_reuses_single_commit_and_pr(self):
original = b"# Config\r\nmod_version=2.0.0\r\njava_version=25\r\n"
files = {"base": original}
original = b"# Config\r\nmod_version=1.6.0+snapshot.42\r\njava_version=25\r\n"
files = {"base": original, "sha-26.1": original}
head, prs, payloads = [], [], []
def github(path, data=None, **kwargs):
if data is not None:
Expand Down Expand Up @@ -198,13 +222,13 @@ def github(path, data=None, **kwargs):
patch.object(release, "github", side_effect=github):
release.bump(TAG, "26.1")
release.bump(TAG, "26.1")
self.assertEqual(files["bump"], original.replace(b"2.0.0", b"2.0.1"))
self.assertEqual(files["bump"], original.replace(b"1.6.0+snapshot.42", b"2.0.1"))
self.assertEqual(len(payloads), 4)
self.assertEqual(prs[0]["title"], next(d["message"] for key, d in payloads if key == "git/commits"))
self.assertIn("[skip ci]", prs[0]["title"])
files["base"] = files["bump"]
release.bump(TAG, "26.1")
files["base"] = original.replace(b"2.0.0", b"2.1.0")
files["base"] = original.replace(b"1.6.0+snapshot.42", b"2.1.0")
with self.assertRaisesRegex(ValueError, "expected"):
release.bump(TAG, "26.1")
files["base"] = original
Expand All @@ -213,8 +237,8 @@ def github(path, data=None, **kwargs):
release.bump(TAG, "26.1")
self.assertEqual(len(payloads), 4)

def test_invalid_release_versions(self):
for raw in (b"", b"mod_version=2.0", b"mod_version=02.0.0", b"mod_version=2.0.0+snapshot.5",
def test_requires_one_nonempty_development_version(self):
for raw in (b"", b"mod_version=", b"mod_version= ",
b"mod_version=2.0.0\nmod_version=2.0.1"):
with self.assertRaises(ValueError):
mod_version(raw)
Expand Down
39 changes: 29 additions & 10 deletions .github/release/test_release_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,24 +42,24 @@ def test_exact_version_formats_and_types(self):
with self.subTest(version=version):
self.assertEqual(common.mod_version(f"mod_version={version}\r\n".encode()), version)
self.assertEqual(common.release_type(version), kind)
for version in ("2.0.0-beta.1", "2.0.0+alpha", "2.0.0+rc.01", "2.0.0+beta.-1",
for version in ("2.0", "02.0.0", "2.0.0-beta.1", "2.0.0+alpha", "2.0.0+rc.01", "2.0.0+beta.-1",
"2.0.0+snapshot.5", "2.0.0+unknown.1", "2.0.0+build.1+rc.2", "2.0.0+hotfix.1/other"):
with self.subTest(invalid=version), self.assertRaises(ValueError):
common.mod_version(f"mod_version={version}".encode())
common.release_type(version)

def test_prepare_freezes_native_notes_and_bump_policy(self):
refs = [{"ref": f"refs/heads/dev/{line}", "object": {"sha": f"sha-{line}"}} for line in common.LINES]
notes = "## What's Changed\n* Main branch PR #42\n\n## New Contributors\n* @contributor\n"
for version, _, bump in CASES:
for enabled in (False, True):
def content(path, _sha):
return f"mod_version={version}\njava_version=25\n".encode() if path == "gradle.properties" else b""
return b"mod_version=1.6.0\njava_version=25\n" if path == "gradle.properties" else b""
def api(path, data=None):
if path == "git/matching-refs/heads/dev/":
return refs
self.fail(path)
with self.subTest(version=version, notes=enabled), tempfile.TemporaryDirectory() as tmp, chdir(tmp), \
patch.dict(os.environ, {"GENERATE_RELEASE_NOTES": str(enabled).lower(), "DRY_RUN": "true"}), \
patch.dict(os.environ, {"GENERATE_RELEASE_NOTES": str(enabled).lower(), "DRY_RUN": "true", "RELEASE_TAG": version}), \
patch.object(release, "artifact", side_effect=[None, {"id": 1}]), \
patch.object(release, "contents", side_effect=content), \
patch.object(release, "github", side_effect=api) as github, \
Expand All @@ -77,9 +77,28 @@ def api(path, data=None):
self.assertEqual(plan.get("previous_tag_name"), "v1.9.0" if enabled else None)
self.assertEqual(json.loads(Path("release-plan.json").read_text()), plan)

def test_invalid_tag_is_rejected_before_preflight(self):
for tag in ("", "v2.0.0", "2.0", "2.0.0-beta.1", "2.0.0+snapshot.5", "2.0.0\n", "$(example)"):
with self.subTest(tag=tag), patch.dict(os.environ, {"RELEASE_TAG": tag}), \
patch.object(release, "artifact") as artifact, patch.object(release, "github") as github:
with self.assertRaises(ValueError):
release.prepare()
artifact.assert_not_called()
github.assert_not_called()

def test_rerun_cannot_relabel_a_frozen_plan(self):
with tempfile.TemporaryDirectory() as tmp, chdir(tmp), \
patch.dict(os.environ, {"RELEASE_TAG": "2.0.1"}), \
patch.object(release, "artifact", return_value={"id": 1}), patch.object(release, "gh"), \
patch.object(release, "github") as github:
Path("release-plan.json").write_text(json.dumps(PLAN))
with self.assertRaisesRegex(ValueError, "tag differs"):
release.prepare()
github.assert_not_called()

def test_qualified_tags_preserve_version_and_original_notes(self):
for version, _, _ in CASES:
tag = "v" + version
tag = version
targets = {}
for line in common.LINES:
stem = f"anvillib-neoforge-{line}-{version}"
Expand All @@ -97,7 +116,7 @@ def test_qualified_tags_preserve_version_and_original_notes(self):

def test_seal_preserves_generated_notes_when_reusing_a_draft(self):
native = "## What's Changed\n* PR #42 by @contributor\n"
plan = dict(PLAN, tag="v2.0.0+beta.1", version="2.0.0+beta.1", release_notes=native)
plan = dict(PLAN, tag="2.0.0+beta.1", version="2.0.0+beta.1", release_notes=native)
drafts, tags = [], []
def api(path, data=None, **kwargs):
if path == "git/tags":
Expand Down Expand Up @@ -136,7 +155,7 @@ def test_publication_type_and_latest_follow_the_version(self):
patch.object(release, "release_by_tag", return_value=draft), \
patch.object(release, "commit_status", return_value={"state": "success"}), \
patch.object(release, "github", return_value={"html_url": "example"}) as github:
release.publish("v" + version)
release.publish(version)
github.assert_called_once_with("releases/42", {
"draft": False, "prerelease": prerelease, "make_latest": "false" if prerelease else "true",
}, method="PATCH")
Expand All @@ -148,10 +167,10 @@ def test_prerelease_waits_for_central_and_rejects_type_drift(self):
patch.object(release, "commit_status", return_value={"state": "pending"}), \
patch.object(release, "github") as github:
with self.assertRaisesRegex(ValueError, "Central has not published"):
release.publish("v" + plan["version"])
release.publish(plan["version"])
lookup.return_value = dict(DRAFT, prerelease=False)
with self.assertRaisesRegex(ValueError, "Conflicting release type"):
release.publish("v" + plan["version"])
release.publish(plan["version"])
github.assert_not_called()

def test_all_qualified_versions_skip_bump_without_writing(self):
Expand All @@ -161,7 +180,7 @@ def test_all_qualified_versions_skip_bump_without_writing(self):
with self.subTest(version=version), patch.object(release, "tag_manifest", return_value=dict(PLAN, version=version)), \
patch.object(release, "release_by_tag", return_value={"draft": False}), \
patch.object(release, "github") as github, patch.object(release, "contents") as contents:
release.bump("v" + version, "26.1")
release.bump(version, "26.1")
github.assert_not_called()
contents.assert_not_called()

Expand Down
Loading
Loading