Apache Grails - Gradle Plugin - Grails Publish

Clone this repo:
  1. 2633b80 Merge pull request #45 from apache/1.0.x by James Daugherty · 3 weeks ago main
  2. d02aa00 Merge main into 1.0.x by James Daugherty · 3 weeks ago 1.0.x
  3. e8b0911 Write the Gradle caches from every release branch by James Daugherty · 3 weeks ago
  4. 626baae Merge pull request #44 from apache/merge-back-1.0.0 by James Daugherty · 3 weeks ago
  5. 045b7e9 [skip ci] Bump version to 1.0.1-SNAPSHOT by jdaugherty · 3 weeks ago

Apache Grails - Gradle Plugin - Grails Publish

Grails Publish is a Gradle plugin to ease publishing with the maven publish plugin or the nexus publish plugin. Artifacts published by this plugin include sources, the jar file, and a javadoc jar (generated from the groovydoc for projects that apply the groovy plugin, and from the javadoc otherwise). This plugin is expected to be used with snapshot and release builds and assumes releases will require signed artifacts.

Examples

The examples directory contains standalone Gradle projects for every supported project shape (single and multi-project builds, BOMs, Grails plugins, Gradle plugins, companion artifacts, test fixtures, Kotlin DSL, a subclassed plugin, and the layout of the grails-core build) together with a matrix of every publishing target (snapshot and release, Maven and Nexus, keyring and gpg signing, publishToMavenLocal). The functional tests of this repository run all of them against a local Nexus endpoint, so they always reflect what the plugin supports.

Limitations

This plugin currently acts as a wrapper around the maven-publish & nexus-publish plugins. There are known limitations with the nexus-publish plugin - specifically, when it can be applied in multiproject setups. Check out the functional test resources for specific scenarios that work and do not work.

Development Setup

If obtaining the source from the source distribution and you intend to build from source, you also need to download and install Gradle and use it to execute the bootstrap step so the correct version of Gradle is used. This command will bootstrap gradle:

gradle -p gradle-bootstrap

Building

Building runs the functional tests, which require Docker (as the grails-core build does): one of them releases an example project from inside a container to prove signing with the gpg command in an isolated environment. No other tool is needed on the machine; keys and certificates are generated by the tests themselves. To build this project, execute the following command:

./gradlew clean build

Publishing Locally

This project can be published to your local Maven repository by running:

./gradlew publishToMavenLocal

Plugin Installation

To include this plugin in your project, add the following to your build.gradle file:

buildscript {
    dependencies {
        classpath "org.apache.grails.gradle:grails-publish:$latestVersion"
    }
}

And then apply the plugin:

apply plugin: 'org.apache.grails.gradle.grails-publish'

Plugin Configuration

Initial Setup

Example Configuration:

grailsPublish {
    websiteUrl = 'http://foo.com/myplugin'
    license {
        name = 'Apache-2.0'
    }
    issueTrackerUrl = 'https://github.com/myname/myplugin/issues'
    vcsUrl = 'https://github.com/myname/myplugin'
    title = 'My plugin title'
    desc = 'My plugin description'
    developers = [janedoe: 'Jane Doe', johndoe: 'John Doe']
}

or

grailsPublish {
    githubSlug = 'foo/bar'
    license {
        name = 'Apache-2.0'
    }
    title = 'My plugin title'
    desc = 'My plugin description'
    developers = [janedoe: 'Jane Doe', johndoe: 'John Doe']
    organization {
        name = 'My Company'
        url = 'http://mycompany.com'
    }
}

or

grailsPublish {
    githubSlug = 'foo/bar'
    license {
        name = 'Apache-2.0'
    }
    title = 'My plugin title'
    desc = 'My plugin description'
    developer {
        id = 'janedoe'
        name = 'Jane Doe'
    }
    developer {
        id = 'johndoe'
        name = 'John Doe'
    }
    organization {
        name = 'My Company'
        url = 'http://mycompany.com'
    }
}

By default, this plugin will publish to the specified MAVEN_PUBLISH instance for snapshots, and NEXUS_PUBLISH for releases. To change the snapshot publish behavior, set the property snapshotPublishType to NEXUS_PUBLISH. To change the release publish behavior, set the property releasePublishType to MAVEN_PUBLISH.

The credentials and connection url must be specified as a project property or an environment variable.

Project properties are read from:

  • the root project's gradle.properties, the one in the Gradle user home, -P or ORG_GRADLE_PROJECT_ environment variables

  • the project applying the plugin: its own gradle.properties, or ext in its build script before the plugin is applied

  • a parent project's ext, as a fallback: Gradle 10 removes the implicit lookup of parent project properties that used to find these, so the plugin reads them explicitly and reports it (at warn level, or info for projectVersion and githubSlug, which every project of a build is expected to inherit). With Isolated Projects enabled, parent projects cannot be read at all, so such builds must declare the properties on the project itself or as Gradle properties.

A property, from any of these, takes precedence over the matching environment variable.

MAVEN_PUBLISH Environment Variables are:

MAVEN_PUBLISH_USERNAME
MAVEN_PUBLISH_PASSWORD
MAVEN_PUBLISH_URL

The MAVEN_PUBLISH repository defaults to the name maven, so when MAVEN_PUBLISH_USERNAME and MAVEN_PUBLISH_PASSWORD (or the mavenPublishUsername and mavenPublishPassword properties) are not set, the credentials of an http or https repository can instead be given as the Gradle properties mavenUsername and mavenPassword, for example as the environment variables ORG_GRADLE_PROJECT_mavenUsername and ORG_GRADLE_PROJECT_mavenPassword. Gradle then reads them itself when it publishes, which, unlike MAVEN_PUBLISH_USERNAME and MAVEN_PUBLISH_PASSWORD, lets the build store a configuration cache entry. If a publishing repository already has that name, Gradle gives the new repository a unique name such as maven2; use the matching properties maven2Username and maven2Password in that case. Only properties matching the new repository's final name enable this credential lookup.

NEXUS_PUBLISH Environment Variables are:

NEXUS_PUBLISH_USERNAME
NEXUS_PUBLISH_PASSWORD
NEXUS_PUBLISH_URL
NEXUS_PUBLISH_SNAPSHOT_URL
NEXUS_PUBLISH_STAGING_PROFILE_ID

By default, a release or snapshot build is determined by the project.version or projectVersion gradle property. To override this behavior, use the environment variable GRAILS_PUBLISH_RELEASE with a boolean value to indicate if the build is a release or snapshot.

Grails Plugin Descriptors

A Grails plugin project publishes its META-INF/grails-plugin.xml descriptor as an extra artifact with the plugin classifier (<artifactId>-<version>-plugin.xml). The Grails compiler writes the descriptor while compiling the main Groovy sources (when it compiles a *GrailsPlugin class), so when the Grails plugin Gradle plugin (org.apache.grails.gradle.grails-plugin) is applied and such a class exists, the artifact is wired to compileGroovy and is published from a clean checkout. A build that produces the descriptor some other way can point the plugin at it, with a provider that carries the producing task:

grailsPublish {
    pluginDescriptor = tasks.named('compileGroovy', GroovyCompile).flatMap {
        it.destinationDirectory.file('META-INF/grails-plugin.xml')
    }
}

Without either, a descriptor that already exists in the compiled classes when the project is configured is still published.

Gradle Plugin Projects

The java-gradle-plugin creates its own pluginMaven publication from the java component, plus a marker publication for each plugin. When it is applied, and its automatedPublishing isn't disabled, publicationName defaults to pluginMaven, so this plugin configures that publication instead of creating a second one with the same artifacts, and leaves adding the java component to the java-gradle-plugin. Everything else, such as the test sources jar and the pom, is still added. The plugins can be applied in either order.

Additional publications are not supported in this setup, since they need this plugin to add a component that links to them.

Additional Publications

A project can publish companion artifacts under their own Maven coordinates — for example a -cli artifact built from a dedicated cli source set — alongside the primary publication. Each additional publication has its own pom & Gradle module metadata, so its dependency graph stays off the primary artifact entirely, and it is signed and staged exactly like the primary publication. Sources and javadoc (groovydoc for Groovy source sets) jars are created automatically from the publication's source set, as required by Maven Central.

The build must supply a software component holding the variants to publish. The typical setup uses a feature variant whose configurations are exposed through an adhoc component and skipped from the default java component:

sourceSets {
    cli
}

java {
    registerFeature('cli') {
        usingSourceSet(sourceSets.cli)
        capability(project.group.toString(), "${project.name}-cli", project.version.toString())
    }
}

// keep the cli variants out of the primary publication
components.java.withVariantsFromConfiguration(configurations.cliApiElements) { skip() }
components.java.withVariantsFromConfiguration(configurations.cliRuntimeElements) { skip() }

// expose the cli variants as their own component named `cli`
// (requires a small plugin class to inject SoftwareComponentFactory — see
// plugin/src/functionalTest/resources/publish-projects/other-artifacts/additional-publication)
// cliComponent.addVariantsFromConfiguration(configurations.cliApiElements) {
//     if (isDirectoryVariant(it)) { it.skip() } else { it.mapToMavenScope('compile') }
// }
// cliComponent.addVariantsFromConfiguration(configurations.cliRuntimeElements) {
//     if (isDirectoryVariant(it)) { it.skip() } else { it.mapToMavenScope('runtime') }
// }

grailsPublish {
    // ... primary configuration ...

    additionalPublication('cli') {
        // all optional — the defaults derive from the publication name:
        artifactId = "${project.name}-cli"   // the published coordinate
        componentName = 'cli'                // the software component to publish
        sourceSetName = 'cli'                // used for the sources & javadoc jars
        desc = 'The cli companion artifact'  // defaults to the primary description
    }
}

A feature‘s configurations also carry variants for its compiled class and resource directories. The java component leaves those out, but an adhoc component includes them unless they are skipped, as isDirectoryVariant does in the example project by checking for the artifact types ArtifactTypeDefinition.JVM_CLASS_DIRECTORY and ArtifactTypeDefinition.JVM_RESOURCES_DIRECTORY. A directory cannot be published or signed, so the plugin fails the build when an additional publication’s component contains one.

Because one project now publishes multiple coordinates, the plugin publishes the primary component as the root of a component tree with each additional publication‘s component as a child (the same model Kotlin Multiplatform uses). The primary Gradle module metadata therefore lists the additional variants as available-at redirects to their own coordinates; those variants carry the feature’s capability, so consumers that do not explicitly request the capability are unaffected. This component tree is also what allows Gradle to resolve a project dependency (including a self dependency such as cliApi project(path)) on a project with multiple publications.

Configuration Cache

The plugin is compatible with the configuration cache, publishing included. The versions written into the pom and the Gradle module metadata for dependencies declared without one (when transitiveDependencies is enabled, the default) are resolved when the configuration cache entry is stored, and a build reusing the entry publishes them without resolving the configurations again.

The pomCustomization closure is stored with the task generating the pom, so it must only use its XmlProvider argument and the pom it is delegated to, not the project or other state of the build script. Read any other value it needs into a local variable beforehand, as the bom example does.

With the configuration cache, the tasks of a project run in parallel. Publications with the same coordinates publish to the same location, so the plugin publishes them one after the other, to each repository.

A dependency that cannot be resolved while the versions are resolved fails the build, and with the configuration cache storing the entry, so a temporary repository failure is never stored in an entry.

Some builds still cannot store a configuration cache entry, because of the plugins this one works with:

  • Gradle does not store publishing tasks of a repository with explicit credentials, so builds publishing with the MAVEN_PUBLISH_USERNAME and MAVEN_PUBLISH_PASSWORD credentials run without storing an entry; give them as the <repositoryName>Username and <repositoryName>Password Gradle properties instead (normally mavenUsername and mavenPassword, as described above). The Nexus publish plugin configures its repository with explicit credentials, so builds publishing through Nexus do not store an entry either.
  • The close and release tasks of the Nexus publish plugin reference the project when they run without its publishing tasks, as when a separate build closes the staging repository. This plugin marks them as not compatible with the configuration cache, so such builds also run without storing an entry instead of failing.
  • The Spring dependency management plugin (io.spring.dependency-management) adds a pom customization referencing the project to every Maven publication, even with generatedPomCustomization disabled. A build applying it has to publish with --no-configuration-cache, or manage its versions with Gradle platforms instead.

Release Signing

release builds are expected to be signed by this build. To disable this behavior, add the following Gradle code:

project.pluginManager.withPlugin('signing') {
    project.tasks.withType(Sign).configureEach {
        it.enabled = false
    }
}

Signing requires a valid GPG key. The key ID, or the abbreviated ID, must always be set using the property signing.keyId or environment variable SIGNING_KEY. This can be extracted with the following GPG command: gpg --list-keys --keyid-format short | grep -Eo '[A-F0-9]{8}' | head -n 1.

The Gradle Signing Plugin supports many different signing configurations, this plugin configures the Signing plugin by one of the following:

  1. Using a secring.gpg file: This file can be created with the command gpg --keyring secring.gpg --export-secret-keys > ./secring.gpg. For CI environments, it's often stored in base64 format as a secret and decoded at runtime into a local file. To use this configuration specify the property signing.secretKeyRingFile or the environment variable SIGNING_KEYRING with path to the file. The property signing.password or environment variable SIGNING_PASSPHRASE can be set to specify a passphrase for the key.
  2. Using the local GPG command with a private GPG or OpenPGP key. This behavior is the default if the secring file is not configured. In a CI environment, such as GitHub actions, GPG can be setup with the command: echo "${{ secrets.MY_GPG_KEY }}" | gpg --batch --import and the import can be confirmed successful by running the command gpg --list-keys to show the key ID.

Grails Publish Release Verification

To verify a staged release of Grails Publish is reproducible, you can use a containerized environment such as docker to run in an environment equivalent to GitHub actions. First, ensure the gradle wrapper is downloaded by running:

gradle -p gradle-bootstrap

Then, run the container that matches the CI environment:

docker build -t grails:testing -f etc/bin/Dockerfile . && docker run -it --rm -v $(pwd):/home/groovy/project -p 8080:8080 grails:testing bash

Once in an environment with similar settings to the CI environment, you can run the following commands to verify a release:

cd grails-verify
verify.sh v0.0.1 .