Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 25 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,23 +23,45 @@ jobs:
- uses: gradle/actions/setup-gradle@v4

- name: Build and test
run: ./gradlew build
run: ./gradlew build -PrequireShellHarness=true

- name: Retain build test reports
if: always()
uses: actions/upload-artifact@v4
with:
name: build-test-results
path: build/test-results/

compatibility-test:
runs-on: ubuntu-latest
strategy:
matrix:
gradle-version: ['8.13', '9.4.0']
name: Compatibility tests (Gradle ${{ matrix.gradle-version }})
java-version: ['17', '21']
name: Compatibility tests (Gradle ${{ matrix.gradle-version }}, JDK ${{ matrix.java-version }})
steps:
- uses: actions/checkout@v4

- uses: actions/setup-java@v4
id: compile_jdk
with:
distribution: temurin
java-version: 17

- uses: actions/setup-java@v4
id: runtime_jdk
with:
distribution: temurin
java-version: ${{ matrix.java-version }}

- uses: gradle/actions/setup-gradle@v4

- name: Compatibility tests with Gradle ${{ matrix.gradle-version }}
run: ./gradlew test --tests "se.alipsa.gradle.plugin.release.GradleCompatibilityMatrixTest" -PtestGradleVersion=${{ matrix.gradle-version }}
run: ./gradlew test --tests "*WsClientTimeoutTest" --tests "*CentralUploadApiTest" --tests "*GradleCompatibilityMatrixTest" --tests "*BundleMetadataTest" -PtestGradleVersion=${{ matrix.gradle-version }} -PtestJavaVersion=${{ matrix.java-version }} -PrequireShellHarness=true -Dorg.gradle.java.installations.paths="${{ steps.compile_jdk.outputs.path }},${{ steps.runtime_jdk.outputs.path }}"

- name: Retain runtime proof reports
if: always()
uses: actions/upload-artifact@v4
with:
name: runtime-proof-gradle-${{ matrix.gradle-version }}-jdk-${{ matrix.java-version }}
path: build/test-results/test/
22 changes: 19 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# nexus-release-plugin

[![CI](https://github.com/Alipsa/nexus-release-plugin/actions/workflows/ci.yml/badge.svg)](https://github.com/Alipsa/nexus-release-plugin/actions/workflows/ci.yml)
[![Java](https://img.shields.io/badge/Java-17+-blue)](https://docs.oracle.com/en/java/javase/17/)
[![Groovy](https://img.shields.io/badge/Groovy-4.0.30-blue)](https://groovy-lang.org/)
[![Java](https://img.shields.io/badge/Java-17%20%26%2021-blue)](https://docs.oracle.com/en/java/javase/17/)
[![Groovy](https://img.shields.io/badge/Groovy-4.0.31-blue)](https://groovy-lang.org/)
[![Javadoc](https://javadoc.io/badge2/se.alipsa.nexus-release-plugin/se.alipsa.nexus-release-plugin.gradle.plugin/javadoc.svg)](https://javadoc.io/doc/se.alipsa.nexus-release-plugin/se.alipsa.nexus-release-plugin.gradle.plugin)

Automates the release process to Sonatype Central.
Expand Down Expand Up @@ -90,14 +90,16 @@ nexusReleasePlugin {
```

The `release` task waits for Central Portal to process the uploaded deployment before it checks status.
The defaults are 10 retries, a 10 second interval between checks, and a 10 second initial delay.
The defaults are one initial check plus 10 additional checks, a 10 second interval between checks, and a 10 second initial delay.
You can tune those values if Central Portal is taking longer to validate or publish your bundle:

```groovy
nexusReleasePlugin {
statusCheckRetries = 30
statusCheckIntervalSeconds = 20
initialStatusCheckDelaySeconds = 20
httpConnectTimeoutSeconds = 30
httpRequestTimeoutSeconds = 1800
}
```
## The latestMavenVersions task
Expand Down Expand Up @@ -222,3 +224,17 @@ The opt-in `se.alipsa.nexus-release-all` support plugin adds manifest planning,
## License

MIT License - see [LICENSE](LICENSE) for details.

### Release deadlines and bundle contents in 2.3.0

`httpConnectTimeoutSeconds` defaults to 30 and `httpRequestTimeoutSeconds` to 1800. Both accept 1–86400 seconds. Set them in `nexusReleasePlugin`; overrides apply to `release`. Uploads use the full request deadline. Deployment status checks and other small calls through `CentralPortalClient` use the shorter of 60 seconds and the configured deadline. The caller stops waiting even if an upload or response body stalls. Cancellation closed stalled connections in the verified runtime matrix; no stronger guarantee is made for arbitrary network implementations.

`releaseAll` uses the default client timeouts. Its separate `PublicationStatus` and HEAD availability checks do not receive this overall deadline. Runtime testing covers JDK 17 and 21 with Gradle 8.13 and 9.4.0; compilation targets Java 17. Other daemon JDKs are outside the verified support matrix.

`statusCheckRetries` is the number of additional checks after the first one. Transient errors consume a check and are retried within that budget. Authentication and permission errors fail immediately. Exhaustion reports an unknown or pending deployment that may still publish, with a Portal link.

Bundles use Maven coordinate names and include all artifacts of the selected publication with a main JAR, including classified ZIPs and generated Gradle Module Metadata. Every primary entry needs a nonempty signature and MD5/SHA-1 checksums. JAR-producing packaging such as `bundle`, `maven-plugin`, and `ejb` remains supported. Publications without a main JAR are rejected by `release`; use `releaseAll` for pom-packaged publications. `bundle` installs only the selected publication to Maven local. Ordinary `publish` and `publishToMavenLocal` builds work without setting `mavenPublication`; cleanup attaches only when a publication is selected.

The selected publication's module metadata and adjacent signature are cleared before metadata generation, including plain `publishToMavenLocal`, publishing to other repositories, and snapshots. Metadata regenerates or restores from cache. Deleting its signature forces the publication signing task to re-sign every artifact; gpg-agent/pinentry may prompt on every publish even when signing previously stayed up to date. Disable generation with `enabled = false` to omit metadata. Skipping it with `onlyIf` while the POM advertises metadata fails before upload.

The publishing-script harness runs as part of `check` when Bash is available. Local builds without Bash skip it with a reason; CI requires it with `-PrequireShellHarness=true`.
34 changes: 32 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -137,10 +137,24 @@ afterEvaluate {
}
}

abstract class PolicyScriptArg implements CommandLineArgumentProvider {
@InputFile @PathSensitive(PathSensitivity.NONE)
abstract RegularFileProperty getScript()
@Override Iterable<String> asArguments() {
["-DshellHarnessPolicyScript=${script.get().asFile.absolutePath}".toString()]
}
}

def selectedTestJavaVersion = providers.gradleProperty('testJavaVersion').getOrElse('17')
test {
useJUnitPlatform()
useJUnitPlatform { excludeTags 'non-utf8-charset' }
javaLauncher = javaToolchains.launcherFor { languageVersion = JavaLanguageVersion.of(selectedTestJavaVersion) }
systemProperty 'testJavaVersion', selectedTestJavaVersion
def policyArg = objects.newInstance(PolicyScriptArg)
policyArg.script.set(layout.projectDirectory.file('gradle/shell-harness.gradle'))
jvmArgumentProviders.add(policyArg)
// Parallelize at JVM-fork level to keep TestKit tests isolated per process.
maxParallelForks = Math.max(1, (Runtime.runtime.availableProcessors().intdiv(2)))
maxParallelForks = 2
// Pass system properties from command line to tests
systemProperty 'testGradleVersion', findProperty('testGradleVersion') ?: ''
}
Expand Down Expand Up @@ -178,3 +192,19 @@ tasks.named("dependencyUpdates").configure {
}
}
}


tasks.register('charsetTest', Test) {
testClassesDirs = sourceSets.test.output.classesDirs
classpath = sourceSets.test.runtimeClasspath
javaLauncher = javaToolchains.launcherFor { languageVersion = JavaLanguageVersion.of(17) }
useJUnitPlatform { includeTags 'non-utf8-charset' }
jvmArgs '-Dfile.encoding=ISO-8859-1'
}
tasks.register('shellHarness', Exec) {
inputs.files('publish.sh', 'src/test/shell/publish-harness.sh', 'gradle/shell-harness.gradle')
args layout.projectDirectory.file('src/test/shell/publish-harness.sh').asFile.absolutePath,
layout.projectDirectory.file('publish.sh').asFile.absolutePath
}
apply from: 'gradle/shell-harness.gradle'
tasks.named('check') { dependsOn 'charsetTest', 'shellHarness' }
8 changes: 7 additions & 1 deletion combined-releases.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ An unselected module can retain an already-released BOM pin with a warning.
Normal JAR components require main, sources and Javadoc JARs plus POM. POM components require
only a POM. An explicitly configured `sourcesAndJavadocRequired: false` policy preserves an
aggregate JAR's existing main-JAR/POM publication set. All expected files need nonempty
signatures. No Gradle metadata, repository bookkeeping or unrelated components are copied.
signatures. Explicitly listed `.module` metadata and its signature are accepted. Repository bookkeeping and unrelated components are not copied.
Source guards hash publication POMs and require a clean, unchanged Git revision with no
assume-unchanged or skip-worktree index flags. Maven preparation requires a POSIX filesystem
and bash, groovy and mvn; Windows users must use WSL. Repository and Portal URLs default to
Expand All @@ -90,9 +90,15 @@ The record is written as `SUBMITTING` before HTTP and stores the ID immediately
Definite HTTP 401/403/422 rejections are archived beside the active record as
`*.rejected-<uuid>.json`, allowing a corrected new invocation. Other upload exceptions persist
`UPLOAD_ERROR` with diagnostics; invalid ID responses retain the raw response in `SUBMITTING`.
Connect failures also leave an active `UPLOAD_ERROR` record that must be manually reconciled
before another upload. An initial connection failure usually means no upload was sent, but a
307/308 redirect can send the first POST before the connection to its target times out.
The client does not distinguish these cases, so it conservatively retains the record.
Never automatically retry an unknown upload outcome. Resume only the saved ID, without
source checks, builds or signing. Polling defaults to 60 attempts at 10-second intervals;
Transient I/O errors are recorded and polling continues within that bound. Availability probes use HEAD.
Central propagation remains separate from Portal publication. A failed record reports all
component coordinates. Archive an `AVAILABLE` record before a new deployment; manually
reconcile every unresolved record rather than deleting it to retry.

CentralPortalClient uploads and deployment status checks use default connect (30 s) and caller deadlines (1800 s uploads, 60 s small calls). The single-publication extension timeout overrides do not apply here. Separate PublicationStatus and HEAD availability checks retain their existing behavior without this overall deadline.
30 changes: 30 additions & 0 deletions gradle/shell-harness.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Shared policy; also applied by isolated TestKit fixtures.
def bashExecutable = providers.gradleProperty('shellHarnessBash').getOrElse('bash')
def required = providers.gradleProperty('requireShellHarness').getOrElse('false').toBoolean()
abstract class BashAvailable implements ValueSource<Boolean, BashAvailable.Parameters> {
interface Parameters extends ValueSourceParameters {
Property<String> getExecutable()
}
Boolean obtain() {
try {
def process = new ProcessBuilder(parameters.executable.get(), '-c', 'exit 0').start()
boolean available = process.waitFor(5, java.util.concurrent.TimeUnit.SECONDS) && process.exitValue() == 0
if (process.isAlive()) process.destroyForcibly()
return available
} catch (IOException ignored) { return false }
}
}
def bashAvailable = providers.of(BashAvailable) { parameters.executable.set(bashExecutable) }
tasks.named('shellHarness') {
if (it instanceof Exec) executable bashExecutable
def policyLogger = logger
onlyIf('Bash is available, or required by CI') {
boolean available = bashAvailable.get()
if (!available && required) {
String message = "Bash is unavailable but requireShellHarness=true: ${bashExecutable}"
policyLogger.error(message)
throw new GradleException(message)
}
available
}
}
7 changes: 6 additions & 1 deletion publish.sh
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#!/usr/bin/env bash
set -eo pipefail
if [ -n "$(git status --porcelain)" ]; then
echo "Error: Git working tree is not clean. Commit or stash changes before publishing."
exit 1
Expand All @@ -9,5 +10,9 @@ if command -v jdk17 &> /dev/null; then
. jdk17
export PATH="$JAVA_HOME/bin:$PATH"
fi
set -u
java -version
./gradlew clean build publishPlugins release
./gradlew clean build
./gradlew publishPlugins --validate-only
./gradlew release
./gradlew publishPlugins
44 changes: 36 additions & 8 deletions release.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,42 @@
# Release history

## Ver 2.3.0, in progress
- Dependency updates:
- com.github.ben-manes.versions:com.github.ben-manes.versions.gradle.plugin [0.53.0 -> 0.54.0]
- com.gradle.plugin-publish:com.gradle.plugin-publish.gradle.plugin [2.1.0 -> 2.1.1]
- org.apache.groovy:groovy [4.0.30 -> 4.0.31]
- org.testcontainers:testcontainers [2.0.3 -> 2.0.5]
- org.testcontainers:testcontainers-junit-jupiter [2.0.3 -> 2.0.5]
- Upgrade gradle version [9.4.0 -> 9.5.0]
- Added LatestGithubRelease Task which, similarly to latestMavenVersions, reports the latest release version of the project published to github releases.
- Fix: Central Portal requests through `WsClient`/`CentralPortalClient` now have connect timeouts and an overall
caller deadline, covering uploads and deployment status checks for `release` and `releaseAll`, including responses that stall after headers.
New properties `httpConnectTimeoutSeconds` (default 30) and `httpRequestTimeoutSeconds` (default 1800), each
1–86400 seconds. Status and other small calls through this client use the shorter of 60 seconds and the configured
request timeout; uploads use the full configured timeout. `releaseAll` uses the default timeouts; the new properties
apply only to `release`. Separate `PublicationStatus` and `ReleaseAllTask` HEAD availability checks are unchanged
and do not receive the new overall deadline.
`WsClient` now uses `java.net.http.HttpClient`; header names in its result map are lower-case, with no null status-line key.
- Fix: `release` status polling now tolerates transient errors (5xx, 429, timeouts, malformed responses) instead of
failing a deployment that was uploaded successfully; 401/403 still fail immediately. The number of status checks is
unchanged: one initial check plus up to `statusCheckRetries` further checks.
- Fix: when status polling times out, the error now says the deployment may still publish and how to check it,
instead of reporting "Failed to release". Replaced the obsolete "staging profile id" error message.
- Fix: a missing signature no longer stops later `.asc` files (including the POM signature) from being added to the bundle.
- Bundle entries are now named from the publication coordinates (`artifactId-version[-classifier].extension`),
so `base.archivesName` no longer has to match `artifactId`.
- The bundle now includes Gradle Module Metadata (`.module` + signature + checksums) when it was generated or restored from cache
in the current build, and all artifacts of the publication, not only `.jar` files. Skipping the
metadata task with `onlyIf` while the POM still advertises it is now an error. The combined release accepts
`.module` files in the manifest. Metadata and its signature are cleared before generation to prevent stale
output from being bundled; generation reruns or restores from cache whenever its task is scheduled. This affects
all publishing builds, including snapshots, and forces the publication signing task to re-sign every artifact;
gpg-agent/pinentry may prompt for a passphrase on every publish.
- `release` retains support for JAR-producing packaging, including `bundle`, `maven-plugin`, and `ejb`.
Other packaging without a main JAR receives a clear error instead of misleading "missing .jar" errors.
- `release` validation now requires a signature and checksums for every artifact in the bundle, not only for a fixed
set of JAR/POM files.
- Ordinary `publish` and `publishToMavenLocal` builds do not require `mavenPublication`; metadata cleanup only attaches when a publication is selected.
- Behaviour change: `bundle` now depends only on the configured publication's
`publish<Name>PublicationToMavenLocal` task instead of `publishToMavenLocal`, so other publications are no
longer installed to `~/.m2` as a side effect.
- Fix: `latestGithubRelease` now detects `origin` remotes that contain user info or tokens
(e.g. `https://x-access-token:...@github.com/...`), and `url=` entries written without spaces.
- Fix: Central Portal credentials are encoded as UTF-8 regardless of the platform charset.
- `publish.sh` validates Plugin Portal publication before the Central `release`, then publishes to the Portal,
stopping on the first failure. A Portal failure after Central release requires retrying only Portal publication.

## Ver 2.2.0, 2026-05-01
- fixed groovy doc generation
Expand Down
Loading
Loading