Skip to main content

Publishing

Prism publishes your mod JARs directly to CurseForge, Modrinth, GitHub Releases, Gitea, and GitLab, then optionally posts an announcement to Discord. The upload code is a vendored port of mod-publish-plugin (MIT) — Prism owns the task graph end-to-end, with no external publish plugin applied.

Publishing is optional. If you do not configure any curseforge { }, modrinth { }, github { }, gitea { }, or gitlab { } block inside publishing { }, no publish tasks are registered.

Configuration​

prism {
publishing {
changelog = "Fixed bugs and added features."
// Or read from a file:
// changelogFile = "CHANGELOG.md"
// Per-version overrides are also supported (see below)

type = STABLE // STABLE, BETA, or ALPHA

// Display name on CurseForge/Modrinth
// Defaults to the JAR filename (e.g. mymod-1.21.1-NeoForge-1.0.0.jar)
// displayName = "My Mod v1.0.0"

// Optional override for CurseForge/Modrinth artifact selection
// artifactTask("reobfJar")
// artifactFile("build/libs/custom.jar")

curseforge {
accessToken = providers.environmentVariable("CURSEFORGE_TOKEN")
projectId = "123456"
// gameVersion("Forge") // add extra CF game version IDs (Java, Client, Server, etc.)
}

modrinth {
accessToken = providers.environmentVariable("MODRINTH_TOKEN")
projectId = "abcdef12"
// featured = true // default: true; feature the version on Modrinth
// loader("quilt") // add extra Modrinth loader slugs beyond the auto-detected one
}

github {
// accessToken = providers.environmentVariable("GITHUB_TOKEN") // default: GITHUB_TOKEN then GH_TOKEN
repository = "MyName/my-mod"
// tagName = "v1.0.0" // default: modVersion
// commitish = "main"
// prerelease = false
// generateReleaseNotes = false // ask GitHub to auto-generate release notes
// reuseExistingRelease = true
}

gitea {
accessToken = providers.environmentVariable("GITEA_TOKEN")
apiEndpoint = "https://gitea.example.com/api/v1"
repository = "MyName/my-mod"
}

gitlab {
accessToken = providers.environmentVariable("GITLAB_TOKEN")
projectId = 12345L
// apiEndpoint = "https://gitlab.com/api/v4" // default
}

discord {
webhookUrl = providers.environmentVariable("DISCORD_WEBHOOK_URL")
username = "Prism"
embedTitle = "New release"
embedColor = 0x5865F2
}

// Global publishing dependencies (apply to ALL versions and loaders)
dependencies {
requires("fabric-api")
optional("jei")
}
}
}

Per-version and per-loader changelog​

Override the global changelog at the version or loader level. Resolution order is loader → version → global; the first non-null value wins. If none is set, an empty string is used.

Per version​

prism {
publishing {
changelog = "General changelog for all versions."
}
}

version("1.21.1") {
changelog = "1.21.1: Added new features."
// Or from a file:
// changelogFile = "changelogs/1.21.1.md"
}

version("1.20.1") {
changelog = "1.20.1: Backport fixes."
}

Per loader​

version("1.21.1") {
changelog = "1.21.1 changelog."

neoforge {
changelog = "1.21.1 NeoForge-specific notes."
// changelogFile = "changelogs/1.21.1-neoforge.md"
}

fabric {
changelog = "1.21.1 Fabric-specific notes."
}
}

Publishing dependencies​

Declare mod relationships that show up on CurseForge and Modrinth project pages. Dependencies can be set at three levels, and they stack:

Global (all versions, all loaders)​

publishing {
dependencies {
requires("jei")
optional("jade")
incompatible("optifine")
}
}

Per version (all loaders for that version)​

version("1.21.1") {
publishingDependencies {
requires("fabric-api")
}
}

Per loader (specific loader for specific version)​

version("1.21.1") {
fabric {
publishingDependencies {
requires("fabric-api")
optional("modmenu")
}
}
neoforge {
publishingDependencies {
optional("jei")
}
}
}

Platform-specific dependencies​

If a slug differs between CurseForge and Modrinth, use platform-specific blocks:

publishingDependencies {
curseforge {
requires("jei") // CurseForge slug
}
modrinth {
requires("jei-mod") // Modrinth slug (if different)
}
}

Or use the platform parameter:

publishingDependencies {
requires("fabric-api", PublishingPlatform.MODRINTH)
requires("fabric-api", PublishingPlatform.CURSEFORGE)
requires("some-mod", PublishingPlatform.BOTH) // default
}

Dependency types​

MethodCurseForgeModrinth
requires(slug)RequiredRequired
optional(slug)OptionalOptional
incompatible(slug)IncompatibleIncompatible
embeds(slug)EmbeddedEmbedded

What Prism configures​

For each loader subproject, Prism registers its own upload tasks and sets:

  • file to the output JAR of that subproject (selected per loader — see below)
  • loader to the slug expected by CurseForge/Modrinth (fabric, neoforge, or forge — both MDG-Legacy forge { } and LexForge lexForge { } publish as forge)
  • minecraftVersions to the Minecraft version of that subproject (plus any gameVersion("...") extras declared in curseforge { })
  • For CurseForge: auto-populates Java version + Client + Server gameVersion IDs from the loader's resolved Java toolchain
  • changelog, version, and type — resolved loader → version → global (first non-null wins)
  • Platform credentials from the root configuration or environment variables
  • Publishing dependencies from global + version + loader levels (deduped, most-specific wins)

Artifact selection​

Prism picks the publishable artifact task per loader:

LoaderTaskCF/Modrinth loader slug
Fabric (Loom)remapJarfabric
NeoForge (ModDevGradle)jarneoforge
Forge 1.17–1.20.1 (MDG Legacy)reobfJarforge
LexForge 1.21.1+ (ForgeGradle 7)jarforge
Legacy Forge 1.7.10–1.12.2 (RFG)reobfJarforge

LexForge (lexForge {}) publishes with loader slug forge — the same slug used by Forge (1.17–1.20.1) and Legacy Forge. This matches what CurseForge and Modrinth expect for MinecraftForge projects on 1.21.1+.

Override with artifactTask("myTask") or artifactFile("build/libs/custom.jar") under publishing { }. This only affects platform publishing; Maven publishing still uses the Gradle Java component unless you override it through raw Gradle hooks.

Platform publish tasks build the selected artifact before upload. Each leaf loader's prismPublish<Platform> task:

  • depends on clean
  • depends on the selected artifact task (or your artifactTask(...) override)
  • uploads the freshly built file
  • (optional) triggers prismAnnounceDiscord as a finalizer once the upload succeeds

Duplicate dependency handling​

Dependencies declared at multiple tiers (global + version + loader) are merged with the most-specific tier winning per (platform, slug) pair. Declaring the same slug at two tiers is safe — Prism dedupes silently — but the per-loader entry overrides the global one if the type differs. For example:

publishing {
dependencies { requires("modmenu") } // global: REQUIRED
}
fabric {
publishingDependencies { optional("modmenu") } // fabric: OPTIONAL → wins
}

The final CurseForge/Modrinth upload for the fabric loader sends modmenu as OPTIONAL exactly once.

Git platforms (GitHub / Gitea / GitLab)​

These platforms publish the jar as an asset on a named release. Prism creates the release if it doesn't exist, uploads the artifact, and prints the release URL.

GitHub Releases​

publishing {
github {
// Token: auto-picks GITHUB_TOKEN (Actions), falls back to GH_TOKEN,
// or override explicitly:
// accessToken = providers.environmentVariable("MY_GH_PAT")

repository = "MyName/my-mod" // required
tagName = "v1.0.0" // default: mod version
commitish = "main" // default: main
draft = false
prerelease = false
generateReleaseNotes = false // ask GitHub to auto-generate release notes
reuseExistingRelease = true // if a release for this tag exists, upload there
}
}

Gitea​

publishing {
gitea {
accessToken = providers.environmentVariable("GITEA_TOKEN")
apiEndpoint = "https://gitea.example.com/api/v1" // your instance
repository = "MyName/my-mod"
tagName = "v1.0.0"
draft = false
prerelease = false
}
}

GitLab​

publishing {
gitlab {
accessToken = providers.environmentVariable("GITLAB_TOKEN")
apiEndpoint = "https://gitlab.com/api/v4" // default; override for self-hosted
projectId = 12345L // numeric project ID
tagName = "v1.0.0"
commitish = "main"
}
}

Discord announcements​

Post a webhook message to Discord after the CF/Modrinth/GitHub/Gitea/GitLab uploads succeed. The announcement runs as a finalizer on the platform tasks and includes a link to each published artifact.

publishing {
discord {
webhookUrl = providers.environmentVariable("DISCORD_WEBHOOK_URL")
username = "Prism" // optional
avatarUrl = "https://example.com/avatar.png" // optional
content = "New release of My Mod" // optional plain content
embedTitle = "My Mod 1.0.0 released"
embedDescription = "Highlights: …"
embedColor = 0x5865F2
includeProjectLinks = true // default: true; adds an embed field per platform
}
}

Each platform upload task calls finalizedBy(prismAnnounceDiscord). A single Discord message is posted after the finalized upload(s) finish — not one per platform.

Maven publishing​

Publish your mod as a library to Maven repositories for other mods to depend on.

Local Maven​

publishing {
mavenLocal()
}

Custom Maven repository​

publishing {
maven {
name = "MyMaven"
url = "https://maven.example.com/releases"
credentialsFromEnv("MAVEN_USER", "MAVEN_PASS")
}
}

GitHub Packages​

publishing {
githubPackages("YourName", "your-repo")
}

Automatically uses GITHUB_ACTOR and GITHUB_TOKEN environment variables.

Multiple Maven repositories​

publishing {
mavenLocal()
githubPackages("YourName", "your-repo")
maven {
name = "Production"
url = "https://maven.example.com/releases"
credentials("admin", "password") // or credentialsFromEnv()
}
}

Publishing common JARs (for library mods)​

If you're building a library mod, other multiloader mods need to depend on your common API. Enable common JAR publishing:

publishing {
publishCommonJar = true
mavenLocal()
}

This publishes each version's common project as a separate artifact alongside the loader JARs:

com.example:mymod-1.21.1-common:1.0.0    # common API
com.example:mymod-1.21.1-fabric:1.0.0 # fabric implementation
com.example:mymod-1.21.1-neoforge:1.0.0 # neoforge implementation

Consumers of your library can then:

  • Depend on mymod-1.21.1-common in their common
  • Depend on mymod-1.21.1-fabric in their fabric
  • Depend on mymod-1.21.1-neoforge in their neoforge

Only works for multi-loader versions (versions with common()). Single-loader versions don't have a separate common.

Maven artifact coordinates​

Each loader subproject publishes with:

  • groupId: your project group
  • artifactId: {modId}-{mcVersion}-{loader} (e.g. mymod-1.21.1-neoforge)
  • version: your project version

With publishCommonJar = true, common subprojects also publish as {modId}-{mcVersion}-common.

Running​

Prism registers prism* tasks at every level of the project tree. All tasks are owned by Prism — there is no longer any collision with a third-party publish plugin.

Task hierarchy​

LevelTask pathScope
RootprismPublishCurseforgeEvery mod × version × loader
Module:<mod>:prismPublishCurseforgeOne mod, every version × loader
Version:<mod>:<mc>:prismPublishCurseforgeOne version of one mod, every loader
Leaf (loader):<mod>:<mc>:<loader>:prismPublishCurseforgeJust that one loader

Same pattern for:

TaskPlatform
prismPublishCurseforgeCurseForge
prismPublishModrinthModrinth
prismPublishGithubGitHub Releases
prismPublishGiteaGitea Releases
prismPublishGitlabGitLab Releases
prismPublishAllAll of the above
prismPublishMavenConfigured Maven repos (separate aggregate)

Discord announcements run as a finalizer (prismAnnounceDiscord) attached to the per-loader platform tasks. After the uploads succeed, Prism posts a single message to the configured webhook with links to each uploaded artifact.

Examples​

# Everything, everywhere
./gradlew prismPublishAll

# Just the "boids" mod, all versions, all configured platforms
./gradlew :boids:prismPublishAll

# Boids 1.21.1 to CurseForge only
./gradlew :boids:1.21.1:prismPublishCurseforge

# One loader jar to Modrinth
./gradlew :boids:1.20.1:fabric:prismPublishModrinth

# Maven to your own repo
./gradlew :boids:prismPublishMaven

Dry run​

Every upload task supports a dry-run mode that logs the outgoing payload without making a network call. Useful for validating CI configs and secrets plumbing before shipping anything real.

./gradlew :boids:1.20.1:fabric:prismPublishCurseforge -Pprism.publishDryRun=true

The task logs the projectId, Minecraft versions, loader, declared deps, and file path that would be uploaded. Same flag works for Modrinth, GitHub, Gitea, GitLab, and Discord.

Access tokens​

Store tokens as environment variables. Never commit them to source control.

export CURSEFORGE_TOKEN=your_token_here
export MODRINTH_TOKEN=your_token_here

In CI (GitHub Actions):

env:
CURSEFORGE_TOKEN: ${{ secrets.CURSEFORGE_TOKEN }}
MODRINTH_TOKEN: ${{ secrets.MODRINTH_TOKEN }}