Skip to content

Commit 67c0111

Browse files
committed
Release everything from a tag, publishing only once both deployments are in
Each Maven Central deployment waited in the Portal until someone pressed publish, and the Gradle plugins went to the Gradle Plugin Portal by hand once Maven Central served the jars. A tag now releases everything: - the jars and the native images are uploaded and validated as before, and each upload hands its deployment ID on; - a publish job publishes both only once both are in, so a version whose native build fails publishes nothing and its deployments can be dropped in the Portal. It runs JReleaser again with the ID in JRELEASER_MAVENCENTRAL_DEPLOYMENT_ID and builds nothing; JReleaser 1.26 drops a deploymentId set in the DSL. There is one leg per deployment, so a re-run repeats only the leg that failed; - once Maven Central serves both, a job publishes the Gradle plugins with the new GRADLE_PUBLISH_KEY and GRADLE_PUBLISH_SECRET secrets; - the draft GitHub release opens after publishing. The mise tasks for releasing by hand are gone, and gh:secrets sets the two Gradle Plugin Portal secrets as well. Checked: the workflow parses and act orders its jobs as above. A JReleaser dry run uploads with stage UPLOAD. With a deployment ID, the task graph is jreleaserDeploy alone, and JReleaser resolves stage PUBLISH with that ID, on a clean build too; the dry run then stops at the Portal status call it skips. publishPlugins --validate-only passes. Handing the ID on and the publishing itself can only run on a real tag.
1 parent e2142e5 commit 67c0111

4 files changed

Lines changed: 149 additions & 59 deletions

File tree

‎.github/workflows/release.yml‎

Lines changed: 118 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
11
name: Release
22

3-
# Tags only, and the tag is the version: the build reads it from GITHUB_REF_NAME and jreleaserDeploy
4-
# refuses anything that is not a clean X.Y.Z or X.Y.Z.N.
3+
# Tags only, and the tag is the version: the build reads it from GITHUB_REF_NAME, and jreleaserDeploy and
4+
# publishPlugins refuse anything that is not a clean X.Y.Z or X.Y.Z.N.
55
#
66
# Two deployments per tag, because a published groupId:artifactId:version can never gain files
7-
# afterwards: the jars go up as one, and every platform's native binary as another. Both are uploaded
8-
# and validated only — the Portal holds them until someone presses publish.
7+
# afterwards: the jars go up as one, and every platform's native binary as another. Each is uploaded and
8+
# validated first, and the publish job publishes both only once both are in. A version whose native build
9+
# fails publishes nothing, and its deployments can be dropped in the Portal.
910
#
10-
# After both, a draft GitHub release on the tag collects the runnable jar, the IDE plugins and the native
11-
# binaries.
11+
# Once Maven Central serves both, the Gradle plugins go to the Gradle Plugin Portal. After publishing, a
12+
# draft GitHub release on the tag collects the runnable jar, the IDE plugins and the native binaries.
1213
#
13-
# Needs four repository secrets: JRELEASER_MAVENCENTRAL_USERNAME and JRELEASER_MAVENCENTRAL_PASSWORD
14-
# (the Central Portal user token) plus JRELEASER_GPG_SECRET_KEY and JRELEASER_GPG_PASSPHRASE. The draft
15-
# release uses the workflow's own GITHUB_TOKEN.
14+
# Needs six repository secrets: JRELEASER_MAVENCENTRAL_USERNAME and JRELEASER_MAVENCENTRAL_PASSWORD (the
15+
# Central Portal user token), JRELEASER_GPG_SECRET_KEY and JRELEASER_GPG_PASSPHRASE, and
16+
# GRADLE_PUBLISH_KEY and GRADLE_PUBLISH_SECRET (the Gradle Plugin Portal key). The draft release uses the
17+
# workflow's own GITHUB_TOKEN.
1618
on:
1719
push:
1820
tags:
@@ -29,6 +31,8 @@ jobs:
2931
jars:
3032
name: jars
3133
runs-on: ubuntu-latest
34+
outputs:
35+
deployment-id: ${{ steps.deployment.outputs.id }}
3236
steps:
3337
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
3438
with:
@@ -48,6 +52,16 @@ jobs:
4852
JRELEASER_GPG_SECRET_KEY: ${{ secrets.JRELEASER_GPG_SECRET_KEY }}
4953
JRELEASER_GPG_PASSPHRASE: ${{ secrets.JRELEASER_GPG_PASSPHRASE }}
5054

55+
- name: Hand the deployment to the publish job
56+
id: deployment
57+
run: |
58+
id="$(sed -n 's/^deployMavenCentralSonatypeDeploymentId=//p' build/jreleaser/output.properties)"
59+
if [ -z "$id" ]; then
60+
echo "No deployment ID in build/jreleaser/output.properties"
61+
exit 1
62+
fi
63+
echo "id=$id" >> "$GITHUB_OUTPUT"
64+
5165
- name: Keep JReleaser's log
5266
if: ${{ failure() }}
5367
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
@@ -121,6 +135,8 @@ jobs:
121135
name: native deploy
122136
needs: native-images
123137
runs-on: ubuntu-latest
138+
outputs:
139+
deployment-id: ${{ steps.deployment.outputs.id }}
124140
steps:
125141
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
126142
with:
@@ -144,7 +160,7 @@ jobs:
144160
- name: Show what was collected
145161
run: ls -l native-images
146162

147-
# No GraalVM here: nothing is compiled, the binaries are published exactly as they arrived.
163+
# No GraalVM here: nothing is compiled, the binaries are uploaded exactly as they arrived.
148164
- name: Stage, sign and upload the native images
149165
run: ./gradlew -PreleaseTarget=native -PnativeImages=native-images jreleaserDeploy
150166
env:
@@ -153,6 +169,16 @@ jobs:
153169
JRELEASER_GPG_SECRET_KEY: ${{ secrets.JRELEASER_GPG_SECRET_KEY }}
154170
JRELEASER_GPG_PASSPHRASE: ${{ secrets.JRELEASER_GPG_PASSPHRASE }}
155171

172+
- name: Hand the deployment to the publish job
173+
id: deployment
174+
run: |
175+
id="$(sed -n 's/^deployMavenCentralSonatypeDeploymentId=//p' build/jreleaser/output.properties)"
176+
if [ -z "$id" ]; then
177+
echo "No deployment ID in build/jreleaser/output.properties"
178+
exit 1
179+
fi
180+
echo "id=$id" >> "$GITHUB_OUTPUT"
181+
156182
- name: Keep JReleaser's log
157183
if: ${{ failure() }}
158184
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
@@ -162,13 +188,92 @@ jobs:
162188
if-no-files-found: ignore
163189
retention-days: 7
164190

191+
# Both deployments are uploaded and validated by now. Publishing one is a JReleaser run given its ID,
192+
# which builds nothing. One leg per deployment, so that a re-run repeats only the leg that failed.
193+
publish:
194+
name: publish (${{ matrix.deployment }})
195+
needs: [jars, native-deploy]
196+
runs-on: ubuntu-latest
197+
strategy:
198+
matrix:
199+
include:
200+
- deployment: jars
201+
id: ${{ needs.jars.outputs.deployment-id }}
202+
- deployment: native
203+
id: ${{ needs.native-deploy.outputs.deployment-id }}
204+
steps:
205+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
206+
with:
207+
fetch-depth: 0
208+
209+
- name: Install JDK 21
210+
uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
211+
with:
212+
distribution: temurin
213+
java-version: '21'
214+
215+
- name: Publish the deployment
216+
run: ./gradlew jreleaserDeploy
217+
env:
218+
JRELEASER_MAVENCENTRAL_DEPLOYMENT_ID: ${{ matrix.id }}
219+
JRELEASER_DEPLOY_MAVEN_MAVENCENTRAL_SONATYPE_USERNAME: ${{ secrets.JRELEASER_MAVENCENTRAL_USERNAME }}
220+
JRELEASER_DEPLOY_MAVEN_MAVENCENTRAL_SONATYPE_PASSWORD: ${{ secrets.JRELEASER_MAVENCENTRAL_PASSWORD }}
221+
JRELEASER_GPG_SECRET_KEY: ${{ secrets.JRELEASER_GPG_SECRET_KEY }}
222+
JRELEASER_GPG_PASSPHRASE: ${{ secrets.JRELEASER_GPG_PASSPHRASE }}
223+
224+
- name: Keep JReleaser's log
225+
if: ${{ failure() }}
226+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
227+
with:
228+
name: jreleaser-log-publish-${{ matrix.deployment }}
229+
path: build/jreleaser/trace.log
230+
if-no-files-found: ignore
231+
retention-days: 7
232+
233+
# The plugins depend on the version's jars and native images, so they go up only once Maven Central
234+
# serves both, which takes some minutes after the publish job. The Gradle Plugin Portal never takes a
235+
# version back.
236+
gradle-plugins:
237+
name: Gradle plugins
238+
needs: publish
239+
runs-on: ubuntu-latest
240+
timeout-minutes: 90
241+
steps:
242+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
243+
with:
244+
fetch-depth: 0
245+
246+
- name: Install JDK 21
247+
uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
248+
with:
249+
distribution: temurin
250+
java-version: '21'
251+
252+
- name: Wait for Maven Central to serve the version
253+
env:
254+
VERSION: ${{ github.ref_name }}
255+
run: |
256+
for artifact in open-java-format open-java-format-native; do
257+
url="https://repo1.maven.org/maven2/dev/openjavaformat/${artifact}/${VERSION}/${artifact}-${VERSION}.pom"
258+
until curl --silent --fail --head --output /dev/null "$url"; do
259+
echo "Waiting for ${url}"
260+
sleep 30
261+
done
262+
done
263+
264+
- name: Publish the Gradle plugins
265+
run: ./gradlew :gradle-open-java-format:publishPlugins
266+
env:
267+
GRADLE_PUBLISH_KEY: ${{ secrets.GRADLE_PUBLISH_KEY }}
268+
GRADLE_PUBLISH_SECRET: ${{ secrets.GRADLE_PUBLISH_SECRET }}
269+
165270
# What Maven Central does not carry — the runnable formatter jar, the IntelliJ plugin zip, the Eclipse
166271
# plugin jar, and every platform's native binary as a plain download — goes into a draft GitHub release
167-
# on the tag, each file signed with the release key. Only once both deployments are in; publishing the
168-
# draft is a click on GitHub, and a re-run fails while a release for the tag exists.
272+
# on the tag, each file signed with the release key. Only once both deployments are published; publishing
273+
# the draft is a click on GitHub, and a re-run fails while a release for the tag exists.
169274
github-release:
170275
name: draft GitHub release
171-
needs: [jars, native-deploy]
276+
needs: publish
172277
runs-on: ubuntu-latest
173278
permissions:
174279
contents: write

‎buildSrc/src/main/groovy/open-java-format.release-conventions.gradle‎

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// Applied to the root project: signs the staged publications and uploads them to the Maven Central
2-
// Portal. `mise run release` runs it; `mise run release:dry-run` rehearses without uploading.
2+
// Portal. The release workflow, .github/workflows/release.yml, runs it on a tag; --dryrun rehearses
3+
// without uploading.
34
plugins {
45
id 'base'
56
id 'org.jreleaser'
@@ -15,9 +16,16 @@ plugins {
1516
// plugin zip, the Eclipse plugin jar and every platform's native binary as a plain download — each file
1617
// signed, plus a checksum file.
1718
//
18-
// gradle-open-java-format is not released here: its plugins go to the Gradle Plugin Portal by hand, with
19-
// `mise run release:gradle-plugin`, once the jars they depend on are published.
19+
// Publishing is a run of its own: with JRELEASER_MAVENCENTRAL_DEPLOYMENT_ID set, JReleaser publishes that
20+
// deployment, which an earlier run uploaded and the Portal validated, and nothing is built. The ID has to
21+
// be an environment variable: JReleaser 1.26 drops a deploymentId set in the DSL. The release workflow
22+
// uploads both deployments first and publishes them only once both are in, so a version whose native
23+
// build fails publishes nothing.
24+
//
25+
// gradle-open-java-format is not released here: the release workflow publishes its plugins to the Gradle
26+
// Plugin Portal once Maven Central serves the jars and native images they depend on.
2027
def releaseTarget = providers.gradleProperty('releaseTarget').getOrElse('jars')
28+
def deploymentToPublish = providers.environmentVariable('JRELEASER_MAVENCENTRAL_DEPLOYMENT_ID').getOrNull()
2129
def releasedProjects
2230
if (releaseTarget == 'jars') {
2331
releasedProjects = [
@@ -79,7 +87,7 @@ jreleaser {
7987
skipTag = true
8088
tagName = '{{projectVersion}}'
8189
releaseName = '{{projectVersion}}'
82-
// Published by hand after a look, like the deployments waiting in the Portal.
90+
// Published by hand after a look.
8391
draft = true
8492
changelog {
8593
formatted = 'ALWAYS'
@@ -131,10 +139,12 @@ jreleaser {
131139
url = 'https://central.sonatype.com/api/v1/publisher'
132140
namespace = 'dev.openjavaformat'
133141
applyMavenCentralRules = true
134-
// Upload and validate, then stop. The deployment waits in the Portal until someone
135-
// presses publish, so a version stays reversible until every one of its deployments
136-
// is in and has been looked at.
137-
stage = 'UPLOAD'
142+
// Upload and wait for the Portal to validate the deployment, then stop; its ID goes to
143+
// build/jreleaser/output.properties as deployMavenCentralSonatypeDeploymentId. Given
144+
// an ID, publish that deployment instead, and end once publishing has started rather
145+
// than waiting, up to half an hour, for Maven Central to serve the files.
146+
stage = deploymentToPublish == null ? 'UPLOAD' : 'PUBLISH'
147+
skipPublicationCheck = true
138148
stagingRepositories.each { stagingRepository(it) }
139149
}
140150
}
@@ -143,8 +153,11 @@ jreleaser {
143153
}
144154

145155
tasks.named('jreleaserDeploy') {
146-
releasedProjects.each { path ->
147-
dependsOn "${path}:publishAllPublicationsToStagingRepository"
156+
// Publishing a deployment that is already uploaded needs nothing built.
157+
if (deploymentToPublish == null) {
158+
releasedProjects.each { path ->
159+
dependsOn "${path}:publishAllPublicationsToStagingRepository"
160+
}
148161
}
149162
}
150163

‎gradle-open-java-format/build.gradle‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
// For the Gradle Plugin Portal: publishPlugins, run by hand once the version's jars are published on Maven
2-
// Central — the plugin depends on them. It applies java-gradle-plugin and maven-publish itself.
1+
// For the Gradle Plugin Portal: publishPlugins, run by the release workflow once Maven Central serves the
2+
// version's jars and native images — the plugins depend on them. The key comes from GRADLE_PUBLISH_KEY and
3+
// GRADLE_PUBLISH_SECRET. It applies java-gradle-plugin and maven-publish itself.
34
apply plugin: 'com.gradle.plugin-publish'
45
apply plugin: 'groovy'
56
apply plugin: 'open-java-format.publishing-conventions'

‎mise.toml‎

Lines changed: 5 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -20,38 +20,6 @@ run = "act push --job build --env JAVA_HOME=/opt/hostedtoolcache/Java_Temurin-Ho
2020
description = "Run the linux-aarch64 native job locally in Docker"
2121
run = "act push --job native --matrix platform:linux-aarch64"
2222

23-
# ---- Releases ----
24-
# JReleaser stages, signs and uploads to the Maven Central Portal the modules listed in
25-
# buildSrc/src/main/groovy/open-java-format.release-conventions.gradle. The PGP key and the Portal
26-
# token come from JRELEASER_* variables or ~/.jreleaser/config.properties. Only a clean checkout of a
27-
# release tag is accepted. Run these through mise, not ./gradlew: JReleaser's POM check finds Java
28-
# through JAVA_HOME, which mise sets, and without it logs an error and carries on unchecked.
29-
#
30-
# These deploy the jars. The native images are a deployment of their own — one binary per platform,
31-
# built by one CI job per platform and uploaded together by .github/workflows/release.yml on a tag, because a
32-
# published version can never gain files afterwards.
33-
34-
[tasks.release]
35-
description = "Stage, sign, and upload the release to the Maven Central Portal (publishing is a click there)"
36-
run = "./gradlew clean jreleaserDeploy"
37-
38-
[tasks."release:dry-run"]
39-
description = "Rehearse the release: stage, sign and check the artifacts, upload nothing"
40-
run = "./gradlew clean jreleaserDeploy --dryrun"
41-
42-
# The Gradle plugins go to the Gradle Plugin Portal instead, by hand and last: they depend on the version's
43-
# jars, so publish only once that deployment is live on Maven Central. The key and secret are
44-
# gradle.publish.key and gradle.publish.secret in ~/.gradle/gradle.properties. The Portal never takes a
45-
# version back; publishPlugins refuses anything but a release tag's version.
46-
47-
[tasks."release:gradle-plugin"]
48-
description = "Publish the Gradle plugins to the Gradle Plugin Portal (once the jars are live on Maven Central)"
49-
run = "./gradlew :gradle-open-java-format:publishPlugins"
50-
51-
[tasks."release:gradle-plugin:validate"]
52-
description = "Check the Gradle plugins' Portal metadata, publish nothing"
53-
run = "./gradlew :gradle-open-java-format:publishPlugins --validate-only"
54-
5523
# ---- GitHub Actions secrets ----
5624
# Read from 1Password at apply time; nothing secret is stored in the repo. Requires gh
5725
# (authenticated with admin on this repository) and the 1Password CLI (`op signin` first).
@@ -107,10 +75,13 @@ apply() {
10775
10876
apply BOT_PAT 'op://Private/open-java-format/GitHub/BOT_PAT'
10977
110-
# What .github/workflows/release.yml signs and uploads with. Actions only: a Dependabot run never
111-
# releases, and the signing key has no business being readable from one.
78+
# What .github/workflows/release.yml signs and publishes with: the Central Portal token, the signing key
79+
# and the Gradle Plugin Portal key. Actions only: a Dependabot run never releases, and the signing key has
80+
# no business being readable from one.
11281
apply JRELEASER_MAVENCENTRAL_USERNAME 'op://Private/open-java-format/GitHub/JRELEASER_MAVENCENTRAL_USERNAME' actions
11382
apply JRELEASER_MAVENCENTRAL_PASSWORD 'op://Private/open-java-format/GitHub/JRELEASER_MAVENCENTRAL_PASSWORD' actions
11483
apply JRELEASER_GPG_SECRET_KEY 'op://Private/open-java-format/GitHub/JRELEASER_GPG_SECRET_KEY' actions
11584
apply JRELEASER_GPG_PASSPHRASE 'op://Private/open-java-format/GitHub/JRELEASER_GPG_PASSPHRASE' actions
85+
apply GRADLE_PUBLISH_KEY 'op://Private/open-java-format/GitHub/GRADLE_PUBLISH_KEY' actions
86+
apply GRADLE_PUBLISH_SECRET 'op://Private/open-java-format/GitHub/GRADLE_PUBLISH_SECRET' actions
11687
"""

0 commit comments

Comments
 (0)