From 6a4bb612cd4a2cbd038c2550d1dcc43c5e0bb631 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Fri, 25 Sep 2026 13:37:10 -0700 Subject: [PATCH 1/5] Add recovery publishing for existing library release tags --- .github/workflows/deploy.yml | 40 ++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 0e002f7533..26efc4641f 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -28,6 +28,11 @@ on: - plugin-maven - all - lib + - lib-recover + lib_version: + description: 'Existing lib tag version to publish when using lib-recover (e.g. 4.10.3)' + required: false + type: string jobs: build: @@ -49,9 +54,19 @@ jobs: ORG_GRADLE_PROJECT_signingInMemoryKeyId: "0xB74EC7C6" ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.GPG_PASSPHRASE }} steps: + - name: Validate library recovery input + if: "${{ inputs.to_publish == 'lib-recover' }}" + env: + LIB_VERSION: ${{ inputs.lib_version }} + run: | + [[ "$LIB_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { + echo 'lib-recover requires a released library version, e.g. 4.10.3' + exit 1 + } - uses: actions/checkout@v7 with: persist-credentials: false + ref: ${{ inputs.to_publish == 'lib-recover' && format('refs/tags/lib/{0}', inputs.lib_version) || github.ref }} - name: Configure git credentials for JGit run: | git config --local http.https://github.com/.extraheader "AUTHORIZATION: basic $(echo -n x-access-token:${{ secrets.GITHUB_TOKEN }} | base64 -w0)" @@ -83,3 +98,28 @@ jobs: if: "${{ github.event.inputs.to_publish == 'lib' }}" run: | ./gradlew :changelogPush -Prelease=true --stacktrace --warning-mode all --no-configuration-cache + - name: Recover library artifacts from an existing tag + if: "${{ inputs.to_publish == 'lib-recover' }}" + env: + LIB_VERSION: ${{ inputs.lib_version }} + run: | + # Verify the build's versions without printing Gradle properties or secrets. + cat > "$RUNNER_TEMP/verify-library-version.gradle" <<'EOF' + gradle.projectsEvaluated { + if (rootProject.name != 'spotless') { + return // Init scripts also run for the included build-logic build. + } + [':lib', ':lib-extra'].each { projectPath -> + def library = rootProject.project(projectPath) + if (library.version.toString() != System.getenv('LIB_VERSION')) { + throw new GradleException("Library version does not match the recovery tag: ${projectPath}") + } + } + } + EOF + # End this build successfully so the publishing plugin uploads its bundle. + # Do not run changelogPush: the version and tag already exist. + ./gradlew :lib:publishToMavenCentral :lib-extra:publishToMavenCentral \ + -Prelease=true -PmavenCentralDeploymentValidation=PUBLISHED \ + --init-script "$RUNNER_TEMP/verify-library-version.gradle" \ + --stacktrace --warning-mode all --no-configuration-cache --no-scan From a7416c189f4f8051d18fa03eeb365931f1f494bf Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Fri, 25 Sep 2026 13:37:28 -0700 Subject: [PATCH 2/5] Document library publication recovery (#3108) --- CHANGES.md | 3 +++ plugin-gradle/CHANGES.md | 3 +++ plugin-maven/CHANGES.md | 3 +++ 3 files changed, 9 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 77bd03bf2b..3419e7c9ee 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -11,6 +11,9 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format ( ## [Unreleased] +### Fixed +- Add recovery publishing for missing library artifacts from existing release tags. ([#3108](https://github.com/diffplug/spotless/pull/3108)) + ## [4.10.3] - 2026-09-25 ### Changes diff --git a/plugin-gradle/CHANGES.md b/plugin-gradle/CHANGES.md index 03d0e63310..b77dbd0182 100644 --- a/plugin-gradle/CHANGES.md +++ b/plugin-gradle/CHANGES.md @@ -4,6 +4,9 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format ( ## [Unreleased] +### Fixed +- Add recovery publishing for missing library artifacts from existing release tags. ([#3108](https://github.com/diffplug/spotless/pull/3108)) + ## [8.10.3] - 2026-09-25 ### Changes - Generate formatter defaults from version catalog. ([#3045](https://github.com/diffplug/spotless/pull/3045)) diff --git a/plugin-maven/CHANGES.md b/plugin-maven/CHANGES.md index 11cf94060d..83b7e11f65 100644 --- a/plugin-maven/CHANGES.md +++ b/plugin-maven/CHANGES.md @@ -4,6 +4,9 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format ( ## [Unreleased] +### Fixed +- Add recovery publishing for missing library artifacts from existing release tags. ([#3108](https://github.com/diffplug/spotless/pull/3108)) + ## [3.10.3] - 2026-09-25 ### Changes - Generate formatter defaults from version catalog. ([#3045](https://github.com/diffplug/spotless/pull/3045)) From 6f404512ede8ead231033c82a75e96da638bc3cb Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Fri, 25 Sep 2026 13:48:46 -0700 Subject: [PATCH 3/5] Confirm publication before updating release changelogs and tags --- .github/scripts/publish-release.sh | 41 +++++++++++ .github/scripts/test_publish_release.py | 70 +++++++++++++++++++ .github/workflows/ci.yml | 2 + .github/workflows/deploy.yml | 18 +++-- .../main/kotlin/spotless.changelog.gradle.kts | 4 ++ .../kotlin/spotless.java-publish.gradle.kts | 4 +- 6 files changed, 132 insertions(+), 7 deletions(-) create mode 100644 .github/scripts/publish-release.sh create mode 100644 .github/scripts/test_publish_release.py diff --git a/.github/scripts/publish-release.sh b/.github/scripts/publish-release.sh new file mode 100644 index 0000000000..14e6ce3400 --- /dev/null +++ b/.github/scripts/publish-release.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +set -euo pipefail + +case "${1:-}" in + lib) + changelog_prefix=: + publish_tasks=(:lib:publishToMavenCentral :lib-extra:publishToMavenCentral) + ;; + plugin-gradle) + changelog_prefix=:plugin-gradle: + publish_tasks=(:plugin-gradle:publishPlugins :plugin-gradle:publishToMavenCentral) + ;; + plugin-maven) + changelog_prefix=:plugin-maven: + publish_tasks=(:plugin-maven:publishToMavenCentral) + ;; + *) + echo 'Usage: publish-release.sh {lib|plugin-gradle|plugin-maven}' >&2 + exit 2 + ;; +esac + +gradle_args=(-Prelease=true -PmavenCentralDeploymentValidation=PUBLISHED + --stacktrace --warning-mode all --no-configuration-cache --no-scan) +if [[ "$1" == plugin-gradle ]]; then + gradle_args+=("-Pgradle.publish.key=${GRADLE_KEY:?}" "-Pgradle.publish.secret=${GRADLE_SECRET:?}") +fi + +# Central uploads happen at the end of a successful Gradle build. Finish that +# build, including waiting for PUBLISHED, before mutating the changelog or Git. +./gradlew "${changelog_prefix}changelogInternalPushWillRun" "${changelog_prefix}changelogCheck" \ + "${publish_tasks[@]}" "${gradle_args[@]}" + +# The artifacts are already published. Exclude the publishing dependencies from +# the changelog build so it cannot upload them again or defer publication to its end. +exclude_tasks=() +for task in "${publish_tasks[@]}"; do + exclude_tasks+=(-x "$task") +done +./gradlew "${changelog_prefix}changelogPush" -PpublicationConfirmed=true \ + "${exclude_tasks[@]}" "${gradle_args[@]}" diff --git a/.github/scripts/test_publish_release.py b/.github/scripts/test_publish_release.py new file mode 100644 index 0000000000..320d153295 --- /dev/null +++ b/.github/scripts/test_publish_release.py @@ -0,0 +1,70 @@ +import json +import os +from pathlib import Path +import subprocess +import tempfile +import unittest + + +SCRIPT = Path(__file__).with_name("publish-release.sh").resolve() +PUBLICATIONS = { + "lib": [":lib:publishToMavenCentral", ":lib-extra:publishToMavenCentral"], + "plugin-gradle": [":plugin-gradle:publishPlugins", ":plugin-gradle:publishToMavenCentral"], + "plugin-maven": [":plugin-maven:publishToMavenCentral"], +} + + +class ReleasePublicationTest(unittest.TestCase): + def run_release(self, component, publish_exit): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + wrapper = root / "gradlew" + wrapper.write_text( + "#!/usr/bin/env python3\n" + "import json, os, sys\n" + "with open('calls.jsonl', 'a') as calls:\n" + " calls.write(json.dumps(sys.argv[1:]) + '\\n')\n" + "if any(arg.endswith(':changelogPush') for arg in sys.argv):\n" + " open('tag-created', 'w').close()\n" + "else:\n" + " # Model a failure at build shutdown, after publication tasks finish.\n" + " sys.exit(int(os.environ['PUBLISH_EXIT']))\n" + ) + wrapper.chmod(0o755) + result = subprocess.run( + ["bash", str(SCRIPT), component], + cwd=root, + env=dict(os.environ, PUBLISH_EXIT=str(publish_exit), + GRADLE_KEY="test-key", GRADLE_SECRET="test-secret"), + capture_output=True, + text=True, + ) + calls = [json.loads(line) for line in (root / "calls.jsonl").read_text().splitlines()] + return result, calls, (root / "tag-created").exists() + + def test_failed_publication_never_updates_changelog_or_pushes_tag(self): + for component in PUBLICATIONS: + with self.subTest(component=component): + result, calls, tagged = self.run_release(component, 42) + self.assertEqual(result.returncode, 42) + self.assertEqual(len(calls), 1) + self.assertFalse(tagged) + self.assertNotIn("-PpublicationConfirmed=true", calls[0]) + + def test_confirmed_publication_precedes_changelog_without_republishing(self): + for component, publications in PUBLICATIONS.items(): + with self.subTest(component=component): + result, calls, tagged = self.run_release(component, 0) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(len(calls), 2) + self.assertTrue(tagged) + self.assertIn("-PmavenCentralDeploymentValidation=PUBLISHED", calls[0]) + self.assertNotIn("-PpublicationConfirmed=true", calls[0]) + self.assertIn("-PpublicationConfirmed=true", calls[1]) + for task in publications: + self.assertIn(task, calls[0]) + self.assertEqual(calls[1][calls[1].index(task) - 1], "-x") + + +if __name__ == "__main__": + unittest.main() diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f34b197ce0..26d2b0026d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,6 +33,8 @@ jobs: distribution: "temurin" java-version: 21 - uses: gradle/actions/setup-gradle@v6 + - name: Check release publication ordering + run: python3 .github/scripts/test_publish_release.py - run: ./gradlew spotlessCheck - run: ./gradlew assemble testClasses publishToMavenLocal diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 26efc4641f..a44ac86557 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -82,22 +82,28 @@ jobs: run: git -c http.version=HTTP/1.1 fetch origin main - name: publish all if: "${{ github.event.inputs.to_publish == 'all' }}" + env: + GRADLE_KEY: ${{ secrets.GRADLE_KEY }} + GRADLE_SECRET: ${{ secrets.GRADLE_SECRET }} run: | - ./gradlew :changelogPush -Prelease=true --stacktrace --warning-mode all --no-configuration-cache - ./gradlew :plugin-gradle:changelogPush -Prelease=true -Pgradle.publish.key=${{ secrets.GRADLE_KEY }} -Pgradle.publish.secret=${{ secrets.GRADLE_SECRET }} --stacktrace --warning-mode all --no-configuration-cache - ./gradlew :plugin-maven:changelogPush -Prelease=true --stacktrace --warning-mode all --no-configuration-cache + bash .github/scripts/publish-release.sh lib + bash .github/scripts/publish-release.sh plugin-gradle + bash .github/scripts/publish-release.sh plugin-maven - name: publish just plugin-gradle if: "${{ github.event.inputs.to_publish == 'plugin-gradle' }}" + env: + GRADLE_KEY: ${{ secrets.GRADLE_KEY }} + GRADLE_SECRET: ${{ secrets.GRADLE_SECRET }} run: | - ./gradlew :plugin-gradle:changelogPush -Prelease=true -Pgradle.publish.key=${{ secrets.GRADLE_KEY }} -Pgradle.publish.secret=${{ secrets.GRADLE_SECRET }} --stacktrace --warning-mode all --no-configuration-cache + bash .github/scripts/publish-release.sh plugin-gradle - name: publish just plugin-maven if: "${{ github.event.inputs.to_publish == 'plugin-maven' }}" run: | - ./gradlew :plugin-maven:changelogPush -Prelease=true --stacktrace --warning-mode all --no-configuration-cache + bash .github/scripts/publish-release.sh plugin-maven - name: publish just lib if: "${{ github.event.inputs.to_publish == 'lib' }}" run: | - ./gradlew :changelogPush -Prelease=true --stacktrace --warning-mode all --no-configuration-cache + bash .github/scripts/publish-release.sh lib - name: Recover library artifacts from an existing tag if: "${{ inputs.to_publish == 'lib-recover' }}" env: diff --git a/build-logic/src/main/kotlin/spotless.changelog.gradle.kts b/build-logic/src/main/kotlin/spotless.changelog.gradle.kts index 5db9e0ba04..6004ed1627 100644 --- a/build-logic/src/main/kotlin/spotless.changelog.gradle.kts +++ b/build-logic/src/main/kotlin/spotless.changelog.gradle.kts @@ -36,6 +36,10 @@ if (project == rootProject) { ) } if (changelogPushTasks.size == 1) { + check(rootProject.findProperty("publicationConfirmed")?.toString() == "true") { + "Publish and confirm the artifacts before pushing release tags. " + + "Use bash .github/scripts/publish-release.sh {lib|plugin-gradle|plugin-maven}." + } // if the one thing being published is a plugin, make sure there aren't any unreleased // changes in lib val isPlugin = diff --git a/build-logic/src/main/kotlin/spotless.java-publish.gradle.kts b/build-logic/src/main/kotlin/spotless.java-publish.gradle.kts index d7e52a0b69..11dc18cc5c 100644 --- a/build-logic/src/main/kotlin/spotless.java-publish.gradle.kts +++ b/build-logic/src/main/kotlin/spotless.java-publish.gradle.kts @@ -42,7 +42,9 @@ tasks.jar { dependsOn(changelogTasks.named("changelogCheck")) } -// ensures that changelog bump and push only happens if the publish was successful +// These tasks prepare publications, but Central uploads only when the build ends. +// CI uses publish-release.sh to finish publishing before a separate changelog build, +// which excludes these dependencies because the artifacts are already published. changelogTasks.named("changelogBump") { dependsOn(tasks.named("publishToMavenCentral")) // if we have a Gradle plugin, we need to push it up to the plugin portal too From 762b4f5e3da1a004d616a818d7b49f2c86eb51e7 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Fri, 25 Sep 2026 13:49:10 -0700 Subject: [PATCH 4/5] Document publish-before-tag safeguard (#3109) --- CHANGES.md | 1 + plugin-gradle/CHANGES.md | 1 + plugin-maven/CHANGES.md | 1 + 3 files changed, 3 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 3419e7c9ee..90a81c8e10 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -12,6 +12,7 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format ( ## [Unreleased] ### Fixed +- Confirm artifact publication before updating release changelogs and pushing tags. ([#3109](https://github.com/diffplug/spotless/pull/3109)) - Add recovery publishing for missing library artifacts from existing release tags. ([#3108](https://github.com/diffplug/spotless/pull/3108)) ## [4.10.3] - 2026-09-25 diff --git a/plugin-gradle/CHANGES.md b/plugin-gradle/CHANGES.md index b77dbd0182..34aeb7d916 100644 --- a/plugin-gradle/CHANGES.md +++ b/plugin-gradle/CHANGES.md @@ -5,6 +5,7 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format ( ## [Unreleased] ### Fixed +- Confirm artifact publication before updating release changelogs and pushing tags. ([#3109](https://github.com/diffplug/spotless/pull/3109)) - Add recovery publishing for missing library artifacts from existing release tags. ([#3108](https://github.com/diffplug/spotless/pull/3108)) ## [8.10.3] - 2026-09-25 diff --git a/plugin-maven/CHANGES.md b/plugin-maven/CHANGES.md index 83b7e11f65..ee938fe6c3 100644 --- a/plugin-maven/CHANGES.md +++ b/plugin-maven/CHANGES.md @@ -5,6 +5,7 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format ( ## [Unreleased] ### Fixed +- Confirm artifact publication before updating release changelogs and pushing tags. ([#3109](https://github.com/diffplug/spotless/pull/3109)) - Add recovery publishing for missing library artifacts from existing release tags. ([#3108](https://github.com/diffplug/spotless/pull/3108)) ## [3.10.3] - 2026-09-25 From 67b4dbfdff010a8702a093bbb22c770910b8dcc1 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Thu, 8 Oct 2026 13:59:32 -0700 Subject: [PATCH 5/5] Remove internal publishing changes from release notes --- CHANGES.md | 4 ---- plugin-gradle/CHANGES.md | 4 ---- plugin-maven/CHANGES.md | 4 ---- 3 files changed, 12 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 90a81c8e10..77bd03bf2b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -11,10 +11,6 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format ( ## [Unreleased] -### Fixed -- Confirm artifact publication before updating release changelogs and pushing tags. ([#3109](https://github.com/diffplug/spotless/pull/3109)) -- Add recovery publishing for missing library artifacts from existing release tags. ([#3108](https://github.com/diffplug/spotless/pull/3108)) - ## [4.10.3] - 2026-09-25 ### Changes diff --git a/plugin-gradle/CHANGES.md b/plugin-gradle/CHANGES.md index 34aeb7d916..03d0e63310 100644 --- a/plugin-gradle/CHANGES.md +++ b/plugin-gradle/CHANGES.md @@ -4,10 +4,6 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format ( ## [Unreleased] -### Fixed -- Confirm artifact publication before updating release changelogs and pushing tags. ([#3109](https://github.com/diffplug/spotless/pull/3109)) -- Add recovery publishing for missing library artifacts from existing release tags. ([#3108](https://github.com/diffplug/spotless/pull/3108)) - ## [8.10.3] - 2026-09-25 ### Changes - Generate formatter defaults from version catalog. ([#3045](https://github.com/diffplug/spotless/pull/3045)) diff --git a/plugin-maven/CHANGES.md b/plugin-maven/CHANGES.md index ee938fe6c3..11cf94060d 100644 --- a/plugin-maven/CHANGES.md +++ b/plugin-maven/CHANGES.md @@ -4,10 +4,6 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format ( ## [Unreleased] -### Fixed -- Confirm artifact publication before updating release changelogs and pushing tags. ([#3109](https://github.com/diffplug/spotless/pull/3109)) -- Add recovery publishing for missing library artifacts from existing release tags. ([#3108](https://github.com/diffplug/spotless/pull/3108)) - ## [3.10.3] - 2026-09-25 ### Changes - Generate formatter defaults from version catalog. ([#3045](https://github.com/diffplug/spotless/pull/3045))