From 7b7993cd1797d4aeaa421d83e1a3d0512d3825b1 Mon Sep 17 00:00:00 2001 From: Liu Rui Date: Sun, 28 Jun 2026 14:05:34 +0800 Subject: [PATCH] Add tag-triggered Maven Central release workflow --- .github/workflows/release.yml | 52 +++++++++++++++++++++ build.gradle.kts | 86 +++++++++++++++++++++++++++-------- docs/RELEASE.md | 43 ++++++++++++++++++ 3 files changed, 163 insertions(+), 18 deletions(-) create mode 100644 .github/workflows/release.yml create mode 100644 docs/RELEASE.md diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..53b80e7 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,52 @@ +name: Release to Maven Central + +on: + push: + tags: + - "v*.*.*" + +concurrency: + group: release-${{ github.ref_name }} + cancel-in-progress: false + +jobs: + publish: + name: Publish release artifacts + runs-on: ubuntu-latest + permissions: + contents: read + env: + SONATYPE_TOKEN: ${{ secrets.SONATYPE_TOKEN }} + SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} + SIGNING_KEY_ID: ${{ secrets.SIGNING_KEY_ID }} + SIGNING_SECRET_KEY: ${{ secrets.SIGNING_SECRET_KEY }} + SIGNING_SECRET_KEY_BASE64: ${{ secrets.SIGNING_SECRET_KEY_BASE64 }} + SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Validate Gradle Wrapper + uses: gradle/actions/wrapper-validation@af1da67850ed9a4cedd57bfd976089dd991e2582 # v4.0.0 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: "21" + distribution: "temurin" + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@af1da67850ed9a4cedd57bfd976089dd991e2582 # v4.0.0 + + - name: Verify release tag + run: ./gradlew verifyReleaseTagVersion + + - name: Verify release credentials + run: ./gradlew verifyReleaseCredentials + + - name: Test + run: ./gradlew test + + - name: Publish to Maven Central + run: ./gradlew publishReleaseToSonatype diff --git a/build.gradle.kts b/build.gradle.kts index 37ed1ac..89dc9f5 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -5,6 +5,8 @@ plugins { id("io.github.jeadyx.sonatype-uploader") version "2.8" } +import java.util.Base64 + val releasePublishModules = listOf( "muyun-database-core", "muyun-database-core-json-jackson", @@ -14,6 +16,43 @@ val releasePublishModules = listOf( "muyun-database-quarkus-deployment" ) +fun Project.releaseValue(propertyName: String, environmentName: String): String? { + return findProperty(propertyName) + ?.toString() + ?.trim() + ?.takeIf { it.isNotEmpty() && it != "null" } + ?: providers.environmentVariable(environmentName) + .orNull + ?.trim() + ?.takeIf { it.isNotEmpty() } +} + +fun Project.releaseSigningSecretKey(): String? { + releaseValue("signing.secretKey", "SIGNING_SECRET_KEY")?.let { return it } + + return releaseValue("signing.secretKeyBase64", "SIGNING_SECRET_KEY_BASE64")?.let { encoded -> + String(Base64.getDecoder().decode(encoded), Charsets.UTF_8) + } +} + +fun Project.releaseCredentialValues(): Map { + return mapOf( + "sonatype.token or SONATYPE_TOKEN" to releaseValue("sonatype.token", "SONATYPE_TOKEN"), + "sonatype.password or SONATYPE_PASSWORD" to releaseValue("sonatype.password", "SONATYPE_PASSWORD"), + "signing.keyId or SIGNING_KEY_ID" to releaseValue("signing.keyId", "SIGNING_KEY_ID"), + "signing.secretKey/signing.secretKeyBase64 or SIGNING_SECRET_KEY/SIGNING_SECRET_KEY_BASE64" to + releaseSigningSecretKey(), + "signing.password or SIGNING_PASSWORD" to releaseValue("signing.password", "SIGNING_PASSWORD") + ) +} + +fun Project.requireReleaseCredentials() { + val missing = releaseCredentialValues().filterValues { it.isNullOrBlank() }.keys + require(missing.isEmpty()) { + "Missing required Gradle properties for Sonatype publish: ${missing.joinToString(", ")}" + } +} + allprojects { group = "net.ximatai.muyun.database" // version = "1.0.0-SNAPSHOT" @@ -29,24 +68,35 @@ tasks.register("publishReleaseToSonatype") { group = "publishing" description = "Publish release modules (core/jdbi/starter/quarkus) to Sonatype." + dependsOn("verifyReleaseCredentials") dependsOn(releasePublishModules.map { ":$it:clean" }) dependsOn("cleanLocalDeploymentDir") dependsOn(releasePublishModules.map { ":$it:publishToSonatype" }) doFirst { - val requiredKeys = listOf( - "sonatype.token", - "sonatype.password", - "signing.keyId", - "signing.secretKey", - "signing.password" - ) - val missing = requiredKeys.filter { key -> - val value = findProperty(key)?.toString()?.trim() - value.isNullOrEmpty() || value == "null" - } - require(missing.isEmpty()) { - "Missing required Gradle properties for Sonatype publish: ${missing.joinToString(", ")}" + requireReleaseCredentials() + } +} + +tasks.register("verifyReleaseCredentials") { + group = "verification" + description = "Verify that all Sonatype and signing credentials are available." + + doLast { + requireReleaseCredentials() + } +} + +tasks.register("verifyReleaseTagVersion") { + group = "verification" + description = "Verify that the release tag matches the Gradle project version." + + doLast { + val tag = releaseValue("release.tag", "GITHUB_REF_NAME") + ?: error("Missing release tag. Provide -Prelease.tag=v${project.version} or set GITHUB_REF_NAME.") + val expectedTag = "v${project.version}" + require(tag == expectedTag) { + "Release tag '$tag' does not match project version '${project.version}'. Expected '$expectedTag'." } } } @@ -114,16 +164,16 @@ subprojects { sonatypeUploader { repositoryPath = layout.buildDirectory.dir("repo").get().asFile.path - tokenName = findProperty("sonatype.token").toString() - tokenPasswd = findProperty("sonatype.password").toString() + tokenName = releaseValue("sonatype.token", "SONATYPE_TOKEN").orEmpty() + tokenPasswd = releaseValue("sonatype.password", "SONATYPE_PASSWORD").orEmpty() } signing { sign(publishing.publications["mavenJava"]) useInMemoryPgpKeys( - findProperty("signing.keyId").toString(), - findProperty("signing.secretKey").toString(), - findProperty("signing.password").toString() + releaseValue("signing.keyId", "SIGNING_KEY_ID").orEmpty(), + releaseSigningSecretKey().orEmpty(), + releaseValue("signing.password", "SIGNING_PASSWORD").orEmpty() ) } diff --git a/docs/RELEASE.md b/docs/RELEASE.md new file mode 100644 index 0000000..212713e --- /dev/null +++ b/docs/RELEASE.md @@ -0,0 +1,43 @@ +# Release + +Releases are published from GitHub Actions when a release tag is pushed. + +## Required Repository Secrets + +Configure these repository secrets before pushing a release tag: + +- `SONATYPE_TOKEN`: Sonatype Central user token name. +- `SONATYPE_PASSWORD`: Sonatype Central user token password. +- `SIGNING_KEY_ID`: PGP signing key id. +- `SIGNING_PASSWORD`: PGP signing key passphrase. +- `SIGNING_SECRET_KEY_BASE64`: Base64-encoded ASCII-armored PGP private key. + +`SIGNING_SECRET_KEY` is also supported for a plain ASCII-armored private key, but +`SIGNING_SECRET_KEY_BASE64` avoids newline handling issues in CI secret values. + +Create the base64 secret from a local signing key with: + +```bash +gpg --armor --export-secret-keys | base64 | tr -d '\n' +``` + +## Publish a Release + +1. Update `version` in the root `build.gradle.kts` and any documentation that + embeds the public dependency version. +2. Merge the version bump to `master`. +3. Push a matching tag: + +```bash +git tag v3.26.12 +git push origin v3.26.12 +``` + +The release workflow validates that the tag equals `v`, runs +`./gradlew test`, then runs `./gradlew publishReleaseToSonatype`. + +For a local dry check of the tag guard: + +```bash +./gradlew verifyReleaseTagVersion -Prelease.tag=v3.26.12 +```