Releasing and sharing your plugin
Version numbers, release builds, licenses and where to publish.
Your plugin works on your PC. Now you want other server owners to use it. On this page you will pick version numbers that make sense, follow a release checklist, choose where to upload, pick a license, write a README, and add the two small extras many plugins have: usage statistics and an update notice.
Version numbers that mean something
Every release needs a version number, so server owners can tell which one they have and whether a bug report is about the latest. The common system is semantic versioning, which uses three numbers separated by dots: MAJOR.MINOR.PATCH.
- PATCH (1.4.0 to 1.4.1): you fixed a bug. Nothing new, nothing removed.
- MINOR (1.4.1 to 1.5.0): you added a feature, and every old setup still works.
- MAJOR (1.5.0 to 2.0.0): you changed something that can break old setups, such as renaming a config option, removing a command or changing what a permission does.
Start at 1.0.0 when the plugin is ready for real servers. While you are still testing with friends, versions like 0.1.0 are fine. A suffix like 1.5.0-beta.1 marks a test version that is not final.
The version lives in your project in one place that Paper reads from plugin.yml. This is the file of the example plugin on this page:
Where this file livespublishing-update-checksrcmainresourcesplugin.yml
Files in src/main/resources are copied into the jar exactly as they are. Paper looks for plugin.yml at the top of the jar.
- publishing-update-check/
- src/main/
- java/com/example/publishingupdatecheck/Package com.example.publishingupdatecheck
- UpdateCheckPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- Version.javaRecord: a small data class
- resources/Files copied into the jar as they are
- config.ymlDefault settings that server owners can change
- plugin.ymlyou are hereTells Paper the plugin's name, version and main class
- java/com/example/publishingupdatecheck/Package com.example.publishingupdatecheck
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
1name: UpdateCheck2version: '1.4.0'3main: com.example.publishingupdatecheck.UpdateCheckPlugin4api-version: '26.3'5description: Tells the console when a newer version of this plugin exists, unless the server owner turns it off.6website: https://example.com/updatecheck7authors: [YourName]- The number players see in
/pluginsand in your logs. If the file in your download says1.4.0, the version in the file name should say it too. - A link shown to server owners. Point it at your page on the download site or your GitHub repo.
- Your name. Shown by tools that list plugin details.
The paper-templates projects and the standard Gradle project of this guide keep the number in gradle.properties (version=1.0.0) and fill it into plugin.yml while building, so you change it in one spot. Gradle also names your jar after it, so version 1.4.0 of my-plugin builds build\libs\my-plugin-1.4.0.jar.
Why not compare versions as text?
Later on this page your plugin will compare two versions to see if a newer one exists. Beginners often try the obvious thing, comparing the two pieces of text. Run this program to see why that is wrong:
1record Version(int major, int minor, int patch) {2 3 static Version parse(String text) {4 String[] parts = text.trim().split("[.]");5 return new Version(6 Integer.parseInt(parts[0]),7 Integer.parseInt(parts[1]),8 Integer.parseInt(parts[2]));9 }10 11 boolean isNewerThan(Version other) {12 if (major != other.major) {13 return major > other.major;14 }15 if (minor != other.minor) {16 return minor > other.minor;17 }18 return patch > other.patch;19 }20}21 22void main() {23 String installed = "1.9.0";24 String latest = "1.10.0";25 26 boolean textSaysNewer = latest.compareTo(installed) > 0;27 IO.println("Comparing as text, is " + latest + " newer than " + installed + "? " + textSaysNewer);28 29 boolean numbersSayNewer = Version.parse(latest).isNewerThan(Version.parse(installed));30 IO.println("Comparing the numbers, is " + latest + " newer than " + installed + "? " + numbersSayNewer);31}- A
record is a short way to write a class that holds a few values: here three whole numbers. - Cuts
"1.10.0"at every dot into"1","10"and"0". The dot goes in square brackets because a plain.would mean "any character" in this pattern language. - Turns the text
"10"into the number 10. Comparing numbers puts 10 above 9, but comparing text compares character by character and puts"1.10.0"below"1.9.0". - Compare the biggest part first. Only when the majors match do you look at the minors, and then the patches.
- The text comparison. It looks at
"1"vs"1", then"."vs".", then"1"vs"9", and decides that 1 comes before 9.
The release checklist
Most broken releases come from skipped steps. Follow the same list every time:
Bump the version
Change
version=ingradle.properties(or inplugin.ymlif you write it there by hand). Decide MAJOR, MINOR or PATCH from your changes.Do a clean build
cleandeletes old build output first, so a leftover class from an earlier version cannot sneak in:PowerShell.\gradlew.bat clean buildBUILD SUCCESSFUL in 9s ls build\libsmy-plugin-1.4.0.jarTest on a fresh server
Your test server in
run/is full of leftovers: old config files, saved data, other plugins. Make a new empty folder, download Paper 26.3 from papermc.io, start it once to accept the EULA, copy only your jar intoplugins, and start it again. Check that the plugin starts without errors, thatconfig.ymlis created, and that every command does something. Then test an update: copy the older jar's config over and make sure the new version still reads it.Write the changelog
A changelog is a short list of what changed. Server owners read it before they update:
CHANGELOG.md11.4.02 Added: /fullheal now works on other players with a name argument3 Fixed: the join message showed twice when two plugins edited it4 Changed: the heal message is now greenSort lines under Added, Fixed and Changed. If a line under Changed would break an old setup, such as renaming a config option, that release needs a major bump.
Tag the release in Git
A tag is a permanent label on one commit. It lets you find the exact code of a released version later, which helps when someone reports a bug in 1.4.0 and you have already moved on:
PowerShellgit tag v1.4.0 git push origin v1.4.0Upload the jar
Upload
build\libs\my-plugin-1.4.0.jar(not the file ending in-sourcesor-plainif you see one) to your download sites, with the changelog text.
Where to publish
You can put the jar in many places. Most authors use two or three at once. The sites change their rules over time, so read each site's current publishing guide before your first upload.
| Place | What it is | You will need |
|---|---|---|
| Hangar | The plugin site run by the PaperMC team. It lists plugins for Paper, Velocity and Waterfall, and there is a Gradle plugin that uploads releases for you. | An account, a project page with a description and icon, then one version entry per release with its jar and supported Paper versions. |
| Modrinth | A popular open site for mods and plugins with good search and a clean download page. | An account and a project page. Pick the correct loader (Paper) and the Minecraft versions each file works with. |
| SpigotMC | The oldest plugin site, with a huge audience. Many servers still look here first. | An account and a resource page. New resources may be reviewed by staff before they appear. |
| GitHub Releases | A "Releases" tab on your own repo where you attach jars to a tag. Great as the official home of every version. | Only your repo. Click Create a new release, choose the tag, paste the changelog, and drag the jar in. |
Whichever you pick, a good plugin page has: a one-sentence summary, a short feature list, the Paper and Java versions it needs, installation steps, a couple of screenshots, a link to your issue tracker, and the license.
Licenses in plain words
A license is a note that tells other people what they may do with your code. If your repo has no license, the default law is "all rights reserved": people can read the code, but they may not copy it, change it or reuse it. That is a valid choice. If you want others to learn from or build on your plugin, choose a license:
- MIT: very short and permissive. Anyone may use, change and even sell your code, as long as they keep your copyright note. Good for examples and small helpers.
- GPL-3.0: anyone may use and change it, but anything they build from it and share must use the same open license. The goal is that improvements stay open.
- All rights reserved: you add a note saying so, or you simply add no license.
To add a license, create a file named LICENSE in your project's top folder and paste the license text. choosealicense.com explains each license and gives the text. GitHub can also add one when you create a repo.
A README that people read
README.md is the first thing someone sees on your GitHub page, and download sites reuse it as the description. The file is written in Markdown: plain text where # makes a heading and - makes a list item. Cover these sections, in this order:
1# FullHeal2 3Restores a player's health after a short delay with /fullheal.4 5## Requirements6- Paper 26.37- Java 258 9## Install101. Download FullHeal-1.4.0.jar from the Releases tab.112. Put it in your server's plugins folder.123. Restart the server. Do not use /reload.13 14## Commands and permissions15| Command | Permission | What it does |16| --- | --- | --- |17| /fullheal | fullheal.use | Heals you after 3 seconds |18 19## Configuration20heal-delay: seconds to wait before healing (default 3)21 22## Support23Open an issue on GitHub, and include your Paper version and the error from the console.24 25## License26MIT- The two questions you will be asked the most: which Minecraft and which Java version.
- Saves you from bug reports that are really reload problems.
- A table is the easiest way for owners to set up their permissions plugin. Keep it in sync with
plugin.yml. - Document every option in
config.ymlhere: its name, what it does and its default.
Screenshots help a lot. Press F2 in Minecraft to save one in the screenshots folder of your game. Show your plugin doing its job: a chat message, a menu, a scoreboard. In a README, drop the image into the repo and show it with .
Usage statistics and update notices
bStats: how many servers use my plugin?
bStats (bstats.org) is a free service that counts how many servers run your plugin, with which Paper and Java versions. It tells you what to support. You add a small library to your project and create one object when the plugin starts, using the plugin number bStats gives you when you register the plugin on its website:
1int pluginId = 12345;2Metrics metrics = new Metrics(this, pluginId);- A made-up number. Use the one from your bStats plugin page.
- Starts sending anonymous counts. The
Metricsclass comes from the bStats library, not from Paper, so it needs the library in your build.
The bStats library has to be packed into your jar and moved under your own package name so it does not clash with other plugins using it. That is shading and relocation, explained in Gradle and Maven reference. The bStats site has an up to date setup guide with the exact build lines. Being fair to server owners:
- Say in your README that you use bStats and what it collects.
- bStats gives every server owner an off switch in its own config file. Never try to work around it.
- Collect nothing about players: no names, no addresses.
An update notice
A plugin can look up the newest version and print a line in the console when yours is old. This one runs the check at startup, but only when the server owner left it on:
Where this file livespublishing-update-checksrcmainjavacomexamplepublishingupdatecheckUpdateCheckPlugin.java
The package com.example.publishingupdatecheck is the folder path com/example/publishingupdatecheck inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- publishing-update-check/
- src/main/
- java/com/example/publishingupdatecheck/Package com.example.publishingupdatecheck
- UpdateCheckPlugin.javayou are hereMain class: Paper starts here (named as main in plugin.yml)
- Version.javaRecord: a small data class
- resources/Files copied into the jar as they are
- config.ymlDefault settings that server owners can change
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/publishingupdatecheck/Package com.example.publishingupdatecheck
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
1package com.example.publishingupdatecheck;2 3import java.net.URI;4import java.net.http.HttpClient;5import java.net.http.HttpRequest;6import java.net.http.HttpResponse;7import java.time.Duration;8import java.util.Optional;9import org.bukkit.plugin.java.JavaPlugin;10 11public final class UpdateCheckPlugin extends JavaPlugin {12 13 @Override14 public void onEnable() {15 saveDefaultConfig();16 getLogger().info("Running version " + getPluginMeta().getVersion());17 if (getConfig().getBoolean("check-for-updates", true)) {18 getServer().getAsyncScheduler().runNow(this, task -> checkForUpdate());19 }20 }21 22 private void checkForUpdate() {23 Optional<Version> installed = Version.tryParse(getPluginMeta().getVersion());24 if (installed.isEmpty()) {25 return;26 }27 try {28 HttpClient client = HttpClient.newBuilder()29 .connectTimeout(Duration.ofSeconds(5))30 .build();31 HttpRequest request = HttpRequest.newBuilder(URI.create(getConfig().getString("update-url", "")))32 .timeout(Duration.ofSeconds(5))33 .GET()34 .build();35 HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());36 Optional<Version> latest = Version.tryParse(response.body());37 if (latest.isPresent() && latest.get().isNewerThan(installed.get())) {38 getLogger().info("A newer version is available: " + latest.get()39 + " (you have " + installed.get() + ")");40 }41 } catch (Exception exception) {42 getLogger().fine("Update check failed: " + exception.getMessage());43 }44 }45}- Your plugin's version, read from
plugin.yml. The code never repeats the number, so it can never disagree with the file. - The owner's off switch. A plugin that contacts the internet must let owners turn that off.
- Runs the check on another thread. A web request can take seconds, and the main thread must never wait that long, or the whole server freezes. See Threads, performance and lag.
- Gives up after 5 seconds if the website does not answer, so a dead site never holds anything up.
- The web address of a plain text file that contains only the newest version, such as
1.5.0. Many sites offer an address like this; check the API pages of the site you publish on. - Compares the numbers, not the text, with the class below.
- Any failure (no internet, bad address) is only logged at the quiet
finelevel. A failed update check must never disturb the server.
Where this file livespublishing-update-checksrcmainjavacomexamplepublishingupdatecheckVersion.java
The package com.example.publishingupdatecheck is the folder path com/example/publishingupdatecheck inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- publishing-update-check/
- src/main/
- java/com/example/publishingupdatecheck/Package com.example.publishingupdatecheck
- UpdateCheckPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- Version.javayou are hereRecord: a small data class
- resources/Files copied into the jar as they are
- config.ymlDefault settings that server owners can change
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/publishingupdatecheck/Package com.example.publishingupdatecheck
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
1package com.example.publishingupdatecheck;2 3import java.util.Optional;4 5public record Version(int major, int minor, int patch) {6 7 public static Optional<Version> tryParse(String text) {8 String[] parts = text.trim().split("[.]");9 if (parts.length != 3) {10 return Optional.empty();11 }12 try {13 return Optional.of(new Version(14 Integer.parseInt(parts[0]),15 Integer.parseInt(parts[1]),16 Integer.parseInt(parts[2])));17 } catch (NumberFormatException exception) {18 return Optional.empty();19 }20 }21 22 public boolean isNewerThan(Version other) {23 if (major != other.major) {24 return major > other.major;25 }26 if (minor != other.minor) {27 return minor > other.minor;28 }29 return patch > other.patch;30 }31 32 @Override33 public String toString() {34 return major + "." + minor + "." + patch;35 }36}- Returns an
Optional: either a version or "nothing". It stops a garbage reply such as an error web page from crashing the plugin. - Anything that is not exactly three pieces, like
1.5.0-beta.1, is treated as "not a version". - Thrown by
parseIntwhen a piece is not a number, such as"x".
[14:02:11 INFO]: [UpdateCheck] Enabling UpdateCheck v1.4.0[14:02:11 INFO]: [UpdateCheck] Running version 1.4.0[14:02:12 INFO]: [UpdateCheck] A newer version is available: 1.5.0 (you have 1.4.0)Good manners for update checkers: only tell the owner. Never download and replace the jar by yourself, which is how malware spreads. Do not nag every minute; once at startup is enough. And never send more than you must: no player lists, no IP addresses.
Supporting more than one version
The api-version line in plugin.yml tells Paper which Minecraft version your plugin was written for. In this guide it is '26.3'. It does not make the plugin work on every version by itself. It does two jobs:
- A server that is older than your
api-versionrefuses to load the plugin, instead of failing in strange ways later. - A newer server uses it to decide whether to apply compatibility fixes for old plugins. Without an
api-version, a server treats your plugin as ancient.
Always write the quoted value, for example '26.3', because an unquoted 26.3 is read by YAML as a number.
Supporting several Minecraft versions in one jar is hard. Code that calls a method added in a newer API crashes on an older server with NoSuchMethodError. Paper also changes its API from time to time, so something you compile now can be removed years later. The beginner-friendly approach:
- Support one version line at a time: today that is Paper 26.3, and say so in your README.
- When a new Minecraft version arrives, update
paperVersion, fix the compile errors, test, and release a new version. Keep the old jar downloadable for owners who have not updated. - If you ever must support two versions, compile against the oldest one, and ask the server for its version with
Bukkit.getMinecraftVersion()before using anything newer. - Paper plugins are not made for Spigot: Paper has hard-forked, so newer Paper API does not exist there. Say "Paper only" on your page.
Prepare a release
Take any plugin from this guide (the update-check example works well) and make it ready for a release:
- Change its version to
1.4.1and say, in one sentence, why this is a PATCH bump. - Write a short changelog for it.
- Run a clean build and find the jar.
- Write the Git tag command for it.
Hint
The clean build is .\gradlew.bat clean build. The jar name starts with the plugin's project name followed by the version.
Show the solution
1.4.0 to 1.4.1 is a PATCH bump because it fixes bugs and adds nothing new. A possible changelog:
11.4.12 Fixed: update check no longer logs an error when the website is down3 Fixed: README listed the wrong permission name .\gradlew.bat clean buildBUILD SUCCESSFUL in 8s git tag v1.4.1 git push origin v1.4.1The finished jar is build\libs\update-check-1.4.1.jar (the first part is your project's rootProject.name).
Is 1.10.2 newer than 1.9.9?
Use the version comparison program above and change it so it answers this question for 1.10.2 and 1.9.9. Then change one more value so the program says an older patch version is not newer.
Hint
Only the two lines at the top of main need to change.
Show the solution
Set installed to "1.9.9" and latest to "1.10.2". The numbers comparison prints true. Swapping the two values makes it print false, because 9 is not greater than 10.
Recap
- Use MAJOR.MINOR.PATCH: bug fix, new feature, breaking change. Compare versions as numbers, never as text.
- Every release: bump the version, clean build, test on a fresh server, write the changelog, tag it, upload the jar.
- Hangar, Modrinth, SpigotMC and GitHub Releases are the common places to publish.
- No license means all rights reserved. MIT is permissive; GPL-3.0 keeps derived work open. Paper itself is GPL-3.0.
- A README answers: what does it do, what does it need, how do I install it, and how do I get help.
- bStats and update checks must be honest and optional. Never download updates automatically.
api-versionnames the version you wrote the plugin for. Support one version line at a time.
Quick quiz
You rename a config option, so old config files stop working. Your plugin is at 1.4.2. What is the next version?
Changes that can break existing setups are MAJOR changes. 1.4.3 is for bug fixes and 1.5.0 is for new features that keep old setups working.Why should you test a release on a fresh server and not on your usual
run/folder?Real users start with nothing. A fresh folder shows what they will see, including whetherconfig.ymlis created correctly.Why does
"1.10.0".compareTo("1.9.0") > 0givefalse?Splitting into numbers fixes it: 10 is greater than 9 when both are numbers.Which is the best behavior for a plugin's update checker?
Self-replacing jars are a security risk, and constant requests are rude and slow. A web request must also stay off the main thread.What does
api-version: '26.3'do?It is only a label that Paper reads. Real multi-version support needs careful code and testing.
Next steps
- Git and GitHub for plugin developers: the repo that your tags and releases live in.
- Using AI assistants well: get help writing your README and changelog, and check what it writes.
- Where to go next: docs, communities and project ideas.