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."

Every change to each field, from 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
datecommitfieldchange
2025-10-049f20d43, cc7b822paper-api, api-version1.21.5 to 1.21.9, then back to 1.21.8 thirteen minutes later
2025-10-04c5cea59shade plugin3.2.4 to 3.5.1, for Java 21
2025-11-16982ab7fpaper-api1.21.10
2026-02-15c7a999apaper-api1.21.11
2026-04-19 13:042dbf75dpaper-api, api-version26.1.2.build.13-alpha, and api-version 1.26.1.2
2026-04-19 15:18c04d972api-version26.1
2026-08-13 13:4946db501paper-api, api-version26.2.build.112-stable, api-version 26.2
2026-08-13 13:55f7bbad7Java, shade pluginJava 21 to 25 in the pom and both workflows, shade 3.5.1 to 3.6.2
2026-09-02ba8cf7cModrinth game-versions26.2, typed by hand
2026-09-06e3fb098 (funminecraft)server's Paper1.21.11 build 132, with Spellbook 1.2.0
2026-09-10027cc88 (funminecraft)server's Paper26.2 build 123, with Spellbook 1.6.0
2026-10-03d59dd4c (funminecraft)server's Paper26.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:

  1. 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.
  2. 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.
  3. 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.
  4. 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.