Versioning against a platform that moves under you
Spellbook is a plugin for Paper, the Minecraft server, that adds enchantments. It is published on Modrinth, the plugin registry, and it runs on funminecraft, a server whose site lives in a second repository. Neither one controls the platform. Minecraft decides the game version, Paper decides what its API looks like and which Java it is compiled for, and both change on their own schedule.
A Paper plugin has to say which API it was built for. That is one line
in its paper-plugin.yml, and the rule that makes it matter is written
in the funminecraft server's README: "Paper refuses plugins declaring a
newer one than the server. Older ones load fine."
A plausible string
In 2026 Minecraft stopped numbering itself 1.21.x and moved to a
year-based 26.x. On 2026-04-19 at 13:04, 2dbf75d moved Spellbook
across:
- <version>1.21.11-R0.1-SNAPSHOT</version>
+ <version>26.1.2.build.13-alpha</version>
-api-version: '1.21.8'
+api-version: '1.26.1.2'
The dependency was right. The declared API version was not a version of
anything. Paper's artifact said 26.1.2, and the Minecraft releases were
26.1, 26.1.1 and 26.1.2. 1.26.1.2 is the old 1. habit glued onto the
new number: plausible at a glance, and wrong. At 15:18, two hours and
thirteen minutes later, c04d972 changed it to '26.1'. The commit is
one line with no message body, so nothing records how the mistake was
caught.
What is recorded is how far the habit had already got. The commit
subject says "Update to Paper 1.26.1.2", and the release notes for 1.4.0
say "Minecraft 1.26.1.2" and "Paper 1.26.1.2". The fix touched one line
and left them alone. The next release notes, in August, describe the
change as 1.26.1.2 → 26.2, though the real previous value was
26.1. And on 2026-09-02 a docs refresh wrote that the plugin uses
Paper's enchantment registry "(1.26.2+)". That is the same mistake, four
and a half months after the fix, and it is still there.
None of this could fail a build. To Maven, paper-plugin.yml is a
resource file to copy into the jar. Nothing in the build checks what the
field says.
The requirement no field showed
The same April commit carried a second change that no line of the diff
showed. The 1.21.11 paper-api jar is Java 21 bytecode, class file major
version 65, all 2,268 classes. The 26.1.2 jar is Java 25, major 69, all
2,279. Spellbook's pom.xml said Java 21, and so did both CI workflows.
A Java 21 build cannot read Java 25 class files.
So from 2026-04-19 the build was broken. Nobody noticed, because nobody committed: there are no Spellbook commits at all between 15:18 on 2026-04-19 and 13:49 on 2026-08-13. CI was red the whole time, and nothing was waiting on it. The repo's own account, written later, is "every CI build red for four months (April-August 2026) after a Paper bump before anyone noticed, since nothing was gating merges on it."
On 2026-08-13 the move to Paper 26.2 (46db501) and its release build
finally ran into the wall. Six minutes later, f7bbad7:
Paper 26.2's paper-api jar ships class files built for Java 25 (major version 69). The compiler needs JDK 25 to read them, and maven-shade-plugin 3.5.1's bundled ASM can't process version-69 class files when shading our own compiled output either (fixed in 3.6.2). CI workflows were still pinned to JDK 21, which made the 1.5.0 release build fail.
The message credits Paper 26.2, because that was the bump in front of it. The requirement had arrived 116 days earlier. And neither the JDK nor the shade plugin was a choice. The shade plugin has been bumped twice in this repository, and both times because Java moved: 3.2.4 to 3.5.1 in October 2025, to fix "invalid target release: 21", and 3.5.1 to 3.6.2 in August 2026, to read class files from Java 25.
Three hours later 37c8f15 wrote it down, under a heading that says
"Java version is tied to the Paper API version, not chosen freely." It
also wrote down the release process. A basic "Creating a Release"
section had been in the README since September 2025, so the happy path
was documented. What arrived that afternoon was everything learned from
the failure: retry a broken release by deleting and recreating it, never
by moving the tag, and always check that a jar is actually attached,
because "a failed build still leaves the release published, just
asset-less."
git log -p on Spellbook's pom.xml, paper-plugin.yml and workflows, and on funminecraft's server/start.sh. The shaded span is the 116 days with no Spellbook commits. Class-file versions are read from the paper-api jars the pom names. Hover a dot for its commit.The changes as a table
| date | commit | field | change |
|---|---|---|---|
| 2025-10-04 | 9f20d43, cc7b822 | paper-api, api-version | 1.21.5 to 1.21.9, then back to 1.21.8 thirteen minutes later |
| 2025-10-04 | c5cea59 | shade plugin | 3.2.4 to 3.5.1, for Java 21 |
| 2025-11-16 | 982ab7f | paper-api | 1.21.10 |
| 2026-02-15 | c7a999a | paper-api | 1.21.11 |
| 2026-04-19 13:04 | 2dbf75d | paper-api, api-version | 26.1.2.build.13-alpha, and api-version 1.26.1.2 |
| 2026-04-19 15:18 | c04d972 | api-version | 26.1 |
| 2026-08-13 13:49 | 46db501 | paper-api, api-version | 26.2.build.112-stable, api-version 26.2 |
| 2026-08-13 13:55 | f7bbad7 | Java, shade plugin | Java 21 to 25 in the pom and both workflows, shade 3.5.1 to 3.6.2 |
| 2026-09-02 | ba8cf7c | Modrinth game-versions | 26.2, typed by hand |
| 2026-09-06 | e3fb098 (funminecraft) | server's Paper | 1.21.11 build 132, with Spellbook 1.2.0 |
| 2026-09-10 | 027cc88 (funminecraft) | server's Paper | 26.2 build 123, with Spellbook 1.6.0 |
| 2026-10-03 | d59dd4c (funminecraft) | server's Paper | 26.3 build 147 |
Six places to type one version
Read the Spellbook lanes as a list of places a person has to remember.
Since September, when publishing to Modrinth was automated, one move of
the platform can mean editing six values by hand: the paper-api version,
api-version, <java.version>, the JDK in each of two CI workflows,
and the game-versions list that tells Modrinth which Minecraft
releases the plugin supports. That last one carries a comment from the
day it was written: "Nothing fails loudly when this drifts".
Most of those six cannot be derived from the others, which is the
platform's doing, not the repo's. The release tool can't read
paper-plugin.yml, and the Java requirement is not printed anywhere,
only compiled into the jar. But every one of them is a claim typed by a
person about something that changes without asking. The April mistake
was one of those claims typed wrong. The 116 days were one of them not
typed at all.
Recording instead of asserting
funminecraft hit the same pressure from the other side. Its guide to the
game's blocks and items was built from imported reference data, and on
2026-09-01 it was still pinned to Minecraft 1.19.4 while the game was
on 26.x. 944f4df is titled "Track the game's version instead of
asserting one", and its message describes the change: "The version is
now a measured fact rather than a constant."
Mechanically, the importer now writes down what it actually imported, a
version and its protocol number, in a meta table. The server-info
endpoint reports that beside the live game server's protocol, and the
page warns when they differ. It compares protocol numbers, not version
strings, because the string is free text: "Hypixel answers 'Requires MC
1.8 / 1.21'". And the comparison has three answers, not two. When it
cannot tell, it says nothing, because "saying 'no' there would be a
claim we had not earned."
That is the better answer to the same problem. A version that was measured can be compared with another measurement, and a mismatch shows up on the page instead of in a red build nobody reads.
It does not cover everything, and the repo is honest about that. The
game server's own Paper version is still pinned by hand in
start.sh, on purpose: "Upgrading is deliberate, in three places."
The README's version table beside it went stale for 23 days in
September, still saying Spellbook 1.2.0 after the server had moved to
1.6.0. Typed versions drift wherever they are typed, including in the
repository that stopped typing the other ones.
Two repos, one third party
On 2026-09-02, funminecraft began reporting the plugins we publish
straight from Modrinth's public API (5cd66c3): name, downloads, latest
version, and the Minecraft versions each release supports. Nothing on
the site had mentioned the plugins before. The list of which projects
to ask about is still kept by hand, on purpose, "a claim we make, not a
discovery."
That list has already broken once. The Portals project's slug on
Modrinth was renamed, and 9201e72, on 2026-09-10, is a one-line edit
to follow it. By design, nothing would have errored. A test pins exactly that
behaviour, a missing project quietly dropping off the list, under the
docstring "Short rather than wrong".
Two hours after funminecraft started reading Modrinth, Spellbook
started writing to it automatically, with game-versions typed as
26.2. Eight days later funminecraft's new plugins page began showing
the game versions of each plugin's latest release, under "Runs on". So a string typed by hand
in one repository's release workflow is shown on the other repository's
website, through a third party's API. Search the full history of each
repo for the other's side of that link and you get nothing: no mention
of funminecraft in any Spellbook revision, no mention of game-versions
or the publishing tool in any funminecraft revision.
Since 2026-10-03 the server has run Paper 26.3. Spellbook 1.6.0 loads on it, and Spellbook's release workflow still says 26.2. The 1.6.0 release notes say releases now go to Modrinth "automatically by CI", so by the code, the plugins page tells visitors that the plugin runs on a version older than the one it is running on. Nothing fails. It drifts.
Is that elegant, or a dependency nobody wrote down? It is both. The facts flow from the place they are made to the place they are shown, which is the right direction. But two repositories now depend on each other through a field that neither one documents, and the only record of it, so far, is this note.
What helps
The platform will move. That part is not up to you. What is up to you:
- Count the places you type the version, and treat each one as a claim that will go stale. Six is a lot of places to remember.
- Measure where you can, and compare. A version read from what was actually built or imported can disagree out loud with the live system. A version typed into a config file can only be wrong quietly.
- Make the build fail when it is broken, and make something wait on it. Four months of red CI is what a check with nothing gated on it is worth.
- Write down the cross-repo link on both sides, even when a third party sits in the middle. Especially then, since the third party will not tell either repository when the other one changes.