Skip to content

Maven publishing

Publish JVM libraries with elide publish: install into the local Maven repository, deploy to Nexus, Artifactory or GitHub Packages, and release to Maven Central with signatures and checksums, no plugins.

On this page

elide publish builds a library’s JARs, writes a POM for its consumers and publishes them to a Maven repository: the local one by default, a team repository with --to, or Maven Central through the Central Publisher Portal. Maven, Gradle and other Elide projects then depend on it like any other artifact. The publishing block in elide.pkl configures it; there are no plugins to add.

TargetCommandNeeds
Local Maven repositoryelide publishgroupId, version, one Jar
A team repositoryelide publish --to internalCredentials in the environment
Maven Centralelide publish --to centralPortal token, gpg, POM metadata, sources and javadoc JARs
Nowhere (check first)elide publish --dry-runNothing

Install locally

A publication needs a groupId, the project’s version and one Jvm.Jar artifact:

amends "elide:project.pkl"
import "elide:Jvm.pkl" as Jvm

name = "greeter"
version = "1.0.0"
description = "Greets people."

artifacts {
  ["lib"] = new Jvm.Jar {}
}

publishing {
  maven {
    groupId = "dev.example"
  }
}
elide publish

Elide builds the JAR (reusing up-to-date outputs), writes the POM and installs both as dev.example:greeter:1.0.0 under ~/.m2/repository, with maven-metadata-local.xml. The artifactId defaults to the project name. Reinstalling the same version replaces it, so a consumer on the same machine picks up each change. From Maven:

<dependency>
  <groupId>dev.example</groupId>
  <artifactId>greeter</artifactId>
  <version>1.0.0</version>
</dependency>

From Gradle:

repositories { mavenLocal() }
dependencies { implementation("dev.example:greeter:1.0.0") }

From another Elide project:

dependencies {
  maven {
    packages { "dev.example:greeter:1.0.0" }
  }
}

A local install needs no network and no credentials. localRepository installs somewhere else; a relative path resolves against the manifest.

Check before publishing

elide publish --dry-run

--dry-run stages the POM and JARs under .dev/staging/publish/maven with Maven file names (greeter-1.0.0.jar, greeter-1.0.0-sources.jar, greeter-1.0.0.pom) and lists them. It signs them when signing is on, and for Maven Central also writes the bundle, so it shows exactly what a real run would send. Nothing is written to a repository.

What goes in the POM

The POM describes the library for its consumers, not the build: coordinates, the pom metadata and dependencies, with no <build>, <repositories> or local paths.

elide.pklPublished POM
packages, modulescompile dependencies
runtimeOnlyruntime dependencies
compileOnly, devPackages, processors, kotlinPlugins, testPackagesLeft out
Versionless exclusionsAn <exclusion> on every dependency
A workspace projectThat project’s publishing coordinates
A Jar.from siblingIts packages, modules and runtimeOnly, unless already listed
A local JAR pathAn error: consumers can’t resolve it

Kotlin libraries

The POM also lists what Elide adds to a Kotlin project’s classpath without a declaration, because consumers need it at run time: kotlin-stdlib and kotlin-reflect at the project’s Kotlin version, and the runtimes kotlin.features turns on (coroutines, serialization). Turn a feature off, set kotlin.compilerOptions.noStdlib, or declare the dependency yourself to pin another version.

By default a publication holds every Jar, SourceJar and JavadocJar the project declares; artifacts picks a subset by artifact name. It must contain exactly one Jar, and each classifier once. A JavadocJar over Kotlin sources is generated with Dokka (see JAR assembly).

Publish to a team repository

Name the repository under repositories, then publish to it by name:

publishing {
  maven {
    groupId = "dev.example"
    repositories {
      ["internal"] = "https://repo.example.dev/releases"
    }
  }
}
export ELIDE_MAVEN_INTERNAL_USERNAME=ci
export ELIDE_MAVEN_INTERNAL_PASSWORD="$REPO_TOKEN"
elide publish --to internal

Credentials come from ELIDE_MAVEN_<NAME>_USERNAME and ELIDE_MAVEN_<NAME>_PASSWORD, where <NAME> is the repository name in upper case with every other character turned into _ (my-repo → ELIDE_MAVEN_MY_REPO_…), so the manifest holds no secrets. Every file is uploaded with .md5, .sha1, .sha256 and .sha512 checksums, and maven-metadata.xml is updated. This works with Nexus, Artifactory, GitHub Packages and any repository that accepts Maven uploads. A plain http:// URL other than localhost is refused without --allow-http-repositories.

Sign with gpg

publishing {
  maven {
    signing { gpg { keyName = "0123ABCD" } }   // `signing {}` uses gpg's default key
  }
}

With signing set, a remote publish signs every file with the gpg binary (<file>.asc). A local install signs only with --sign; --no-sign turns signing off. gpg-agent asks for the passphrase; in CI, set ELIDE_GPG_PASSPHRASE and Elide passes it to gpg through loopback pinentry. Elide never reads the key itself. If gpg is missing or fails, nothing is published.

Release to Maven Central

Maven Central needs POM metadata, sources and javadoc JARs, signatures and a release version. A complete library:

amends "elide:project.pkl"
import "elide:Jvm.pkl" as Jvm
import "elide:Publishing.pkl" as Publishing

name = "greeter"
version = "1.0.0"
description = "Greets people."

artifacts {
  ["lib"] = new Jvm.Jar {}
  ["sources"] = new Jvm.SourceJar {}
  ["javadoc"] = new Jvm.JavadocJar {}
}

publishing {
  maven {
    groupId = "dev.example"
    pom {
      name = "Greeter"
      url = "https://github.com/example/greeter"
      licenses { new { name = "Apache-2.0"; url = "https://www.apache.org/licenses/LICENSE-2.0" } }
      developers { new { id = "jdoe"; name = "J. Doe" } }
      scm { url = "https://github.com/example/greeter" }
    }
    repositories {
      ["central"] = new Publishing.CentralPortal {}
    }
  }
}

Create a user token in the Central Publisher Portal and publish:

export ELIDE_MAVEN_CENTRAL_USERNAME=<token user>
export ELIDE_MAVEN_CENTRAL_PASSWORD=<token password>
elide publish --to central

Before building anything, Elide checks Central’s requirements and reports everything missing at once: the POM’s name, description, url, licenses, developers and scm.url, a SourceJar, a JavadocJar, and a version that isn’t a -SNAPSHOT. The POM’s description defaults to the project’s. Central always signs, with gpg’s default key unless signing picks one, so --no-sign is refused.

Elide uploads one signed, checksummed bundle and waits for the Portal to validate it. By default (publishingType = "USER_MANAGED") you then release the deployment in the Portal; Elide prints its page. With publishingType = "AUTOMATIC" the Portal releases it once validated. A failed validation prints the Portal’s errors. --no-wait returns right after the upload.

Workspaces

In a workspace, set the shared configuration once in the root elide.pkl:

import "elide:Publishing.pkl" as Publishing

publishing {
  maven {
    groupId = "dev.example"
    pom { url = "https://github.com/example/greeter"; scm { url = "https://github.com/example/greeter" } }
    repositories { ["central"] = new Publishing.CentralPortal {} }
  }
}

Members inherit everything except artifactId and artifacts, which stay per project; a member’s own settings win field by field, and repositories merge by name.

RunPublishes
elide publish at the rootEvery project
elide publish in a memberThat member and every workspace project its POM depends on
elide publish core utilExactly core and util; workspace projects they depend on must already be in the target
elide publish --skip-existingEvery selected project whose version isn’t in the target yet

Every project is built, staged and signed before any is published, so a failure there publishes nothing. To Maven Central, every project goes into one deployment that the Portal validates and releases together. --skip-existing also reruns a release that failed partway: it leaves out what already made it, and never skips a snapshot.

In CI

- run: elide publish --to central --skip-existing
  env:
    ELIDE_MAVEN_CENTRAL_USERNAME: ${{ secrets.CENTRAL_USERNAME }}
    ELIDE_MAVEN_CENTRAL_PASSWORD: ${{ secrets.CENTRAL_PASSWORD }}
    ELIDE_GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}

Import the signing key into the runner’s gpg keyring in an earlier step. See Continuous integration to install Elide on the runner.

Reference

Was this page helpful?