From 9e11ddc92b0fad389c2c2949573f441fc3b47b18 Mon Sep 17 00:00:00 2001 From: Julien Herr Date: Sat, 25 Jul 2026 17:28:54 +0200 Subject: [PATCH 1/3] fix: publish releases automatically to Maven Central The release workflow passed `-DautoPublish=true -DwaitUntil=published`, but the root POM binds the plugin through an explicit `` block referencing `${central.autoPublish}` and `${central.waitUntil}`. An explicit plugin configuration takes precedence over the mojo's own user properties, so both flags were silently ignored. The 0.1.0 deployment stopped at `VALIDATED` and required manual publishing from the Sonatype portal. Pass the properties the POM actually reads, and assert on the effective POM during input validation so a future rename fails the release before anything is uploaded rather than ten minutes later. Widen the resolution window to 30 minutes as well: propagation to repo1.maven.org took roughly 15 minutes for 0.1.0, over the previous 10-minute budget. Co-Authored-By: Claude --- .github/workflows/release.yml | 16 ++++++++++++---- CONTRIBUTING.md | 11 +++++++++++ 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c0d7aca..87c080d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -57,6 +57,11 @@ jobs: grep -Fq "## [${RELEASE_VERSION}] -" CHANGELOG.md test -s ".github/release-notes/${RELEASE_VERSION}.md" scripts/verify-revapi-allowlist.sh + ./mvnw --quiet --batch-mode -N -Prelease \ + -Dcentral.autoPublish=true -Dcentral.waitUntil=published \ + help:effective-pom -Doutput="${RUNNER_TEMP}/effective-pom.xml" + grep -Fq "true" "${RUNNER_TEMP}/effective-pom.xml" + grep -Fq "published" "${RUNNER_TEMP}/effective-pom.xml" test -n "${CENTRAL_USERNAME}" test -n "${CENTRAL_TOKEN}" test -n "${GPG_PASSPHRASE}" @@ -153,14 +158,15 @@ jobs: exit 0 fi ./mvnw --batch-mode --no-transfer-progress \ - -Prelease -DskipTests -DautoPublish=true -DwaitUntil=published deploy + -Prelease -DskipTests \ + -Dcentral.autoPublish=true -Dcentral.waitUntil=published deploy - name: Wait for Maven Central resolution shell: bash run: | set -euo pipefail base_url="https://repo1.maven.org/maven2/dev/juherr/datex4j" - for attempt in $(seq 1 30); do + for attempt in $(seq 1 60); do missing=0 while read -r artifact_id _packaging; do [[ -z "${artifact_id}" || "${artifact_id}" == \#* ]] && continue @@ -178,11 +184,13 @@ jobs: fi done < config/release/public-artifacts.txt [[ "${missing}" -eq 0 ]] && break - if [[ "${attempt}" -eq 30 ]]; then + if [[ "${attempt}" -eq 60 ]]; then echo "Release did not become resolvable from Maven Central in time." >&2 + echo "Check the deployment state at https://central.sonatype.com/publishing/deployments" >&2 + echo "Once it reports published, re-run this failed job to keep the released commit." >&2 exit 1 fi - sleep 20 + sleep 30 done consumer_repository=$(mktemp -d) ./mvnw --batch-mode --no-transfer-progress \ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ddf836c..722cfac 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -169,3 +169,14 @@ Release steps: The workflow safely resumes when its signed tag or GitHub Release already exists at the same commit. It refuses conflicting tags or releases. + +Resume a failed release with `gh run rerun --failed` rather than a new dispatch. A re-run +keeps the original commit, so the signed tag still points at the code that produced the published +artifacts. The publishing job skips the deploy when every artifact already resolves from Central, +so the re-run only replays the remaining verification, tag, and release steps. + +The publishing job drives `central-publishing-maven-plugin` through the `central.autoPublish` and +`central.waitUntil` properties. The root POM binds them into an explicit plugin ``, +which takes precedence over the plugin's own `autoPublish` and `waitUntil` user properties — those +`-D` flags are silently ignored. `Validate release inputs` asserts the effective POM resolves to +`true` and `published` before anything is uploaded. From d3100e84a1762db9bae702e34fa884458deeb356 Mon Sep 17 00:00:00 2001 From: Julien Herr Date: Sat, 25 Jul 2026 17:36:17 +0200 Subject: [PATCH 2/3] fix: require every published file before skipping a redeploy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Central availability check only requested the `.pom` of each entry in `config/release/public-artifacts.txt`, ignoring the packaging column. A release whose POMs had propagated but whose jars had not would satisfy the check, so a re-run would skip the deploy and freeze an incomplete release — the one path that cannot be recovered by re-running. Extract the check into `scripts/check-central-release.sh` and assert the same payload set that `verify-central-bundle.sh` already asserts locally: each POM, plus the main, sources, and javadoc jars for `jar` packaging. Both the deploy guard and the resolution wait now share it, so they cannot drift apart. Co-Authored-By: Claude --- .github/workflows/release.yml | 42 ++++----------------------- CONTRIBUTING.md | 6 ++-- scripts/check-central-release.sh | 50 ++++++++++++++++++++++++++++++++ 3 files changed, 59 insertions(+), 39 deletions(-) create mode 100755 scripts/check-central-release.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 87c080d..c50a94c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -136,24 +136,7 @@ jobs: shell: bash run: | set -euo pipefail - base_url="https://repo1.maven.org/maven2/dev/juherr/datex4j" - missing=0 - while read -r artifact_id _packaging; do - [[ -z "${artifact_id}" || "${artifact_id}" == \#* ]] && continue - pom_url="${base_url}/${artifact_id}/${RELEASE_VERSION}/${artifact_id}-${RELEASE_VERSION}.pom" - if ! curl \ - --connect-timeout 5 \ - --max-time 10 \ - --fail \ - --silent \ - --show-error \ - --head \ - "${pom_url}" >/dev/null; then - missing=1 - break - fi - done < config/release/public-artifacts.txt - if [[ "${missing}" -eq 0 ]]; then + if scripts/check-central-release.sh "${RELEASE_VERSION}"; then echo "Release ${RELEASE_VERSION} is already available from Maven Central; skipping deploy." exit 0 fi @@ -165,25 +148,10 @@ jobs: shell: bash run: | set -euo pipefail - base_url="https://repo1.maven.org/maven2/dev/juherr/datex4j" for attempt in $(seq 1 60); do - missing=0 - while read -r artifact_id _packaging; do - [[ -z "${artifact_id}" || "${artifact_id}" == \#* ]] && continue - pom_url="${base_url}/${artifact_id}/${RELEASE_VERSION}/${artifact_id}-${RELEASE_VERSION}.pom" - if ! curl \ - --connect-timeout 5 \ - --max-time 10 \ - --fail \ - --silent \ - --show-error \ - --head \ - "${pom_url}" >/dev/null; then - missing=1 - break - fi - done < config/release/public-artifacts.txt - [[ "${missing}" -eq 0 ]] && break + if scripts/check-central-release.sh "${RELEASE_VERSION}"; then + break + fi if [[ "${attempt}" -eq 60 ]]; then echo "Release did not become resolvable from Maven Central in time." >&2 echo "Check the deployment state at https://central.sonatype.com/publishing/deployments" >&2 @@ -197,7 +165,7 @@ jobs: -f config/release-smoke/pom.xml \ -Dmaven.repo.local="${consumer_repository}" \ -Ddatex4j.version="${RELEASE_VERSION}" \ - -Ddatex4j.repository="${base_url%/dev/juherr/datex4j}" \ + -Ddatex4j.repository="https://repo1.maven.org/maven2" \ verify finalize: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 722cfac..c270b92 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -172,8 +172,10 @@ commit. It refuses conflicting tags or releases. Resume a failed release with `gh run rerun --failed` rather than a new dispatch. A re-run keeps the original commit, so the signed tag still points at the code that produced the published -artifacts. The publishing job skips the deploy when every artifact already resolves from Central, -so the re-run only replays the remaining verification, tag, and release steps. +artifacts. The publishing job skips the deploy only when `scripts/check-central-release.sh` resolves +every published file — each POM plus the main, sources, and javadoc jars — so a partially propagated +release still redeploys instead of being frozen. The re-run then replays the remaining verification, +tag, and release steps. The publishing job drives `central-publishing-maven-plugin` through the `central.autoPublish` and `central.waitUntil` properties. The root POM binds them into an explicit plugin ``, diff --git a/scripts/check-central-release.sh b/scripts/check-central-release.sh new file mode 100755 index 0000000..5d1d623 --- /dev/null +++ b/scripts/check-central-release.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [[ $# -ne 1 ]]; then + echo "Usage: $0 " >&2 + exit 2 +fi + +version=$1 +project_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +inventory="${project_root}/config/release/public-artifacts.txt" +base_url=${CENTRAL_BASE_URL:-https://repo1.maven.org/maven2/dev/juherr/datex4j} + +resolves() { + local url=$1 + curl \ + --connect-timeout 5 \ + --max-time 10 \ + --fail \ + --silent \ + --show-error \ + --head \ + "${url}" >/dev/null 2>&1 +} + +# Mirrors the payload set asserted locally by verify-central-bundle.sh, so a +# release is only considered complete once every published file resolves. +while read -r artifact_id packaging; do + [[ -z "${artifact_id}" || "${artifact_id}" == \#* ]] && continue + + base="${base_url}/${artifact_id}/${version}/${artifact_id}-${version}" + payloads=("${base}.pom") + case "${packaging}" in + jar) + payloads+=("${base}.jar" "${base}-sources.jar" "${base}-javadoc.jar") + ;; + pom) ;; + *) + echo "Unsupported packaging '${packaging}' for ${artifact_id}" >&2 + exit 2 + ;; + esac + + for payload in "${payloads[@]}"; do + if ! resolves "${payload}"; then + echo "Not resolvable from Maven Central: ${payload}" >&2 + exit 1 + fi + done +done < "${inventory}" From 53a910bb7fecbdf461fb844ac394e1e725fc2dd0 Mon Sep 17 00:00:00 2001 From: Julien Herr Date: Sat, 25 Jul 2026 17:41:49 +0200 Subject: [PATCH 3/3] fix: do not treat a Central outage as an unpublished release MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The availability check discarded curl's status, so a DNS failure, a timeout, or a 5xx from Maven Central was indistinguishable from a 404. The deploy guard would then read "unreachable" as "not published" and redeploy a version that may already exist. Separate the two: exit 1 means Central answered and something is absent, exit 2 means Central could not be questioned. The deploy guard refuses to publish on 2 rather than guessing, while the resolution loop retries it — that loop only waits, so an unreachable Central is just another retry there. Co-Authored-By: Claude --- .github/workflows/release.yml | 14 ++++++++++++-- scripts/check-central-release.sh | 29 ++++++++++++++++++++++++----- 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c50a94c..7058f26 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -136,10 +136,16 @@ jobs: shell: bash run: | set -euo pipefail - if scripts/check-central-release.sh "${RELEASE_VERSION}"; then + status=0 + scripts/check-central-release.sh "${RELEASE_VERSION}" || status=$? + if [[ "${status}" -eq 0 ]]; then echo "Release ${RELEASE_VERSION} is already available from Maven Central; skipping deploy." exit 0 fi + if [[ "${status}" -ne 1 ]]; then + echo "Could not determine whether ${RELEASE_VERSION} is published; refusing to deploy." >&2 + exit "${status}" + fi ./mvnw --batch-mode --no-transfer-progress \ -Prelease -DskipTests \ -Dcentral.autoPublish=true -Dcentral.waitUntil=published deploy @@ -149,7 +155,11 @@ jobs: run: | set -euo pipefail for attempt in $(seq 1 60); do - if scripts/check-central-release.sh "${RELEASE_VERSION}"; then + # Unlike the deploy guard, a transport failure is retried here: this + # loop only waits, so an unreachable Central is just another retry. + status=0 + scripts/check-central-release.sh "${RELEASE_VERSION}" || status=$? + if [[ "${status}" -eq 0 ]]; then break fi if [[ "${attempt}" -eq 60 ]]; then diff --git a/scripts/check-central-release.sh b/scripts/check-central-release.sh index 5d1d623..62c887b 100755 --- a/scripts/check-central-release.sh +++ b/scripts/check-central-release.sh @@ -11,16 +11,35 @@ project_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) inventory="${project_root}/config/release/public-artifacts.txt" base_url=${CENTRAL_BASE_URL:-https://repo1.maven.org/maven2/dev/juherr/datex4j} +# Exit 0: every payload resolves. Exit 1: Central answered, something is absent. +# Exit 2: Central could not be questioned. Callers must not read 2 as "absent" — +# deploying on a transport failure would republish an already published version. resolves() { local url=$1 - curl \ + local http_code curl_status=0 + + http_code=$(curl \ --connect-timeout 5 \ --max-time 10 \ - --fail \ --silent \ - --show-error \ --head \ - "${url}" >/dev/null 2>&1 + --output /dev/null \ + --write-out '%{http_code}' \ + "${url}") || curl_status=$? + + if [[ "${curl_status}" -ne 0 ]]; then + echo "Maven Central is unreachable (curl exit ${curl_status}): ${url}" >&2 + exit 2 + fi + + case "${http_code}" in + 200) return 0 ;; + 404) return 1 ;; + *) + echo "Unexpected HTTP ${http_code} from Maven Central: ${url}" >&2 + exit 2 + ;; + esac } # Mirrors the payload set asserted locally by verify-central-bundle.sh, so a @@ -43,7 +62,7 @@ while read -r artifact_id packaging; do for payload in "${payloads[@]}"; do if ! resolves "${payload}"; then - echo "Not resolvable from Maven Central: ${payload}" >&2 + echo "Not published to Maven Central: ${payload}" >&2 exit 1 fi done