The HangarPlatform class implements the Platform interface to upload plugin files to Hangar, the official plugin repository for PaperMC projects.
Package
me.hsgamer.mcreleaser.hangar.HangarPlatform
Class Declaration
public class HangarPlatform implements Platform {
private final String baseUrl = "https://hangar.papermc.io/api/v1";
private final Logger logger = LoggerFactory.getLogger(getClass());
}
Constructor
Creates a new Hangar platform instance and inherits game version configuration from common properties if not set.
public HangarPlatform() {
if (HangarPropertyKey.GAME_VERSIONS.isAbsent() &&
CommonPropertyKey.GAME_VERSIONS.isPresent()) {
HangarPropertyKey.GAME_VERSIONS.setValue(
CommonPropertyKey.GAME_VERSIONS.getValue()
);
}
}
Methods
createUploadRunnable
Creates a batch runnable that uploads files to Hangar.
The bundle of files to upload (only primary file is used)
Returns a BatchRunnable if all required properties are present, otherwise empty
@Override
public Optional<BatchRunnable> createUploadRunnable(FileBundle fileBundle) {
if (PropertyKeyUtil.isAbsentAndAnnounce(logger,
HangarPropertyKey.KEY,
HangarPropertyKey.PROJECT,
HangarPropertyKey.PLATFORM,
HangarPropertyKey.GAME_VERSIONS)) {
return Optional.empty();
}
// Create upload tasks...
}
Upload Process
The upload is executed in three phases:
Phase 0: Authentication
-
Create HTTP Client
CloseableHttpClient client = HttpClients.createMinimal();
-
Authenticate and Get Token
HttpPost tokenRequest = new HttpPost(baseUrl + "/authenticate?apiKey=" + key);
ApiSession apiSession = client.execute(tokenRequest, response -> {
if (response.getCode() != 200) {
logger.error("Failed to get token: {}", response.getCode());
return null;
}
return gson.fromJson(EntityUtils.toString(response.getEntity()), ApiSession.class);
});
String token = apiSession.token();
Phase 0: Version Preparation
-
Normalize Minecraft Versions
List<String> gameVersions = Arrays.asList(
StringUtil.splitSpace(HangarPropertyKey.GAME_VERSIONS.getValue())
);
MinecraftVersionFetcher.normalizeVersions(
gameVersions,
VersionTypeFilter.RELEASE
);
-
Build Version Upload Object
VersionUpload versionUpload = new VersionUpload(
versionValue,
Map.of(hangarPlatform, pluginDependencies),
platformDependencies,
finalDescription,
files,
channel
);
Phase 0: File Upload
Uploads the version to Hangar:
MultipartEntityBuilder builder = MultipartEntityBuilder.create()
.addTextBody("versionUpload", gson.toJson(versionUpload), ContentType.APPLICATION_JSON)
.addBinaryBody("files", fileBundle.primaryFile());
HttpPost request = new HttpPost(baseUrl + "/projects/" + project + "/upload");
request.addHeader("Authorization", "Bearer " + token);
request.setEntity(builder.build());
Required Properties
Hangar project slug or ID
Server platform: “PAPER”, “VELOCITY”, “WATERFALL”, etc.
Space-separated Minecraft versions (e.g., “1.20 1.20.1”)
Optional Properties
Release channel: “Release”, “Beta”, or “Alpha”
JSON array of plugin dependencies (see examples below)
Common Properties
Version description/changelog (supports Markdown)
Configuration Examples
Basic Paper Plugin
export HANGAR_KEY="your-hangar-api-key"
export HANGAR_PROJECT="MyPlugin"
export HANGAR_PLATFORM="PAPER"
export HANGAR_GAME_VERSIONS="1.20 1.20.1 1.20.2"
export NAME="MyPlugin v1.0.0"
export VERSION="1.0.0"
export DESCRIPTION="Initial release"
export FILES="build/libs/my-plugin.jar"
java -jar mcreleaser-cli.jar
Beta Release
export HANGAR_CHANNEL="Beta"
export NAME="MyPlugin v2.0.0-beta.1"
export VERSION="2.0.0-beta.1"
With Dependencies
export HANGAR_DEPENDENCIES='[
{
"namespace": "Vault",
"required": true
},
{
"namespace": "LuckPerms",
"required": false
}
]'
Velocity Plugin
export HANGAR_PLATFORM="VELOCITY"
export HANGAR_GAME_VERSIONS="3.2.0 3.3.0"
Waterfall Plugin
export HANGAR_PLATFORM="WATERFALL"
export HANGAR_GAME_VERSIONS="1.20"
Hangar supports these server platforms:
PAPER - Paper server
VELOCITY - Velocity proxy
WATERFALL - Waterfall proxy
Platform names must be uppercase.
Channel Values
Release - Stable release
Beta - Beta version
Alpha - Alpha/development version
Hangar automatically prepends the version name to the description if they differ:
StringBuilder descriptionBuilder = new StringBuilder();
String nameValue = CommonPropertyKey.NAME.getValue();
String versionValue = CommonPropertyKey.VERSION.getValue();
if (!Objects.equals(nameValue, versionValue)) {
descriptionBuilder.append("# Name: ").append(nameValue).append("\n\n");
}
descriptionBuilder.append(descriptionValue);
Example:
export NAME="MyPlugin v1.0.0 - The Best Plugin"
export VERSION="1.0.0"
export DESCRIPTION="Bug fixes and improvements"
Results in:
# Name: MyPlugin v1.0.0 - The Best Plugin
Bug fixes and improvements
Property Keys Source
Defined in HangarPropertyKey:
public interface HangarPropertyKey {
PropertyPrefix HANGAR = new PropertyPrefix("hangar");
PropertyKey KEY = HANGAR.key("key");
PropertyKey PROJECT = HANGAR.key("project");
PropertyKey CHANNEL = HANGAR.key("channel");
PropertyKey PLATFORM = HANGAR.key("platform");
PropertyKey GAME_VERSIONS = HANGAR.key("gameVersions");
PropertyKey DEPENDENCIES = HANGAR.key("dependencies");
}
File Handling
Important: Hangar only uploads the primary file from the FileBundle:
.addBinaryBody("files", fileBundle.primaryFile())
Secondary files are ignored. Ensure your primary file is the plugin JAR.
Error Handling
Authentication Failure
if (response.getCode() != 200) {
logger.error("Failed to get token: {}", response.getCode());
return null;
}
try {
hangarPlatform = VersionUpload.Platform.valueOf(platformValue.toUpperCase());
} catch (IllegalArgumentException e) {
logger.error("Invalid platform: " + platformValue, e);
process.complete();
}
Upload Failure
if (response.getCode() != 200) {
String responseBody = EntityUtils.toString(response.getEntity());
logger.error("Failed to upload version: {} - {}",
response.getCode(), responseBody);
return false;
}
API Models
Hangar uses these model classes:
ApiSession
public record ApiSession(String token) {}
VersionUpload
public record VersionUpload(
String version,
Map<Platform, List<PluginDependency>> pluginDependencies,
Map<Platform, List<String>> platformDependencies,
String description,
List<MultipartFileOrUrl> files,
String channel
) {}