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
12 changes: 11 additions & 1 deletion .github/maven-release/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,17 @@ If CurseForge has accepted the file but it isn't public yet, wait for approval.
Start a new run with the recorded CurseForge file ID so it verifies that upload
without submitting the jar again. The release workflow sets Java 8 through
Minecraft 1.16.5, Java 16 for Minecraft 1.17.1, and Java 17 for Minecraft 1.18.2
through 1.20.1.
through 1.20.1, Java 21 for Minecraft 1.20.6, 1.21.1 and 1.21.11, and Java 25
for Minecraft 26.1.2. The pure target helper in verify.py uses an explicit
whitelist; unqualified targets, including 26.2, are rejected.

The dispatcher runs its offline tests before staging an artifact. Release
records still require the actual merged source, successful version-branch CI
run, artifact ID and checksums. A prepared route is not an approved release
record and does not publish anything.

The GitHub release targets the exact version-branch source recorded in
releases.json, not the publication workflow's default branch.

For Minecraft 1.20.1, check that basePower and Base Metals have public files
before publishing. Use the release's required dependency relations on CurseForge.
Expand Down
90 changes: 90 additions & 0 deletions .github/maven-release/test_verify.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import contextlib
import inspect
import io
from pathlib import Path
import re
import unittest
from unittest.mock import patch

import verify


class ReleaseTargetTests(unittest.TestCase):
def test_released_minecraft_lines_keep_their_runtime(self):
for java, versions in {
8: ("1.10.2", "1.12.2", "1.13.2", "1.14.4", "1.15.2", "1.16.5"),
16: ("1.17.1",),
17: ("1.18.2", "1.19.4", "1.20.1"),
21: ("1.20.6", "1.21.1", "1.21.11"),
25: ("26.1.2",),
}.items():
for version in versions:
with self.subTest(version=version):
self.assertEqual((version, f"Java {java}"), verify.release_target(f"master-{version}"))

def test_unqualified_or_malformed_targets_are_rejected(self):
for branch in ("master-26.2", "master-26.2.0", "master-26.1.1", "master-26.1.3",
"master-1.21.12", "master-26.1.2-extra", "feature/26.1.2",
"26.1.2", "master-main", "master-26.1.2\n"):
with self.subTest(branch=branch), self.assertRaises(ValueError):
verify.release_target(branch)

def test_missing_java25_entry_is_rejected(self):
source = inspect.getsource(verify.release_target)
marker = '"26.1.2": 25'
self.assertIn(marker, source)
mutated = source.replace(marker + ",", "")
namespace = {"require": verify.require}
exec(compile(mutated, "<missing-java25-control>", "exec"), namespace)
with self.assertRaisesRegex(ValueError, "Unsupported Minecraft version"):
namespace["release_target"]("master-26.1.2")

def test_target_cli_has_no_publication_side_effects(self):
output = io.StringIO()
with patch.object(verify.sys, "argv", ["verify.py", "target", "2.2.16.121111", "unused"]), \
patch.object(verify, "github") as github, patch.object(verify, "read_url") as read_url, \
patch.object(verify, "stage") as stage, contextlib.redirect_stdout(output):
verify.main()
self.assertEqual("minecraft=1.21.11\njava=Java 21\n", output.getvalue())
github.assert_not_called()
read_url.assert_not_called()
stage.assert_not_called()

def test_target_cli_requires_an_approved_version(self):
with patch.object(verify.sys, "argv", ["verify.py", "target", "0.0.0.9999999", "unused"]), \
self.assertRaisesRegex(ValueError, "Version is not approved"):
verify.main()

def test_dispatcher_retains_live_publication_guards(self):
workflow = (Path(__file__).resolve().parent.parent / "workflows/deploy-release.yml").read_text()
for marker in ("default: check-only", "github.event.repository.default_branch",
"inputs.mode == 'publish' && inputs.existing_curseforge_file_id == ''",
"inputs.mode == 'publish' && steps.preflight.outputs.state == 'absent'",
"Do not retry an upload.", "https://maven.moddev.zone/releases",
'verify-curseforge "$RELEASE_VERSION" "$RELEASE_WORK"',
'require-absent "$RELEASE_VERSION" "$RELEASE_WORK"'):
self.assertIn(marker, workflow)
self.assertIn('verify.py target "$RELEASE_VERSION" "$release_work"', workflow)
self.assertNotIn('capture("^master-', workflow)
self.assertIn("python3 -m unittest discover", workflow)
target = re.search(r'--verify-tag --target "\$(target|source_commit|RELEASE_SOURCE_COMMIT)"',
workflow)
self.assertIsNotNone(target)
guards = ('[[ "$' + target.group(1) + '" =~ ^[' + chars + ']{40}$ ]]'
for chars in ("a-f0-9", "0-9a-f"))
self.assertTrue(any(guard in workflow for guard in guards))

def test_ci_and_tag_provenance_stay_sealed(self):
source = Path(verify.__file__).read_text()
for marker in ('ci["head_sha"] == release["commit"]',
'ci["event"] == "push"', 'ci["head_branch"] == release["branch"]',
'ref["sha"] == release["commit"]',
'details["digest"] == "sha256:" + release["archiveSha256"]',
'sums[jars[0]] == release["mainSha256"]',
'bundle.namelist().count(member) == 1'):
self.assertIn(marker, source)


if __name__ == "__main__":
unittest.main()

18 changes: 17 additions & 1 deletion .github/maven-release/verify.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,19 @@ def digest(data):
return hashlib.sha256(data).hexdigest()


def release_target(branch):
require(branch.startswith("master-"), "Release requires a version master branch")
minecraft = branch.removeprefix("master-")
java_versions = {
"1.10.2": 8, "1.12.2": 8, "1.13.2": 8, "1.14.4": 8,
"1.15.2": 8, "1.16.5": 8, "1.17.1": 16, "1.18.2": 17,
"1.19.4": 17, "1.20.1": 17, "1.20.6": 21, "1.21.1": 21,
"1.21.11": 21, "26.1.2": 25,
}
require(minecraft in java_versions, f"Unsupported Minecraft version: {minecraft}")
return minecraft, f"Java {java_versions[minecraft]}"


def read_url(url, allow_absent=False):
if url.startswith(MAVEN + "/"):
# The CDN can cache pre-upload 404s even with a no-cache request header.
Expand Down Expand Up @@ -161,7 +174,10 @@ def main():
manifest = json.loads(MANIFEST.read_text(encoding="utf-8"))
require(version in manifest["releases"], f"Version is not approved: {version}")
release, work = manifest["releases"][version], Path(folder)
if mode in ("stage", "stage-candidate"):
if mode == "target":
minecraft, java = release_target(release["branch"])
print(f"minecraft={minecraft}\njava={java}")
elif mode in ("stage", "stage-candidate"):
stage(manifest, version, release, work, candidate=mode == "stage-candidate")
elif mode == "verify-curseforge":
verify_curseforge(manifest, version, release, int(os.environ["CURSEFORGE_FILE_ID"]),
Expand Down
15 changes: 4 additions & 11 deletions .github/workflows/deploy-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ jobs:
set -euo pipefail
[[ "$RELEASE_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]
[[ -z "$EXISTING_CF_FILE" || "$EXISTING_CF_FILE" =~ ^[1-9][0-9]*$ ]]
python3 -m unittest discover -s .github/maven-release -p 'test_*.py'
release_work="$RUNNER_TEMP/release"
python3 .github/maven-release/verify.py stage-candidate "$RELEASE_VERSION" "$release_work"
bash gradlew -p .github/maven-release generatePomFileForMavenJavaPublication \
Expand All @@ -69,22 +70,14 @@ jobs:
echo "Maven already has this release. Supply its existing CurseForge file ID." >&2
exit 1
fi
mc=$(jq -er --arg v "$RELEASE_VERSION" '.releases[$v].branch | capture("^master-(?<mc>1\\.[0-9]+\\.[0-9]+)$").mc' .github/maven-release/releases.json)
target=$(python3 .github/maven-release/verify.py target "$RELEASE_VERSION" "$release_work")
printf '%s\n' "$target" >> "$GITHUB_OUTPUT"
source_commit=$(jq -er --arg v "$RELEASE_VERSION" '.releases[$v].commit' .github/maven-release/releases.json)
[[ "$source_commit" =~ ^[a-f0-9]{40}$ ]]
case "$mc" in
1.10.2|1.12.2|1.13.2|1.14.4|1.15.2|1.16.5) java="Java 8" ;;
1.17.1) java="Java 16" ;;
1.18.2|1.19.4|1.20.1) java="Java 17" ;;
1.20.6|1.21.1|1.21.11) java="Java 21" ;;
*) echo "Unsupported Minecraft version for this publisher: $mc" >&2; exit 1 ;;
esac
echo "minecraft=$mc" >> "$GITHUB_OUTPUT"
echo "java=$java" >> "$GITHUB_OUTPUT"
echo "state=$state" >> "$GITHUB_OUTPUT"
echo "source_commit=$source_commit" >> "$GITHUB_OUTPUT"
echo "RELEASE_WORK=$release_work" >> "$GITHUB_ENV"
echo "Verified $RELEASE_VERSION for Minecraft $mc. Maven: $state." >> "$GITHUB_STEP_SUMMARY"
echo "Verified $RELEASE_VERSION. $target. Maven: $state." >> "$GITHUB_STEP_SUMMARY"

- name: Check publication credentials
if: inputs.mode == 'publish'
Expand Down
Loading