diff --git a/.github/actions/get-prerelease/action.yml b/.github/actions/get-prerelease/action.yml new file mode 100644 index 0000000..ad4eca3 --- /dev/null +++ b/.github/actions/get-prerelease/action.yml @@ -0,0 +1,28 @@ +name: Return a boolean indicating if the version contains prerelease identifiers + +# +# Returns a simple true/false boolean indicating whether the version indicates it's a prerelease or not. +# + +inputs: + version: + required: true + +outputs: + prerelease: + value: ${{ steps.get_prerelease.outputs.PRERELEASE }} + +runs: + using: composite + + steps: + - id: get_prerelease + shell: bash + run: | + if [[ "${VERSION}" == *"beta"* || "${VERSION}" == *"alpha"* ]]; then + echo "PRERELEASE=true" >> $GITHUB_OUTPUT + else + echo "PRERELEASE=false" >> $GITHUB_OUTPUT + fi + env: + VERSION: ${{ inputs.version }} diff --git a/.github/actions/get-release-notes/action.yml b/.github/actions/get-release-notes/action.yml new file mode 100644 index 0000000..2e7e12c --- /dev/null +++ b/.github/actions/get-release-notes/action.yml @@ -0,0 +1,40 @@ +name: Return the release notes extracted from the body of the PR associated with the release. + +# +# Returns the release notes from the content of a pull request linked to a release branch. It expects the branch name to be in the format release/vX.Y.Z, release/X.Y.Z, release/vX.Y.Z-beta.N. etc. +# +inputs: + version: + required: true + repo_name: + required: false + repo_owner: + required: true + token: + required: true + +outputs: + release-notes: + value: ${{ steps.get_release_notes.outputs.RELEASE_NOTES }} + +runs: + using: composite + + steps: + - uses: actions/github-script@v7 + id: get_release_notes + with: + result-encoding: string + script: | + const { data: pulls } = await github.rest.pulls.list({ + owner: process.env.REPO_OWNER, + repo: process.env.REPO_NAME, + state: 'all', + head: `${process.env.REPO_OWNER}:release/${process.env.VERSION}`, + }); + core.setOutput('RELEASE_NOTES', pulls[0].body); + env: + GITHUB_TOKEN: ${{ inputs.token }} + REPO_OWNER: ${{ inputs.repo_owner }} + REPO_NAME: ${{ inputs.repo_name }} + VERSION: ${{ inputs.version }} diff --git a/.github/actions/get-version/action.yml b/.github/actions/get-version/action.yml new file mode 100644 index 0000000..c739657 --- /dev/null +++ b/.github/actions/get-version/action.yml @@ -0,0 +1,19 @@ +name: Return the version extracted from the branch name + +# +# Returns the version from the .version file. +# + +outputs: + version: + value: ${{ steps.get_version.outputs.VERSION }} + +runs: + using: composite + + steps: + - id: get_version + shell: bash + run: | + VERSION=$(head -1 .version) + echo "VERSION=${VERSION}" >> $GITHUB_OUTPUT diff --git a/.github/actions/maven-publish/action.yml b/.github/actions/maven-publish/action.yml new file mode 100644 index 0000000..20b8e76 --- /dev/null +++ b/.github/actions/maven-publish/action.yml @@ -0,0 +1,40 @@ +name: Publish release to Java + +inputs: + java-version: + required: true + ossr-username: + required: true + ossr-token: + required: true + signing-key: + required: true + signing-password: + required: true + + +runs: + using: composite + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Java + uses: actions/setup-java@c1e323688fd81a25caa38c78aa6df2d33d3e20d9 # pin@v4 + with: + distribution: 'temurin' + java-version: ${{ inputs.java-version }} + cache: 'gradle' + + - name: Set up Gradle + uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # pin@v5 + + - name: Publish Android/Java Packages to Maven + shell: bash + run: ./gradlew publishToSonatype closeSonatypeStagingRepository -PisSnapshot=false --stacktrace + env: + MAVEN_USERNAME: ${{ inputs.ossr-username }} + MAVEN_PASSWORD: ${{ inputs.ossr-token }} + SIGNING_KEY: ${{ inputs.signing-key}} + SIGNING_PASSWORD: ${{ inputs.signing-password}} diff --git a/.github/actions/release-create/action.yml b/.github/actions/release-create/action.yml new file mode 100644 index 0000000..ef5c886 --- /dev/null +++ b/.github/actions/release-create/action.yml @@ -0,0 +1,45 @@ +name: Create a GitHub release + +# +# Creates a GitHub release with the given version. +# + +inputs: + token: + required: true + files: + required: false + name: + required: true + body: + required: true + tag: + required: true + commit: + required: true + draft: + default: false + required: false + prerelease: + default: false + required: false + fail_on_unmatched_files: + default: true + required: false + +runs: + using: composite + + steps: + - uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 + with: + body: ${{ inputs.body }} + name: ${{ inputs.name }} + tag_name: ${{ inputs.tag }} + target_commitish: ${{ inputs.commit }} + draft: ${{ inputs.draft }} + prerelease: ${{ inputs.prerelease }} + fail_on_unmatched_files: ${{ inputs.fail_on_unmatched_files }} + files: ${{ inputs.files }} + env: + GITHUB_TOKEN: ${{ inputs.token }} diff --git a/.github/actions/tag-exists/action.yml b/.github/actions/tag-exists/action.yml new file mode 100644 index 0000000..57b410a --- /dev/null +++ b/.github/actions/tag-exists/action.yml @@ -0,0 +1,34 @@ +name: Return a boolean indicating if a tag already exists for the repository + +# +# Returns a simple true/false boolean indicating whether the tag exists or not. +# + +inputs: + token: + required: true + tag: + required: true + +outputs: + exists: + description: 'Whether the tag exists or not' + value: ${{ steps.tag-exists.outputs.EXISTS }} + +runs: + using: composite + + steps: + - id: tag-exists + shell: bash + run: | + GET_API_URL="https://api.github.com/repos/${GITHUB_REPOSITORY}/git/ref/tags/${TAG_NAME}" + http_status_code=$(curl -LI $GET_API_URL -o /dev/null -w '%{http_code}\n' -s -H "Authorization: token ${GITHUB_TOKEN}") + if [ "$http_status_code" -ne "404" ] ; then + echo "EXISTS=true" >> $GITHUB_OUTPUT + else + echo "EXISTS=false" >> $GITHUB_OUTPUT + fi + env: + TAG_NAME: ${{ inputs.tag }} + GITHUB_TOKEN: ${{ inputs.token }} diff --git a/.github/workflows/java-release.yml b/.github/workflows/java-release.yml new file mode 100644 index 0000000..7cc9e31 --- /dev/null +++ b/.github/workflows/java-release.yml @@ -0,0 +1,81 @@ +name: Create Java and GitHub Release + +on: + workflow_call: + inputs: + java-version: + required: true + type: string + + secrets: + ossr-username: + required: true + ossr-token: + required: true + signing-key: + required: true + signing-password: + required: true + github-token: + required: true + +jobs: + release: + if: github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && github.event.pull_request.merged && startsWith(github.event.pull_request.head.ref, 'release/')) + runs-on: ubuntu-latest + environment: release + + steps: + # Checkout the code + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + # Get the version from the branch name + - id: get_version + uses: ./.github/actions/get-version + + # Get the prerelease flag from the branch name + - id: get_prerelease + uses: ./.github/actions/get-prerelease + with: + version: ${{ steps.get_version.outputs.version }} + + # Get the release notes + - id: get_release_notes + uses: ./.github/actions/get-release-notes + with: + token: ${{ secrets.github-token }} + version: ${{ steps.get_version.outputs.version }} + repo_owner: ${{ github.repository_owner }} + repo_name: ${{ github.event.repository.name }} + + # Check if the tag already exists + - id: tag_exists + uses: ./.github/actions/tag-exists + with: + tag: ${{ steps.get_version.outputs.version }} + token: ${{ secrets.github-token }} + + # If the tag already exists, exit with an error + - if: steps.tag_exists.outputs.exists == 'true' + run: exit 1 + + # Publish the release to Maven + - uses: ./.github/actions/maven-publish + with: + java-version: ${{ inputs.java-version }} + ossr-username: ${{ secrets.OSSR_USERNAME }} + ossr-token: ${{ secrets.OSSR_TOKEN }} + signing-key: ${{ secrets.SIGNING_KEY }} + signing-password: ${{ secrets.SIGNING_PASSWORD }} + + # Create a release for the tag + - uses: ./.github/actions/release-create + with: + token: ${{ secrets.github-token }} + name: ${{ steps.get_version.outputs.version }} + body: ${{ steps.get_release_notes.outputs.release-notes }} + tag: ${{ steps.get_version.outputs.version }} + commit: ${{ github.sha }} + prerelease: ${{ steps.get_prerelease.outputs.prerelease }} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml deleted file mode 100644 index dbf2b08..0000000 --- a/.github/workflows/publish.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: Publish Release - -on: - workflow_dispatch: - inputs: - branch: - description: The branch to release from. - required: true - default: master - -permissions: - contents: read - -jobs: - publish: - name: Publish to Maven Central - runs-on: ubuntu-latest - environment: release - - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - ref: ${{ github.event.inputs.branch }} - - - uses: ./.github/actions/setup - - - run: ./gradlew clean assemble -PisSnapshot=false - - - run: ./gradlew exportVersion -PisSnapshot=false - - - env: - OSSR_USERNAME: ${{ secrets.OSSR_USERNAME }} - OSSR_PASSWORD: ${{ secrets.OSSR_PASSWORD }} - SIGNING_KEY: ${{ secrets.SIGNING_KEY }} - SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }} - run: ./gradlew publishAndroidLibraryPublicationToMavenRepository -PossrhUsername="$OSSR_USERNAME" -PossrhPassword="$OSSR_PASSWORD" -PsigningKey="$SIGNING_KEY" -PsigningPassword="$SIGNING_PASSWORD" -PisSnapshot=false diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..fd3961c --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,23 @@ +name: Create GitHub Release + +on: + pull_request: + types: + - closed + workflow_dispatch: + +permissions: + id-token: write + contents: write + +jobs: + release: + uses: ./.github/workflows/java-release.yml + with: + java-version: '11' + secrets: + ossr-username: ${{ secrets.OSSR_USERNAME }} + ossr-token: ${{ secrets.OSSR_TOKEN }} + signing-key: ${{ secrets.SIGNING_KEY }} + signing-password: ${{ secrets.SIGNING_PASSWORD }} + github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.shiprc b/.shiprc new file mode 100644 index 0000000..287226b --- /dev/null +++ b/.shiprc @@ -0,0 +1,8 @@ +{ + "files": { + "lib/build.gradle": [], + ".version": [], + "README.md": [] + }, + "prefixVersion": false +} diff --git a/.version b/.version new file mode 100644 index 0000000..e9307ca --- /dev/null +++ b/.version @@ -0,0 +1 @@ +2.0.2 diff --git a/build.gradle b/build.gradle index 1764108..4d3f5b0 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,6 @@ buildscript { repositories { google() mavenCentral() - jcenter() } dependencies { @@ -12,6 +11,19 @@ buildscript { plugins { id 'lifecycle-base' + id 'io.github.gradle-nexus.publish-plugin' version '2.0.0' +} + +apply plugin: 'io.github.gradle-nexus.publish-plugin' + +nexusPublishing { + repositories { + sonatype { + nexusUrl.set(uri('https://ossrh-staging-api.central.sonatype.com/service/local/')) + username.set(System.getenv("MAVEN_USERNAME")) + password.set(System.getenv("MAVEN_PASSWORD")) + } + } } allprojects { @@ -20,7 +32,6 @@ allprojects { repositories { google() mavenCentral() - jcenter() } } diff --git a/gradle.properties b/gradle.properties index c7fb972..ecbf843 100644 --- a/gradle.properties +++ b/gradle.properties @@ -16,4 +16,25 @@ org.gradle.jvmargs=-Xmx1536m # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true -android.useAndroidX=true \ No newline at end of file +android.useAndroidX=true + +# Maven publishing / POM metadata +GROUP=com.auth0.android +POM_ARTIFACT_ID=jwtdecode + +POM_NAME=JWTDecode.Android +POM_DESCRIPTION=JWT Decoding library for Android +POM_PACKAGING=aar + +POM_URL=https://github.com/auth0/JWTDecode.Android +POM_SCM_URL=https://github.com/auth0/JWTDecode.Android +POM_SCM_CONNECTION=scm:git@github.com:auth0/JWTDecode.Android.git +POM_SCM_DEV_CONNECTION=scm:git@github.com:auth0/JWTDecode.Android.git + +POM_LICENCE_NAME=The MIT License (MIT) +POM_LICENCE_URL=https://raw.githubusercontent.com/auth0/JWTDecode.Android/master/LICENSE +POM_LICENCE_DIST=repo + +POM_DEVELOPER_ID=auth0 +POM_DEVELOPER_NAME=Auth0 +POM_DEVELOPER_EMAIL=oss@auth0.com \ No newline at end of file diff --git a/gradle/jacoco.gradle b/gradle/jacoco.gradle new file mode 100644 index 0000000..1fbe767 --- /dev/null +++ b/gradle/jacoco.gradle @@ -0,0 +1,50 @@ +apply plugin: 'jacoco' + +android { + testOptions { + unitTests.all { + jacoco { + includeNoLocationClasses = true + jacoco.excludes = ['jdk.internal.*'] + } + } + } +} + +afterEvaluate { + def jacocoTestReportTask = tasks.findByName("jacocoTestReport") + if (!jacocoTestReportTask) { + jacocoTestReportTask = tasks.create("jacocoTestReport") + jacocoTestReportTask.group = "Reporting" + jacocoTestReportTask.description = "Generate Jacoco coverage reports for all builds." + } + + android.libraryVariants.all { variant -> + def name = variant.name + def testTaskName = "test${name.capitalize()}UnitTest" + + def reportTask = tasks.create(name: "jacocoTest${name.capitalize()}UnitTestReport", type: JacocoReport, dependsOn: testTaskName) { + group = "Reporting" + description = "Generate Jacoco coverage reports for the ${name.capitalize()} build." + + classDirectories.from = fileTree( + dir: "${buildDir}/intermediates/javac/${name}", + excludes: ['**/R.class', + '**/R$*.class', + '**/*$ViewInjector*.*', + '**/*$ViewBinder*.*', + '**/BuildConfig.*', + '**/Manifest*.*'] + ) + + sourceDirectories.from = ['src/main/java'].plus(android.sourceSets[name].java.srcDirs) + executionData.from = "${buildDir}/jacoco/${testTaskName}.exec" + + reports { + xml.required = true + html.required = true + } + } + jacocoTestReportTask.dependsOn reportTask + } +} diff --git a/gradle/maven-publish.gradle b/gradle/maven-publish.gradle new file mode 100644 index 0000000..035fa94 --- /dev/null +++ b/gradle/maven-publish.gradle @@ -0,0 +1,103 @@ +apply plugin: 'maven-publish' +apply plugin: 'signing' + +apply from: rootProject.file('gradle/versioning.gradle') + +task sourcesJar(type: Jar) { + archiveClassifier = 'sources' + from android.sourceSets.main.java.srcDirs +} + +task javadoc(type: Javadoc) { + source = android.sourceSets.main.java.srcDirs + classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) + android.libraryVariants.all { variant -> + if (variant.name == 'release') { + if (variant.hasProperty('javaCompileProvider')) { + owner.classpath += variant.javaCompileProvider.get().classpath + } else { + owner.classpath += variant.javaCompile.classpath + } + } + } + exclude '**/BuildConfig.java' + exclude '**/R.java' + failOnError false +} + +task javadocJar(type: Jar, dependsOn: javadoc) { + archiveClassifier = 'javadoc' + from javadoc.destinationDir +} + +publishing { + publications { + release(MavenPublication) { + groupId = GROUP + artifactId = POM_ARTIFACT_ID + version = getVersionName() + + artifact("$buildDir/outputs/aar/${project.getName()}-release.aar") + artifact sourcesJar + artifact javadocJar + + pom { + name = POM_NAME + packaging = POM_PACKAGING + description = POM_DESCRIPTION + url = POM_URL + + licenses { + license { + name = POM_LICENCE_NAME + url = POM_LICENCE_URL + distribution = POM_LICENCE_DIST + } + } + + developers { + developer { + id = POM_DEVELOPER_ID + name = POM_DEVELOPER_NAME + email = POM_DEVELOPER_EMAIL + } + } + + scm { + url = POM_SCM_URL + connection = POM_SCM_CONNECTION + developerConnection = POM_SCM_DEV_CONNECTION + } + + // Replace this with components.release after we update the Android Gradle Plugin version + withXml { + def dependenciesNode = asNode().appendNode('dependencies') + + project.configurations.implementation.allDependencies.each { + if (it.group == null || it.version == null || it.name == null || it.name == "unspecified") { + return + } + def dependencyNode = dependenciesNode.appendNode('dependency') + dependencyNode.appendNode('groupId', it.group) + dependencyNode.appendNode('artifactId', it.name) + dependencyNode.appendNode('version', it.version) + } + } + } + } + } +} + +signing { + def signingKey = System.getenv("SIGNING_KEY") + def signingPassword = System.getenv("SIGNING_PASSWORD") + useInMemoryPgpKeys(signingKey, signingPassword) + sign publishing.publications +} + +publish.dependsOn build + +// Ensure the release AAR exists before the publication is signed. +tasks.matching { it.name == 'signReleasePublication' }.configureEach { + dependsOn tasks.matching { it.name == 'assembleRelease' || it.name == 'bundleReleaseAar' } +} diff --git a/gradle/versioning.gradle b/gradle/versioning.gradle new file mode 100644 index 0000000..a9e0d06 --- /dev/null +++ b/gradle/versioning.gradle @@ -0,0 +1,19 @@ +def getVersionFromFile() { + def versionFile = rootProject.file('.version') + return versionFile.text.readLines().first().trim() +} + +def isSnapshot() { + // Use project.property(...) explicitly: a bare `isSnapshot` resolves to this + // method (same name), not the -PisSnapshot project property. + return hasProperty('isSnapshot') ? project.property('isSnapshot').toBoolean() : true +} + +def getVersionName() { + return isSnapshot() ? project.version+"-SNAPSHOT" : project.version +} + +ext { + getVersionName = this.&getVersionName + getVersionFromFile = this.&getVersionFromFile +} diff --git a/lib/build.gradle b/lib/build.gradle index 27866d3..1bc3513 100644 --- a/lib/build.gradle +++ b/lib/build.gradle @@ -1,26 +1,12 @@ plugins { - id "com.auth0.gradle.oss-library.android" version "0.17.1" + id 'com.android.library' } -logger.lifecycle("Using version ${version} for ${name}") +apply from: rootProject.file('gradle/versioning.gradle') -oss { - name 'jwtdecode' - repository 'jwtdecode.android' - organization 'auth0' - description 'JWT Decoding library for Android' - - developers { - auth0 { - displayName = 'Auth0' - email = 'oss@auth0.com' - } - lbalmaceda { - displayName = 'Luciano Balmaceda' - email = 'luciano.balmaceda@auth0.com' - } - } -} +version = getVersionFromFile() + +logger.lifecycle("Using version ${version} for ${name}") android { compileSdkVersion 29 @@ -29,7 +15,7 @@ android { minSdkVersion 15 targetSdkVersion 29 versionCode 1 - versionName "1.0" + versionName project.version } } @@ -46,18 +32,5 @@ dependencies { testImplementation 'org.mockito:mockito-core:3.2.4' } -tasks.withType(Test) { - jacoco.includeNoLocationClasses = true - jacoco.excludes = ['jdk.internal.*'] -} - -afterEvaluate { - publishing.publications.all { - pom.withXml { - def licenseNode = asNode().getAt('licenses')[0]?.getAt('license')[0] - if (licenseNode) { - licenseNode.getAt('url')[0].setValue('https://spdx.org/licenses/MIT.html') - } - } - } -} +apply from: rootProject.file('gradle/jacoco.gradle') +apply from: rootProject.file('gradle/maven-publish.gradle')