From fbcc7cf4e4b52c22eb17dd27cdc7f34f2a6d9fbe Mon Sep 17 00:00:00 2001 From: Alexander DiBenedetto Date: Mon, 2 Nov 2026 14:00:57 -0700 Subject: [PATCH 1/5] Initial commit of a release pipeline and CI updates. --- .github/workflows/ci.yml | 8 +- .github/workflows/release.yml | 152 +++++++++++++++ README.md | 13 ++ deploymentScripts/publish-mavencentral.gradle | 174 ++++++------------ gradle/libs.versions.toml | 4 +- 5 files changed, 227 insertions(+), 124 deletions(-) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 54de035..7697cc4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,6 +7,7 @@ on: pull_request: branches: - master + workflow_call: permissions: contents: read @@ -40,8 +41,11 @@ jobs: - name: Run Android lint run: ./gradlew lint - - name: Build TrustKit library - run: ./gradlew :trustkit:assembleRelease + - name: Run unit tests + run: ./gradlew :trustkit:testDebugUnitTest + + - name: Build TrustKit library and Maven publication + run: ./gradlew :trustkit:assembleRelease :trustkit:publishReleasePublicationToMavenLocal - name: Build Java demo app run: ./gradlew :app:assembleDebug diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..bca6735 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,152 @@ +name: Release + +on: + release: + types: [published] + +permissions: + contents: read + +# Maven Central releases cannot be undone. Never cancel or overlap them. +concurrency: + group: maven-central-release + cancel-in-progress: false + +env: + ARTIFACT_PATH: com/datatheorem/android/trustkit/trustkit + # Must match the verified namespace at https://central.sonatype.com/publishing/namespaces. + CENTRAL_NAMESPACE: com.datatheorem.android.trustkit + +jobs: + verify: + name: Verify release version + runs-on: ubuntu-latest + timeout-minutes: 5 + outputs: + version: ${{ steps.version.outputs.version }} + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Check tag, version, and Maven Central + id: version + env: + TAG: ${{ github.event.release.tag_name }} + run: | + version=$(sed -n 's/^trustkitVersionName = "\(.*\)"$/\1/p' gradle/libs.versions.toml) + if [[ -z "$version" ]]; then + echo "::error::Could not read trustkitVersionName from gradle/libs.versions.toml." + exit 1 + fi + if [[ "${TAG#v}" != "$version" ]]; then + echo "::error::Release tag '$TAG' does not match trustkitVersionName '$version'. Tag the commit that sets this version." + exit 1 + fi + if [[ "$version" == *-SNAPSHOT ]]; then + echo "::error::Snapshot version '$version' cannot be released to Maven Central." + exit 1 + fi + status=$(curl -sS --retry 3 -o /dev/null -w '%{http_code}' \ + "https://repo1.maven.org/maven2/$ARTIFACT_PATH/$version/trustkit-$version.pom") + case "$status" in + 404) ;; + 200) + echo "::error::Version $version is already on Maven Central. Bump trustkitVersionName and create a new release." + exit 1 + ;; + *) + echo "::error::Could not check Maven Central for $version (HTTP $status). Re-run the workflow." + exit 1 + ;; + esac + echo "version=$version" >> "$GITHUB_OUTPUT" + + ci: + name: CI + needs: verify + uses: ./.github/workflows/ci.yml + + publish: + name: Publish to Maven Central + needs: + - verify + - ci + runs-on: ubuntu-latest + timeout-minutes: 60 + environment: maven-central + permissions: + contents: write + env: + VERSION: ${{ needs.verify.outputs.version }} + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Set up JDK 17 + uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: 17 + + - name: Set up Gradle + uses: gradle/actions/setup-gradle@v6 + + - name: Attach AAR to GitHub release + env: + GH_TOKEN: ${{ github.token }} + TAG: ${{ github.event.release.tag_name }} + run: | + ./gradlew :trustkit:assembleRelease + cp trustkit/build/outputs/aar/trustkit-release.aar "trustkit-$VERSION.aar" + gh release upload "$TAG" "trustkit-$VERSION.aar" --clobber + + - name: Upload signed artifacts to Central Portal + env: + ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.MAVEN_CENTRAL_USERNAME }} + ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} + ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNING_KEY }} + ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_KEY_PASSWORD }} + run: ./gradlew :trustkit:publishReleasePublicationToMavenCentralRepository + + - name: Release on Maven Central + env: + MAVEN_CENTRAL_USERNAME: ${{ secrets.MAVEN_CENTRAL_USERNAME }} + MAVEN_CENTRAL_PASSWORD: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} + run: | + token=$(printf '%s:%s' "$MAVEN_CENTRAL_USERNAME" "$MAVEN_CENTRAL_PASSWORD" | base64 -w0) + echo "::add-mask::$token" + auth="Authorization: Bearer $token" + staging=https://ossrh-staging-api.central.sonatype.com + deployments=https://central.sonatype.com/publishing/deployments + + # The staging API groups uploads by IP address, so this must run on the runner that uploaded. + curl --fail-with-body -sS -X POST -H "$auth" \ + "$staging/manual/upload/defaultRepository/$CENTRAL_NAMESPACE?publishing_type=automatic&deployment_name=trustkit-$VERSION" + + id=$(curl --fail-with-body -sS --retry 3 -H "$auth" "$staging/manual/search/repositories?ip=client" \ + | jq -r '[.repositories[].portal_deployment_id | select(.)] | last // empty') + if [[ -z "$id" ]]; then + echo "::error::No Central Portal deployment was found for this upload. Check $deployments." + exit 1 + fi + echo "Central Portal deployment $id: $deployments" + + for _ in $(seq 120); do + response=$(curl --fail-with-body -sS --retry 3 -X POST -H "$auth" \ + "https://central.sonatype.com/api/v1/publisher/status?id=$id") + state=$(jq -r .deploymentState <<< "$response") + echo "Deployment state: $state" + case "$state" in + PUBLISHED) exit 0 ;; + FAILED) + echo "::error::Central Portal rejected deployment $id. See the errors below or $deployments." + jq . <<< "$response" + exit 1 + ;; + esac + sleep 20 + done + echo "::error::Deployment $id is still $state after 40 minutes. Check $deployments." + exit 1 diff --git a/README.md b/README.md index 4792b80..dccaddf 100644 --- a/README.md +++ b/README.md @@ -205,6 +205,19 @@ On Android M and earlier devices, TrustKit provides uses its own implementation For consumers of TrustKit's OkHttpHelper solutions, redirects must to be disabled as Pinning will currently only work properly on the initial request and not any redirects +Releasing +--------- + +1. Set `trustkitVersionName` (and increment `trustkitVersionCode`) in `gradle/libs.versions.toml` and merge to `master`. +2. Publish a GitHub release whose tag equals that version, for example `1.2.0`. Every published release, including pre-releases, goes to Maven Central. + +The `Release` workflow then runs CI, attaches the AAR to the GitHub release, and publishes to Maven Central. A Maven Central release cannot be removed, so check the version before you publish. + +The workflow reads these secrets from the `maven-central` GitHub environment: + +* `MAVEN_CENTRAL_USERNAME`, `MAVEN_CENTRAL_PASSWORD`: a Central Portal user token. +* `SIGNING_KEY`, `SIGNING_KEY_PASSWORD`: the ASCII-armored PGP private key and its passphrase. + License ------- diff --git a/deploymentScripts/publish-mavencentral.gradle b/deploymentScripts/publish-mavencentral.gradle index 1c6adfa..2b8f3e7 100644 --- a/deploymentScripts/publish-mavencentral.gradle +++ b/deploymentScripts/publish-mavencentral.gradle @@ -1,138 +1,72 @@ apply plugin: 'maven-publish' apply plugin: 'signing' -ext { - PUBLISH_GROUP_ID = 'com.datatheorem.android.trustkit' - PUBLISH_VERSION = project.version.toString() - PUBLISH_ARTIFACT_ID = 'trustkit' - DESCRIPTION = 'TrustKit Android is an open source library that makes it easy to deploy SSL public key pinning and reporting in any Android App.' - LICENSE_NAME = 'The MIT License (MIT)' - LICENSE_URL = 'https://github.com/datatheorem/TrustKit-Android/blob/master/LICENSE' - // Name of properties file stored in project root - contains signing key, auth details, etc. - PROPERTIES_FILE_NAME = 'local.properties' - // Version control info - VERSION_CONTROL_CONNECTION = 'scm:git:github.com/datatheorem/TrustKit-Android.git' - VERSION_CONTROL_BROWSABLE_URL = 'https://github.com/datatheorem/TrustKit-Android' -} - -// Gradle task to create a 'sources' jar -task androidSourcesJar(type: Jar) { - archiveClassifier.set('sources') - from android.sourceSets.main.java.srcDirs -} - -// Specify 'androidSourcesJar' as an artifact -// This artifact will get packaged along with the executable, compiled code. -artifacts { - archives androidSourcesJar -} +group = 'com.datatheorem.android.trustkit' -// 'group' = namespace for identifying the artifact (.aar file) produced by the build -group = PUBLISH_GROUP_ID - -// 'version' = version of the artifact (.aar file) being published -version = PUBLISH_VERSION - -// Set all the variables to a dummy empty string to enable project sync and build without the publication values set up -// Useful for contributors to the open-source project who don't have MavenCentral deployment credentials in the local.properties file -ext["signing.keyId"] = '' -ext["signing.password"] = '' -ext["signing.secretKeyRingFile"] = '' -ext["ossrhUsername"] = '' -ext["ossrhPassword"] = '' -ext["sonatypeStagingProfileId"] = '' - -/* -Read attributes from the 'local.properties' file placed inside project root (Not in version control) -'local.properties' should contain the following attributes for the deployment to succeed: -- ossrhPassword -- signing.secretKeyRingFile -- signing.password -- ossrhUsername -- signing.keyId -*/ -File secretPropsFile = project.rootProject.file(PROPERTIES_FILE_NAME) -Properties properties = new Properties() -if (secretPropsFile.exists()) { - new FileInputStream(secretPropsFile).withCloseable { is -> properties.load(is)} -} -// Create 'ext' key-value pairs accessible throughout the gradle file -properties.each { - name, value -> ext[name] = value +android { + publishing { + singleVariant('release') { + withSourcesJar() + withJavadocJar() + } + } } -publishing { - publications { - release(MavenPublication) { - // 'groupId' = namespace, 'artifactId' = library name, 'version' = library version - groupId = PUBLISH_GROUP_ID - artifactId = PUBLISH_ARTIFACT_ID - version = PUBLISH_VERSION - // Two artifacts, the truskit '.aar' and the sources '.jar' - artifact("$buildDir/outputs/aar/${project.getName()}-release.aar") - artifact androidSourcesJar - // .pom file metadata - pom { - name = PUBLISH_ARTIFACT_ID - description = DESCRIPTION - url = VERSION_CONTROL_BROWSABLE_URL - licenses { - license { - name = LICENSE_NAME - url = LICENSE_URL - } - } - developers { - developer { - id = 'nabla-c0d3' - name = 'Alban Diquet' - email = 'ad@datatheorem.io' +// AGP only creates components.release after the project is evaluated. +afterEvaluate { + publishing { + publications { + release(MavenPublication) { + from components.release + artifactId = 'trustkit' + pom { + name = 'trustkit' + description = 'TrustKit Android is an open source library that makes it easy to deploy SSL public key pinning and reporting in any Android App.' + url = 'https://github.com/datatheorem/TrustKit-Android' + licenses { + license { + name = 'The MIT License (MIT)' + url = 'https://github.com/datatheorem/TrustKit-Android/blob/master/LICENSE' + } } - developer { - id = 'jobot0' - name = 'Jordan Bouellat' - email = 'jb@datatheorem.com' + developers { + developer { + id = 'nabla-c0d3' + name = 'Alban Diquet' + email = 'ad@datatheorem.io' + } + developer { + id = 'jobot0' + name = 'Jordan Bouellat' + email = 'jb@datatheorem.com' + } } - } - // Version control metadata - scm { - connection = VERSION_CONTROL_CONNECTION - developerConnection = VERSION_CONTROL_CONNECTION - url = VERSION_CONTROL_BROWSABLE_URL - } - // Create and populate a 'dependencies' node in .pom file - withXml { - def dependenciesNode = asNode().appendNode('dependencies') - project.configurations.implementation.allDependencies.each { - def dependencyNode = dependenciesNode.appendNode('dependency') - dependencyNode.appendNode('groupId', it.group) - dependencyNode.appendNode('artifactId', it.name) - dependencyNode.appendNode('version', it.version) + scm { + connection = 'scm:git:github.com/datatheorem/TrustKit-Android.git' + developerConnection = 'scm:git:github.com/datatheorem/TrustKit-Android.git' + url = 'https://github.com/datatheorem/TrustKit-Android' } } } } - } - // Specify repositories where the library needs to be published - repositories { - maven { - /* The name field can be given an arbitrary value. However changing the name will change the name of the deployment gradle task. - For example, - if name = "mavencentral", task name will be "publishReleasePublicationToMavencentralRepository" - if name = "foo", task name will be "publishReleasePublicationToFooRepository" - */ - name = "mavencentral" - url = "https://oss.sonatype.org/service/local/staging/deploy/maven2/" - credentials { - username = ossrhUsername - password = ossrhPassword + repositories { + // Reads the mavenCentralUsername and mavenCentralPassword Gradle properties (Central Portal user token). + maven { + name = 'mavenCentral' + url = 'https://ossrh-staging-api.central.sonatype.com/service/local/staging/deploy/maven2/' + credentials(PasswordCredentials) } } } -} - -signing { - sign publishing.publications + signing { + def signingKey = findProperty('signingInMemoryKey') + if (signingKey) { + useInMemoryPgpKeys(signingKey, findProperty('signingInMemoryKeyPassword')) + } + // Local and CI publishes stay unsigned. A Maven Central upload fails without a key. + required { gradle.taskGraph.allTasks.any { it.name.endsWith('ToMavenCentralRepository') } } + sign publishing.publications.release + } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 23088aa..0de067f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -22,8 +22,8 @@ okhttp3 = "3.11.0" play-services-base = "11.0.0" spotless = "8.10.0" targetSdk = "36" -trustkitVersionCode = "10" -trustkitVersionName = "1.1.5" +trustkitVersionCode = "11" +trustkitVersionName = "1.2.0" [libraries] androidx-annotation = { module = "androidx.annotation:annotation", version.ref = "androidx-annotation" } From cbeb6dc68fddc2ebfd01a62c6fc2847fbfed120a Mon Sep 17 00:00:00 2001 From: Alexander DiBenedetto Date: Mon, 2 Nov 2026 15:00:19 -0700 Subject: [PATCH 2/5] Publish TrustKit releases as a Maven bundle from scripts CI now runs on every pull request: lint first, then unit tests, the release bundle, demo apps and instrumentation tests in parallel. A published GitHub release attaches the signed bundle zip and uploads the same zip to Maven Central. Build and release logic lives in scripts/, and publishing config moves into trustkit/build.gradle. --- .github/workflows/ci.yml | 158 +++++++++--------- .github/workflows/release.yml | 125 ++------------ .gitignore | 1 + README.md | 4 +- deploymentScripts/publish-mavencentral.gradle | 72 -------- scripts/build_release_bundle.sh | 16 ++ scripts/enable_kvm.sh | 9 + scripts/lib.sh | 24 +++ scripts/publish_maven_central.sh | 46 +++++ scripts/verify_release_version.sh | 19 +++ trustkit/build.gradle | 67 +++++++- 11 files changed, 274 insertions(+), 267 deletions(-) delete mode 100644 deploymentScripts/publish-mavencentral.gradle create mode 100755 scripts/build_release_bundle.sh create mode 100755 scripts/enable_kvm.sh create mode 100755 scripts/lib.sh create mode 100755 scripts/publish_maven_central.sh create mode 100755 scripts/verify_release_version.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7697cc4..349430c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,11 +2,9 @@ name: CI on: push: - branches: - - master + branches: [master] pull_request: - branches: - - master + # Lets release.yml run these checks on the release tag before it publishes. workflow_call: permissions: @@ -17,96 +15,104 @@ concurrency: cancel-in-progress: true jobs: - build: - name: Format, lint, and build + lint: + name: Lint runs-on: ubuntu-latest - timeout-minutes: 30 - + timeout-minutes: 15 steps: - - name: Checkout repository - uses: actions/checkout@v6 - - - name: Set up JDK 17 - uses: actions/setup-java@v5 + - uses: actions/checkout@v6 + - uses: actions/setup-java@v5 with: distribution: temurin java-version: 17 + - uses: gradle/actions/setup-gradle@v6 + - run: ./gradlew spotlessCheck lint + - uses: actions/upload-artifact@v7 + if: failure() + with: + name: lint-reports + path: "**/build/reports/**" + if-no-files-found: ignore + retention-days: 7 - - name: Set up Gradle - uses: gradle/actions/setup-gradle@v6 - - - name: Check formatting - run: ./gradlew spotlessCheck - - - name: Run Android lint - run: ./gradlew lint - - - name: Run unit tests - run: ./gradlew :trustkit:testDebugUnitTest - - - name: Build TrustKit library and Maven publication - run: ./gradlew :trustkit:assembleRelease :trustkit:publishReleasePublicationToMavenLocal - - - name: Build Java demo app - run: ./gradlew :app:assembleDebug - - - name: Build Kotlin demo app - run: ./gradlew :demoappkotlin:assembleDebug - - - name: Upload build reports + unit-tests: + name: Unit tests + needs: lint + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: 17 + - uses: gradle/actions/setup-gradle@v6 + - run: ./gradlew :trustkit:testDebugUnitTest + - uses: actions/upload-artifact@v7 if: failure() - uses: actions/upload-artifact@v7 with: - name: build-reports - path: | - **/build/reports/** - **/build/outputs/logs/** + name: unit-test-reports + path: trustkit/build/reports/tests/** if-no-files-found: ignore retention-days: 7 + release-bundle: + name: Build release bundle + needs: lint + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: 17 + - uses: gradle/actions/setup-gradle@v6 + - run: ./scripts/build_release_bundle.sh + + demo: + name: Build demo ${{ matrix.module }} + needs: lint + runs-on: ubuntu-latest + timeout-minutes: 15 + strategy: + fail-fast: false + matrix: + module: [app, demoappkotlin] + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: 17 + - uses: gradle/actions/setup-gradle@v6 + - run: ./gradlew :${{ matrix.module }}:assembleDebug + instrumentation-tests: name: Instrumentation tests (API ${{ matrix.api-level }}) - needs: build + needs: lint runs-on: ubuntu-latest timeout-minutes: 30 strategy: fail-fast: false matrix: - api-level: - - 27 - - 36 - + api-level: [27, 36] steps: - - name: Checkout repository - uses: actions/checkout@v6 - - - name: Set up JDK 17 - uses: actions/setup-java@v5 + - uses: actions/checkout@v6 + - uses: actions/setup-java@v5 with: distribution: temurin java-version: 17 - - - name: Set up Gradle - uses: gradle/actions/setup-gradle@v6 - - - name: Enable KVM - run: | - echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' \ - | sudo tee /etc/udev/rules.d/99-kvm4all.rules - sudo udevadm control --reload-rules - sudo udevadm trigger --name-match=kvm - - - name: Run instrumentation tests - uses: reactivecircus/android-emulator-runner@v2.37.0 + - uses: gradle/actions/setup-gradle@v6 + - run: ./scripts/enable_kvm.sh + - uses: reactivecircus/android-emulator-runner@v2.37.0 with: api-level: ${{ matrix.api-level }} arch: x86_64 target: default script: ./gradlew :trustkit:connectedDebugAndroidTest - - - name: Upload instrumentation test reports + - uses: actions/upload-artifact@v7 if: failure() - uses: actions/upload-artifact@v7 with: name: instrumentation-reports-api-${{ matrix.api-level }} path: | @@ -118,20 +124,10 @@ jobs: ci-checks: name: CI checks + if: always() + needs: [lint, unit-tests, release-bundle, demo, instrumentation-tests] runs-on: ubuntu-latest - if: ${{ always() }} - needs: - - build - - instrumentation-tests steps: - - name: Verify required jobs succeeded - env: - BUILD_RESULT: ${{ needs.build.result }} - INSTRUMENTATION_TESTS_RESULT: ${{ needs.instrumentation-tests.result }} - run: | - if [[ "$BUILD_RESULT" != "success" || "$INSTRUMENTATION_TESTS_RESULT" != "success" ]]; then - echo "Build result: $BUILD_RESULT" - echo "Instrumentation tests result: $INSTRUMENTATION_TESTS_RESULT" - exit 1 - fi - echo "All necessary CI jobs completed successfully" + - name: Fail unless every required job succeeded + if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') || contains(needs.*.result, 'skipped') + run: exit 1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bca6735..19cc701 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,55 +12,14 @@ concurrency: group: maven-central-release cancel-in-progress: false -env: - ARTIFACT_PATH: com/datatheorem/android/trustkit/trustkit - # Must match the verified namespace at https://central.sonatype.com/publishing/namespaces. - CENTRAL_NAMESPACE: com.datatheorem.android.trustkit - jobs: verify: name: Verify release version runs-on: ubuntu-latest timeout-minutes: 5 - outputs: - version: ${{ steps.version.outputs.version }} - steps: - - name: Checkout repository - uses: actions/checkout@v6 - - - name: Check tag, version, and Maven Central - id: version - env: - TAG: ${{ github.event.release.tag_name }} - run: | - version=$(sed -n 's/^trustkitVersionName = "\(.*\)"$/\1/p' gradle/libs.versions.toml) - if [[ -z "$version" ]]; then - echo "::error::Could not read trustkitVersionName from gradle/libs.versions.toml." - exit 1 - fi - if [[ "${TAG#v}" != "$version" ]]; then - echo "::error::Release tag '$TAG' does not match trustkitVersionName '$version'. Tag the commit that sets this version." - exit 1 - fi - if [[ "$version" == *-SNAPSHOT ]]; then - echo "::error::Snapshot version '$version' cannot be released to Maven Central." - exit 1 - fi - status=$(curl -sS --retry 3 -o /dev/null -w '%{http_code}' \ - "https://repo1.maven.org/maven2/$ARTIFACT_PATH/$version/trustkit-$version.pom") - case "$status" in - 404) ;; - 200) - echo "::error::Version $version is already on Maven Central. Bump trustkitVersionName and create a new release." - exit 1 - ;; - *) - echo "::error::Could not check Maven Central for $version (HTTP $status). Re-run the workflow." - exit 1 - ;; - esac - echo "version=$version" >> "$GITHUB_OUTPUT" + - uses: actions/checkout@v6 + - run: ./scripts/verify_release_version.sh "$GITHUB_REF_NAME" ci: name: CI @@ -68,85 +27,29 @@ jobs: uses: ./.github/workflows/ci.yml publish: - name: Publish to Maven Central - needs: - - verify - - ci + name: Publish release bundle + needs: ci runs-on: ubuntu-latest timeout-minutes: 60 environment: maven-central permissions: contents: write - env: - VERSION: ${{ needs.verify.outputs.version }} - steps: - - name: Checkout repository - uses: actions/checkout@v6 - - - name: Set up JDK 17 - uses: actions/setup-java@v5 + - uses: actions/checkout@v6 + - uses: actions/setup-java@v5 with: distribution: temurin java-version: 17 - - - name: Set up Gradle - uses: gradle/actions/setup-gradle@v6 - - - name: Attach AAR to GitHub release + - uses: gradle/actions/setup-gradle@v6 + - run: ./scripts/build_release_bundle.sh env: - GH_TOKEN: ${{ github.token }} - TAG: ${{ github.event.release.tag_name }} - run: | - ./gradlew :trustkit:assembleRelease - cp trustkit/build/outputs/aar/trustkit-release.aar "trustkit-$VERSION.aar" - gh release upload "$TAG" "trustkit-$VERSION.aar" --clobber - - - name: Upload signed artifacts to Central Portal + SIGNING_KEY: ${{ secrets.SIGNING_KEY }} + SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }} + # Attach first: this step can be re-run, but the Maven Central upload cannot. + - run: gh release upload "$GITHUB_REF_NAME" output/trustkit-*.zip --clobber env: - ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.MAVEN_CENTRAL_USERNAME }} - ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} - ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNING_KEY }} - ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_KEY_PASSWORD }} - run: ./gradlew :trustkit:publishReleasePublicationToMavenCentralRepository - - - name: Release on Maven Central + GH_TOKEN: ${{ github.token }} + - run: ./scripts/publish_maven_central.sh env: MAVEN_CENTRAL_USERNAME: ${{ secrets.MAVEN_CENTRAL_USERNAME }} MAVEN_CENTRAL_PASSWORD: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} - run: | - token=$(printf '%s:%s' "$MAVEN_CENTRAL_USERNAME" "$MAVEN_CENTRAL_PASSWORD" | base64 -w0) - echo "::add-mask::$token" - auth="Authorization: Bearer $token" - staging=https://ossrh-staging-api.central.sonatype.com - deployments=https://central.sonatype.com/publishing/deployments - - # The staging API groups uploads by IP address, so this must run on the runner that uploaded. - curl --fail-with-body -sS -X POST -H "$auth" \ - "$staging/manual/upload/defaultRepository/$CENTRAL_NAMESPACE?publishing_type=automatic&deployment_name=trustkit-$VERSION" - - id=$(curl --fail-with-body -sS --retry 3 -H "$auth" "$staging/manual/search/repositories?ip=client" \ - | jq -r '[.repositories[].portal_deployment_id | select(.)] | last // empty') - if [[ -z "$id" ]]; then - echo "::error::No Central Portal deployment was found for this upload. Check $deployments." - exit 1 - fi - echo "Central Portal deployment $id: $deployments" - - for _ in $(seq 120); do - response=$(curl --fail-with-body -sS --retry 3 -X POST -H "$auth" \ - "https://central.sonatype.com/api/v1/publisher/status?id=$id") - state=$(jq -r .deploymentState <<< "$response") - echo "Deployment state: $state" - case "$state" in - PUBLISHED) exit 0 ;; - FAILED) - echo "::error::Central Portal rejected deployment $id. See the errors below or $deployments." - jq . <<< "$response" - exit 1 - ;; - esac - sleep 20 - done - echo "::error::Deployment $id is still $state after 40 minutes. Check $deployments." - exit 1 diff --git a/.gitignore b/.gitignore index 769831a..b31d8dd 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ *.iml local.properties */debug/* +output/ diff --git a/README.md b/README.md index dccaddf..335d3d7 100644 --- a/README.md +++ b/README.md @@ -211,7 +211,9 @@ Releasing 1. Set `trustkitVersionName` (and increment `trustkitVersionCode`) in `gradle/libs.versions.toml` and merge to `master`. 2. Publish a GitHub release whose tag equals that version, for example `1.2.0`. Every published release, including pre-releases, goes to Maven Central. -The `Release` workflow then runs CI, attaches the AAR to the GitHub release, and publishes to Maven Central. A Maven Central release cannot be removed, so check the version before you publish. +The `Release` workflow then runs CI, attaches `trustkit-.zip` to the GitHub release, and publishes the same zip to Maven Central. A Maven Central release cannot be removed, so check the version before you publish. + +The zip is a Maven repository: the AAR, POM, Gradle module metadata, sources and javadoc, each with checksums and a signature. To use it without Maven Central, unzip it and add the folder as a `maven { url }` repository. `scripts/build_release_bundle.sh` builds it locally (unsigned unless `SIGNING_KEY` is set). The workflow reads these secrets from the `maven-central` GitHub environment: diff --git a/deploymentScripts/publish-mavencentral.gradle b/deploymentScripts/publish-mavencentral.gradle deleted file mode 100644 index 2b8f3e7..0000000 --- a/deploymentScripts/publish-mavencentral.gradle +++ /dev/null @@ -1,72 +0,0 @@ -apply plugin: 'maven-publish' -apply plugin: 'signing' - -group = 'com.datatheorem.android.trustkit' - -android { - publishing { - singleVariant('release') { - withSourcesJar() - withJavadocJar() - } - } -} - -// AGP only creates components.release after the project is evaluated. -afterEvaluate { - publishing { - publications { - release(MavenPublication) { - from components.release - artifactId = 'trustkit' - pom { - name = 'trustkit' - description = 'TrustKit Android is an open source library that makes it easy to deploy SSL public key pinning and reporting in any Android App.' - url = 'https://github.com/datatheorem/TrustKit-Android' - licenses { - license { - name = 'The MIT License (MIT)' - url = 'https://github.com/datatheorem/TrustKit-Android/blob/master/LICENSE' - } - } - developers { - developer { - id = 'nabla-c0d3' - name = 'Alban Diquet' - email = 'ad@datatheorem.io' - } - developer { - id = 'jobot0' - name = 'Jordan Bouellat' - email = 'jb@datatheorem.com' - } - } - scm { - connection = 'scm:git:github.com/datatheorem/TrustKit-Android.git' - developerConnection = 'scm:git:github.com/datatheorem/TrustKit-Android.git' - url = 'https://github.com/datatheorem/TrustKit-Android' - } - } - } - } - - repositories { - // Reads the mavenCentralUsername and mavenCentralPassword Gradle properties (Central Portal user token). - maven { - name = 'mavenCentral' - url = 'https://ossrh-staging-api.central.sonatype.com/service/local/staging/deploy/maven2/' - credentials(PasswordCredentials) - } - } - } - - signing { - def signingKey = findProperty('signingInMemoryKey') - if (signingKey) { - useInMemoryPgpKeys(signingKey, findProperty('signingInMemoryKeyPassword')) - } - // Local and CI publishes stay unsigned. A Maven Central upload fails without a key. - required { gradle.taskGraph.allTasks.any { it.name.endsWith('ToMavenCentralRepository') } } - sign publishing.publications.release - } -} diff --git a/scripts/build_release_bundle.sh b/scripts/build_release_bundle.sh new file mode 100755 index 0000000..6c2dce9 --- /dev/null +++ b/scripts/build_release_bundle.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +. "$(dirname "$0")/lib.sh" + +# Signs the bundle when SIGNING_KEY and SIGNING_KEY_PASSWORD are set. +VERSION="$(trustkit_version)" +REPOSITORY="trustkit/build/release-bundle" +BUNDLE="${PWD}/output/trustkit-${VERSION}.zip" +rm -rf "$REPOSITORY" "$BUNDLE" +mkdir -p output + +./gradlew :trustkit:publishReleasePublicationToReleaseBundleRepository + +# Only the version folder: Central does not want the artifact-level maven-metadata.xml. +(cd "$REPOSITORY" && zip -qr "$BUNDLE" "com/datatheorem/android/trustkit/trustkit/${VERSION}") +unzip -l "$BUNDLE" diff --git a/scripts/enable_kvm.sh b/scripts/enable_kvm.sh new file mode 100755 index 0000000..5959056 --- /dev/null +++ b/scripts/enable_kvm.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +. "$(dirname "$0")/lib.sh" + +# Hardware acceleration for the Android emulator on GitHub's Linux runners. +echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | + sudo tee /etc/udev/rules.d/99-kvm4all.rules +sudo udevadm control --reload-rules +sudo udevadm trigger --name-match=kvm diff --git a/scripts/lib.sh b/scripts/lib.sh new file mode 100755 index 0000000..1435c15 --- /dev/null +++ b/scripts/lib.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash + +set -o errexit +set -o nounset +set -o pipefail + +cd "$(dirname "$0")/.." + +# stderr, so a failure inside $(...) is still shown. +fail() { + if [[ -n "${GITHUB_ACTIONS:-}" ]]; then + echo "::error::$*" >&2 + else + echo "error: $*" >&2 + fi + exit 1 +} + +trustkit_version() { + local version + version="$(sed -n 's/^trustkitVersionName = "\(.*\)"$/\1/p' gradle/libs.versions.toml)" + [[ -n "$version" ]] || fail "Could not read trustkitVersionName from gradle/libs.versions.toml." + echo "$version" +} diff --git a/scripts/publish_maven_central.sh b/scripts/publish_maven_central.sh new file mode 100755 index 0000000..f851036 --- /dev/null +++ b/scripts/publish_maven_central.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash + +. "$(dirname "$0")/lib.sh" + +: "${MAVEN_CENTRAL_USERNAME:?Set MAVEN_CENTRAL_USERNAME to the Central Portal user token username}" +: "${MAVEN_CENTRAL_PASSWORD:?Set MAVEN_CENTRAL_PASSWORD to the Central Portal user token password}" + +VERSION="$(trustkit_version)" +BUNDLE="output/trustkit-${VERSION}.zip" +API="https://central.sonatype.com/api/v1/publisher" +DEPLOYMENTS="https://central.sonatype.com/publishing/deployments" + +[[ -f "$BUNDLE" ]] || fail "$BUNDLE not found. Run scripts/build_release_bundle.sh first." +unzip -l "$BUNDLE" "*/trustkit-${VERSION}.aar.asc" >/dev/null 2>&1 || + fail "$BUNDLE is not signed. Set SIGNING_KEY and SIGNING_KEY_PASSWORD, then run scripts/build_release_bundle.sh again." + +TOKEN="$(printf '%s:%s' "$MAVEN_CENTRAL_USERNAME" "$MAVEN_CENTRAL_PASSWORD" | base64 | tr -d '\n')" +if [[ -n "${GITHUB_ACTIONS:-}" ]]; then + echo "::add-mask::$TOKEN" +fi +AUTH="Authorization: Bearer $TOKEN" + +# No --retry here: a repeated upload creates a second deployment. +if ! DEPLOYMENT_ID="$(curl --fail-with-body -sS -X POST -H "$AUTH" --form "bundle=@${BUNDLE}" \ + "$API/upload?publishingType=AUTOMATIC&name=trustkit-${VERSION}")"; then + fail "Central Portal rejected the upload: $DEPLOYMENT_ID" +fi +echo "Uploaded $BUNDLE as Central Portal deployment $DEPLOYMENT_ID: $DEPLOYMENTS" + +STATE="" +for _ in $(seq 120); do + if ! RESPONSE="$(curl --fail-with-body -sS --retry 3 -X POST -H "$AUTH" "$API/status?id=${DEPLOYMENT_ID}")"; then + fail "Could not read the status of deployment $DEPLOYMENT_ID: $RESPONSE. Check $DEPLOYMENTS." + fi + STATE="$(jq -r .deploymentState <<<"$RESPONSE")" + echo "Deployment state: $STATE" + case "$STATE" in + PUBLISHED) exit 0 ;; + FAILED) + jq . <<<"$RESPONSE" + fail "Central Portal rejected deployment $DEPLOYMENT_ID. See the errors above or $DEPLOYMENTS." + ;; + esac + sleep 20 +done +fail "Deployment $DEPLOYMENT_ID is still $STATE after 40 minutes. Check $DEPLOYMENTS." diff --git a/scripts/verify_release_version.sh b/scripts/verify_release_version.sh new file mode 100755 index 0000000..e2b3c5d --- /dev/null +++ b/scripts/verify_release_version.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +. "$(dirname "$0")/lib.sh" + +TAG="${1:?usage: $0 }" +VERSION="$(trustkit_version)" + +[[ "${TAG#v}" == "$VERSION" ]] || + fail "Release tag '$TAG' does not match trustkitVersionName '$VERSION'. Tag the commit that sets this version." +[[ "$VERSION" != *-SNAPSHOT ]] || + fail "Snapshot version '$VERSION' cannot be released to Maven Central." + +POM_URL="https://repo1.maven.org/maven2/com/datatheorem/android/trustkit/trustkit/${VERSION}/trustkit-${VERSION}.pom" +STATUS="$(curl -sS --retry 3 -o /dev/null -w '%{http_code}' "$POM_URL" || true)" +case "$STATUS" in + 404) echo "TrustKit $VERSION is not on Maven Central yet. OK to release." ;; + 200) fail "Version $VERSION is already on Maven Central. Bump trustkitVersionName and create a new release." ;; + *) fail "Could not check Maven Central for $VERSION (HTTP $STATUS). Re-run the workflow." ;; +esac diff --git a/trustkit/build.gradle b/trustkit/build.gradle index b9f1594..3c12290 100644 --- a/trustkit/build.gradle +++ b/trustkit/build.gradle @@ -1,8 +1,11 @@ plugins { alias(libs.plugins.android.library) alias(libs.plugins.kotlin.android) + id 'maven-publish' + id 'signing' } +group = 'com.datatheorem.android.trustkit' version = libs.versions.trustkitVersionName.get() android { @@ -29,6 +32,13 @@ android { kotlinOptions { jvmTarget = libs.versions.jvmTarget.get() } + + publishing { + singleVariant('release') { + withSourcesJar() + withJavadocJar() + } + } } dependencies { @@ -49,5 +59,58 @@ dependencies { androidTestImplementation libs.google.play.services.base } -// MavenCentral deployment gradle script -apply from: "${rootProject.projectDir}/deploymentScripts/publish-mavencentral.gradle" +// AGP only creates components.release after the project is evaluated. +afterEvaluate { + publishing { + publications { + release(MavenPublication) { + from components.release + artifactId = 'trustkit' + pom { + name = 'trustkit' + description = 'TrustKit Android is an open source library that makes it easy to deploy SSL public key pinning and reporting in any Android App.' + url = 'https://github.com/datatheorem/TrustKit-Android' + licenses { + license { + name = 'The MIT License (MIT)' + url = 'https://github.com/datatheorem/TrustKit-Android/blob/master/LICENSE' + } + } + developers { + developer { + id = 'nabla-c0d3' + name = 'Alban Diquet' + email = 'ad@datatheorem.io' + } + developer { + id = 'jobot0' + name = 'Jordan Bouellat' + email = 'jb@datatheorem.com' + } + } + scm { + connection = 'scm:git:github.com/datatheorem/TrustKit-Android.git' + developerConnection = 'scm:git:github.com/datatheorem/TrustKit-Android.git' + url = 'https://github.com/datatheorem/TrustKit-Android' + } + } + } + } + + repositories { + // scripts/build_release_bundle.sh zips this repository for GitHub and Maven Central. + maven { + name = 'releaseBundle' + url = layout.buildDirectory.dir('release-bundle') + } + } + } + + def signingKey = providers.environmentVariable('SIGNING_KEY').orNull + if (signingKey) { + signing { + useInMemoryPgpKeys(signingKey, providers.environmentVariable('SIGNING_KEY_PASSWORD').orNull) + sign publishing.publications.release + } + } +} From c6d8b32e1fb6b09542bd84e42fbee696274ba9a1 Mon Sep 17 00:00:00 2001 From: Alexander DiBenedetto Date: Mon, 2 Nov 2026 15:34:08 -0700 Subject: [PATCH 3/5] Fix live-site instrumentation tests and simplify CI --- .github/actions/setup/action.yml | 11 +++ .github/workflows/ci.yml | 49 ++++-------- .github/workflows/release.yml | 6 +- .gitignore | 2 +- scripts/publish_maven_central.sh | 4 +- scripts/test_publish_maven_central.sh | 77 +++++++++++++++++++ .../pinning/SSLSocketFactoryTest.java | 8 +- .../res/xml/network_security_config.xml | 18 ++--- 8 files changed, 119 insertions(+), 56 deletions(-) create mode 100644 .github/actions/setup/action.yml create mode 100755 scripts/test_publish_maven_central.sh diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml new file mode 100644 index 0000000..905f673 --- /dev/null +++ b/.github/actions/setup/action.yml @@ -0,0 +1,11 @@ +name: Set up JDK and Gradle +description: The JDK and Gradle setup that every Gradle job uses. Run it after actions/checkout. + +runs: + using: composite + steps: + - uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: 17 + - uses: gradle/actions/setup-gradle@v6 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 349430c..2a6489f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,11 +21,7 @@ jobs: timeout-minutes: 15 steps: - uses: actions/checkout@v6 - - uses: actions/setup-java@v5 - with: - distribution: temurin - java-version: 17 - - uses: gradle/actions/setup-gradle@v6 + - uses: ./.github/actions/setup - run: ./gradlew spotlessCheck lint - uses: actions/upload-artifact@v7 if: failure() @@ -42,11 +38,7 @@ jobs: timeout-minutes: 15 steps: - uses: actions/checkout@v6 - - uses: actions/setup-java@v5 - with: - distribution: temurin - java-version: 17 - - uses: gradle/actions/setup-gradle@v6 + - uses: ./.github/actions/setup - run: ./gradlew :trustkit:testDebugUnitTest - uses: actions/upload-artifact@v7 if: failure() @@ -56,6 +48,15 @@ jobs: if-no-files-found: ignore retention-days: 7 + script-tests: + name: Script tests + needs: lint + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@v6 + - run: ./scripts/test_publish_maven_central.sh + release-bundle: name: Build release bundle needs: lint @@ -63,11 +64,7 @@ jobs: timeout-minutes: 15 steps: - uses: actions/checkout@v6 - - uses: actions/setup-java@v5 - with: - distribution: temurin - java-version: 17 - - uses: gradle/actions/setup-gradle@v6 + - uses: ./.github/actions/setup - run: ./scripts/build_release_bundle.sh demo: @@ -81,11 +78,7 @@ jobs: module: [app, demoappkotlin] steps: - uses: actions/checkout@v6 - - uses: actions/setup-java@v5 - with: - distribution: temurin - java-version: 17 - - uses: gradle/actions/setup-gradle@v6 + - uses: ./.github/actions/setup - run: ./gradlew :${{ matrix.module }}:assembleDebug instrumentation-tests: @@ -99,11 +92,7 @@ jobs: api-level: [27, 36] steps: - uses: actions/checkout@v6 - - uses: actions/setup-java@v5 - with: - distribution: temurin - java-version: 17 - - uses: gradle/actions/setup-gradle@v6 + - uses: ./.github/actions/setup - run: ./scripts/enable_kvm.sh - uses: reactivecircus/android-emulator-runner@v2.37.0 with: @@ -121,13 +110,3 @@ jobs: trustkit/build/outputs/managed_device_android_test_additional_output/** if-no-files-found: ignore retention-days: 7 - - ci-checks: - name: CI checks - if: always() - needs: [lint, unit-tests, release-bundle, demo, instrumentation-tests] - runs-on: ubuntu-latest - steps: - - name: Fail unless every required job succeeded - if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') || contains(needs.*.result, 'skipped') - run: exit 1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 19cc701..2383b05 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -36,11 +36,7 @@ jobs: contents: write steps: - uses: actions/checkout@v6 - - uses: actions/setup-java@v5 - with: - distribution: temurin - java-version: 17 - - uses: gradle/actions/setup-gradle@v6 + - uses: ./.github/actions/setup - run: ./scripts/build_release_bundle.sh env: SIGNING_KEY: ${{ secrets.SIGNING_KEY }} diff --git a/.gitignore b/.gitignore index b31d8dd..d43ff6f 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,4 @@ *.iml local.properties */debug/* -output/ +/output/ diff --git a/scripts/publish_maven_central.sh b/scripts/publish_maven_central.sh index f851036..b2a6a26 100755 --- a/scripts/publish_maven_central.sh +++ b/scripts/publish_maven_central.sh @@ -30,7 +30,7 @@ echo "Uploaded $BUNDLE as Central Portal deployment $DEPLOYMENT_ID: $DEPLOYMENTS STATE="" for _ in $(seq 120); do if ! RESPONSE="$(curl --fail-with-body -sS --retry 3 -X POST -H "$AUTH" "$API/status?id=${DEPLOYMENT_ID}")"; then - fail "Could not read the status of deployment $DEPLOYMENT_ID: $RESPONSE. Check $DEPLOYMENTS." + fail "Could not read the status of deployment $DEPLOYMENT_ID: $RESPONSE. The upload already happened, so do not re-run this job. Check $DEPLOYMENTS." fi STATE="$(jq -r .deploymentState <<<"$RESPONSE")" echo "Deployment state: $STATE" @@ -43,4 +43,4 @@ for _ in $(seq 120); do esac sleep 20 done -fail "Deployment $DEPLOYMENT_ID is still $STATE after 40 minutes. Check $DEPLOYMENTS." +fail "Deployment $DEPLOYMENT_ID is still $STATE after 40 minutes. The upload already happened, so do not re-run this job. Check $DEPLOYMENTS." diff --git a/scripts/test_publish_maven_central.sh b/scripts/test_publish_maven_central.sh new file mode 100755 index 0000000..778945f --- /dev/null +++ b/scripts/test_publish_maven_central.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash + +. "$(dirname "$0")/lib.sh" + +# Runs publish_maven_central.sh in a throwaway copy of the repo, against a fake curl. +WORK="$(mktemp -d)" +trap 'rm -rf "$WORK"' EXIT +mkdir -p "$WORK/gradle" "$WORK/bin" "$WORK/output" +cp -R scripts "$WORK/" +echo 'trustkitVersionName = "9.9.9"' >"$WORK/gradle/libs.versions.toml" + +cat >"$WORK/bin/curl" <<'EOF' +#!/usr/bin/env bash +echo "$*" >>"$CALLS" +case "${*: -1}" in + *publisher/upload*) + if [[ "$SCENARIO" == upload-rejected ]]; then echo '{"error":"Invalid token"}'; exit 22; fi + echo -n "deployment-1" ;; + *publisher/status*) + case "$SCENARIO" in + rejected) echo '{"deploymentState":"FAILED","errors":["Missing javadoc"]}' ;; + stuck) echo '{"deploymentState":"PUBLISHING"}' ;; + *) if (($(grep -c publisher/status "$CALLS") < 2)); then echo '{"deploymentState":"VALIDATING"}'; else echo '{"deploymentState":"PUBLISHED"}'; fi ;; + esac ;; +esac +EOF +printf '#!/usr/bin/env bash\n' >"$WORK/bin/sleep" +chmod +x "$WORK/bin/"* + +# Same files as a real bundle. A short listing hides bugs that only show with the real size. +make_bundle() { + local signed="$1" dir="$WORK/repo/com/datatheorem/android/trustkit/trustkit/9.9.9" suffix sum f + local files=() + rm -rf "$WORK/repo" "$WORK/output/trustkit-9.9.9.zip" + mkdir -p "$dir" + for suffix in .aar .pom .module -sources.jar -javadoc.jar; do + files+=("$dir/trustkit-9.9.9$suffix") + if $signed; then files+=("$dir/trustkit-9.9.9$suffix.asc"); fi + done + for f in "${files[@]}"; do + touch "$f" + for sum in md5 sha1 sha256 sha512; do touch "$f.$sum"; done + done + (cd "$WORK/repo" && zip -qr "$WORK/output/trustkit-9.9.9.zip" .) +} + +FAILED=0 +expect() { + local scenario="$1" want_exit="$2" want_text="$3" out rc + : >"$WORK/calls" + # GITHUB_ACTIONS is unset so the expected failures do not show up as CI annotations. + out="$(env -u GITHUB_ACTIONS PATH="$WORK/bin:$PATH" CALLS="$WORK/calls" SCENARIO="$scenario" \ + MAVEN_CENTRAL_USERNAME=user MAVEN_CENTRAL_PASSWORD=pass \ + "$WORK/scripts/publish_maven_central.sh" 2>&1)" && rc=0 || rc=$? + if [[ $rc -eq $want_exit && "$out" == *"$want_text"* ]]; then + echo "ok: $scenario" + else + echo "FAIL: $scenario exited $rc, expected $want_exit and output containing '$want_text':" + echo "$out" + FAILED=1 + fi +} + +make_bundle true +expect published 0 "Deployment state: PUBLISHED" +expect rejected 1 "Missing javadoc" +expect upload-rejected 1 "Invalid token" +expect stuck 1 "do not re-run this job" + +make_bundle false +expect unsigned 1 "is not signed" +if [[ -s "$WORK/calls" ]]; then + echo "FAIL: unsigned: the bundle was uploaded" + FAILED=1 +fi + +exit $FAILED diff --git a/trustkit/src/androidTest/java/com/datatheorem/android/trustkit/pinning/SSLSocketFactoryTest.java b/trustkit/src/androidTest/java/com/datatheorem/android/trustkit/pinning/SSLSocketFactoryTest.java index 0592c94..09d3a7a 100644 --- a/trustkit/src/androidTest/java/com/datatheorem/android/trustkit/pinning/SSLSocketFactoryTest.java +++ b/trustkit/src/androidTest/java/com/datatheorem/android/trustkit/pinning/SSLSocketFactoryTest.java @@ -237,7 +237,7 @@ public void testPinnedDomainSuccessAnchor() throws IOException { } @Test - public void testPinnedDomainSuccessLeaf() throws IOException { + public void testPinnedDomainSuccessIntermediate() throws IOException { String serverHostname = "datatheorem.com"; TestableTrustKit.initializeWithNetworkSecurityConfiguration( InstrumentationRegistry.getInstrumentation().getContext(), mockReporter); @@ -303,7 +303,7 @@ public void testPinnedDomainInvalidPin() throws IOException { @Test public void testPinnedDomainInvalidPinAndPinningNotEnforced() throws IOException { - String serverHostname = "www.github.com"; + String serverHostname = "www.facebook.com"; TestableTrustKit.initializeWithNetworkSecurityConfiguration( InstrumentationRegistry.getInstrumentation().getContext(), mockReporter); @@ -359,7 +359,7 @@ public void testPinnedDomainInvalidPinAndPolicyExpired() throws IOException { @Test public void testPinnedDomainUntrustedChainAndPinningNotEnforced() throws IOException { - String serverHostname = "untrusted-root.badssl.com"; + String serverHostname = "self-signed.badssl.com"; TestableTrustKit.initializeWithNetworkSecurityConfiguration( InstrumentationRegistry.getInstrumentation().getContext(), mockReporter); @@ -614,7 +614,7 @@ public void testDebugOverridesInvalidPin() throws IOException, CertificateExcept // region Tests for when the domain is NOT pinned @Test public void testNonPinnedDomainUntrustedRootChain() throws IOException { - String serverHostname = "www.cacert.org"; + String serverHostname = "self-signed.badssl.com"; final DomainPinningPolicy domainPolicy = new DomainPinningPolicy.Builder() .setHostname("other.domain.com") diff --git a/trustkit/src/androidTest/res/xml/network_security_config.xml b/trustkit/src/androidTest/res/xml/network_security_config.xml index 3b4fa6a..831c6f1 100644 --- a/trustkit/src/androidTest/res/xml/network_security_config.xml +++ b/trustkit/src/androidTest/res/xml/network_security_config.xml @@ -5,20 +5,20 @@ www.datatheorem.com - + + hxqRlPTu1bMS/0DITB1SSu0vd4u/8l8TjPgfaAp63Gc= AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= - u91x/rv9ivMrXO0du6N2ABI6BiiVfD0T99DTEzoqSv4= - + datatheorem.com - + + OdSlmQD9NWJh4EbcOHBxkhygPwNSwA9Q91eounfbcoE= AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= - hxqRlPTu1bMS/0DITB1SSu0vd4u/8l8TjPgfaAp63Gc= @@ -36,7 +36,7 @@ - www.github.com + www.facebook.com AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= @@ -69,11 +69,11 @@ - untrusted-root.badssl.com + self-signed.badssl.com - + + 9SLklscvzMYj8f+52lp5ze/hY0CFHyLSPQzSpYYIBm8= AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= - BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB= From 5fc7427c654662f412ea0b1bf40307e1560c76cd Mon Sep 17 00:00:00 2001 From: Alexander DiBenedetto Date: Mon, 2 Nov 2026 15:59:04 -0700 Subject: [PATCH 4/5] Use only datatheorem.com and yahoo.com hosts in live tests Replace facebook, microsoft and google hosts with yahoo.com and datatheorem.com equivalents. Stop re-running CI at release time, and replace the dead Bintray badge with a Maven Central badge. --- .github/workflows/ci.yml | 2 -- .github/workflows/release.yml | 7 +------ README.md | 4 ++-- scripts/publish_maven_central.sh | 10 +++++++--- scripts/test_publish_maven_central.sh | 7 +++++++ .../android/trustkit/pinning/SSLSocketFactoryTest.java | 8 ++++---- .../trustkit/reporting/BackgroundReporterTaskTest.java | 6 +++--- .../trustkit/reporting/BackgroundReporterTest.java | 10 +++++----- .../androidTest/res/xml/network_security_config.xml | 4 ++-- 9 files changed, 31 insertions(+), 27 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2a6489f..330117e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,8 +4,6 @@ on: push: branches: [master] pull_request: - # Lets release.yml run these checks on the release tag before it publishes. - workflow_call: permissions: contents: read diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2383b05..bf00776 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,14 +21,9 @@ jobs: - uses: actions/checkout@v6 - run: ./scripts/verify_release_version.sh "$GITHUB_REF_NAME" - ci: - name: CI - needs: verify - uses: ./.github/workflows/ci.yml - publish: name: Publish release bundle - needs: ci + needs: verify runs-on: ubuntu-latest timeout-minutes: 60 environment: maven-central diff --git a/README.md b/README.md index 335d3d7..e379d80 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ TrustKit Android [![CI](https://github.com/datatheorem/TrustKit-Android/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/datatheorem/TrustKit-Android/actions/workflows/ci.yml?query=branch%3Amaster) [![API](https://img.shields.io/badge/API-16%2B-blue.svg?style=flat)](https://android-arsenal.com/api?level=16) -[![Version](https://img.shields.io/bintray/v/datatheoremoss/TrustKit-Android/trustkit.svg)](https://bintray.com/datatheoremoss/TrustKit-Android/trustkit) +[![Maven Central](https://img.shields.io/maven-central/v/com.datatheorem.android.trustkit/trustkit.svg)](https://central.sonatype.com/artifact/com.datatheorem.android.trustkit/trustkit) [![MIT License](https://img.shields.io/github/license/datatheorem/trustkit-android.svg)](https://en.wikipedia.org/wiki/MIT_License) [![Gitter chat](https://badges.gitter.im/datatheorem/gitter.png)](https://gitter.im/TrustKit/Lobby) @@ -211,7 +211,7 @@ Releasing 1. Set `trustkitVersionName` (and increment `trustkitVersionCode`) in `gradle/libs.versions.toml` and merge to `master`. 2. Publish a GitHub release whose tag equals that version, for example `1.2.0`. Every published release, including pre-releases, goes to Maven Central. -The `Release` workflow then runs CI, attaches `trustkit-.zip` to the GitHub release, and publishes the same zip to Maven Central. A Maven Central release cannot be removed, so check the version before you publish. +The `Release` workflow then attaches `trustkit-.zip` to the GitHub release and publishes the same zip to Maven Central. It does not run CI again: release a commit that already passed CI on `master`. A Maven Central release cannot be removed, so check the version before you publish. The zip is a Maven repository: the AAR, POM, Gradle module metadata, sources and javadoc, each with checksums and a signature. To use it without Maven Central, unzip it and add the folder as a `maven { url }` repository. `scripts/build_release_bundle.sh` builds it locally (unsigned unless `SIGNING_KEY` is set). diff --git a/scripts/publish_maven_central.sh b/scripts/publish_maven_central.sh index b2a6a26..5f8a582 100755 --- a/scripts/publish_maven_central.sh +++ b/scripts/publish_maven_central.sh @@ -18,10 +18,14 @@ TOKEN="$(printf '%s:%s' "$MAVEN_CENTRAL_USERNAME" "$MAVEN_CENTRAL_PASSWORD" | ba if [[ -n "${GITHUB_ACTIONS:-}" ]]; then echo "::add-mask::$TOKEN" fi -AUTH="Authorization: Bearer $TOKEN" + +# The header goes in on stdin: a -H argument would show the token in the process list. +central_api() { + printf 'Authorization: Bearer %s\n' "$TOKEN" | curl --fail-with-body -sS -X POST -H @- "$@" +} # No --retry here: a repeated upload creates a second deployment. -if ! DEPLOYMENT_ID="$(curl --fail-with-body -sS -X POST -H "$AUTH" --form "bundle=@${BUNDLE}" \ +if ! DEPLOYMENT_ID="$(central_api --form "bundle=@${BUNDLE}" \ "$API/upload?publishingType=AUTOMATIC&name=trustkit-${VERSION}")"; then fail "Central Portal rejected the upload: $DEPLOYMENT_ID" fi @@ -29,7 +33,7 @@ echo "Uploaded $BUNDLE as Central Portal deployment $DEPLOYMENT_ID: $DEPLOYMENTS STATE="" for _ in $(seq 120); do - if ! RESPONSE="$(curl --fail-with-body -sS --retry 3 -X POST -H "$AUTH" "$API/status?id=${DEPLOYMENT_ID}")"; then + if ! RESPONSE="$(central_api --retry 3 "$API/status?id=${DEPLOYMENT_ID}")"; then fail "Could not read the status of deployment $DEPLOYMENT_ID: $RESPONSE. The upload already happened, so do not re-run this job. Check $DEPLOYMENTS." fi STATE="$(jq -r .deploymentState <<<"$RESPONSE")" diff --git a/scripts/test_publish_maven_central.sh b/scripts/test_publish_maven_central.sh index 778945f..a4fec99 100755 --- a/scripts/test_publish_maven_central.sh +++ b/scripts/test_publish_maven_central.sh @@ -12,6 +12,7 @@ echo 'trustkitVersionName = "9.9.9"' >"$WORK/gradle/libs.versions.toml" cat >"$WORK/bin/curl" <<'EOF' #!/usr/bin/env bash echo "$*" >>"$CALLS" +cat >>"$CALLS.stdin" case "${*: -1}" in *publisher/upload*) if [[ "$SCENARIO" == upload-rejected ]]; then echo '{"error":"Invalid token"}'; exit 22; fi @@ -48,6 +49,7 @@ FAILED=0 expect() { local scenario="$1" want_exit="$2" want_text="$3" out rc : >"$WORK/calls" + : >"$WORK/calls.stdin" # GITHUB_ACTIONS is unset so the expected failures do not show up as CI annotations. out="$(env -u GITHUB_ACTIONS PATH="$WORK/bin:$PATH" CALLS="$WORK/calls" SCENARIO="$scenario" \ MAVEN_CENTRAL_USERNAME=user MAVEN_CENTRAL_PASSWORD=pass \ @@ -63,6 +65,11 @@ expect() { make_bundle true expect published 0 "Deployment state: PUBLISHED" +# dXNlcjpwYXNz is base64 of user:pass. Arguments show in the process list; stdin does not. +if grep -q dXNlcjpwYXNz "$WORK/calls" || ! grep -qx 'Authorization: Bearer dXNlcjpwYXNz' "$WORK/calls.stdin"; then + echo "FAIL: published: the token must reach curl on stdin, never as an argument" + FAILED=1 +fi expect rejected 1 "Missing javadoc" expect upload-rejected 1 "Invalid token" expect stuck 1 "do not re-run this job" diff --git a/trustkit/src/androidTest/java/com/datatheorem/android/trustkit/pinning/SSLSocketFactoryTest.java b/trustkit/src/androidTest/java/com/datatheorem/android/trustkit/pinning/SSLSocketFactoryTest.java index 09d3a7a..4c4a029 100644 --- a/trustkit/src/androidTest/java/com/datatheorem/android/trustkit/pinning/SSLSocketFactoryTest.java +++ b/trustkit/src/androidTest/java/com/datatheorem/android/trustkit/pinning/SSLSocketFactoryTest.java @@ -303,7 +303,7 @@ public void testPinnedDomainInvalidPin() throws IOException { @Test public void testPinnedDomainInvalidPinAndPinningNotEnforced() throws IOException { - String serverHostname = "www.facebook.com"; + String serverHostname = "yahoo.com"; TestableTrustKit.initializeWithNetworkSecurityConfiguration( InstrumentationRegistry.getInstrumentation().getContext(), mockReporter); @@ -335,7 +335,7 @@ public void testPinnedDomainInvalidPinAndPinningNotEnforced() throws IOException @Test public void testPinnedDomainInvalidPinAndPolicyExpired() throws IOException { - String serverHostname = "www.microsoft.com"; + String serverHostname = "login.yahoo.com"; TestableTrustKit.initializeWithNetworkSecurityConfiguration( InstrumentationRegistry.getInstrumentation().getContext(), mockReporter); @@ -669,7 +669,7 @@ public void testNonPinnedDomainUntrustedRootChain() throws IOException { @Test public void testNonPinnedDomainSuccess() throws IOException { // Initialize TrustKit - String serverHostname = "www.google.com"; + String serverHostname = "overmind.datatheorem.com"; TestableTrustKit.initializeWithNetworkSecurityConfiguration( InstrumentationRegistry.getInstrumentation().getContext(), mockReporter); @@ -764,7 +764,7 @@ public void testDebugOverridesSystemCa() throws IOException, CertificateExceptio return; } - String serverHostname = "www.google.com"; + String serverHostname = "overmind.datatheorem.com"; // Create a policy for a different domain final DomainPinningPolicy domainPolicy = new DomainPinningPolicy.Builder() diff --git a/trustkit/src/androidTest/java/com/datatheorem/android/trustkit/reporting/BackgroundReporterTaskTest.java b/trustkit/src/androidTest/java/com/datatheorem/android/trustkit/reporting/BackgroundReporterTaskTest.java index ab6b42f..6bbcfcf 100644 --- a/trustkit/src/androidTest/java/com/datatheorem/android/trustkit/reporting/BackgroundReporterTaskTest.java +++ b/trustkit/src/androidTest/java/com/datatheorem/android/trustkit/reporting/BackgroundReporterTaskTest.java @@ -74,7 +74,7 @@ public void testExecuteSucceedHttps() throws MalformedURLException { // Add two report URIs with the first one failing, to ensure both are called and last one // succeeded - taskParameters.add(new URL("https://www.google.com/fake")); + taskParameters.add(new URL("https://overmind.datatheorem.com/fake")); taskParameters.add(new URL("https://overmind.datatheorem.com/trustkit/report")); // Run the task synchronously and ensure it succeeded @@ -97,7 +97,7 @@ public void testExecuteSucceedHttp() throws MalformedURLException { // Add two report URIs with the first one failing, to ensure both are called and last one // succeeded - taskParameters.add(new URL("http://www.google.com/fake")); + taskParameters.add(new URL("http://overmind.datatheorem.com/fake")); taskParameters.add(new URL("http://overmind.datatheorem.com/trustkit/report")); // Run the task synchronously and ensure it succeeded @@ -121,7 +121,7 @@ public void testExecuteFailedHttpError() throws MalformedURLException { // Add two report URIs with the first one succeeding, to ensure both are called // and last one failed taskParameters.add(new URL("https://overmind.datatheorem.com/trustkit/report")); - taskParameters.add(new URL("https://www.google.com/fake")); + taskParameters.add(new URL("https://overmind.datatheorem.com/fake")); // Run the task synchronously and ensure it failed Integer lastResponseCode = testTask.doInBackground(taskParameters.toArray()); diff --git a/trustkit/src/androidTest/java/com/datatheorem/android/trustkit/reporting/BackgroundReporterTest.java b/trustkit/src/androidTest/java/com/datatheorem/android/trustkit/reporting/BackgroundReporterTest.java index efe9ca6..2515c4d 100644 --- a/trustkit/src/androidTest/java/com/datatheorem/android/trustkit/reporting/BackgroundReporterTest.java +++ b/trustkit/src/androidTest/java/com/datatheorem/android/trustkit/reporting/BackgroundReporterTest.java @@ -51,10 +51,10 @@ public void testPinValidationFailed() throws MalformedURLException, JSONExceptio } Context context = InstrumentationRegistry.getInstrumentation().getContext(); // Initialize TrustKit - String serverHostname = "mail.google.com"; + String serverHostname = "mail.yahoo.com"; final DomainPinningPolicy domainPolicy = new DomainPinningPolicy.Builder() - .setHostname("google.com") + .setHostname("yahoo.com") .setShouldIncludeSubdomains(true) .setShouldEnforcePinning(true) .setPublicKeyHashes( @@ -130,15 +130,15 @@ private void validateSentReport(PinningFailureReport reportSent) throws JSONExce assertEquals("com.unit.tests", reportSentJson.getString("app-bundle-id")); assertEquals("1.2", reportSentJson.getString("app-version")); assertEquals("ANDROID", reportSentJson.getString("app-platform")); - assertEquals("mail.google.com", reportSentJson.getString("hostname")); - assertEquals("google.com", reportSentJson.getString("noted-hostname")); + assertEquals("mail.yahoo.com", reportSentJson.getString("hostname")); + assertEquals("yahoo.com", reportSentJson.getString("noted-hostname")); assertEquals(443, reportSentJson.getInt("port")); assertTrue(reportSentJson.getBoolean("include-subdomains")); assertTrue(reportSentJson.getBoolean("enforce-pinning")); assertEquals( PinningValidationResult.FAILED.ordinal(), reportSentJson.getInt("validation-result")); - assertEquals("google.com", reportSentJson.getString("noted-hostname")); + assertEquals("yahoo.com", reportSentJson.getString("noted-hostname")); assertNotNull(reportSentJson.getString("app-vendor-id")); assertNotNull(reportSentJson.getString("trustkit-version")); diff --git a/trustkit/src/androidTest/res/xml/network_security_config.xml b/trustkit/src/androidTest/res/xml/network_security_config.xml index 831c6f1..a7b4da0 100644 --- a/trustkit/src/androidTest/res/xml/network_security_config.xml +++ b/trustkit/src/androidTest/res/xml/network_security_config.xml @@ -36,7 +36,7 @@ - www.facebook.com + yahoo.com AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= @@ -47,7 +47,7 @@ - www.microsoft.com + login.yahoo.com AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= From 38accd7286147bcfda8a2b1c12fde12251d02082 Mon Sep 17 00:00:00 2001 From: Alexander DiBenedetto Date: Wed, 4 Nov 2026 10:02:21 -0700 Subject: [PATCH 5/5] Address review feedback on the release workflow - Read the tag from release.tag_name and use one concurrency group per tag - Print the bundle contents from zip instead of a second listing - List Data Theorem Support as the POM developer - Document the localbroadcastmanager dependency for report receivers --- .github/workflows/release.yml | 12 ++++++++---- README.md | 4 ++++ scripts/build_release_bundle.sh | 3 +-- trustkit/build.gradle | 11 +++-------- 4 files changed, 16 insertions(+), 14 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bf00776..1791bd4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -7,9 +7,13 @@ on: permissions: contents: read -# Maven Central releases cannot be undone. Never cancel or overlap them. +# github.ref_name can be empty on release events. +env: + TAG: ${{ github.event.release.tag_name }} + +# One group per tag: a shared group cancels pending runs, which would skip a release. concurrency: - group: maven-central-release + group: maven-central-release-${{ github.event.release.tag_name }} cancel-in-progress: false jobs: @@ -19,7 +23,7 @@ jobs: timeout-minutes: 5 steps: - uses: actions/checkout@v6 - - run: ./scripts/verify_release_version.sh "$GITHUB_REF_NAME" + - run: ./scripts/verify_release_version.sh "$TAG" publish: name: Publish release bundle @@ -37,7 +41,7 @@ jobs: SIGNING_KEY: ${{ secrets.SIGNING_KEY }} SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }} # Attach first: this step can be re-run, but the Maven Central upload cannot. - - run: gh release upload "$GITHUB_REF_NAME" output/trustkit-*.zip --clobber + - run: gh release upload "$TAG" output/trustkit-*.zip --clobber env: GH_TOKEN: ${{ github.token }} - run: ./scripts/publish_maven_central.sh diff --git a/README.md b/README.md index e379d80..122f7d8 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,10 @@ Add TrustKit to your project's _build.gradle_: `implementation 'com.datatheorem.android.trustkit:trustkit:'` +To receive pinning failure reports with a local broadcast receiver, also add: + +`implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.1.0'` + ### Configuring a Pinning Policy Deploying SSL pinning in the App requires initializing TrustKit with a pinning policy (domains, pins, and additional settings). The policy is wrapped in the official [Android N Network Security Configuration](https://developer.android.com/training/articles/security-config.html) i.e : diff --git a/scripts/build_release_bundle.sh b/scripts/build_release_bundle.sh index 6c2dce9..4bc585c 100755 --- a/scripts/build_release_bundle.sh +++ b/scripts/build_release_bundle.sh @@ -12,5 +12,4 @@ mkdir -p output ./gradlew :trustkit:publishReleasePublicationToReleaseBundleRepository # Only the version folder: Central does not want the artifact-level maven-metadata.xml. -(cd "$REPOSITORY" && zip -qr "$BUNDLE" "com/datatheorem/android/trustkit/trustkit/${VERSION}") -unzip -l "$BUNDLE" +(cd "$REPOSITORY" && zip -r "$BUNDLE" "com/datatheorem/android/trustkit/trustkit/${VERSION}") diff --git a/trustkit/build.gradle b/trustkit/build.gradle index 3c12290..e6c94de 100644 --- a/trustkit/build.gradle +++ b/trustkit/build.gradle @@ -78,14 +78,9 @@ afterEvaluate { } developers { developer { - id = 'nabla-c0d3' - name = 'Alban Diquet' - email = 'ad@datatheorem.io' - } - developer { - id = 'jobot0' - name = 'Jordan Bouellat' - email = 'jb@datatheorem.com' + id = 'datatheorem' + name = 'Data Theorem Support' + email = 'support@datatheorem.com' } } scm {