ci: publish each release to Modrinth from the release workflow - #79
Merged
Conversation
Connect's Modrinth listing is currently fed by hand, so it freezes at whatever version was last uploaded while GitHub releases move on. A listing that silently stops tracking releases is the failure this closes: nothing goes red, operators just keep installing an old build. The step uploads the jars this run built, straight off the runner. It deliberately does not fetch them from the release: doing so would couple Modrinth publishing to the release having landed correctly, which is the exact failure "Verify published release assets" exists to catch, and one broken release would then corrupt both. Three versions per release, not one. Modrinth runs every validator whose loaders intersect the declared loaders against every file in a version, so a single version declaring velocity + bungeecord + paper is rejected: the velocity jar carries no plugin.yml and the spigot jar carries no velocity-plugin.json. Each upload is confirmed by reading the stored version back and comparing sha1 and sha512 against the jar this run built. The create call returning 200 is the API describing its own request; size would not do either, since two different jars can share a size and cannot share a digest. The event condition is the safety property. It is identical to the one on "Upload Release Artifacts" and "Update Latest Release", so a push to main publishes nothing. Removed, it would not fail loudly - every merge would quietly add a development build to a public listing and the first report would come from a user. ReleaseModrinthPublishTest pins it byte-identical to those two steps, along with the ordering, the build-output source, the digest check, publish-time game version resolution, and the credential staying out of the script body. Minecraft versions come from Modrinth's tag list at publish time, floored at the api-version the shipped plugin.yml declares. A hard-coded list would stop matching searches the day Mojang ships a release, without anything going red.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Check this first: the event condition
release.ymlalso runs on every push tomain, where it builds the samejars and updates
latest-prerelease. Without that condition this step wouldpublish a development build to the public Modrinth listing on every merge —
and it would not fail loudly. The run stays green, the release is fine, the
repository looks fine; the listing just quietly fills with builds we never
released, and the first report comes from a user who installed one.
It is the same condition, character for character, as the two steps that
already publish only on a real release (
Upload Release Artifacts,Update Latest Release), so this workflow has one event gate rather than twothat can drift apart.
ReleaseModrinthPublishTestasserts that byte-identity,so removing or reshaping the condition fails the build instead of shipping.
Evidence that the condition actually blocks a push, from GitHub evaluating
this exact expression in this exact workflow, in both directions:
if: pushstepspushtomainworkflow_dispatch(0.13.0)The push run is the control that matters: the job ran to completion and other
steps in it executed, so "skipped" is the condition doing its work rather than
the job never starting.
What is not yet observed: the new step has never itself been through a real
push run, because it does not exist on
mainyet. What is established is thatthe expression blocks pushes and that this step carries that expression
unchanged. The first push to
mainafter merge is the direct observation —worth a glance at that run to see
Publish to Modrinthskipped.What the step does
Publishes the jars this run built, off the runner — never fetched back from
the release. Fetching from the release would couple Modrinth publishing to the
release having landed correctly, which is the exact failure
Verify published release assetsexists to catch, so one broken release wouldcorrupt the listing too. The step runs after that verification, so a run whose
release did not land does not reach the listing either.
Three versions per release, not one. Modrinth runs every validator whose
loaders intersect the declared loaders against every file in a version
(
validate/plugin.rs),so one version declaring
velocity+bungeecord+paperis rejectedoutright: the velocity jar has no
plugin.yml, the spigot jar has novelocity-plugin.json. Version numbers are<tag>+velocity,<tag>+spigot,<tag>+bungee, matching the versions already on the listing.Every upload is confirmed by digest. After the upload, the stored version is
read back and its
sha1andsha512compared against the jar this run built.The create call returning 200 is the API describing its own request — the same
"trust the run, not the artifact" mistake the release verification avoids. Size
would not do either: two different jars can share a size and cannot share a
digest.
Minecraft versions are resolved at publish time from Modrinth's tag list,
floored at the
api-versionthe shippedplugin.ymldeclares (so the floorcannot drift from the jar). A hard-coded list would stop matching searches the
day Mojang ships a release, without anything going red.
The credential is passed as an environment variable and reaches
curlthrough aconfig on stdin, so it is never in a process argument list, never on disk, and
never interpolated into the script body.
Two things for the reviewer to decide
workflow_dispatchat an OLD tag will publish that tag to Modrinth.That path exists to repair a release, and
release-pleaseuses it for everynormal release, so it cannot be gated out without breaking releases. If the
listing should never receive an old tag, that needs a rule this PR does not
invent. The step will not create a duplicate of a version the listing
already holds; a tag that was never published would be published.
MODRINTH_TOKENscopes. The step needsVERSION_CREATEto upload andVERSION_READto read the version back for the digest check. It also listsexisting versions first, which needs
PROJECT_READ+VERSION_READ; thatcall is allowed to be refused (it warns and continues), so the read scopes
for the digest check are the hard requirement. A refusal names the exact
missing scope in the error rather than failing vaguely. The scopes on the
configured secret have not been exercised — the first release after merge is
what proves them.
Verification
ReleaseModrinthPublishTestpins was proven to fail againstits own defect before being trusted: condition deleted, condition replaced by
a behaviourally-equivalent
!= 'push', step moved before verification, jarssourced from the release,
sha512dropped, game versions hard-coded, and thesecret interpolated into the script body — each fails exactly its own test and
no other.
the stored bytes match, and fails when the stored file has the same name and
the same size but different bytes — the case this check exists to catch, as
opposed to a 404 or an auth error, which are different failures. It also fails
when the stored version does not carry the expected filename, when the token is
empty, when the tag is empty, when a jar is missing, and when either scope is
refused; it is a no-op when the listing already carries the version.
./gradlew :core:testgreen;shellcheckclean.