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
52 changes: 52 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -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
86 changes: 68 additions & 18 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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<String, String?> {
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"
Expand All @@ -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'."
}
}
}
Expand Down Expand Up @@ -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()
)
}

Expand Down
43 changes: 43 additions & 0 deletions docs/RELEASE.md
Original file line number Diff line number Diff line change
@@ -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 <KEY_ID> | 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<project.version>`, runs
`./gradlew test`, then runs `./gradlew publishReleaseToSonatype`.

For a local dry check of the tag guard:

```bash
./gradlew verifyReleaseTagVersion -Prelease.tag=v3.26.12
```
Loading