diff --git a/.github/workflows/javadoc.yaml b/.github/workflows/javadoc.yaml index 0b00df5..2090191 100644 --- a/.github/workflows/javadoc.yaml +++ b/.github/workflows/javadoc.yaml @@ -11,16 +11,18 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-java@v3.11.0 with: - java-version: '8' + fetch-depth: 0 # Required for git-version plugin + - uses: actions/setup-java@v4 + with: + java-version: '11' distribution: 'corretto' - name: Validate Gradle wrapper - uses: gradle/wrapper-validation-action@v1 + uses: gradle/wrapper-validation-action@v2 + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v3 - name: Generate Javadoc - uses: gradle/gradle-build-action@v2 - with: - arguments: javadoc + run: ./gradlew javadoc - name: Check Javadoc generation run: | if [ -d "build/docs/javadoc" ]; then diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 7d1c73d..853f1bd 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -1,4 +1,4 @@ -name: Publish package to GitHub Packages +name: Publish package to GitHub Packages and Maven Central on: release: types: [ created ] @@ -11,17 +11,19 @@ jobs: contents: read packages: write steps: - - uses: actions/checkout@v2 - - uses: actions/setup-java@v3.11.0 + - uses: actions/checkout@v4 with: - java-version: '8' + fetch-depth: 0 # Required for git-version plugin + - uses: actions/setup-java@v4 + with: + java-version: '11' distribution: 'corretto' - name: Validate Gradle wrapper - uses: gradle/wrapper-validation-action@v1 + uses: gradle/wrapper-validation-action@v2 + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v3 - name: Verify Javadoc generation - uses: gradle/gradle-build-action@v2 - with: - arguments: javadoc + run: ./gradlew javadoc - name: Check Javadoc generation run: | if [ -d "build/docs/javadoc" ]; then @@ -30,15 +32,17 @@ jobs: echo "ERROR: Javadoc directory not found" exit 1 fi - - name: Publish package - uses: gradle/gradle-build-action@v2 - with: - arguments: publish publishToSonatype closeAndReleaseSonatypeStagingRepository + - name: Publish to GitHub Packages + run: ./gradlew publish env: + GITHUB_ACTOR: ${{ github.actor }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - OSSRH_USERNAME: ${{ secrets.OSSRH_USERNAME }} - OSSRH_PASSWORD: ${{ secrets.OSSRH_PASSWORD }} - GPG_SIGNING_KEY: ${{ secrets.GPG_SIGNING_KEY }} - GPG_SIGNING_PASSPHRASE: ${{ secrets.GPG_SIGNING_PASSPHRASE }} - NEXUS_TOKEN_USERNAME: ${{ secrets.NEXUS_TOKEN_USERNAME }} - NEXUS_TOKEN_PASSWORD: ${{ secrets.NEXUS_TOKEN_PASSWORD }} + - name: Publish to Maven Central + run: ./gradlew publishAndReleaseToMavenCentral + env: + # Nexus tokens were generated before we migrated to the new Maven Central, + # it's backward compatible. + ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.NEXUS_TOKEN_USERNAME }} + ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.NEXUS_TOKEN_PASSWORD }} + ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.GPG_SIGNING_KEY }} + ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.GPG_SIGNING_PASSPHRASE }} diff --git a/.gitignore b/.gitignore index 9b21127..f9b596c 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ build # Ignore stg schemas stg-schemas/ bin +/key.asc diff --git a/PUBLISHING.md b/PUBLISHING.md new file mode 100644 index 0000000..882e1ca --- /dev/null +++ b/PUBLISHING.md @@ -0,0 +1,183 @@ +# Publishing Guide + +This document describes how to publish the Permit.io Java SDK to Maven Central and GitHub Packages. + +## Overview + +The SDK is published to two repositories: + +- **Maven Central** - Primary distribution for public consumption +- **GitHub Packages** - Secondary distribution for GitHub-based workflows + +## Prerequisites + +### Maven Central Portal Account + +1. Create an account at [central.sonatype.com](https://central.sonatype.com) +2. Verify ownership of the `io.permit` namespace +3. Generate a User Token: Account → Generate User Token + +### GPG Signing Key + +Maven Central requires all artifacts to be signed with GPG. +[For more info see here.](https://central.sonatype.org/publish/requirements/gpg/) + +#### Generate a new key (if you don't have one) + +```bash +gpg --full-generate-key +``` + +When prompted: + +1. **Key type**: Select `1` (RSA and RSA) +2. **Key size**: Enter `4096` +3. **Expiration**: Enter `0` (doesn't expire) or set a reasonable expiration +4. **Name and email**: Use the same email as your Maven Central account +5. **Passphrase**: Set a strong passphrase (this is your `signingInMemoryKeyPassword`) + +#### List your keys + +```bash +gpg --list-secret-keys --keyid-format LONG +``` + +#### Export the private key + +For local use: + +```bash +gpg --armor --export-secret-keys YOUR_KEY_ID > key.asc +``` + +For CI/CD (base64 encoded): + +```bash +gpg --armor --export-secret-keys YOUR_KEY_ID | base64 +``` + +#### Publish your public key (required for Maven Central verification) + +```bash +gpg --keyserver keyserver.ubuntu.com --send-keys YOUR_KEY_ID +# Note: if you get a "no route to host" error, try an alternative keyserver such as keys.openpgp.org or pgp.mit.edu. +``` + +## GitHub Secrets + +Configure these secrets in your GitHub repository: + +| Secret | Description | +|--------------------------|--------------------------------------------------------| +| `MAVEN_CENTRAL_USERNAME` | Username from Central Portal TOKEN (not the user) | +| `MAVEN_CENTRAL_PASSWORD` | Password from Central Portal TOKEN (not user password) | +| `GPG_SIGNING_KEY` | Base64-encoded GPG private key | +| `GPG_SIGNING_PASSPHRASE` | Passphrase for the GPG key | + +## Publishing Methods + +### Automatic (CI/CD) + +Publishing is triggered automatically when: + +- A GitHub Release is created +- The workflow is manually dispatched + +The workflow (`.github/workflows/publish.yaml`) handles: + +1. Javadoc verification +2. Publishing to GitHub Packages +3. Publishing to Maven Central + +### Manual (Local) + +#### Publish to Local Maven Repository + +Test artifact generation without uploading: + +```bash +./gradlew publishToMavenLocal -PskipSigning +``` + +Artifacts are published to `~/.m2/repository/io/permit/permit-sdk-java/` + +Note: Use `-PskipSigning` for local testing without GPG keys. This flag is not available for Maven Central publishing (signing is required). + +#### Publish to Maven Central (Staging Only) + +Upload to Central Portal without releasing: + +```bash +./gradlew publishToMavenCentral \ + -PmavenCentralUsername=TOKEN_USERNAME \ + -PmavenCentralPassword=TOKEN_PASSWORD \ + -PsigningInMemoryKey="$(cat key.asc)" \ + -PsigningInMemoryKeyPassword=KEY_PASSPHRASE +``` + +Review at [Central Portal Deployments](https://central.sonatype.com/publishing/deployments) + +#### Publish and Release to Maven Central + +Full publish with automatic release: + +```bash +./gradlew publishAndReleaseToMavenCentral \ + -PmavenCentralUsername=TOKEN_USERNAME \ + -PmavenCentralPassword=TOKEN_PASSWORD \ + -PsigningInMemoryKey="$(cat key.asc)" \ + -PsigningInMemoryKeyPassword=KEY_PASSPHRASE +``` + +#### Publish to GitHub Packages + +```bash +GITHUB_ACTOR=username GITHUB_TOKEN=token ./gradlew publish +``` + +## Gradle Tasks + +| Task | Description | +|-----------------------------------|----------------------------------------------------------| +| `publishToMavenLocal` | Publish to local Maven cache (~/.m2) | +| `publishToMavenCentral` | Upload to Central Portal (staging) | +| `publishAndReleaseToMavenCentral` | Upload and release to Maven Central | +| `publish` | Publish to all configured repositories (GitHub Packages) | + +## Versioning + +Version is automatically determined by the `com.palantir.git-version` plugin based on git tags: + +- Tagged commit: `2.2.0` +- Commits after tag: `2.2.0-1-gabcdef` +- Dirty working directory: `2.2.0-1-gabcdef.dirty` + +To release a new version: + +```bash +git tag 2.3.0 +git push origin 2.3.0 +``` + +## Troubleshooting + +### 403 Forbidden + +- Credentials may be invalid or expired +- Regenerate token at Central Portal + +### Signature Verification Failed + +- GPG key may be malformed +- Ensure key is base64 encoded without line breaks + +### Version Already Exists + +- Maven Central doesn't allow overwriting versions +- Bump the version and try again + +## References + +- [Maven Central Portal](https://central.sonatype.com) +- [vanniktech/gradle-maven-publish-plugin](https://vanniktech.github.io/gradle-maven-publish-plugin/central/) +- [Sonatype Publishing Guide](https://central.sonatype.org/publish/publish-portal-gradle/) diff --git a/README.md b/README.md index 8547414..aa3039e 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ For [Maven](https://maven.apache.org/) projects, use: io.permit permit-sdk-java - 2.0.0 + 2.2.2 ``` @@ -25,7 +25,7 @@ For [Gradle](https://gradle.org/) projects, configure `permit-sdk-java` as a dep dependencies { // ... - implementation 'io.permit:permit-sdk-java:2.0.0' + implementation 'io.permit:permit-sdk-java:2.2.2' } ``` @@ -149,6 +149,6 @@ CreateOrUpdateResult result = permit.api.users.sync(new UserCreate("[U ## Javadoc reference -To view the javadoc reference, [click here](https://javadoc.io/doc/io.permit/permit-sdk-java/2.0.0/index.html). +To view the javadoc reference, [click here](https://javadoc.io/doc/io.permit/permit-sdk-java/latest/index.html). -It's easiest to start with the root [Permit](https://javadoc.io/static/io.permit/permit-sdk-java/2.0.0/io/permit/sdk/Permit.html) class. +It's easiest to start with the root [Permit](https://javadoc.io/doc/io.permit/permit-sdk-java/latest/io/permit/sdk/Permit.html) class. diff --git a/build.gradle b/build.gradle index 8d15bdb..917ee97 100644 --- a/build.gradle +++ b/build.gradle @@ -9,20 +9,19 @@ plugins { // Apply the java-library plugin for API and implementation separation. id 'java-library' - // Maven publish plugins helps us to publish our library to maven repos - id 'maven-publish' // the signing plugin helps us to crypto-sign on our package (PGP key) id 'signing' // the git-version plugin helps us to publish an auto version (taken from git tags) id 'com.palantir.git-version' version '0.13.0' - // auto release to maven central (skip sonatype manual nexus release process) - id("io.github.gradle-nexus.publish-plugin") version "1.1.0" + // Maven Central Portal publishing (community plugin, recommended by Sonatype) + // See: https://vanniktech.github.io/gradle-maven-publish-plugin/central/ + id 'com.vanniktech.maven.publish' version '0.28.0' // translate json schemas to java classes // id "org.jsonschema2pojo" version "1.1.3" } // It is important to set the group and the version to the root project -// so the nexus-publish plugin can detect if it is a snapshot version +// so the maven-publish plugin can detect if it is a snapshot version // or not in order to select the correct repository where artifacts will // be published group = 'io.permit' @@ -36,12 +35,9 @@ repositories { java { toolchain { - languageVersion = JavaLanguageVersion.of(8) + languageVersion = JavaLanguageVersion.of(8) } - // sources are required by maven central in order to accept the package - withSourcesJar() - // javadoc jar is required by maven central in order to accept the package - withJavadocJar() + // Note: sources and javadoc JARs are created by the vanniktech maven-publish plugin } // package dependencies @@ -69,8 +65,6 @@ dependencies { implementation 'ch.qos.logback:logback-core:1.4.14' implementation 'org.slf4j:slf4j-api:1.7.33' - - // Use JUnit Jupiter for testing. testImplementation 'org.junit.jupiter:junit-jupiter:5.7.2' @@ -81,145 +75,66 @@ dependencies { } -//jsonSchema2Pojo { -// // Location of the JSON Schema file(s). This may refer to a single file or a directory of files. -// source = files("schemas/") -// -// // Target directory for generated Java source files. The plugin will add this directory to the -// // java source set so the compiler will find and compile the newly generated source files. -// targetDirectory = file("src/main/java") -// -// // Package name used for generated Java classes (for types where a fully qualified name has not -// // been supplied in the schema using the 'javaType' property). -// targetPackage = 'io.permit.sdk.openapi.models' -// -// // Whether to allow 'additional' properties to be supported in classes by adding a map to -// // hold these. This is true by default, meaning that the schema rule 'additionalProperties' -// // controls whether the map is added. Set this to false to globally disable additional properties. -// includeAdditionalProperties = false -// -// // Whether to include a javax.annotation.Generated (Java 8 and lower) or -// // javax.annotation.processing.Generated (Java 9+) in on generated types (default true). -// // See also: targetVersion. -// includeGeneratedAnnotation = true -// -// // Whether to use the 'title' property of the schema to decide the class name (if not -// // set to true, the filename and property names are used). -// useTitleAsClassname = true -// -// // Whether to empty the target directory before generation occurs, to clear out all source files -// // that have been generated previously. Be warned, when activated this option -// // will cause jsonschema2pojo to indiscriminately delete the entire contents of the target -// // directory (all files and folders) before it begins generating sources. -// removeOldOutput = false -// -// // Whether to generate builder-style methods of the form withXxx(value) (that return this), -// // alongside the standard, void-return setters. -// generateBuilders = true -// -// // If set to true, then the gang of four builder pattern will be used to generate builders on -// // generated classes. Note: This property works in collaboration with generateBuilders. -// // If generateBuilders is false then this property will not do anything. -// useInnerClassBuilders = false -// -// // Whether to include hashCode and equals methods in generated Java types. -// includeHashcodeAndEquals = false -// -// // Whether to include a toString method in generated Java types. -// includeToString = false -// -// // Whether to include getters or to omit these accessor methods and create public fields instead. -// includeGetters = false -// -// // Whether to include setters or to omit these accessor methods and create public fields instead. -// includeSetters = false -// -// // Whether to use java.util.Optional for getters on properties that are not required -// useOptionalForGetters = true -// -// // Whether to generate constructors or not. -// includeConstructors = true -// -// // Whether to include only 'required' fields in generated constructors -// constructorsRequiredPropertiesOnly = true -// -// annotationStyle = 'gson' -// -// // Whether to initialize Set and List fields as empty collections, or leave them as null. -// initializeCollections = false -//} +// Maven Central Portal publishing configuration +// See: https://vanniktech.github.io/gradle-maven-publish-plugin/central/ +mavenPublishing { + // Publish to Maven Central Portal + publishToMavenCentral(com.vanniktech.maven.publish.SonatypeHost.CENTRAL_PORTAL) -publishing { - repositories { - maven { - name = "OSSRH" - url = "https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/" - credentials { - username = System.getenv("OSSRH_USERNAME") - password = System.getenv("OSSRH_PASSWORD") + // Sign all publications with GPG (required for Maven Central) + // For local testing without GPG, use: ./gradlew publishToMavenLocal -PskipSigning + if (!project.hasProperty('skipSigning')) { + signAllPublications() + } + + // Artifact coordinates + coordinates("io.permit", "permit-sdk-java", version.toString()) + + // POM configuration required by Maven Central + pom { + name = "Permit.io Java SDK" + description = "Java SDK for Permit.io: fullstack permissions for cloud native applications" + url = "https://permit.io" + inceptionYear = "2021" + + licenses { + license { + name = "The Apache License, Version 2.0" + url = "http://www.apache.org/licenses/LICENSE-2.0.txt" + distribution = "repo" } } - maven { - name = "GitHubPackages" - url = "https://maven.pkg.github.com/permitio/permit-java" - credentials { - username = System.getenv("GITHUB_ACTOR") - password = System.getenv("GITHUB_TOKEN") + + developers { + developer { + id = "permit-io" + name = "Permit Team" + email = "support@permit.io" } } - } - - publications { - maven(MavenPublication) { - groupId = 'io.permit' - artifactId = 'permit-sdk-java' - - from components.java - - pom { - name = "Permit.io Java SDK" - description = 'Java SDK for Permit.io: fullstack permissions for cloud native applications' - url = 'https://permit.io' - licenses { - license { - name = 'The Apache License, Version 2.0' - url = 'http://www.apache.org/licenses/LICENSE-2.0.txt' - } - } - developers { - developer { - id = 'asafc' - name = 'Asaf Cohen' - email = 'asaf@permit.io' - } - } - scm { - url = 'https://github.com/permitio/permit-java' - } - } + scm { + connection = "scm:git:git://github.com/permitio/permit-java.git" + developerConnection = "scm:git:ssh://github.com/permitio/permit-java.git" + url = "https://github.com/permitio/permit-java" } } } -nexusPublishing { +// GitHub Packages publishing (separate from Maven Central) +publishing { repositories { - sonatype { - nexusUrl.set(uri("https://s01.oss.sonatype.org/service/local/")) - snapshotRepositoryUrl.set(uri("https://s01.oss.sonatype.org/content/repositories/snapshots/")) - username = System.getenv("NEXUS_TOKEN_USERNAME") - password = System.getenv("NEXUS_TOKEN_PASSWORD") + maven { + name = "GitHubPackages" + url = "https://maven.pkg.github.com/permitio/permit-java" + credentials { + username = System.getenv("GITHUB_ACTOR") + password = System.getenv("GITHUB_TOKEN") + } } } } -signing { - def GPG_SIGNING_KEY = findProperty("signingKey") ?: System.getenv("GPG_SIGNING_KEY") - def GPG_SIGNING_PASSPHRASE = findProperty("signingPassword") ?: System.getenv("GPG_SIGNING_PASSPHRASE") - useInMemoryPgpKeys(GPG_SIGNING_KEY, GPG_SIGNING_PASSPHRASE) - sign publishing.publications.maven -} - tasks.named('test') { // Use JUnit Platform for unit tests. useJUnitPlatform() @@ -232,7 +147,12 @@ tasks.named('javadoc') { tasks.named('jar') { manifest { - attributes('Implementation-Title': project.name, - 'Implementation-Version': project.version) + attributes('Implementation-Title': project.name, 'Implementation-Version': project.version) } -} \ No newline at end of file +} + +// Fix Gradle 8.x task dependency validation issue with vanniktech plugin +// The plainJavadocJar task must run before generateMetadataFileForMavenPublication +tasks.withType(GenerateModuleMetadata).configureEach { + dependsOn tasks.matching { it.name == 'plainJavadocJar' } +} diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 2e6e589..a595206 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists