diff --git a/.github/workflows/publish-kotlin.yml b/.github/workflows/publish-kotlin.yml index ae6610f2..4faf1ece 100644 --- a/.github/workflows/publish-kotlin.yml +++ b/.github/workflows/publish-kotlin.yml @@ -250,7 +250,7 @@ jobs: retention-days: 1 publish: - name: Publish to GitHub Packages + name: Publish runs-on: ubuntu-latest needs: [prepare, build-host, build-android] environment: ${{ needs.prepare.outputs.environment }} @@ -324,6 +324,53 @@ jobs: env: PKG_VERSION: ${{ needs.prepare.outputs.version }} + # Production only. The -Pidkit.publish.mavenCentral=true flag registers the + # Maven Central repository and signing tasks; without it (every dev release) the + # step below publishes to GitHub Packages alone. + # + # This task only UPLOADS the deployment to the Central Portal and returns; Portal + # validation is asynchronous. The next step polls until the deployment validates, + # so GitHub Packages does not publish if Central validation fails. USER_MANAGED + # then leaves the validated deployment awaiting a manual Publish in the Portal UI, + # the final gate on what actually goes live on Maven Central. + - name: Publish to Maven Central + if: needs.prepare.outputs.environment == 'production' + uses: gradle/gradle-build-action@v3 + with: + gradle-version: 8.9 + arguments: -p kotlin bindings:publishToMavenCentral -Pidkit.publish.mavenCentral=true + env: + PKG_VERSION: ${{ needs.prepare.outputs.version }} + ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.MAVEN_CENTRAL_USERNAME }} + ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} + ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.MAVEN_SIGNING_KEY }} + ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.MAVEN_SIGNING_KEY_PASSWORD }} + + # Gate GitHub Packages on Central validation. The upload above returns before the + # Portal finishes its asynchronous validation, so without this a validation failure + # would still let GitHub Packages publish. Poll the "download from a validated + # deployment" endpoint, which returns 200 only once the deployment is VALIDATED. + - name: Wait for Maven Central validation + if: needs.prepare.outputs.environment == 'production' + env: + MAVEN_CENTRAL_USERNAME: ${{ secrets.MAVEN_CENTRAL_USERNAME }} + MAVEN_CENTRAL_PASSWORD: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} + PKG_VERSION: ${{ needs.prepare.outputs.version }} + run: | + TOKEN=$(printf '%s:%s' "$MAVEN_CENTRAL_USERNAME" "$MAVEN_CENTRAL_PASSWORD" | base64 | tr -d '\n') + URL="https://central.sonatype.com/api/v1/publisher/deployments/download/com/worldcoin/idkit/${PKG_VERSION}/idkit-${PKG_VERSION}.pom" + for i in $(seq 1 40); do + CODE=$(curl -s -o /dev/null -w '%{http_code}' -H "Authorization: Bearer $TOKEN" "$URL") + if [ "$CODE" = "200" ]; then + echo "Maven Central deployment for $PKG_VERSION validated." + exit 0 + fi + echo "Not validated yet (HTTP $CODE); attempt $i/40, retrying in 15s..." + sleep 15 + done + echo "::error::Maven Central deployment for $PKG_VERSION did not validate within ~10 minutes; skipping GitHub Packages. Check the Central Portal." + exit 1 + - name: Publish to GitHub Packages uses: gradle/gradle-build-action@v3 with: diff --git a/kotlin/README.md b/kotlin/README.md index 36799fb5..13f15782 100644 --- a/kotlin/README.md +++ b/kotlin/README.md @@ -4,8 +4,7 @@ Kotlin SDK for World ID verification, backed by the Rust core via UniFFI. ## Installation -The Kotlin package is published to GitHub Packages as `com.worldcoin:idkit`. -The same publication is also prepared for `mavenLocal()` and Maven Central. Maven Central upload is available as an explicit local opt-in, but it is intentionally not wired into GitHub release workflows until the required secrets are available there and the workflow has been tested end to end. +The Kotlin SDK is published to Maven Central as `com.worldcoin:idkit` — once a version is released there, add `mavenCentral()` to your repositories and depend on it with no authentication. Release builds are also published to GitHub Packages; dev builds (`X.Y.Z-dev.`) are published there only. GitHub Packages requires authentication for Maven downloads, even for public packages. Create a token with `read:packages` and expose it through environment variables. @@ -215,7 +214,7 @@ gradle -p kotlin bindings:test ## Publishing -The existing Kotlin release workflow publishes to GitHub Packages. That path is still active and uses GitHub's package credentials: +On production releases the Kotlin release workflow publishes to both Maven Central and GitHub Packages. The GitHub Packages path uses GitHub's package credentials and can also be run locally: ```bash ./kotlin/Examples/IDKitSampleApp/gradlew -p kotlin :bindings:publish @@ -255,7 +254,7 @@ To upload and release from the Central Portal deployment in one command, run: :bindings:publishAndReleaseToMavenCentral ``` -The automated release workflow continues publishing Kotlin artifacts to GitHub Packages, but it does not publish to Maven Central yet. Add Maven Central release-workflow steps only after the required credentials and end-to-end release path are ready. +On production releases the workflow runs the upload-only `:bindings:publishToMavenCentral` step automatically (not `publishAndReleaseToMavenCentral`), using the Sonatype and GPG signing credentials stored as `production` environment secrets. The first release uploads to the Central Portal for manual confirmation before going live; a follow-up change switches it to fully automatic. ## Troubleshooting diff --git a/scripts/publish-relocation-pom.sh b/scripts/publish-relocation-pom.sh new file mode 100755 index 00000000..221524d7 --- /dev/null +++ b/scripts/publish-relocation-pom.sh @@ -0,0 +1,127 @@ +#!/bin/bash +set -euo pipefail + +# One-time script: publish a relocation POM for the superseded com.worldcoin:idkit-kotlin +# artifact (last real release: 3.1.0, from the archived worldcoin/idkit-kotlin repo), +# pointing consumers at the renamed com.worldcoin:idkit artifact in this monorepo. +# +# Maven resolves automatically; for Gradle users the POM mainly serves as +# discoverable documentation of the rename on central.sonatype.com / mvnrepository.com. +# +# Run AFTER the first com.worldcoin:idkit release is live on Maven Central, so the +# relocation target exists. +# +# Requirements: +# - gpg with the release signing key imported (same key used by CI for Central) +# - Central Portal user token with access to the com.worldcoin namespace +# +# Usage: +# MAVEN_CENTRAL_USERNAME=... MAVEN_CENTRAL_PASSWORD=... \ +# scripts/publish-relocation-pom.sh [signing-key-id] +# +# the com.worldcoin:idkit version on Central to relocate to (e.g. 4.1.0) +# [signing-key-id] optional gpg key selector; defaults to gpg's default key +# +# If MAVEN_CENTRAL_USERNAME/PASSWORD are unset, the script still produces the bundle zip +# and prints instructions for manual upload in the Central Portal UI. + +TARGET_VERSION="${1:?usage: publish-relocation-pom.sh [signing-key-id]}" +SIGNING_KEY_ID="${2:-}" + +# Must sort above 3.1.0 so resolvers treat the relocation POM as the latest version +# of the old coordinates. +RELOCATION_VERSION="3.2.0" +GROUP_ID="com.worldcoin" +OLD_ARTIFACT_ID="idkit-kotlin" +NEW_ARTIFACT_ID="idkit" + +WORK_DIR="$(mktemp -d)" +trap 'rm -rf "$WORK_DIR"' EXIT + +ARTIFACT_DIR="$WORK_DIR/com/worldcoin/$OLD_ARTIFACT_ID/$RELOCATION_VERSION" +POM_FILE="$ARTIFACT_DIR/$OLD_ARTIFACT_ID-$RELOCATION_VERSION.pom" +BUNDLE="$PWD/$OLD_ARTIFACT_ID-$RELOCATION_VERSION-relocation-bundle.zip" + +mkdir -p "$ARTIFACT_DIR" + +echo "📝 Generating relocation POM ($GROUP_ID:$OLD_ARTIFACT_ID:$RELOCATION_VERSION -> $GROUP_ID:$NEW_ARTIFACT_ID:$TARGET_VERSION)" + +cat > "$POM_FILE" < + + 4.0.0 + $GROUP_ID + $OLD_ARTIFACT_ID + $RELOCATION_VERSION + pom + IDKit (Kotlin) — relocated + This artifact has been renamed. Use com.worldcoin:idkit, published from the worldcoin/idkit monorepo. + https://github.com/worldcoin/idkit + + + MIT License + https://opensource.org/licenses/MIT + + + + + worldcoin + Worldcoin + + + + scm:git:https://github.com/worldcoin/idkit.git + scm:git:ssh://git@github.com/worldcoin/idkit.git + https://github.com/worldcoin/idkit + + + + $GROUP_ID + $NEW_ARTIFACT_ID + $TARGET_VERSION + idkit-kotlin moved to the worldcoin/idkit monorepo and was renamed to com.worldcoin:idkit. + + + +EOF + +echo "🔏 Signing POM" +GPG_ARGS=(--armor --detach-sign --output "$POM_FILE.asc") +if [ -n "$SIGNING_KEY_ID" ]; then + GPG_ARGS=(--local-user "$SIGNING_KEY_ID" "${GPG_ARGS[@]}") +fi +gpg "${GPG_ARGS[@]}" "$POM_FILE" + +echo "🧮 Writing checksums" +# Signature files (.asc) are exempt from the checksum requirement — only the POM needs them. +if command -v md5sum >/dev/null; then + md5sum "$POM_FILE" | cut -d' ' -f1 > "$POM_FILE.md5" + sha1sum "$POM_FILE" | cut -d' ' -f1 > "$POM_FILE.sha1" +else + md5 -q "$POM_FILE" > "$POM_FILE.md5" # macOS + shasum -a 1 "$POM_FILE" | cut -d' ' -f1 > "$POM_FILE.sha1" +fi + +echo "đŸ“Ļ Creating bundle" +rm -f "$BUNDLE" +(cd "$WORK_DIR" && zip -qr "$BUNDLE" com) +echo " $BUNDLE" + +if [ -n "${MAVEN_CENTRAL_USERNAME:-}" ] && [ -n "${MAVEN_CENTRAL_PASSWORD:-}" ]; then + echo "🚀 Uploading to Central Portal (USER_MANAGED — release it in the Portal UI)" + # tr guards against GNU base64's 76-char line wrapping on Linux + TOKEN=$(printf '%s:%s' "$MAVEN_CENTRAL_USERNAME" "$MAVEN_CENTRAL_PASSWORD" | base64 | tr -d '\n') + DEPLOYMENT_ID=$(curl --fail --silent --show-error \ + --request POST \ + --header "Authorization: Bearer $TOKEN" \ + --form "bundle=@$BUNDLE" \ + "https://central.sonatype.com/api/v1/publisher/upload?name=$OLD_ARTIFACT_ID-$RELOCATION_VERSION-relocation&publishingType=USER_MANAGED") + echo " Deployment ID: $DEPLOYMENT_ID" + echo " Review and publish at: https://central.sonatype.com/publishing/deployments" +else + echo "â„šī¸ MAVEN_CENTRAL_USERNAME/PASSWORD not set — skipping upload." + echo " Upload the bundle manually: central.sonatype.com -> Publish -> Upload Component," + echo " or re-run with credentials set." +fi