diff --git a/.github/workflows/publish-maven.yml b/.github/workflows/publish-maven.yml new file mode 100644 index 00000000..d4712b57 --- /dev/null +++ b/.github/workflows/publish-maven.yml @@ -0,0 +1,128 @@ +name: Publish to Maven Central + +on: + workflow_dispatch: + inputs: + publishing_type: + description: 'Publishing type for the deployment' + required: true + default: 'user_managed' + type: choice + options: + - user_managed + - automatic + version_override: + description: 'Version to publish (leave empty to use gradle.properties VERSION_NAME)' + required: false + type: string + +permissions: + contents: read + +jobs: + publish: + name: Publish to Maven Central Portal + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '17' + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Setup Gradle + uses: gradle/gradle-build-action@v2 + + - name: Configure signing + env: + SIGNING_KEY: ${{ secrets.SIGNING_KEY }} + SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }} + run: | + echo "signing.keyId=${{ secrets.SIGNING_KEY_ID }}" >> ~/.gradle/gradle.properties + echo "signing.password=$SIGNING_PASSWORD" >> ~/.gradle/gradle.properties + echo "signing.secretKeyRingFile=$HOME/.gnupg/secring.gpg" >> ~/.gradle/gradle.properties + mkdir -p ~/.gnupg + echo "$SIGNING_KEY" | base64 --decode > ~/.gnupg/secring.gpg + + - name: Configure Portal credentials + env: + CENTRAL_PORTAL_TOKEN: ${{ secrets.CENTRAL_PORTAL_TOKEN }} + CENTRAL_PORTAL_PASSWORD: ${{ secrets.CENTRAL_PORTAL_PASSWORD }} + run: | + echo "centralPortalToken=$CENTRAL_PORTAL_TOKEN" >> ~/.gradle/gradle.properties + echo "centralPortalPassword=$CENTRAL_PORTAL_PASSWORD" >> ~/.gradle/gradle.properties + + - name: Override version if specified + if: inputs.version_override != '' + run: | + sed -i "s/VERSION_NAME=.*/VERSION_NAME=${{ inputs.version_override }}/" gradle.properties + echo "Updated version to: ${{ inputs.version_override }}" + + - name: Build artifacts + run: | + ./gradlew clean build + ./gradlew androidJavadocsJar androidSourcesJar + + - name: Publish to Maven Central Portal + run: ./gradlew publishRelease + + - name: Upload to Maven Central Portal + env: + CENTRAL_PORTAL_TOKEN: ${{ secrets.CENTRAL_PORTAL_TOKEN }} + CENTRAL_PORTAL_PASSWORD: ${{ secrets.CENTRAL_PORTAL_PASSWORD }} + run: | + # Get the namespace from gradle.properties + NAMESPACE=$(grep "^GROUP=" gradle.properties | cut -d'=' -f2) + + # Create Bearer auth token (as per docs) + AUTH_TOKEN=$(echo -n "$CENTRAL_PORTAL_TOKEN:$CENTRAL_PORTAL_PASSWORD" | base64) + + # Call the manual upload endpoint + RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \ + -H "Authorization: Bearer $AUTH_TOKEN" \ + "https://ossrh-staging-api.central.sonatype.com/manual/upload/defaultRepository/${NAMESPACE}?publishing_type=${{ inputs.publishing_type }}") + + HTTP_CODE=$(echo "$RESPONSE" | tail -n 1) + BODY=$(echo "$RESPONSE" | sed '$d') + + if [ "$HTTP_CODE" != "200" ] && [ "$HTTP_CODE" != "201" ] && [ "$HTTP_CODE" != "204" ]; then + echo "Error: Portal upload failed with HTTP $HTTP_CODE" + echo "Response: $BODY" + exit 1 + fi + + echo "Success! Deployment uploaded to Portal." + + - name: Summary + run: | + VERSION=$(grep "^VERSION_NAME=" gradle.properties | cut -d'=' -f2) + echo "## Publishing Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "- **Version**: $VERSION" >> $GITHUB_STEP_SUMMARY + echo "- **Publishing Type**: ${{ inputs.publishing_type }}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + if [ "${{ inputs.publishing_type }}" == "user_managed" ]; then + echo "### Next Steps" >> $GITHUB_STEP_SUMMARY + echo "1. Go to [Maven Central Portal Deployments](https://central.sonatype.com/publishing/deployments)" >> $GITHUB_STEP_SUMMARY + echo "2. Find your deployment (version $VERSION)" >> $GITHUB_STEP_SUMMARY + echo "3. Review and click 'Publish' to release to Maven Central" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "⏱️ **Timeline**:" >> $GITHUB_STEP_SUMMARY + echo "- Validation: 15-30 minutes" >> $GITHUB_STEP_SUMMARY + echo "- Portal search: 1-2 hours" >> $GITHUB_STEP_SUMMARY + echo "- Maven Central sync: 2-4 hours" >> $GITHUB_STEP_SUMMARY + else + echo "### Status" >> $GITHUB_STEP_SUMMARY + echo "✅ Deployment will be automatically released if validation passes" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "⏱️ **Timeline**:" >> $GITHUB_STEP_SUMMARY + echo "- Validation and release: 15-30 minutes" >> $GITHUB_STEP_SUMMARY + echo "- Portal search: 1-2 hours" >> $GITHUB_STEP_SUMMARY + echo "- Maven Central sync: 2-4 hours" >> $GITHUB_STEP_SUMMARY + fi \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4113652d..69541556 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -69,4 +69,8 @@ jobs: with: tag_name: ${{ github.ref }} release_name: Release ${{ github.ref }} - body: ${{ steps.generate-release-changelog.outputs.changelog }} + body: | + ${{ steps.generate-release-changelog.outputs.changelog }} + + --- + **Note**: After this GitHub release is created, the Maven Central deployment must be manually released at https://central.sonatype.com/publishing/deployments diff --git a/CLAUDE.md b/CLAUDE.md index 5e0e507c..ec584360 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -84,6 +84,62 @@ The project uses semantic versioning (X.Y.Z) and publishes to Maven Central: - Release script: `./release.sh [version]` - Published as: `com.mixpanel.android:mixpanel-android:X.Y.Z` +### Automated Release Process +The `release.sh` script handles the complete release workflow: +1. Updates version in gradle.properties and README.md +2. Builds and publishes artifacts to OSSRH staging +3. Automatically uploads to Maven Central Portal (requires env vars) +4. Creates git tag and updates documentation +5. Updates to next snapshot version + +**Required Environment Variables**: +```bash +export CENTRAL_PORTAL_TOKEN= +export CENTRAL_PORTAL_PASSWORD= +``` + +### Maven Central Portal Setup + +The SDK publishes via the new Maven Central Portal: + +1. **Generate Portal Tokens**: + - Log in to https://central.sonatype.com with your OSSRH credentials + - Navigate to your account settings + - Generate a user token (username and password pair) + - Store these securely in `~/.gradle/gradle.properties`: + ``` + centralPortalToken= + centralPortalPassword= + ``` + - **Security Note**: For enhanced security, consider using encrypted storage options instead of plain text: + - Environment variables: `export CENTRAL_PORTAL_TOKEN=...` + - gradle-credentials-plugin for encrypted storage + - System keychain integration (e.g., macOS Keychain, Windows Credential Store) + - CI/CD secret management systems + +2. **Publishing Process**: + - Artifacts are uploaded to OSSRH staging API: `https://ossrh-staging-api.central.sonatype.com/` + - The Portal upload is triggered via the manual API endpoint + - Deployments appear at https://central.sonatype.com/publishing/deployments + - Manual release to Maven Central is required from the Portal UI (unless using automatic publishing) + +3. **GitHub Actions**: + - Use the `publish-maven.yml` workflow for automated publishing + - Portal tokens should be stored as repository secrets: + - `CENTRAL_PORTAL_TOKEN` (the token username) + - `CENTRAL_PORTAL_PASSWORD` (the token password) + - Publishing types available: + - `user_managed`: Manual release from Portal UI (default) + - `automatic`: Auto-release if validation passes + +4. **Manual Portal Upload** (if automation fails): + ```bash + AUTH_TOKEN=$(echo -n "username:password" | base64) + curl -X POST \ + -H "Authorization: Bearer $AUTH_TOKEN" \ + "https://ossrh-staging-api.central.sonatype.com/manual/upload/defaultRepository/com.mixpanel.android?publishing_type=user_managed" + ``` + ## Project Configuration - Min SDK: 21 @@ -141,7 +197,6 @@ The project uses semantic versioning (X.Y.Z) and publishes to Maven Central: - Prepared statements for performance - Automatic cleanup based on data age -For detailed patterns and examples, see: -- `.claude/context/discovered-patterns.md` -- `.claude/context/architecture/system-design.md` -- `.claude/context/workflows/` \ No newline at end of file +## Memories + +- Ensured CLAUDE.md accurately reflects the most recent changes \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 8de6f691..7a96dbe2 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -VERSION_NAME=8.2.0 +VERSION_NAME=8.2.1-SNAPSHOT POM_PACKAGING=aar GROUP=com.mixpanel.android @@ -15,8 +15,8 @@ POM_DEVELOPER_ID=mixpanel_dev POM_DEVELOPER_NAME=Mixpanel Developers POM_DEVELOPER_EMAIL=dev+android@mixpanel.com -RELEASE_REPOSITORY_URL=https://oss.sonatype.org/service/local/staging/deploy/maven2/ -SNAPSHOT_REPOSITORY_URL=https://oss.sonatype.org/content/repositories/snapshots/ +RELEASE_REPOSITORY_URL=https://ossrh-staging-api.central.sonatype.com/service/local/staging/deploy/maven2/ +SNAPSHOT_REPOSITORY_URL=https://central.sonatype.com/repository/maven-snapshots/ org.gradle.jvmargs=-Xmx1536M android.useAndroidX=true diff --git a/maven.gradle b/maven.gradle index c852bd7b..5035c83d 100644 --- a/maven.gradle +++ b/maven.gradle @@ -123,14 +123,9 @@ publishing { repositories { maven { - url = uri(RELEASE_REPOSITORY_URL) - credentials { - username = getRepositoryUsername() - password = getRepositoryPassword() - } - } - maven { - url = uri(SNAPSHOT_REPOSITORY_URL) + def releasesRepoUrl = uri(RELEASE_REPOSITORY_URL) + def snapshotsRepoUrl = uri(SNAPSHOT_REPOSITORY_URL) + url = version.endsWith('SNAPSHOT') ? snapshotsRepoUrl : releasesRepoUrl credentials { username = getRepositoryUsername() password = getRepositoryPassword() @@ -140,6 +135,7 @@ publishing { } signing { + required { !version.endsWith("SNAPSHOT") && gradle.taskGraph.hasTask("publishReleasePublicationToMavenRepository") } sign publishing.publications.release } @@ -201,9 +197,17 @@ tasks.named('signReleasePublication') { } def getRepositoryUsername() { + // Support both old OSSRH credentials and new Portal tokens + if (hasProperty('centralPortalToken')) { + return centralPortalToken + } return hasProperty('sonatypeUsername') ? sonatypeUsername : "" } def getRepositoryPassword() { + // Support both old OSSRH credentials and new Portal tokens + if (hasProperty('centralPortalPassword')) { + return centralPortalPassword + } return hasProperty('sonatypePassword') ? sonatypePassword : "" } diff --git a/release.sh b/release.sh old mode 100755 new mode 100644 index ee1c5c3a..55e8f11a --- a/release.sh +++ b/release.sh @@ -128,6 +128,39 @@ if ! ./gradlew publishRelease ; then abort fi +# Upload to Maven Central Portal +printf "\n${YELLOW}Uploading to Maven Central Portal...${NC}\n" + +# Check for Portal credentials +if [ -z "$CENTRAL_PORTAL_TOKEN" ] || [ -z "$CENTRAL_PORTAL_PASSWORD" ]; then + printf "${RED}Error: CENTRAL_PORTAL_TOKEN and CENTRAL_PORTAL_PASSWORD environment variables must be set${NC}\n" + printf "${ORANGE}Please set these variables and run the manual upload command:\n" + printf "curl -X POST -H \"Authorization: Bearer \$(echo -n \"TOKEN:PASSWORD\" | base64)\" \\\n" + printf " \"https://ossrh-staging-api.central.sonatype.com/manual/upload/defaultRepository/com.mixpanel.android?publishing_type=user_managed\"${NC}\n\n" + abort +fi + +# Create auth token +AUTH_TOKEN=$(echo -n "$CENTRAL_PORTAL_TOKEN:$CENTRAL_PORTAL_PASSWORD" | base64) + +# Upload to Portal +RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \ + -H "Authorization: Bearer $AUTH_TOKEN" \ + "https://ossrh-staging-api.central.sonatype.com/manual/upload/defaultRepository/com.mixpanel.android?publishing_type=user_managed") + +HTTP_CODE=$(echo "$RESPONSE" | tail -n 1) +BODY=$(echo "$RESPONSE" | sed '$d') + +if [ "$HTTP_CODE" != "200" ] && [ "$HTTP_CODE" != "201" ] && [ "$HTTP_CODE" != "204" ]; then + printf "${RED}Error: Portal upload failed with HTTP $HTTP_CODE${NC}\n" + printf "${ORANGE}Response: $BODY${NC}\n" + printf "\n${ORANGE}The artifacts were published to staging, but not uploaded to the Portal.\n" + printf "You can manually upload using the command above.${NC}\n\n" + abort +fi + +printf "${GREEN}Success! Deployment uploaded to Portal.${NC}\n" + read -r -p "Continue pushing to github? [y/n]: " key if ! [[ "$key" =~ ^([yY][eE][sS]|[yY])+$ ]]; then abort @@ -137,7 +170,7 @@ fi printf "\n\n${YELLOW}Pushing changes...${NC}\n" git commit -am "New release: $releaseVersion" # push changes -git push origin $releaseBranchx +git push origin $releaseBranch # create new tag newTag=v$releaseVersion @@ -155,6 +188,10 @@ git add . git commit -m "Update documentation for $releaseVersion" git push origin gh-pages +printf "\n${YELLOW}Checking out $releaseBranch to update snapshot version...${NC}\n" +git checkout $releaseBranch +git pull origin $releaseBranch + # update next snapshot version printf "\n${YELLOW}Updating next snapshot version...${NC}\n" sed -i.bak 's,^\(VERSION_NAME=\).*,\1'$nextSnapshotVersion',' gradle.properties @@ -164,7 +201,7 @@ printf '\n\n\n' read -r -p "Does this look right to you and the github action 'Release' has finished? [y/n]: " key if [[ "$key" =~ ^([yY][eE][sS]|[yY])+$ ]]; then git pull - git commit -am "Update master with next snasphot version $nextSnapshotVersion" + git commit -am "Update master with next snapshot version $nextSnapshotVersion" git push origin master else printf "${ORANGE}Make sure to update gradle.properties manually.${NC}\n" @@ -178,6 +215,6 @@ cleanUp printf "\n${GREEN}All done! ¯\_(ツ)_/¯ \n" printf "Make sure you make a new release at https://github.com/mixpanel/mixpanel-android/releases/new\n" printf "Also, do not forget to update our CHANGELOG (https://github.com/mixpanel/mixpanel-android/wiki/Changelog)\n" -printf "And finally, release the library from https://oss.sonatype.org/index.html\n\n${NC}" +printf "And finally, release the library from https://central.sonatype.com/publishing/deployments\n\n${NC}" quit