govno kod
Some checks failed
Build / build (push) Failing after 3h3m20s

This commit is contained in:
2026-05-01 23:51:16 +03:00
commit 6179d4fbc9
906 changed files with 116097 additions and 0 deletions

2
release-scripts/hangar/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
.vscode
.metals

View File

@@ -0,0 +1,257 @@
//| mvnDeps:
//| - com.google.code.gson:gson:2.13.2
//| - org.apache.httpcomponents.client5:httpclient5:5.6
//| - org.slf4j:slf4j-api:2.0.17
//| - org.slf4j:slf4j-jdk14:2.0.17
//| - org.checkerframework:checker-qual:4.0.0
//| - org.yaml:snakeyaml:2.6
// Adapted from: https://gist.github.com/kennytv/a227d82249f54e0ad35005330256fee2
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import org.apache.hc.client5.http.classic.HttpClient;
import org.apache.hc.client5.http.classic.methods.HttpPost;
import org.apache.hc.client5.http.entity.mime.FileBody;
import org.apache.hc.client5.http.entity.mime.MultipartEntityBuilder;
import org.apache.hc.client5.http.entity.mime.StringBody;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.core5.http.ContentType;
import org.apache.hc.core5.http.HttpMessage;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.yaml.snakeyaml.Yaml;
import java.util.zip.ZipFile;
import java.util.zip.ZipEntry;
import java.io.InputStream;
import org.checkerframework.checker.nullness.qual.Nullable;
public final class HangarVersionUploader {
private static final String HANGAR_API_PRODUCTION = "https://hangar.papermc.io/api/v1";
private static final String HANGAR_API_STAGING = "https://hangar.papermc.io/api/v1";
private static final String HANGAR_API_LOCAL = "http://localhost:3333/api/v1";
private static final Logger LOGGER = LoggerFactory.getLogger(HangarVersionUploader.class);
private static final String HANGAR_API_URL = HANGAR_API_PRODUCTION;
private static final Gson GSON = new Gson();
private final String apiKey;
private ActiveJWT activeJWT;
public HangarVersionUploader(final String apiKey) {
this.apiKey = apiKey;
}
private static String readPluginVersion(Path pluginFile) {
try (ZipFile zipFile = new ZipFile(pluginFile.toFile())) {
ZipEntry entry = zipFile.getEntry("plugin.yml");
if (entry != null) {
try (InputStream inputStream = zipFile.getInputStream(entry)) {
Yaml yaml = new Yaml();
Map<String, Object> data = yaml.load(inputStream);
return (String) data.get("version");
}
}
} catch (IOException e) {
e.printStackTrace();
}
throw new RuntimeException("plugin.yml file inside plugin jar missing.");
}
private static String getApiKey() {
return System.getenv("HANGAR_TOKEN");
}
private static String[] getMinecraftVersions() {
return System.getenv("MINECRAFT_VERSIONS").split(" ");
}
public static void main(final String[] args) throws IOException {
final String projectName = "InvSee-plus-plus";
final String apiKey = getApiKey();
final List<Path> filePaths = List.of(
Path.of("plugins/InvSee++.jar"),
Path.of("plugins/InvSee++_Give.jar"),
Path.of("plugins/InvSee++_Clear.jar")
);
String pluginVersion = readPluginVersion(filePaths.get(0));
List<String> minecraftVersions = List.of(getMinecraftVersions());
String channel = pluginVersion.endsWith("-SNAPSHOT") ? "Alpha" : "Release";
final List<MultipartFileOrUrl> fileInfo = List.of(
new MultipartFileOrUrl(List.of(Platform.PAPER), null)
// TODO are these wrong? should they be here for InvSee++_Give and InvSee++_Clear? Hangar rejects them when I use these.
// new MultipartFileOrUrl(List.of(Platform.PAPER), null),
// new MultipartFileOrUrl(List.of(Platform.PAPER), null)
);
final VersionUpload versionUpload = new VersionUpload(
pluginVersion,
Map.of(
Platform.PAPER, List.of()
),
Map.of(
Platform.PAPER, minecraftVersions
),
"Automated deployment for InvSee++ v" + pluginVersion,
fileInfo,
channel
);
LOGGER.info("VersionUpload: {}.", versionUpload);
final HangarVersionUploader uploader = new HangarVersionUploader(apiKey);
try (final CloseableHttpClient client = HttpClients.createDefault()) {
uploader.uploadVersion(client, projectName, versionUpload, filePaths);
}
}
/**
* Uploads a new version to Hangar.
*
* @param client http client to use
* @param project unique project name
* @param versionUpload version upload data
* @param filePaths paths to the files to upload for platforms without external urls
* @throws IOException if an error occurs while uploading
*/
public void uploadVersion(
final HttpClient client,
final String project,
final VersionUpload versionUpload,
final List<Path> filePaths
) throws IOException {
// The data needs to be sent as multipart form data
final MultipartEntityBuilder builder = MultipartEntityBuilder.create();
builder.addPart("versionUpload", new StringBody(GSON.toJson(versionUpload), ContentType.APPLICATION_JSON));
// Attach files (one file for each platform where no external url is defined in the version upload data)
for (final Path filePath : filePaths) {
LOGGER.info("Adding file {} as multipart.", filePath.toAbsolutePath());
builder.addPart("files", new FileBody(filePath.toFile(), ContentType.DEFAULT_BINARY));
}
final HttpPost post = new HttpPost("%s/projects/%s/upload".formatted(HANGAR_API_URL, project));
post.setEntity(builder.build());
this.addAuthorizationHeader(client, post);
final boolean success = client.execute(post, response -> {
if (response.getCode() != 200) {
LOGGER.error("Error uploading version {}: {}", response.getCode(), response.getReasonPhrase());
if (response instanceof org.apache.hc.core5.http.HttpEntityContainer entityResponse) {
var httpEntity = response.getEntity();
var contents = new String(httpEntity.getContent().readAllBytes(), StandardCharsets.UTF_8);
LOGGER.error("Response body: {}", contents);
}
return false;
}
return true;
});
if (!success) {
throw new RuntimeException("Error uploading version");
}
}
private synchronized void addAuthorizationHeader(final HttpClient client, final HttpMessage message) throws IOException {
if (this.activeJWT != null && !this.activeJWT.hasExpired()) {
// Add the active JWT
message.addHeader("Authorization", this.activeJWT.jwt());
return;
}
// Request a new JWT
final ActiveJWT jwt = client.execute(new HttpPost("%s/authenticate?apiKey=%s".formatted(HANGAR_API_URL, this.apiKey)), response -> {
if (response.getCode() == 400) {
LOGGER.error("Bad JWT request; is the API key correct?");
return null;
} else if (response.getCode() != 200) {
LOGGER.error("Error requesting JWT {}: {}", response.getCode(), response.getReasonPhrase());
return null;
}
final String json = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
final JsonObject object = GSON.fromJson(json, JsonObject.class);
final String token = object.getAsJsonPrimitive("token").getAsString();
final long expiresIn = object.getAsJsonPrimitive("expiresIn").getAsLong();
return new ActiveJWT(token, System.currentTimeMillis() + expiresIn);
});
if (jwt == null) {
throw new RuntimeException("Error getting JWT");
}
this.activeJWT = jwt;
message.addHeader("Authorization", jwt.jwt());
}
/**
* Represents an active JSON Web Token used for authentication with Hangar.
*
* @param jwt Active JWT
* @param expiresAt time in milliseconds when the JWT expires
*/
private record ActiveJWT(String jwt, long expiresAt) {
public boolean hasExpired() {
// Make sure we request a new one before it expires
return System.currentTimeMillis() < this.expiresAt + TimeUnit.SECONDS.toMillis(3);
}
}
}
record VersionUpload(String version, Map<Platform, List<PluginDependency>> pluginDependencies,
Map<Platform, List<String>> platformDependencies, String description,
List<MultipartFileOrUrl> files, String channel) {
}
record PluginDependency(String name, boolean required, @Nullable String externalUrl) {
/**
* Creates a new PluginDependency with the given name, whether the dependency is required, and the namespace of the dependency.
*
* @param hangarProjectName name of the dependency, being its Hangar project id
* @param required whether the dependency is required
* @return a new PluginDependency
*/
static PluginDependency createWithHangarNamespace(final String hangarProjectName, final boolean required) {
return new PluginDependency(hangarProjectName, required, null);
}
/**
* Creates a new PluginDependency with the given name, external url, and whether the dependency is required.
*
* @param name name of the dependency
* @param required whether the dependency is required
* @param externalUrl url to the dependency
* @return a new PluginDependency
*/
static PluginDependency createWithUrl(final String name, final String externalUrl, final boolean required) {
return new PluginDependency(name, required, externalUrl);
}
}
enum Platform {
PAPER,
WATERFALL,
VELOCITY
}
/**
* Represents a file that is either uploaded or downloaded from an external url.
*
* @param platforms platforms the download is compatible with
* @param externalUrl external url of the download, or null if the download is a file
*/
record MultipartFileOrUrl(List<Platform> platforms, @Nullable String externalUrl) {
}

View File

@@ -0,0 +1,199 @@
#!/usr/bin/env sh
set -e
if [ -z "${DEFAULT_MILL_VERSION}" ] ; then DEFAULT_MILL_VERSION="1.1.5-jvm"; fi
if [ -z "${GITHUB_RELEASE_CDN}" ] ; then GITHUB_RELEASE_CDN=""; fi
if [ -z "$MILL_MAIN_CLI" ] ; then MILL_MAIN_CLI="${0}"; fi
MILL_REPO_URL="https://github.com/com-lihaoyi/mill"
MILL_BUILD_SCRIPT=""
if [ -f "build.mill" ] ; then
MILL_BUILD_SCRIPT="build.mill"
elif [ -f "build.mill.scala" ] ; then
MILL_BUILD_SCRIPT="build.mill.scala"
elif [ -f "build.sc" ] ; then
MILL_BUILD_SCRIPT="build.sc"
fi
# `s/.*://`:
# This is a greedy match that removes everything from the beginning of the line up to (and including) the last
# colon (:). This effectively isolates the value part of the declaration.
#
# `s/#.*//`:
# This removes any comments at the end of the line.
#
# `s/['\"]//g`:
# This removes all single and double quotes from the string, wherever they appear (g is for "global").
#
# `s/^[[:space:]]*//; s/[[:space:]]*$//`:
# These two expressions trim any leading or trailing whitespace ([[:space:]] matches spaces and tabs).
TRIM_VALUE_SED="s/.*://; s/#.*//; s/['\"]//g; s/^[[:space:]]*//; s/[[:space:]]*$//"
if [ -z "${MILL_VERSION}" ] ; then
if [ -f ".mill-version" ] ; then
MILL_VERSION="$(tr '\r' '\n' < .mill-version | head -n 1 2> /dev/null)"
elif [ -f ".config/mill-version" ] ; then
MILL_VERSION="$(tr '\r' '\n' < .config/mill-version | head -n 1 2> /dev/null)"
elif [ -f "build.mill.yaml" ] ; then
MILL_VERSION="$(grep -E "mill-version:" "build.mill.yaml" | sed -E "$TRIM_VALUE_SED")"
elif [ -n "${MILL_BUILD_SCRIPT}" ] ; then
MILL_VERSION="$(grep -E "//\|.*mill-version" "${MILL_BUILD_SCRIPT}" | sed -E "$TRIM_VALUE_SED")"
fi
fi
if [ -z "${MILL_VERSION}" ] ; then MILL_VERSION="${DEFAULT_MILL_VERSION}"; fi
MILL_USER_CACHE_DIR="${XDG_CACHE_HOME:-${HOME}/.cache}/mill"
if [ -z "${MILL_FINAL_DOWNLOAD_FOLDER}" ] ; then MILL_FINAL_DOWNLOAD_FOLDER="${MILL_USER_CACHE_DIR}/download"; fi
MILL_NATIVE_SUFFIX="-native"
MILL_JVM_SUFFIX="-jvm"
ARTIFACT_SUFFIX=""
# Check if GLIBC version is at least the required version
# Returns 0 (true) if GLIBC >= required version, 1 (false) otherwise
check_glibc_version() {
required_version="2.39"
required_major=$(echo "$required_version" | cut -d. -f1)
required_minor=$(echo "$required_version" | cut -d. -f2)
# Get GLIBC version from ldd --version (first line contains version like "ldd (GNU libc) 2.31")
glibc_version=$(ldd --version 2>/dev/null | head -n 1 | grep -oE '[0-9]+\.[0-9]+$' || echo "")
if [ -z "$glibc_version" ]; then
# If we can't determine GLIBC version, assume it's too old
return 1
fi
glibc_major=$(echo "$glibc_version" | cut -d. -f1)
glibc_minor=$(echo "$glibc_version" | cut -d. -f2)
if [ "$glibc_major" -gt "$required_major" ]; then
return 0
elif [ "$glibc_major" -eq "$required_major" ] && [ "$glibc_minor" -ge "$required_minor" ]; then
return 0
else
return 1
fi
}
set_artifact_suffix() {
if [ "$(uname -s 2>/dev/null | cut -c 1-5)" = "Linux" ]; then
# Native binaries require new enough GLIBC; fall back to JVM launcher if older
if ! check_glibc_version; then
return
fi
if [ "$(uname -m)" = "aarch64" ]; then ARTIFACT_SUFFIX="-native-linux-aarch64"
else ARTIFACT_SUFFIX="-native-linux-amd64"; fi
elif [ "$(uname)" = "Darwin" ]; then
if [ "$(uname -m)" = "arm64" ]; then ARTIFACT_SUFFIX="-native-mac-aarch64"
else ARTIFACT_SUFFIX="-native-mac-amd64"; fi
else
echo "This native mill launcher supports only Linux and macOS." 1>&2
exit 1
fi
}
case "$MILL_VERSION" in
*"$MILL_NATIVE_SUFFIX")
MILL_VERSION=${MILL_VERSION%"$MILL_NATIVE_SUFFIX"}
set_artifact_suffix
;;
*"$MILL_JVM_SUFFIX")
MILL_VERSION=${MILL_VERSION%"$MILL_JVM_SUFFIX"}
;;
*)
case "$MILL_VERSION" in
0.1.* | 0.2.* | 0.3.* | 0.4.* | 0.5.* | 0.6.* | 0.7.* | 0.8.* | 0.9.* | 0.10.* | 0.11.* | 0.12.*)
;;
*)
set_artifact_suffix
;;
esac
;;
esac
MILL="${MILL_FINAL_DOWNLOAD_FOLDER}/$MILL_VERSION$ARTIFACT_SUFFIX"
# If not already downloaded, download it
if [ ! -s "${MILL}" ] || [ "$MILL_TEST_DRY_RUN_LAUNCHER_SCRIPT" = "1" ] ; then
case $MILL_VERSION in
0.0.* | 0.1.* | 0.2.* | 0.3.* | 0.4.*)
MILL_DOWNLOAD_SUFFIX=""
MILL_DOWNLOAD_FROM_MAVEN=0
;;
0.5.* | 0.6.* | 0.7.* | 0.8.* | 0.9.* | 0.10.* | 0.11.0-M*)
MILL_DOWNLOAD_SUFFIX="-assembly"
MILL_DOWNLOAD_FROM_MAVEN=0
;;
*)
MILL_DOWNLOAD_SUFFIX="-assembly"
MILL_DOWNLOAD_FROM_MAVEN=1
;;
esac
case $MILL_VERSION in
0.12.0 | 0.12.1 | 0.12.2 | 0.12.3 | 0.12.4 | 0.12.5 | 0.12.6 | 0.12.7 | 0.12.8 | 0.12.9 | 0.12.10 | 0.12.11)
MILL_DOWNLOAD_EXT="jar"
;;
0.12.*)
MILL_DOWNLOAD_EXT="exe"
;;
0.*)
MILL_DOWNLOAD_EXT="jar"
;;
*)
MILL_DOWNLOAD_EXT="exe"
;;
esac
MILL_TEMP_DOWNLOAD_FILE="${MILL_OUTPUT_DIR:-out}/mill-temp-download"
mkdir -p "$(dirname "${MILL_TEMP_DOWNLOAD_FILE}")"
if [ "$MILL_DOWNLOAD_FROM_MAVEN" = "1" ] ; then
MILL_DOWNLOAD_URL="https://repo1.maven.org/maven2/com/lihaoyi/mill-dist${ARTIFACT_SUFFIX}/${MILL_VERSION}/mill-dist${ARTIFACT_SUFFIX}-${MILL_VERSION}.${MILL_DOWNLOAD_EXT}"
else
MILL_VERSION_TAG=$(echo "$MILL_VERSION" | sed -E 's/([^-]+)(-M[0-9]+)?(-.*)?/\1\2/')
MILL_DOWNLOAD_URL="${GITHUB_RELEASE_CDN}${MILL_REPO_URL}/releases/download/${MILL_VERSION_TAG}/${MILL_VERSION}${MILL_DOWNLOAD_SUFFIX}"
unset MILL_VERSION_TAG
fi
if [ "$MILL_TEST_DRY_RUN_LAUNCHER_SCRIPT" = "1" ] ; then
echo "$MILL_DOWNLOAD_URL"
echo "$MILL"
exit 0
fi
echo "Downloading mill ${MILL_VERSION} from ${MILL_DOWNLOAD_URL} ..." 1>&2
curl -f -L -o "${MILL_TEMP_DOWNLOAD_FILE}" "${MILL_DOWNLOAD_URL}"
chmod +x "${MILL_TEMP_DOWNLOAD_FILE}"
mkdir -p "${MILL_FINAL_DOWNLOAD_FOLDER}"
mv "${MILL_TEMP_DOWNLOAD_FILE}" "${MILL}"
unset MILL_TEMP_DOWNLOAD_FILE
unset MILL_DOWNLOAD_SUFFIX
fi
MILL_FIRST_ARG=""
if [ "$1" = "--bsp" ] || [ "${1#"-i"}" != "$1" ] || [ "$1" = "--interactive" ] || [ "$1" = "--no-server" ] || [ "$1" = "--no-daemon" ] || [ "$1" = "--help" ] ; then
# Need to preserve the first position of those listed options
MILL_FIRST_ARG=$1
shift
fi
unset MILL_FINAL_DOWNLOAD_FOLDER
unset MILL_OLD_DOWNLOAD_PATH
unset OLD_MILL
unset MILL_VERSION
unset MILL_REPO_URL
# -D mill.main.cli is for compatibility with Mill 0.10.9 - 0.13.0-M2
# We don't quote MILL_FIRST_ARG on purpose, so we can expand the empty value without quotes
# shellcheck disable=SC2086
exec "${MILL}" $MILL_FIRST_ARG -D "mill.main.cli=${MILL_MAIN_CLI}" "$@"

2
release-scripts/modrinth/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
.vscode
.metals

View File

@@ -0,0 +1,186 @@
//| mvnDeps:
//| - com.google.code.gson:gson:2.13.2
//| - org.yaml:snakeyaml:2.6
//| - org.apache.httpcomponents.client5:httpclient5:5.6
import com.google.gson.*;
import java.nio.file.Path;
import java.util.*;
import java.util.logging.*;
import org.yaml.snakeyaml.Yaml;
import java.util.zip.ZipFile;
import java.util.zip.ZipEntry;
import java.io.*;
import org.apache.hc.client5.http.classic.*;
import org.apache.hc.client5.http.classic.methods.HttpPost;
import org.apache.hc.client5.http.entity.mime.*;
import org.apache.hc.client5.http.impl.classic.*;
import org.apache.hc.core5.http.*;
import org.apache.hc.core5.http.io.entity.EntityUtils;
public class ModrinthVersionUploader {
private static final Logger LOGGER = Logger.getLogger(ModrinthVersionUploader.class.getName());
public static void main(String[] args) throws Exception {
try (var client = new ModrinthClient(getAccessToken())) {
var plugins = getPluginFiles();
var versionInfo = createVersion(plugins);
LOGGER.log(Level.INFO, "Uploading version: {0}", versionInfo);
var response = client.createVersion(versionInfo, plugins);
LOGGER.log(Level.INFO, "Got response from Modrinth API: {0}", response);
}
}
private static Version createVersion(List<Path> plugins) {
String pluginVersion = readPluginVersion(plugins.getFirst());
boolean isSnapshot = pluginVersion.endsWith("-SNAPSHOT");
String versionName = "InvSee++ " + pluginVersion;
String versionNumber = pluginVersion;
String changeLog = "Automated deployment for InvSee++ v" + pluginVersion;
List<String> dependencies = List.of();
List<String> gameVersions = List.of(getMinecraftVersions());
String versionType = isSnapshot ? "alpha" : "release";
List<String> loaders = List.of("bukkit", "spigot", "paper", "purpur");
boolean featured = !isSnapshot;
String status = "listed";
String projectId = "bYazc7fd"; // obtained from https://api.modrinth.com/v2/project/invsee++;
List<String> fileParts = plugins.stream()
.map(path -> getFilePartName(path))
.toList();
String primaryFile = getFilePartName(plugins.getFirst());
return new Version(
versionName,
versionNumber,
changeLog,
dependencies,
gameVersions,
versionType,
loaders,
featured,
status,
projectId,
fileParts,
primaryFile
);
}
private static List<Path> getPluginFiles() {
return List.of(
Path.of("plugins/InvSee++.jar"),
Path.of("plugins/InvSee++_Give.jar"),
Path.of("plugins/InvSee++_Clear.jar")
);
}
static String getFilePartName(Path path) {
String fileName = path.getFileName().toString();
return fileName.substring(0, fileName.length() - 4); // omit ".jar" suffix
// alternative implementation: read plugin name from 'name' or 'prefix' field from the plugin.yml in the jar file.
}
private static String getAccessToken() {
return System.getenv("MODRINTH_TOKEN");
}
private static String[] getMinecraftVersions() {
return System.getenv("MINECRAFT_VERSIONS").split(" ");
}
private static String readPluginVersion(Path pluginFile) {
try (ZipFile zipFile = new ZipFile(pluginFile.toFile())) {
ZipEntry entry = zipFile.getEntry("plugin.yml");
if (entry != null) {
try (InputStream inputStream = zipFile.getInputStream(entry)) {
Yaml yaml = new Yaml();
Map<String, Object> data = yaml.load(inputStream);
return (String) data.get("version");
}
}
} catch (IOException e) {
e.printStackTrace();
}
throw new RuntimeException("plugin.yml file inside plugin jar missing.");
}
}
class ModrinthClient implements AutoCloseable {
private static final String MODRINTH_BASE_URL = "https://api.modrinth.com/";
private static final String USER_AGENT = "Jannyboy11/InvSee++/ModrinthVersionUploader";
private final CloseableHttpClient client;
private final String token;
private final Gson gson;
ModrinthClient(String token) {
this.token = token;
this.gson = new GsonBuilder()
.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
.create();
this.client = HttpClients.createDefault();
}
// https://docs.modrinth.com/api/operations/createversion/
VersionResponse createVersion(Version versionInfo, List<Path> files) throws Exception {
HttpPost post = new HttpPost(MODRINTH_BASE_URL + "v2/version");
post.addHeader("Authorization", token);
post.addHeader("User-Agent", USER_AGENT);
String json = gson.toJson(versionInfo);
var entityBuilder = MultipartEntityBuilder.create()
.setMode(HttpMultipartMode.STRICT)
.addTextBody("data", json, ContentType.APPLICATION_JSON);
for (Path file : files) {
String fileName = file.getFileName().toString();
String name = ModrinthVersionUploader.getFilePartName(file);
entityBuilder.addBinaryBody(name, file, ContentType.DEFAULT_BINARY, fileName);
}
var entity = entityBuilder.build();
post.setEntity(entity);
try (CloseableHttpResponse response = client.execute(post)) {
String responseBody = EntityUtils.toString(response.getEntity());
int status = response.getCode();
if (status >= 400) {
throw new RuntimeException("Modrinth API error (" + status + "): " + responseBody);
}
return gson.fromJson(responseBody, VersionResponse.class);
}
}
@Override
public void close() throws IOException {
client.close();
}
}
record Version(
String name,
String versionNumber,
String changeLog,
List<String> dependencies,
List<String> gameVersions,
String versionType,
List<String> loaders,
boolean featured,
String status,
String projectId,
List<String> fileParts,
String primaryFile
) { }
record VersionResponse(
String id,
String name,
String versionNumber
) { }