Skip to main content
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

HangarPlatform()

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.
fileBundle
FileBundle
required
The bundle of files to upload (only primary file is used)
runnable
Optional<BatchRunnable>
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

  1. Create HTTP Client
    CloseableHttpClient client = HttpClients.createMinimal();
    
  2. 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

  1. Normalize Minecraft Versions
    List<String> gameVersions = Arrays.asList(
        StringUtil.splitSpace(HangarPropertyKey.GAME_VERSIONS.getValue())
    );
    MinecraftVersionFetcher.normalizeVersions(
        gameVersions, 
        VersionTypeFilter.RELEASE
    );
    
  2. 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_KEY
String
required
Hangar API key
HANGAR_PROJECT
String
required
Hangar project slug or ID
HANGAR_PLATFORM
String
required
Server platform: “PAPER”, “VELOCITY”, “WATERFALL”, etc.
HANGAR_GAME_VERSIONS
String
required
Space-separated Minecraft versions (e.g., “1.20 1.20.1”)

Optional Properties

HANGAR_CHANNEL
String
default:"Release"
Release channel: “Release”, “Beta”, or “Alpha”
HANGAR_DEPENDENCIES
String
JSON array of plugin dependencies (see examples below)

Common Properties

NAME
String
required
Version name/title
VERSION
String
required
Version number
DESCRIPTION
String
required
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"

Supported Platforms

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

Description Formatting

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;
}

Invalid Platform

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
) {}

Build docs developers (and LLMs) love