Setting up automated plugin release

Continuous Delivery of Jenkins Components and Plugin (JEP-229) is relatively new. Any feedback from early adopters will be appreciated.

Introduction

Maintainers of Jenkins plugin repositories on GitHub can opt into continuous delivery (CD). In this mode, every successful build of the default branch (master or main) by ci.jenkins.io results in a new plugin release. GitHub Actions are used to rebuild the code and deploy it to Artifactory, without the need for maintainers to use personal credentials or local builds. Release notes are generated automatically according to pull request (PR) titles and some predefined labels.

maven-release-plugin (MRP) is not used in this mode. Rather than traditional version numbers like 1.23 or 2.3.4 which are chosen by the maintainer, a CD release will have a version of the form 123.vabcdef456789: an integer component determined by the Git history depth, and an informational component with the Git commit hash.
Push the work to prepare your plugin for CD to a dedicated branch, which you file a pull request from to your default branch. When enabling CD, the hosting team reviews this PR containing all necessary changes in your plugin, to prevent improper releases and mistakes. Don’t push the changes directly to the default branch or merge the PR before the hosting team has reviewed it.

Checklist

Incrementals Enablement

Verify that the plugin has already been incrementalified. The file pom.xml should contain <version>${revision}${changelist}</version> and the files .mvn/extensions.xml and .mvn/maven.config should exist before proceeding with subsequent steps.

A new plugin created from the archetypes will already have this setup.

Here are some PR examples for various plugins to enable CD:

Configure CD Workflow

Create the CD workflow as a copy of the template:

mkdir -p .github/workflows
curl --silent --show-error --location --output .github/workflows/cd.yaml https://raw.githubusercontent.com/jenkinsci/.github/master/workflow-templates/cd.yaml
git add .github/workflows/cd.yaml

Configure Release Drafter

You can delete any existing Release Drafter configuration, as the default is to assume _extends: .github; and/or workflow, as that will be subsumed by the CD workflow:

git rm .github/release-drafter.y*ml
git rm .github/workflows/release-drafter.y*ml

Also remove the App from the repository if it was configured that way.

These files may have been set up this way by the plugin archetype that you used to initialize the plugin.

Configure Dependabot

If you have a .github/dependabot.yml, add:

- package-ecosystem: github-actions
  directory: /
  schedule:
    interval: monthly

If you did not yet have such a file, create it now:

curl --silent --show-error --location --output .github/dependabot.yml https://raw.githubusercontent.com/jenkinsci/archetypes/master/common-files/.github/dependabot.yml

Update Maven POM and Config

Update the file .mvn/maven.config so it looks like this:

-Pconsume-incrementals
-Pmight-produce-incrementals
-Dchangelist.format=%d.v%s

Update the Maven pom.xml as described below, depending on your preferred version number format.

Once you’re done with these changes, commit all of the source file changes in a branch and file a pull request for them. Do not forget to git add . to make sure any newly created files are included. Merge this PR activating CD. Be sure to apply one of the predefined labels to this and every subsequent PR before merging so that Release Drafter can properly categorize changes.

Fully automated versioning (recommended)

For a regular component whose version number is not that meaningful, let the version number be automatically determined by setting a top-level <version>${changelist}</version> and having an entry <changelist>999999-SNAPSHOT</changelist> in the <properties> section, removing <revision> and <project.build.outputTimestamp> (if present):

--- a/.mvn/maven.config
+++ b/.mvn/maven.config
@@ -1,2 +1,3 @@
 -Pconsume-incrementals
 -Pmight-produce-incrementals
+-Dchangelist.format=%d.v%s
--- a/pom.xml
+++ b/pom.xml
@@ -7,7 +7,7 @@
-    <version>${revision}${changelist}</version>
+    <version>${changelist}</version>
     <packaging>hpi</packaging>
@@ -26,8 +26,7 @@
     <properties>
-        <revision>1.23</revision>
-        <changelist>-SNAPSHOT</changelist>
+        <changelist>999999-SNAPSHOT</changelist>
         <jenkins.version>2.361.4</jenkins.version>
-        <project.build.outputTimestamp>2023-01-01T00:00:00Z</project.build.outputTimestamp>
     </properties>

In this typical case, a CI/release build (-Dset.changelist specified) will be of the form 123.vabcdef456789. A snapshot build will be 999999-SNAPSHOT: arbitrary but treated as a snapshot by Maven and newer than any release. You can see examples of the proposed snapshot and release versions in your case by running:

mvn help:evaluate -Dexpression=project.version
mvn help:evaluate -Dexpression=project.version -Dset.changelist -Dignore.dirt
Note that you will very quickly create releases with version numbers greater than 100, as the major version component corresponds to the number of commits in the branch you’re releasing from. If you’re not ready to commit to all future versions of your plugin being this large, see the next option.
It is worth communicating this to your users, as they will see a very different version number format than before. The best way to do this is to add a line to the release notes: example note.
Manually controlled prefix (optional)

If you do not want to have large major version numbers, like with fully automated versioning described above, keep <revision> in the <properties> section, setting it to the prefix (major, major.minor, etc., depending on how much of the version number you want to manually manage) and use it as part of the top-level <version> element, removing <project.build.outputTimestamp> (if present):

--- a/.mvn/maven.config
+++ b/.mvn/maven.config
@@ -1,2 +1,3 @@
 -Pconsume-incrementals
 -Pmight-produce-incrementals
+-Dchangelist.format=%d.v%s
--- a/pom.xml
+++ b/pom.xml
@@ -10,12 +10,12 @@
   <artifactId>some-library-wrapper</artifactId>
-  <version>${revision}${changelist}</version>
+  <version>${revision}.${changelist}</version>
   <packaging>hpi</packaging>
   <properties>
-    <revision>1.2.3</revision>
-    <changelist>-SNAPSHOT</changelist>
+    <revision>1</revision>
+    <changelist>999999-SNAPSHOT</changelist>
     <jenkins.version>2.361.4</jenkins.version>
-    <project.build.outputTimestamp>2023-01-01T00:00:00Z</project.build.outputTimestamp>

Here the version numbers will look like 1.321.vabcdef456789 or 1.999999-SNAPSHOT, respectively. This could be appropriate if you are leery of committing up front to having major version numbers be in the triple digits, with no option of going back to maven-release-plugin-style versioning except by starting at say 1000.1, because version numbers going forward must be mathematically larger than any currently on the update center.

You can see examples of the proposed snapshot and release versions in your case by running:

mvn help:evaluate -Dexpression=project.version
mvn help:evaluate -Dexpression=project.version -Dset.changelist -Dignore.dirt
It is not recommended to implement actual semantic versioning with automated releases performed by CD, as that requires great care in always changing the revision as part of the changes that semantically would require a revision change for the next release. Otherwise, automated releases may have version numbers that semantically would not make sense.
Versioning with wrapped components (optional)

Similar to the previous option, for a component whose version number ought to reflect a release version of some wrapped component, use a hyphen (-) as the separator between the prefix corresponding to the wrapped component’s version and the CD-generated suffix, removing <project.build.outputTimestamp> (if present):

--- a/.mvn/maven.config
+++ b/.mvn/maven.config
@@ -1,2 +1,3 @@
 -Pconsume-incrementals
 -Pmight-produce-incrementals
+-Dchangelist.format=%d.v%s
--- a/pom.xml
+++ b/pom.xml
@@ -10,12 +10,12 @@
   <artifactId>some-library-wrapper</artifactId>
-  <version>${revision}${changelist}</version>
+  <version>${revision}-${changelist}</version>
   <packaging>hpi</packaging>
   <properties>
-    <revision>4.0.0-1.3</revision>
-    <changelist>-SNAPSHOT</changelist>
+    <revision>4.0.0</revision>
+    <changelist>999999-SNAPSHOT</changelist>
     <jenkins.version>2.361.4</jenkins.version>
-    <project.build.outputTimestamp>2023-01-01T00:00:00Z</project.build.outputTimestamp>

Here the version numbers will look like 4.0.0-123.vabcdef456789 or 4.0.0-999999-SNAPSHOT, respectively. You can see examples of the proposed snapshot and release versions in your case by running:

mvn help:evaluate -Dexpression=project.version
mvn help:evaluate -Dexpression=project.version -Dset.changelist -Dignore.dirt

Use the revision property for the <dependency> declaration to ensure they always match:

<dependency>
    <groupId>org.elsewhere</groupId>
    <artifactId>some-lib</artifactId>
    <version>${revision}</version>
</dependency>

Enable CD Permissions

Enable the continuous delivery flag in repository permission updater (RPU) for your plugin by filing a pull request adding to permissions/plugin-xxx.yml:

cd:
  enabled: true

By default, this enables exclusive JEP-229 CD, which does not grant the listed account upload permissions.

In your PR towards the repository permission updater, include a link to the PR in your plugin, which contains all the necessary changes, like described above.

Once that has been merged, start checking https://github.com/jenkinsci/your-plugin/settings/secrets/actions until you see MAVEN_TOKEN and MAVEN_USERNAME appear under Repository secrets.

Releasing

Now whenever Jenkins reports a successful build of your default branch, and at least one pull request had a label indicating it was of interest to users (e.g., enhancement rather than chore), your component will be released to Artifactory and release notes published in GitHub. You do not need any special credentials or local checkout; just merge pull requests with suitable titles and labels.

You will see a lot of workflow runs in the Actions tab in GitHub, only a small proportion of which are actual releases. Due to technical limitations in GitHub Actions it is not possible to suppress the extraneous runs. Actual releases will display a green check next to the release stage.

You can also trigger a deployment explicitly, if the current commit has a passing check from Jenkins. Visit https://github.com/jenkinsci/your-plugin/actions?query=workflow%3Acd and click Run workflow. If you prefer to only deploy explicitly, not on every push, just comment out the check_run section in the workflow.

Noting incompatible changes

It is best to avoid ever making incompatible changes to your plugin. If you must make one, then you can define hpi.compatibleSinceVersion as for any plugins. If master is currently 123.vXXX according to

mvn validate -Dset.changelist

then you can set

<hpi.compatibleSinceVersion>124</hpi.compatibleSinceVersion>

in your pull request with the breaking changes, since the new release version will be 124.vXXX if you squash-merge this PR, or something higher (at least 125.vXXX) if you true-merge it. (It is only important that the value is greater than that of the previous actual release, and less than or equal to that of the release containing the breaking change.)

Do not forget to mark the PR with the label breaking (or removed) to get an appropriate categorization in release notes. (These labels also normally cause a release to be triggered automatically upon merge.)

Fallback

If exclusive JEP-229 CD is disabled for your component, you can also release manually if you have configured your machine for manual release. To cut a release:

# Checkout the primary branch of the repository.
# If your branch is not named 'main' or 'master', update it as needed.
git checkout main || git checkout master
git pull --ff-only
mvn -Dset.changelist \
  -Pquick-build \
  -P-consume-incrementals \
  -DaltDeploymentRepository=maven.jenkins-ci.org::default::https://repo.jenkins-ci.org/releases/ \
  clean deploy

Troubleshooting

Check that MAVEN_TOKEN and MAVEN_USERNAME appear under Repository secrets.

No release has been triggered after merging a pull request

Ensure that the pull request has a label indicating it was of interest to users (e.g., enhancement rather than chore).

You can use the developer label to release a pull request even if its not interesting to end users, e.g. it is a dependency of a downstream pull request or it fixes tests for Plugin Compatibility Testing.

If you add an "interesting" label on a pull request already merged then merge another pull request (even with an uninteresting label), you will get a release since the workflow will see that there has been at least one interesting pull request since the last release.

Finally, if you manually trigger the CD action and the build is passing on the primary branch, it will publish a new release regardless of any labels.

The upload to the Maven repository fails with "401 Unauthorized"

Unauthorized means that the credentials were invalid, or not sent by Maven.

This normally means that the secrets configured in the repository have expired, create an issue in the INFRA helpdesk on GitHub, and let the team know in #jenkins-infra on Libera Chat.

Alternatively you can temporarily update the secrets yourself with your own personal credentials.

The upload to the Maven repository fails with "403 Forbidden"

The two most common explanations for this error are:

  • You don’t have permission to upload to the specified path. Learn more about how to request upload permissions. Check that the path you’re allowed to upload to matches the actual upload attempt (i.e. no typos).

  • The specified release already exists and you try to overwrite it. We do not allow replacing existing releases.

Further troubleshooting help

If none of the provided solutions help, send an email to the Jenkins developers mailing list and explain what you did, and how it failed.

References