You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@groovy.apache.org by pa...@apache.org on 2026/08/27 06:34:03 UTC
(groovy-release) branch master updated: update scripts to close/release artifactory repos
This is an automated email from the ASF dual-hosted git repository.
paulk-asert pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/groovy-release.git
The following commit(s) were added to refs/heads/master by this push:
new 21f6a42 update scripts to close/release artifactory repos
21f6a42 is described below
commit 21f6a426f7a8d60686d391850b8d5b4abdb53580
Author: Paul King <pa...@asert.com.au>
AuthorDate: Thu Aug 27 16:33:47 2026 +1000
update scripts to close/release artifactory repos
---
buildSrc/src/main/groovy/HttpUtil.groovy | 21 ++++++++
buildSrc/src/main/groovy/NexusUtil.groovy | 83 +++++++++++++++++++++++++++++++
gradle/phase1.gradle | 36 +++++++++++++-
gradle/phase2.gradle | 35 ++++++++++++-
4 files changed, 173 insertions(+), 2 deletions(-)
diff --git a/buildSrc/src/main/groovy/HttpUtil.groovy b/buildSrc/src/main/groovy/HttpUtil.groovy
index 471af6d..775b554 100644
--- a/buildSrc/src/main/groovy/HttpUtil.groovy
+++ b/buildSrc/src/main/groovy/HttpUtil.groovy
@@ -22,6 +22,27 @@ class HttpUtil {
.build()
}
+ // Nexus returns XML unless we explicitly ask for JSON
+ static HttpRequest getJsonRequest(url, user, password) {
+ HttpRequest.newBuilder()
+ .uri(new URI(url))
+ .header('Authorization', 'Basic ' + "$user:$password".getBytes('iso-8859-1').encodeBase64())
+ .header('Accept', 'application/json')
+ .GET()
+ .build()
+ }
+
+ static HttpRequest postJsonRequest(url, String json, user, password) {
+ def body = HttpRequest.BodyPublishers.ofString(json)
+ HttpRequest.newBuilder()
+ .uri(new URI(url))
+ .header('Authorization', 'Basic ' + "$user:$password".getBytes('iso-8859-1').encodeBase64())
+ .header('Accept', 'application/json')
+ .header('Content-Type', 'application/json')
+ .POST(body)
+ .build()
+ }
+
static HttpRequest headRequest(url) {
HttpRequest.newBuilder()
.uri(new URI(url))
diff --git a/buildSrc/src/main/groovy/NexusUtil.groovy b/buildSrc/src/main/groovy/NexusUtil.groovy
new file mode 100644
index 0000000..c6cddb2
--- /dev/null
+++ b/buildSrc/src/main/groovy/NexusUtil.groovy
@@ -0,0 +1,83 @@
+/**
+ * Helpers for the Nexus 2 staging API used by the Apache repository at repository.apache.org.
+ * Gradle's maven-publish plugin just PUTs to the implicit staging endpoint - it has no notion
+ * of the staging repository Nexus creates behind the scenes, so we drive the lifecycle
+ * (close in phase1, release in phase2) via the staging REST API.
+ */
+class NexusUtil {
+ private NexusUtil() {}
+
+ static String stagingBase(nexusBase) {
+ "$nexusBase/service/local/staging"
+ }
+
+ /**
+ * Finds the staging repository of the given type ('open', 'closed', ...) for the given groupId.
+ * Nexus creates these implicitly, so we can't label them at deploy time - instead we match on
+ * the staging profile, since the implicit repository id is the profile name minus the dots,
+ * e.g. profile 'org.apache.groovy' gives 'orgapachegroovy-1118'. That is enough to ignore other
+ * projects releasing at the same time; only if we have two of our own releases in flight do we
+ * look at the repository contents to work out which one holds the version being released.
+ * Pass a repoId to skip the search altogether.
+ */
+ static findRepo(client, nexusBase, user, password, String type, String groupId, String version, repoId = null) {
+ def staging = stagingBase(nexusBase)
+ if (repoId) {
+ def response = HttpUtil.send(client, HttpUtil.getJsonRequest("$staging/repository/$repoId", user, password))
+ assert response.statusCode() == 200, "Staging repository $repoId not found"
+ return response.json
+ }
+ def response = HttpUtil.send(client, HttpUtil.getJsonRequest("$staging/profile_repositories", user, password))
+ assert response.statusCode() == 200
+ def matching = response.json.data.findAll { it.type == type && !it.transitioning }
+ println "${type.capitalize()} staging repositories: ${matching*.repositoryId.join(', ') ?: 'none'}"
+
+ def prefix = groupId.replace('.', '')
+ def ours = matching.findAll { it.profileName == groupId || it.repositoryId?.startsWith(prefix + '-') }
+ assert ours, "No $type staging repository found for $groupId " +
+ "(if it is no longer $type, rerun with -PstagingRepoId=the_repo_id)"
+ if (ours.size() == 1) return ours[0]
+
+ // more than one of our own releases in flight, so pick the repo actually holding this version
+ def candidates = ours.findAll { containsRelease(client, it.repositoryURI, groupId, version, user, password) }
+ assert candidates.size() == 1, "Cannot tell which $type staging repository holds groovy $version: " +
+ "${ours*.repositoryId} - rerun with -PstagingRepoId=the_repo_id"
+ candidates[0]
+ }
+
+ /** Checks whether a staging repository holds the core pom for the version being released. */
+ static boolean containsRelease(client, repoURI, String groupId, String version, user, password) {
+ def url = "$repoURI/${groupId.replace('.', '/')}/groovy/$version/groovy-${version}.pom"
+ // staging content is readable anonymously - only fall back to our credentials if it isn't,
+ // otherwise a rejected login would look just like a repository without the artifacts
+ def status = HttpUtil.send(client, HttpUtil.headRequest(url)).statusCode()
+ if (status in [401, 403]) {
+ status = HttpUtil.send(client, HttpUtil.headRequest(url, user, password)).statusCode()
+ }
+ status == 200
+ }
+
+ /**
+ * Polls until the given repository stops transitioning and has the expected type.
+ * A released repository is dropped automatically, so a repository which has vanished
+ * also counts as done. Returns false if we gave up waiting.
+ */
+ static boolean awaitTransition(client, nexusBase, user, password, repoId, String expectedType,
+ int delay = 15000, int numTries = 40) {
+ def staging = stagingBase(nexusBase)
+ while (numTries-- > 0) {
+ sleep delay
+ def response = HttpUtil.send(client, HttpUtil.getJsonRequest("$staging/repository/$repoId", user, password))
+ if (response.statusCode() == 404) return true
+ assert response.statusCode() == 200
+ def current = response.json
+ if (!current.transitioning) {
+ assert current.type == expectedType,
+ "Staging repository $repoId is '$current.type' not '$expectedType' - " +
+ "check the Activity tab at $nexusBase/#stagingRepositories"
+ return true
+ }
+ }
+ false
+ }
+}
diff --git a/gradle/phase1.gradle b/gradle/phase1.gradle
index 7a5cbf5..19f34af 100644
--- a/gradle/phase1.gradle
+++ b/gradle/phase1.gradle
@@ -14,6 +14,9 @@ buildscript {
apply plugin: at.bxm.gradleplugins.svntools.SvnToolsPlugin
ext.grgitClass = org.ajoberstar.grgit.Grgit
+ext.nexusBase = 'https://repository.apache.org'
+// set by closeStagingRepo, referenced by prepareVoteThread
+ext.stagingRepoUrl = null
svntools {
username = apacheUser
@@ -186,8 +189,38 @@ tasks.register('uploadStep2', Exec) {
}
uploadStep2.onlyIf{ apacheGroupId }
+// Gradle's maven-publish plugin just PUTs to the Nexus implicit staging endpoint - it has no notion
+// of the staging repository Nexus creates behind the scenes, so we close it via the Nexus 2 staging API.
+tasks.register('closeStagingRepo') {
+ dependsOn([assumesPropsSet, uploadStep2])
+ group = "Pre-vote phase"
+ description = "Closes the Nexus staging repository holding the uploaded Maven artifacts"
+ doLast {
+ def client = HttpUtil.newClient()
+ def staging = NexusUtil.stagingBase(nexusBase)
+ def groupId = apacheGroupId ? 'org.apache.groovy' : 'org.codehaus.groovy'
+ def repo = NexusUtil.findRepo(client, nexusBase, apacheUser, apachePassword, 'open',
+ groupId, relVersion, project.findProperty('stagingRepoId'))
+
+ if (repo.type == 'open') {
+ println "Closing staging repository $repo.repositoryId ..."
+ def body = /{"data":{"stagedRepositoryId":"$repo.repositoryId","description":"Apache Groovy $relVersion"}}/
+ def response = HttpUtil.send(client, HttpUtil.postJsonRequest("$staging/profiles/$repo.profileId/finish", body, apacheUser, apachePassword))
+ assert response.statusCode() in [200, 201], "Close request failed: ${response.statusCode()} ${response.body()}"
+ def closed = NexusUtil.awaitTransition(client, nexusBase, apacheUser, apachePassword, repo.repositoryId, 'closed')
+ assert closed, "Timed out waiting for $repo.repositoryId to close - please check manually"
+ } else {
+ println "Staging repository $repo.repositoryId is already $repo.type"
+ }
+
+ project.ext.stagingRepoUrl = repo.repositoryURI ?: "$nexusBase/content/repositories/$repo.repositoryId"
+ println "Staging repository: $stagingRepoUrl"
+ }
+}
+closeStagingRepo.onlyIf{ apacheGroupId && releaseBuild }
+
tasks.register('cleanSvnDevWorkspace', Delete) {
- dependsOn([assumesBranch, uploadStep2])
+ dependsOn([assumesBranch, closeStagingRepo])
delete devWorkspaceRoot
}
@@ -317,6 +350,7 @@ Tag commit id: $tagId
The artifacts to be voted on are located as follows (r$svnVersion.maxRevisionNumber).
Source release: https://dist.apache.org/repos/dist/dev/groovy/$relVersion/sources
Convenience binaries: https://dist.apache.org/repos/dist/dev/groovy/$relVersion/distribution
+Temporary artifacts: ${stagingRepoUrl ?: 'N/A (staging repository not closed)'}
Release artifacts are signed with a key from the following file:
https://dist.apache.org/repos/dist/release/groovy/KEYS
diff --git a/gradle/phase2.gradle b/gradle/phase2.gradle
index 50c06b7..be0e630 100644
--- a/gradle/phase2.gradle
+++ b/gradle/phase2.gradle
@@ -67,6 +67,37 @@ tasks.register('jiraCheckPhase2') {
}
}
+// The vote has passed, so the staging repository closed in phase1 can be released - Nexus 2 calls
+// this staging action "promote". Releasing publishes the artifacts into the Apache release repo
+// from where they sync to Maven Central; the staging repo itself is dropped once released.
+// We use the bulk endpoint here (what the Release button in the Nexus UI uses) - the per-profile
+// promote DTO has no autoDropAfterRelease field and XStream rejects the whole body if we send it.
+tasks.register('releaseStagingRepo') {
+ dependsOn([assumesRelVersion, jiraCheckPhase2])
+ group = "Post-passed phase"
+ description = "Releases the closed Nexus staging repository holding the Maven artifacts"
+ doLast {
+ def client = HttpUtil.newClient()
+ def staging = NexusUtil.stagingBase(nexusBase)
+ def groupId = apacheGroupId ? 'org.apache.groovy' : 'org.codehaus.groovy'
+ def repo = NexusUtil.findRepo(client, nexusBase, apacheUser, apachePassword, 'closed',
+ groupId, relVersion, project.findProperty('stagingRepoId'))
+
+ if (repo.type == 'closed') {
+ println "Releasing staging repository $repo.repositoryId ..."
+ def body = /{"data":{"stagedRepositoryIds":["$repo.repositoryId"],"description":"Apache Groovy $relVersion","autoDropAfterRelease":true}}/
+ def response = HttpUtil.send(client, HttpUtil.postJsonRequest("$staging/bulk/promote", body, apacheUser, apachePassword))
+ assert response.statusCode() in [200, 201], "Release request failed: ${response.statusCode()} ${response.body()}"
+ def released = NexusUtil.awaitTransition(client, nexusBase, apacheUser, apachePassword, repo.repositoryId, 'released')
+ assert released, "Timed out waiting for $repo.repositoryId to be released - please check manually"
+ } else {
+ println "Staging repository $repo.repositoryId is already $repo.type"
+ }
+ println "Artifacts should appear on Maven Central within a few hours - the checkMavenStatus task confirms it"
+ }
+}
+releaseStagingRepo.onlyIf{ apacheGroupId && releaseBuild }
+
/*
task promoteJarsOnArtifactory(dependsOn: jiraCheckPhase2) {
group = "Post-passed phase"
@@ -466,8 +497,10 @@ tasks.register('jiraTasks') { dependsOn([findGroovyVersions, createNextVersionIn
tasks.register('sdkmanTasks') { dependsOn([jiraCheckPhase2, findGroovyVersions, makeDefaultOnSDKman]) }
+tasks.register('mavenTasks') { dependsOn([jiraCheckPhase2, releaseStagingRepo]) }
+
tasks.register('proposeAnnouncementEmail') {
- dependsOn([gitTasks, jiraTasks, sdkmanTasks, findGroovyVersions])
+ dependsOn([gitTasks, jiraTasks, sdkmanTasks, mavenTasks, findGroovyVersions])
group = "Post-passed phase"
description = "Generates an [ANNOUNCE] thread to be tweaked and sent to the dev@, user@ and announce@ mailing lists"
doLast {