diff --git a/.github/workflows/gradle-publish.yml b/.github/workflows/gradle-publish.yml
index 1c33ed8..451d328 100644
--- a/.github/workflows/gradle-publish.yml
+++ b/.github/workflows/gradle-publish.yml
@@ -10,6 +10,18 @@ name: Gradle Package
on:
release:
types: [ created ]
+ workflow_dispatch:
+ inputs:
+ ref:
+ description: 'Git ref (branch or tag) to build'
+ required: false
+ default: 'main'
+ type: string
+ publish_to_remote:
+ description: 'Publish artifacts to Sonatype OSSRH and the Gradle Plugin Portal'
+ required: false
+ default: false
+ type: boolean
jobs:
build:
@@ -18,9 +30,18 @@ jobs:
permissions:
contents: read
packages: write
+ env:
+ OSSRH_TOKEN_USERNAME: ${{ secrets.OSSRH_TOKEN_USERNAME }}
+ OSSRH_TOKEN_PASSWORD: ${{ secrets.OSSRH_TOKEN_PASSWORD }}
+ SIGNING_KEY: ${{ secrets.SIGNING_KEY }}
+ SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }}
+ SIGNING_KEY_ID: ${{ secrets.SIGNING_KEY_ID }}
+ PUBLISH_TO_REMOTE: ${{ github.event_name == 'release' || inputs.publish_to_remote == true }}
steps:
- uses: actions/checkout@v4
+ with:
+ ref: ${{ inputs.ref || github.ref }}
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
@@ -32,19 +53,45 @@ jobs:
- name: Setup Gradle
uses: gradle/actions/setup-gradle@af1da67850ed9a4cedd57bfd976089dd991e2582 # v4.0.0
+ - name: Configure Gradle publishing credentials
+ run: |
+ mkdir -p "$HOME/.gradle"
+
+ {
+ if [[ -n "${OSSRH_TOKEN_USERNAME}" ]]; then
+ echo "ossrhTokenUsername=${OSSRH_TOKEN_USERNAME}"
+ fi
+ if [[ -n "${OSSRH_TOKEN_PASSWORD}" ]]; then
+ echo "ossrhTokenPassword=${OSSRH_TOKEN_PASSWORD}"
+ fi
+ if [[ -n "${SIGNING_PASSWORD}" ]]; then
+ echo "signingPassword=${SIGNING_PASSWORD}"
+ fi
+ if [[ -n "${SIGNING_KEY}" ]]; then
+ echo "signingKey=${SIGNING_KEY}"
+ fi
+ # Optionally expose SIGNING_KEY_ID for workflows that delegate signing to gpg.
+ if [[ -n "${SIGNING_KEY_ID}" ]]; then
+ echo "signing.keyId=${SIGNING_KEY_ID}"
+ fi
+ } > "$HOME/.gradle/gradle.properties"
+
+ chmod 600 "$HOME/.gradle/gradle.properties"
+
- name: Build with Gradle
run: ./gradlew build
- # The USERNAME and TOKEN need to correspond to the credentials environment variables used in
- # the publishing section of your build.gradle
- - name: Publish to GitHub Packages
+ - name: Publish to Sonatype OSSRH
+ if: env.PUBLISH_TO_REMOTE == 'true'
run: ./gradlew publish
- env:
- USERNAME: ${{ github.actor }}
- TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Publish to Gradle Plugin Portal
+ if: env.PUBLISH_TO_REMOTE == 'true'
run: ./gradlew publishPlugins
env:
GRADLE_PUBLISH_KEY: ${{ secrets.GRADLE_PUBLISH_KEY }}
- GRADLE_PUBLISH_SECRET: ${{ secrets.GRADLE_PUBLISH_SECRET }}
\ No newline at end of file
+ GRADLE_PUBLISH_SECRET: ${{ secrets.GRADLE_PUBLISH_SECRET }}
+
+ - name: Clean up Gradle credentials
+ if: always()
+ run: rm -f "$HOME/.gradle/gradle.properties"
diff --git a/README.md b/README.md
index a8aea06..2ceb479 100644
--- a/README.md
+++ b/README.md
@@ -158,3 +158,148 @@ tasks.checkLicenses {
policiesFile = file("path/to/your/policies.json")
}
```
+
+## Publishing setup TODO (Sonatype OSSRH)
+
+Store secrets in `~/.gradle/gradle.properties`, your CI secret store, or environment
+variables as noted below. The checklist assumes that the project is published through
+the Sonatype OSSRH infrastructure with token-based authentication (preferred) and
+falls back to legacy username/password credentials if necessary. When these
+credentials (and the signing keys described below) are absent, running
+`./gradlew publish` will skip the remote Sonatype repository and signing tasks so
+that local verification builds continue to succeed.
+
+- [ ] Confirm OSSRH project access
+ - Sign in at with the Sonatype account that owns the
+ `io.github.eurofunk` groupId. If the namespace has not yet been approved, follow
+ the steps in the [OSSRH guide](https://central.sonatype.org/publish/publish-guide/)
+ to request access.
+ - Ensure you can see the `Staging Repositories` menu entry before attempting to publish.
+
+- [ ] `ossrhTokenUsername` / `OSSRH_TOKEN_USERNAME`
+ - From the OSSRH web UI, open **Profile → User Token** and click **Access User Token**.
+ Copy the generated **Token Username** and store it as the Gradle property
+ `ossrhTokenUsername` or environment variable `OSSRH_TOKEN_USERNAME`.
+ - Legacy fallback: the build still honors `ossrhUsername` / `OSSRH_USERNAME` if tokens are
+ not available.
+
+- [ ] `ossrhTokenPassword` / `OSSRH_TOKEN_PASSWORD`
+ - In the same dialog, copy the **Token Password** and store it as the Gradle property
+ `ossrhTokenPassword` or environment variable `OSSRH_TOKEN_PASSWORD`.
+ - Legacy fallback: provide `ossrhPassword` / `OSSRH_PASSWORD` if you must use the classic
+ credentials.
+
+- [ ] (optional) Override publishing endpoints
+ - Releases deploy to `https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/` by default.
+ Override with the Gradle property `ossrhReleasesUrl` only if Sonatype assigns a different
+ host for your project.
+ - Snapshot artifacts deploy to `https://s01.oss.sonatype.org/content/repositories/snapshots/`.
+ Override with `ossrhSnapshotsUrl` if required.
+
+- [ ] `signingKeyId` / `SIGNING_KEY_ID` *(optional)*
+ - Run `gpg --list-secret-keys --keyid-format=long` and copy the key ID for your publishing key (for example `ABCDEF12`).
+ - Provide the key ID only if you rely on the local GnuPG executable or need a stable identifier in signing reports. When Gradle loads the ASCII-armored private key directly (`signingKey`/`SIGNING_KEY`), the identifier can be omitted and Gradle derives it from the key material automatically.
+
+- [ ] `signingKey` / `SIGNING_KEY`
+ - Export the ASCII-armored private key with `gpg --armor --export-secret-keys ` (replace `` with the value above).
+ - Paste the full output—including the `BEGIN/END PGP PRIVATE KEY BLOCK` markers—into the Gradle property `signingKey` or environment variable `SIGNING_KEY`. A fingerprint alone is not sufficient; the entire private key block must be provided.
+ - The build accepts either the raw ASCII-armored export or a base64-encoded copy of that same text. Gradle checks that the payload contains a complete PGP secret key block; binary exports or truncated blocks are ignored and signing will be skipped with a warning.
+
+- [ ] `signingPassword` / `SIGNING_PASSWORD`
+ - Use the passphrase chosen when creating the GPG key (from `gpg --full-generate-key`).
+ - Store it as the Gradle property `signingPassword` or environment variable `SIGNING_PASSWORD`.
+
+- [ ] `signing.gnupg.keyName` / `SIGNING_GNUPG_KEY_NAME` *(only if using the local GPG executable)*
+ - If you prefer Gradle to call the local `gpg` binary, set this to the key name returned by `gpg --list-secret-keys` (for example `User Name `).
+ - Ensure `signing.gnupg.executable` points to the desired GPG binary and that the key is available in the local keyring or CI agent.
+
+### Using GitHub Actions secrets
+
+When the project builds in GitHub Actions, reference the organization or repository
+secrets as environment variables so Gradle can pick them up automatically. Secrets
+exposed via the workflow `env` section are available to every step; you can also
+scope them to the publish job only. Because the signing key is multi-line, the
+workflow writes the values to `~/.gradle/gradle.properties` before invoking Gradle,
+which ensures the full key material (including embedded newlines or `\n` escape
+sequences) is preserved. Provide the ASCII-armored key directly or a base64-encoded
+copy of that text export; the workflow feeds it to Gradle exactly as provided, and
+the build validates that the payload contains the full private key before enabling
+signing. Invalid or binary private keys are ignored so the publish run continues
+without signing.
+
+```yaml
+jobs:
+ publish:
+ runs-on: ubuntu-latest
+ env:
+ OSSRH_TOKEN_USERNAME: ${{ secrets.OSSRH_TOKEN_USERNAME }}
+ OSSRH_TOKEN_PASSWORD: ${{ secrets.OSSRH_TOKEN_PASSWORD }}
+ SIGNING_KEY: ${{ secrets.SIGNING_KEY }}
+ SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }}
+ SIGNING_KEY_ID: ${{ secrets.SIGNING_KEY_ID }} # optional; only required for gpg-based signing
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-java@v4
+ with:
+ java-version: '21'
+ distribution: 'temurin'
+ - name: Setup Gradle
+ uses: gradle/actions/setup-gradle@v4
+ - name: Configure Gradle publishing credentials
+ run: |
+ mkdir -p "$HOME/.gradle"
+
+ {
+ if [[ -n "${OSSRH_TOKEN_USERNAME}" ]]; then
+ echo "ossrhTokenUsername=${OSSRH_TOKEN_USERNAME}"
+ fi
+ if [[ -n "${OSSRH_TOKEN_PASSWORD}" ]]; then
+ echo "ossrhTokenPassword=${OSSRH_TOKEN_PASSWORD}"
+ fi
+ if [[ -n "${SIGNING_PASSWORD}" ]]; then
+ echo "signingPassword=${SIGNING_PASSWORD}"
+ fi
+ if [[ -n "${SIGNING_KEY}" ]]; then
+ echo "signingKey=${SIGNING_KEY}"
+ fi
+ # Optionally surface SIGNING_KEY_ID for workflows that delegate signing to gpg.
+ if [[ -n "${SIGNING_KEY_ID}" ]]; then
+ echo "signing.keyId=${SIGNING_KEY_ID}"
+ fi
+ } > "$HOME/.gradle/gradle.properties"
+
+ chmod 600 "$HOME/.gradle/gradle.properties"
+ - name: Publish artifacts
+ run: ./gradlew publish
+ - name: Clean up Gradle credentials
+ if: always()
+ run: rm -f "$HOME/.gradle/gradle.properties"
+```
+
+Gradle reads the variables listed above (and their legacy fallbacks) directly from the
+environment, so no extra configuration is required. If you prefer Gradle properties
+instead, write the secrets to `~/.gradle/gradle.properties` in a preceding workflow
+step and delete the file afterwards to avoid leaking credentials in later jobs. The
+example above performs these steps automatically.
+
+### Running a release test
+
+Use the **Gradle Package** workflow to perform a dry-run of the release
+pipeline before cutting an actual GitHub release:
+
+1. Open **Actions → Gradle Package** in the repository UI and click
+ **Run workflow**.
+2. Pick the branch or tag that should be exercised from the **Branch**
+ drop-down. The release pipeline promotes builds from the `main`
+ branch, which is selected by default for manual runs. Choose a
+ different branch only if you need to validate work-in-progress
+ changes before merging them into `main`.
+3. Leave **Publish artifacts to Sonatype OSSRH and the Gradle Plugin Portal**
+ disabled to run a local verification. The workflow will build the
+ project, resolve signing, and skip the remote publish steps.
+4. When you are satisfied with the dry-run, create a GitHub release from the
+ branch that should ship (typically `main`) and the workflow will execute
+ again with publishing enabled.
+
+This mirrors the branch picker shown in the GitHub release dialog and makes it
+easy to verify release changes from any branch before publishing.
diff --git a/build.gradle.kts b/build.gradle.kts
index 6067fd7..80ba66c 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -1,14 +1,21 @@
plugins {
id("com.gradle.plugin-publish") version "1.3.1"
+ `maven-publish`
+ signing
}
-group = "com.eurofunk.gradle"
+import java.nio.charset.StandardCharsets
+import java.util.Base64
+
+group = "io.github.eurofunk"
version = "0.0.1"
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(21))
}
+ withSourcesJar()
+ withJavadocJar()
}
repositories {
@@ -40,22 +47,152 @@ gradlePlugin {
}
}
+val ossrhTokenUsername = (findProperty("ossrhTokenUsername") as String?)
+ ?: System.getenv("OSSRH_TOKEN_USERNAME")
+val ossrhTokenPassword = (findProperty("ossrhTokenPassword") as String?)
+ ?: System.getenv("OSSRH_TOKEN_PASSWORD")
+val ossrhUsername = (findProperty("ossrhUsername") as String?)
+ ?: System.getenv("OSSRH_USERNAME")
+val ossrhPassword = (findProperty("ossrhPassword") as String?)
+ ?: System.getenv("OSSRH_PASSWORD")
+
+val resolvedOssrhUsername = ossrhTokenUsername ?: ossrhUsername
+val resolvedOssrhPassword = ossrhTokenPassword ?: ossrhPassword
+val hasOssrhCredentials =
+ !resolvedOssrhUsername.isNullOrBlank() && !resolvedOssrhPassword.isNullOrBlank()
+
publishing {
+ publications {
+ create("mavenJava") {
+ from(components["java"])
+
+ pom {
+ name.set("SBOM License Plugin")
+ description.set("Gradle plugin for validating dependency licenses using SBOM metadata.")
+ url.set("https://github.com/eurofunk/sbom-license-plugin")
+ licenses {
+ license {
+ name.set("Apache License, Version 2.0")
+ url.set("https://www.apache.org/licenses/LICENSE-2.0")
+ }
+ }
+ developers {
+ developer {
+ id.set("eurofunk")
+ name.set("eurofunk Kappacher GmbH")
+ email.set("opensource@eurofunk.com")
+ organization.set("eurofunk Kappacher GmbH")
+ organizationUrl.set("https://www.eurofunk.com")
+ }
+ }
+ scm {
+ connection.set("scm:git:https://github.com/eurofunk/sbom-license-plugin.git")
+ developerConnection.set("scm:git:ssh://git@github.com/eurofunk/sbom-license-plugin.git")
+ url.set("https://github.com/eurofunk/sbom-license-plugin")
+ }
+ }
+ }
+ }
+
repositories {
- mavenLocal()
- repositories {
+ if (hasOssrhCredentials) {
maven {
- name = "GitHubPackages"
- url = uri("https://maven.pkg.github.com/eurofunk/sbom-license-plugin")
+ name = "Sonatype"
+ val isSnapshot = version.toString().endsWith("SNAPSHOT")
+ val releasesRepositoryUrl =
+ (findProperty("ossrhReleasesUrl") as String?)
+ ?: "https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/"
+ val snapshotsRepositoryUrl =
+ (findProperty("ossrhSnapshotsUrl") as String?)
+ ?: "https://s01.oss.sonatype.org/content/repositories/snapshots/"
+
+ url = uri(if (isSnapshot) snapshotsRepositoryUrl else releasesRepositoryUrl)
+
credentials {
- username = project.findProperty("gpr.user") as String? ?: System.getenv("USERNAME")
- password = project.findProperty("gpr.key") as String? ?: System.getenv("TOKEN")
+ username = resolvedOssrhUsername
+ password = resolvedOssrhPassword
}
}
+ } else {
+ logger.warn("Skipping Sonatype repository configuration because no OSSRH credentials were found.")
}
}
}
+signing {
+ fun normalizeSigningKey(rawKey: String?): String? {
+ if (rawKey.isNullOrBlank()) {
+ return null
+ }
+
+ val trimmed = rawKey.replace("\\n", "\n").trim()
+ val header = "-----BEGIN PGP PRIVATE KEY BLOCK-----"
+ val footer = "-----END PGP PRIVATE KEY BLOCK-----"
+
+ if (trimmed.contains(header) && trimmed.contains(footer)) {
+ return trimmed
+ }
+
+ val base64Payload = trimmed.filterNot { it.isWhitespace() }
+ val decoded = runCatching { Base64.getDecoder().decode(base64Payload) }
+ .map { String(it, StandardCharsets.UTF_8).replace("\\n", "\n").trim() }
+ .getOrNull()
+
+ if (decoded != null && decoded.contains(header) && decoded.contains(footer)) {
+ return decoded
+ }
+
+ logger.warn(
+ "Skipping in-memory signing key because it does not contain a complete ASCII-armored PGP PRIVATE KEY block."
+ )
+
+ return null
+ }
+
+ val signingKeyId = ((findProperty("signingKeyId") as String?)
+ ?: (findProperty("signing.keyId") as String?)
+ ?: System.getenv("SIGNING_KEY_ID")
+ ?: System.getenv("SIGNING_KEYID"))?.trim()?.takeIf { it.isNotEmpty() }
+ val signingPassword = (findProperty("signingPassword") as String?)
+ ?: (findProperty("signing.password") as String?)
+ ?: System.getenv("SIGNING_PASSWORD")
+ ?: System.getenv("SIGNING_PASSPHRASE")
+ val signingKey = normalizeSigningKey(
+ (findProperty("signingKey") as String?)
+ ?: (findProperty("signing.key") as String?)
+ ?: (findProperty("signingKeyBase64") as String?)
+ ?: (findProperty("signing.keyBase64") as String?)
+ ?: System.getenv("SIGNING_KEY")
+ ?: System.getenv("SIGNING_KEY_BASE64")
+ )
+ val gpgKeyConfigured = project.hasProperty("signing.gnupg.keyName") ||
+ project.hasProperty("signing.gnupg.executable") ||
+ project.hasProperty("signing.gnupg.homeDir") ||
+ !((findProperty("signing.gnupg.keyName") as String?) ?: System.getenv("SIGNING_GNUPG_KEY_NAME")).isNullOrBlank() ||
+ System.getenv("SIGNING_GNUPG_EXECUTABLE") != null ||
+ System.getenv("SIGNING_GNUPG_HOME_DIR") != null
+
+ val hasInMemoryKeys = !signingKey.isNullOrBlank() && !signingPassword.isNullOrBlank()
+
+ if (hasInMemoryKeys) {
+ if (!signingKeyId.isNullOrBlank()) {
+ useInMemoryPgpKeys(signingKeyId, signingKey!!, signingPassword!!)
+ } else {
+ useInMemoryPgpKeys(signingKey!!, signingPassword!!)
+ }
+
+ isRequired = true
+ sign(publishing.publications["mavenJava"])
+ } else if (gpgKeyConfigured) {
+ useGpgCmd()
+ isRequired = true
+ sign(publishing.publications["mavenJava"])
+ } else {
+ isRequired = false
+ logger.warn("Skipping artifact signing because no signing credentials were provided.")
+ }
+}
+
// Add a source set for the functional test suite
val functionalTestSourceSet = sourceSets.create("functionalTest") {
diff --git a/gradle.properties b/gradle.properties
index 377538c..114680d 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,5 +1,3 @@
# This file was generated by the Gradle 'init' task.
# https://docs.gradle.org/current/userguide/build_environment.html#sec:gradle_configuration_properties
-org.gradle.configuration-cache=true
-
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 5f5f17e..799891f 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -1 +1,8 @@
+pluginManagement {
+ repositories {
+ gradlePluginPortal()
+ mavenCentral()
+ }
+}
+
rootProject.name = "sbom-license-plugin"