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
25 changes: 22 additions & 3 deletions .github/modrinth.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,27 @@
"paper",
"purpur"
],
"game_version_min": "1.20.6",
"game_version_max_exclusive": "1.22",
"compatibility_label": "`1.20.6` and `1.21.x`",
"game_versions": [
"1.20",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Do not advertise pre-1.20.6 servers

The repo still declares the runtime baseline as 1.20.6: pom.xml:33 says the jar is compiled against the oldest supported API, README.md:67 lists Minecraft 1.20.6-1.21.x, and the release smoke workflow only exercises the lower bound on Paper 1.20.6. With these new 1.20–1.20.5 tags, the Modrinth payload will publish the release to server versions outside the supported/evidenced range, so admins on those versions can install a jar that is only built and tested for 1.20.6+.

Useful? React with 👍 / 👎.

"1.20.1",
"1.20.2",
"1.20.3",
"1.20.4",
"1.20.5",
"1.20.6",
"1.21",
"1.21.1",
"1.21.2",
"1.21.3",
"1.21.4",
"1.21.5",
"1.21.6",
"1.21.7",
"1.21.8",
"1.21.9",
"1.21.10",
"1.21.11"
],
"compatibility_label": "`1.20.x` and `1.21.x` through `1.21.11`",
"release_notes_directory": "docs/releases"
}
7 changes: 5 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -453,8 +453,11 @@ jobs:
and .status == "listed"
and (.version_type == "release" or .version_type == "beta" or .version_type == "alpha")
and (.featured == (.version_type == "release"))
and (.game_versions | index("1.20.6") != null)
and (.game_versions | all(.[]; test("^(1\\.20\\.6|1\\.21(\\.[0-9]+)?)$")))' \
and .game_versions == [
"1.20", "1.20.1", "1.20.2", "1.20.3", "1.20.4", "1.20.5", "1.20.6",
"1.21", "1.21.1", "1.21.2", "1.21.3", "1.21.4", "1.21.5",
"1.21.6", "1.21.7", "1.21.8", "1.21.9", "1.21.10", "1.21.11"
]' \
"${metadata}/payload.json" >/dev/null
cmp -s "${metadata}/changelog.md" \
<(jq -r '.changelog' "${metadata}/payload.json")
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ Keep a Changelog format.
selects current supported Minecraft tags, renders polished Markdown release notes, and verifies the
Modrinth API readback without exposing the publication token to build or repository code.

### Fixed

- Advertise every stable Minecraft 1.20.x version and Minecraft 1.21.x through 1.21.11 on Modrinth,
while rejecting later game-version tags until compatibility is reviewed explicitly.

## [1.0.9] - 2026-07-16

### Fixed
Expand Down
9 changes: 5 additions & 4 deletions PUBLISHING.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,11 @@ Enter the token at the secure prompt. Never put it in a command argument, commit
workflow log. If the credential is missing or expired, GitHub Release publication remains complete and
the Modrinth job fails explicitly; replace the Environment secret and rerun the failed jobs.

Public channel metadata is versioned in `.github/modrinth.json`. Stable Minecraft releases in the
configured `1.20.6 <= version < 1.22` range are selected automatically, while snapshots and unsupported
loaders are rejected. Modrinth has no Pufferfish loader tag, so releases declare Paper and Purpur; Folia
is intentionally excluded because it is not supported.
Public channel metadata is versioned in `.github/modrinth.json`. The exact stable Modrinth tags for every
`1.20.x` release and every `1.21.x` release through `1.21.11` are validated against the live API before
publication. Snapshots, `1.21.12+`, `1.22+`, and unsupported loaders are rejected until the reviewed
configuration is intentionally changed. Modrinth has no Pufferfish loader tag, so releases declare Paper
and Purpur; Folia is intentionally excluded because it is not supported.

Release notes are rendered from the exact `CHANGELOG.md` version section with a consistent compatibility,
verification, and upgrade panel. For a richer player-facing release, add `docs/releases/x.y.z.md` before
Expand Down
25 changes: 23 additions & 2 deletions scripts/check-release-security.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,8 @@ def main() -> int:
"include_changelog=false",
"Authorization: ${MODRINTH_TOKEN}",
'and .loaders == ["paper", "purpur"]',
"and .game_versions == [",
'"1.21.10", "1.21.11"',
"already-present",
"modrinth-readback.json",
):
Expand Down Expand Up @@ -199,8 +201,27 @@ def main() -> int:
"project_id": "ri1Ibgnf",
"project_slug": "amctimber",
"loaders": ["paper", "purpur"],
"game_version_min": "1.20.6",
"game_version_max_exclusive": "1.22",
"game_versions": [
"1.20",
"1.20.1",
"1.20.2",
"1.20.3",
"1.20.4",
"1.20.5",
"1.20.6",
"1.21",
"1.21.1",
"1.21.2",
"1.21.3",
"1.21.4",
"1.21.5",
"1.21.6",
"1.21.7",
"1.21.8",
"1.21.9",
"1.21.10",
"1.21.11",
],
}
for field, expected in expected_modrinth_config.items():
if modrinth_config.get(field) != expected:
Expand Down
41 changes: 21 additions & 20 deletions scripts/prepare-modrinth-release.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,6 @@ def validate_config(config: Any) -> dict[str, Any]:
"project_id",
"project_slug",
"display_name",
"game_version_min",
"game_version_max_exclusive",
"compatibility_label",
"release_notes_directory",
)
Expand All @@ -74,38 +72,42 @@ def validate_config(config: Any) -> dict[str, Any]:
or len(loaders) != len(set(loaders))
):
raise PreparationError("loaders must be a non-empty list of unique names")
minimum = parse_game_version(config["game_version_min"])
maximum = parse_game_version(config["game_version_max_exclusive"])
if minimum >= maximum:
raise PreparationError("game version range must be non-empty")
game_versions = config.get("game_versions")
if (
not isinstance(game_versions, list)
or not game_versions
or any(not isinstance(version, str) or not version for version in game_versions)
or len(game_versions) != len(set(game_versions))
):
raise PreparationError("game_versions must be a non-empty list of unique names")
parsed_versions = [parse_game_version(version) for version in game_versions]
if parsed_versions != sorted(parsed_versions):
raise PreparationError("game_versions must be in ascending Minecraft version order")
return config


def select_game_versions(
entries: Any, minimum_text: str, maximum_text: str
) -> list[str]:
def select_game_versions(entries: Any, configured: list[str]) -> list[str]:
if not isinstance(entries, list):
raise PreparationError("Modrinth game-version response must be a JSON array")
minimum = parse_game_version(minimum_text)
maximum = parse_game_version(maximum_text)
selected: dict[tuple[int, int, int], str] = {}
available: set[str] = set()
for entry in entries:
if not isinstance(entry, dict) or entry.get("version_type") != "release":
continue
value = entry.get("version")
if not isinstance(value, str):
continue
try:
numeric = parse_game_version(value)
parse_game_version(value)
except PreparationError:
continue
if minimum <= numeric < maximum:
selected[numeric] = value
if minimum not in selected:
available.add(value)
missing = [version for version in configured if version not in available]
if missing:
raise PreparationError(
f"Modrinth does not currently advertise required baseline {minimum_text}"
"Modrinth does not currently advertise configured stable versions: "
+ ", ".join(missing)
)
return [selected[key] for key in sorted(selected)]
return configured


def validate_loaders(entries: Any, configured: list[str]) -> list[str]:
Expand Down Expand Up @@ -231,8 +233,7 @@ def prepare_candidate(
config = validate_config(read_json(config_path))
game_versions = select_game_versions(
read_json(game_versions_path),
config["game_version_min"],
config["game_version_max_exclusive"],
config["game_versions"],
)
loaders = validate_loaders(read_json(loaders_path), config["loaders"])
version_type = classify_version_type(version, marked_prerelease)
Expand Down
41 changes: 31 additions & 10 deletions scripts/test-modrinth-release.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,31 @@
SPEC.loader.exec_module(MODULE)


SUPPORTED_VERSIONS = [
"1.20",
"1.20.1",
"1.20.2",
"1.20.3",
"1.20.4",
"1.20.5",
"1.20.6",
"1.21",
"1.21.1",
"1.21.2",
"1.21.3",
"1.21.4",
"1.21.5",
"1.21.6",
"1.21.7",
"1.21.8",
"1.21.9",
"1.21.10",
"1.21.11",
]
GAME_VERSIONS = [
{"version": "1.20.5", "version_type": "release"},
{"version": "1.20.6", "version_type": "release"},
{"version": "1.21", "version_type": "release"},
{"version": "1.21.1", "version_type": "release"},
*({"version": version, "version_type": "release"} for version in SUPPORTED_VERSIONS),
{"version": "1.21.12-pre1", "version_type": "snapshot"},
{"version": "1.21.12", "version_type": "release"},
{"version": "1.22", "version_type": "release"},
]
LOADERS = [
Expand All @@ -39,13 +58,14 @@
class ModrinthReleaseTests(unittest.TestCase):
def test_selects_only_supported_stable_range(self) -> None:
self.assertEqual(
["1.20.6", "1.21", "1.21.1"],
MODULE.select_game_versions(GAME_VERSIONS, "1.20.6", "1.22"),
SUPPORTED_VERSIONS,
MODULE.select_game_versions(GAME_VERSIONS, SUPPORTED_VERSIONS),
)

def test_rejects_missing_compatibility_baseline(self) -> None:
with self.assertRaisesRegex(MODULE.PreparationError, "required baseline"):
MODULE.select_game_versions(GAME_VERSIONS[2:], "1.20.6", "1.22")
def test_rejects_missing_configured_stable_version(self) -> None:
without_120 = [entry for entry in GAME_VERSIONS if entry["version"] != "1.20"]
with self.assertRaisesRegex(MODULE.PreparationError, "configured stable versions"):
MODULE.select_game_versions(without_120, SUPPORTED_VERSIONS)

def test_validates_plugin_loader_tags(self) -> None:
self.assertEqual(
Expand Down Expand Up @@ -103,7 +123,8 @@ def test_prepares_polished_current_release_payload(self) -> None:
payload = json.loads((output / "payload.json").read_text(encoding="utf-8"))
self.assertEqual("ri1Ibgnf", payload["project_id"])
self.assertEqual(["paper", "purpur"], payload["loaders"])
self.assertEqual(["1.20.6", "1.21", "1.21.1"], payload["game_versions"])
self.assertEqual(SUPPORTED_VERSIONS, payload["game_versions"])
self.assertNotIn("1.21.12", payload["game_versions"])
self.assertEqual("server_only", payload["environment"])
self.assertTrue(payload["featured"])
self.assertIn("# 🌲 AMCTimber 1.0.9", payload["changelog"])
Expand Down