Publishing plugin snapshots

TLDR; What is the recommended approach for publishing plugin SNAPSHOTs?

One thing I struggle with in terms of writing Gradle plugins is how to test them out in the projects in which I use them. Traditionally this would be a SNAPSHOT, but the portal does not handle SNAPSHOT versions well and I think have recently begun rejecting SNAPSHOT versions.

So I started playing with simply publishing the SNAPSHOT directly to Maven Central. This has an odd behavior though that I am trying to understand. I have the following config:

plugins {
    id 'java-gradle-plugin'
    ...
    // to be able to publish SNAPSHOT versions
    id 'maven-publish'
    id 'io.github.gradle-nexus.publish-plugin' version '1.1.0'
}

group = 'io.github.sebersole'
version = '1.3.1-SNAPSHOT'

gradlePlugin {
    plugins {
        testkit {
            id = 'com.github.sebersole.testkit-junit5'
            implementationClass = 'com.github.sebersole.testkit.TestKitPlugin'
        }
    }
}

pluginBundle {
    mavenCoordinates {
        groupId = project.group.toString()
        artifactId = project.name
        version = project.version.toString()
    }

    plugins {
        ...
    }
}



publishing {
    publications {
        java(MavenPublication) {
            from(components.java)
        }
    }
}

nexusPublishing {
    repositories {
        sonatype {
            nexusUrl = uri("https://s01.oss.sonatype.org/service/local/")
            snapshotRepositoryUrl = uri("https://s01.oss.sonatype.org/content/repositories/snapshots/")
            ...
        }
    }
}

However, atm trying to publish this leads to:

 [sebersole@localhost testkit-junit5-plugin]$ ./gradlew publish 

Task :publishPluginMavenPublicationToSonatypeRepository 
Multiple publications with coordinates 'io.github.sebersole:testkit-junit5-plugin:1.3.1-SNAPSHOT' are published to repository 'sonatype'. The publications will overwrite each other!> 

Task :publishTestkitPluginMarkerMavenPublicationToSonatypeRepository FAILED

FAILURE: 

Build failed with an exception.
* What went wrong: 
Execution failed for task ':publishTestkitPluginMarkerMavenPublicationToSonatypeRepository'. 
> Failed to publish publication 'testkitPluginMarkerMaven' to repository 'sonatype' 
> Could not PUT 'https://s01.oss.sonatype.org/content/repositories/snapshots/com/github/sebersole/testkit-junit5/com.github.sebersole.testkit-junit5.gradle.plugin/1.3.1-SNAPSHOT/maven-metadata.xml'. Received status code 403 from server: Forbidden

Sonatype (Central) have begun disallowing com.github as a group-id (Central Repository Changelog - The Central Repository Documentation) so that is why the publishing error.

What I am confused because I explicitly gave the groupId using the io.github group-id prefix. But the error is wrt the com.github prefix. Apparently the plugin-id has something to do with this? Changing the plugin-id is not trivial either since it takes portal approval, so I wanted to find out the reason before I do that

Thanks!

The error you are hitting is because of the plugin id, yes.
What is tried to publish there is the plugin marker artifact.

As a theoretic use-case, imagine you build one plugin JAR with coordinates foo:bar that contains two plugins with id baz.one and baz.two.
This project will publish three artifacts.

  • foo:bar with a JAR with the actual code inside
  • baz.one:baz.one.gradle.plugin as marker artifact for baz.one
  • baz.two:baz.two.gradle.plugin as marker artifact for baz.two

Because when you apply a plugin via plugins { id('baz.one') } block which is the idiomatic way now, somehow Gradle has to know how to translate this to end up with the foo:bar artifact on the build script class path. The idiomatic way to do this is by naming convention, adding <plugin id>:<plugin id>.gradle.plugin to the build script class path and that marker artifact is only a POM referencing foo:bar.

You can also choose to not publish the plugin marker artifact, but then you need to do this local mapping yourself in the settings script of the projects that use the plugin for example like:

pluginManagement {
    resolutionStrategy {
        eachPlugin {
            if (requested.id.namespace == "baz.one") {
                useModule("foo:bar:${requested.version}")
            }
        }
    }
}

But actually to test the new version of the plugin locally in projects, I’d recommend to instead use composite builds.
If you just do in the settings script

pluginManagement {
    includeBuild("../testkit-junit5-plugin")
}

that should be enough to replace the declared plugin by a sub-build of the current sources of the plugin.
This is much easier than a publish usually and also allows for much faster test cycles while you fix things.

Another alternative would be to publishToMavenLocal and then use the plugin from there, but that’s sooo Mavenish. :smiley:

Thanks as always @Vampire

I actually used to use a similar approach to “publish to maven local”, except that I used a custom dir dedicated for locally published plugins. It works but gets ugly.

The included build approach only works if you either (1) have them in a the same scm repo or (2) have all developers (not to mention CI, etc) check out the 2 into the same relative paths (relative to each other). Take my specific use case. This is a plugin that makes it easier (imo) to use TestKit. It’s a “personal plugin” as opposed to plugins I write under the Hibernate umbrella. I apply that plugin in lots of places, including the Hibernate build. Its just not feasible to use it as a included build.

There is another caveat. You helped with this question as well, but I asked how to make classes in the plugin available for use in the JUnit+TestKit tests. Loved your suggestion to split the plugin into 2 projects: one for the shared code and another for the plugin code. Unfortunately that gave me lots of problems (mainly with the nexus publishing plugin). So I started playing with just having the build define a testImplementation dependency on the plugin.

Because when you apply a plugin via plugins { id('baz.one') } block which is the idiomatic way now, somehow Gradle has to know how to translate this to end up with the foo:bar artifact on the build script class path. The idiomatic way to do this is by naming convention, adding <plugin id>:<plugin id>.gradle.plugin to the build script class path and that marker artifact is only a POM referencing foo:bar .

Sure but you can also define versions of the plugin using the buildscript’s classpath dependencies which also works with plugins {} but without Gradle needing to do the indirection resolution.

Anyway, it sounds like just changing the plugin-id to use io.github instead of com.github would work to publish SNAPSHOTs to central?

If you want to use the plugin that wide-spreadly in its SNAPSHOT state, it might be unfeasibly to use composite builds, yes. That’s why I said for locally testing the plugin. :slight_smile:

So I started playing with just having the build define a testImplementation dependency on the plugin.

Sure iirc I said this is an option too, as long as you don’t care that your plugin classes are on the test classpath too then. :slight_smile:

Sure but you can also define versions of the plugin using the buildscript’s classpath dependencies which also works with plugins {} but without Gradle needing to do the indirection resolution.

Yes, absolutely.

Anyway, it sounds like just changing the plugin-id to use io.github instead of com.github would work to publish SNAPSHOTs to central?

I assume you mean to Sonatype, because publishing snapshot builds to central is not allowed as far as I remember, you can just sync the final releases you upload to Sonatype OSS to MC if I don’t remember wrongly.

So given that, this should indeed work if you own the according namespace at Sonatype OSS.
So you can either change the plugin id for the snapshot version and deploy to Sonatype OSS, or if you include the JAR via buildSrc or similar means like you said above, you can of course also just omit publishing the marker artifact.

Right, I meant OSSRH which does allow SNAPSHOTs.

Right. I’ll just change the plugin-id to use io.github... instead of com.github... and deal with the inevitable manual intervention for the next publish to the portal.

Out of curiosity, any plans to have a similar policy regarding reverse domain naming for github for plugins? Technically its not kosher to use com.github as I do not “own” that domain.

I have no idea whether the Gradle guys plan some restriction there.
At least it was half-kosher in the past, with com.github.<youruser>, at least until you change your username and now with io.github.<youruser>.
You don’t legally own that domain or sub-domain, but at least you have the executive power over that sub-domain using GitHub pages. :slight_smile:

True, true :slight_smile:

Its not critical, just curious

Hey,

A couple things from this thread:

  • About the plugin portal changing its stance on com.github vs. io.github: since the JCenter story, we have strengthen the approval rules mainly for making sure that if folks want to cross publish to Maven Central, they will be able to use the same coordinates. So the change around com.github should be reflected on our side as well. We will discuss internally.

  • If you use the maven-publish plugin in a plugin project, you should not use the pluginBundle.mavenCoordinates block. It will end up creating two different POM files between the regular maven publication and the plugin portal publication, which is bad. The main reason for this block is to get rid of a legacy convention. However this does not happen with maven-publish applied in the default setup.

1 Like