diff --git a/.github/workflows/publish_android_maven_central.yml b/.github/workflows/publish_android_maven_central.yml new file mode 100644 index 0000000..3a349c9 --- /dev/null +++ b/.github/workflows/publish_android_maven_central.yml @@ -0,0 +1,49 @@ +name: Publish Capacitor Plugin to Maven Central + +on: + workflow_call: + secrets: + ANDROID_CENTRAL_USERNAME: + required: true + ANDROID_CENTRAL_PASSWORD: + required: true + ANDROID_SIGNING_KEY_ID: + required: true + ANDROID_SIGNING_PASSWORD: + required: true + ANDROID_SIGNING_KEY: + required: true + ANDROID_SONATYPE_STAGING_PROFILE_ID: + required: true + CAP_GH_RELEASE_TOKEN: + required: true + workflow_dispatch: + +jobs: + publish-android: + if: github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + timeout-minutes: 60 + permissions: + contents: read + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + token: ${{ secrets.CAP_GH_RELEASE_TOKEN }} + - name: set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'zulu' + - name: Grant execute permission for publishing script + run: chmod +x ./scripts/publish-android.sh + - name: Run publish script + env: + ANDROID_CENTRAL_USERNAME: ${{ secrets.ANDROID_CENTRAL_USERNAME }} + ANDROID_CENTRAL_PASSWORD: ${{ secrets.ANDROID_CENTRAL_PASSWORD }} + ANDROID_SIGNING_KEY_ID: ${{ secrets.ANDROID_SIGNING_KEY_ID }} + ANDROID_SIGNING_PASSWORD: ${{ secrets.ANDROID_SIGNING_PASSWORD }} + ANDROID_SIGNING_KEY: ${{ secrets.ANDROID_SIGNING_KEY }} + ANDROID_SONATYPE_STAGING_PROFILE_ID: ${{ secrets.ANDROID_SONATYPE_STAGING_PROFILE_ID }} + run: ./scripts/publish-android.sh diff --git a/.github/workflows/publish_ios_cocoapods_trunk.yml b/.github/workflows/publish_ios_cocoapods_trunk.yml new file mode 100644 index 0000000..069616b --- /dev/null +++ b/.github/workflows/publish_ios_cocoapods_trunk.yml @@ -0,0 +1,38 @@ +name: Publish Capacitor Plugin to CocoaPods Trunk + +on: + workflow_call: + secrets: + COCOAPODS_TRUNK_TOKEN: + required: true + CAP_GH_RELEASE_TOKEN: + required: true + workflow_dispatch: + +jobs: + publish-ios: + runs-on: macos-15 + if: github.ref == 'refs/heads/main' + timeout-minutes: 30 + steps: + - run: sudo xcode-select --switch /Applications/Xcode_16.app + - run: xcrun simctl list > /dev/null + - run: xcodebuild -downloadPlatform iOS + - uses: actions/setup-node@v4 + with: + node-version: 20 + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + token: ${{ secrets.CAP_GH_RELEASE_TOKEN }} + - name: Install Cocoapods + run: | + gem install cocoapods + - name: Grant execute permission for publishing script + run: chmod +x ./scripts/publish-ios.sh + - name: Deploy to Cocoapods + env: + COCOAPODS_TRUNK_TOKEN: ${{ secrets.COCOAPODS_TRUNK_TOKEN }} + run: | + set -eo pipefail + ./scripts/publish-ios.sh diff --git a/.github/workflows/release_plugin.yml b/.github/workflows/release_plugin.yml index ef00a2b..819e6ac 100644 --- a/.github/workflows/release_plugin.yml +++ b/.github/workflows/release_plugin.yml @@ -47,4 +47,23 @@ jobs: GITHUB_TOKEN: ${{ secrets.CAP_GH_RELEASE_TOKEN }} GH_TOKEN: ${{ secrets.CAP_GH_RELEASE_TOKEN }} NPM_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} - run: npx semantic-release \ No newline at end of file + run: npx semantic-release + + publish-android: + needs: ['release'] + uses: ./.github/workflows/publish_android_maven_central.yml + secrets: + ANDROID_CENTRAL_USERNAME: ${{ secrets.ANDROID_CENTRAL_USERNAME }} + ANDROID_CENTRAL_PASSWORD: ${{ secrets.ANDROID_CENTRAL_PASSWORD }} + ANDROID_SIGNING_KEY_ID: ${{ secrets.ANDROID_SIGNING_KEY_ID }} + ANDROID_SIGNING_KEY: ${{ secrets.ANDROID_SIGNING_KEY }} + ANDROID_SIGNING_PASSWORD: ${{ secrets.ANDROID_SIGNING_PASSWORD }} + ANDROID_SONATYPE_STAGING_PROFILE_ID: ${{ secrets.ANDROID_SONATYPE_STAGING_PROFILE_ID }} + CAP_GH_RELEASE_TOKEN: ${{ secrets.CAP_GH_RELEASE_TOKEN }} + + publish-ios: + needs: ['release'] + uses: ./.github/workflows/publish_ios_cocoapods_trunk.yml + secrets: + COCOAPODS_TRUNK_TOKEN: ${{ secrets.COCOAPODS_TRUNK_TOKEN }} + CAP_GH_RELEASE_TOKEN: ${{ secrets.CAP_GH_RELEASE_TOKEN }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index df9f0c2..1505600 100644 --- a/.gitignore +++ b/.gitignore @@ -68,3 +68,6 @@ captures # External native build folder generated in Android Studio 2.2 and later .externalNativeBuild + +# Maven Central / CocoaPods Trunk release logs +tmp/*.txt \ No newline at end of file diff --git a/android/build.gradle b/android/build.gradle index 1301eb7..1f58175 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -25,8 +25,8 @@ buildscript { apply plugin: 'com.android.library' if (System.getenv("CAP_PLUGIN_PUBLISH") == "true") { apply plugin: 'io.github.gradle-nexus.publish-plugin' - apply from: file('../../scripts/android/publish-root.gradle') - apply from: file('../../scripts/android/publish-module.gradle') + apply from: file('../scripts/android/publish-root.gradle') + apply from: file('../scripts/android/publish-module.gradle') } android { diff --git a/scripts/android/publish-module.gradle b/scripts/android/publish-module.gradle new file mode 100644 index 0000000..730394d --- /dev/null +++ b/scripts/android/publish-module.gradle @@ -0,0 +1,82 @@ +apply plugin: 'maven-publish' +apply plugin: 'signing' + +def LIB_VERSION = System.getenv('PLUGIN_VERSION') +def PLUGIN_NAME = System.getenv('PLUGIN_NAME') +def PLUGIN_REPO = System.getenv('PLUGIN_REPO') +def PLUGIN_SCM = System.getenv('PLUGIN_SCM') + +task androidSourcesJar(type: Jar) { + archiveClassifier.set('sources') + if (project.plugins.findPlugin("com.android.library")) { + from android.sourceSets.main.java.srcDirs + from android.sourceSets.main.kotlin.srcDirs + } else { + from sourceSets.main.java.srcDirs + from sourceSets.main.kotlin.srcDirs + } +} + +artifacts { + archives androidSourcesJar +} + +group = 'com.capacitorjs' +version = LIB_VERSION + +afterEvaluate { + publishing { + publications { + release(MavenPublication) { + // Coordinates + groupId 'com.capacitorjs' + artifactId PLUGIN_NAME + version LIB_VERSION + + // Two artifacts, the `aar` (or `jar`) and the sources + if (project.plugins.findPlugin("com.android.library")) { + from components.release + } else { + artifact("$buildDir/libs/${project.getName()}-${version}.jar") + } + + artifact androidSourcesJar + + // POM Data + pom { + name = PLUGIN_NAME + description = 'Capacitor Android ' + PLUGIN_NAME + ' plugin native library' + url = PLUGIN_REPO + licenses { + license { + name = 'MIT' + url = PLUGIN_REPO + '/blob/main' + '/LICENSE' + } + } + developers { + developer { + name = 'Ionic' + email = 'hi@ionic.io' + } + } + + // Version Control Info + scm { + connection = 'scm:git:' + PLUGIN_SCM + '.git' + developerConnection = 'scm:git:ssh://' + PLUGIN_SCM + '.git' + url = PLUGIN_REPO + '/tree/main' + } + } + } + } + } +} + +signing { + useInMemoryPgpKeys( + rootProject.ext["signing.keyId"], + rootProject.ext["signing.key"], + rootProject.ext["signing.password"], + ) + sign publishing.publications +} \ No newline at end of file diff --git a/scripts/android/publish-root.gradle b/scripts/android/publish-root.gradle new file mode 100644 index 0000000..2d1f030 --- /dev/null +++ b/scripts/android/publish-root.gradle @@ -0,0 +1,43 @@ +// Create variables with empty default values +ext["signing.keyId"] = '' +ext["signing.key"] = '' +ext["signing.password"] = '' +ext["centralTokenUsername"] = '' +ext["centralTokenPassword"] = '' +ext["sonatypeStagingProfileId"] = '' + +File globalSecretPropsFile = file('../../scripts/android/local.properties') +File secretPropsFile = project.rootProject.file('local.properties') +if (globalSecretPropsFile.exists()) { + // Read global local.properties file first if it exists (scripts/android/local.properties) + Properties p = new Properties() + new FileInputStream(globalSecretPropsFile).withCloseable { is -> p.load(is) } + p.each { name, value -> ext[name] = value } +} else if (secretPropsFile.exists()) { + // Read plugin project specific local.properties file next if it exists + Properties p = new Properties() + new FileInputStream(secretPropsFile).withCloseable { is -> p.load(is) } + p.each { name, value -> ext[name] = value } +} else { + // Use system environment variables + ext["centralTokenUsername"] = System.getenv('ANDROID_CENTRAL_USERNAME') + ext["centralTokenPassword"] = System.getenv('ANDROID_CENTRAL_PASSWORD') + ext["sonatypeStagingProfileId"] = System.getenv('ANDROID_SONATYPE_STAGING_PROFILE_ID') + ext["signing.keyId"] = System.getenv('ANDROID_SIGNING_KEY_ID') + ext["signing.key"] = System.getenv('ANDROID_SIGNING_KEY') + ext["signing.password"] = System.getenv('ANDROID_SIGNING_PASSWORD') +} + +// Set up Sonatype repository +nexusPublishing { + repositories { + sonatype { + stagingProfileId = sonatypeStagingProfileId + username = centralTokenUsername + password = centralTokenPassword + nexusUrl.set(uri("https://ossrh-staging-api.central.sonatype.com/service/local/")) + snapshotRepositoryUrl.set(uri("https://central.sonatype.com/repository/maven-snapshots/")) + } + } + repositoryDescription = 'Capacitor Android ' + System.getenv('PLUGIN_NAME') + ' plugin v' + System.getenv('PLUGIN_VERSION') +} \ No newline at end of file diff --git a/scripts/publish-android.sh b/scripts/publish-android.sh new file mode 100755 index 0000000..70a65c6 --- /dev/null +++ b/scripts/publish-android.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash + +publish_plugin_android () { + PLUGIN_PATH=$1 + if [ -d "$PLUGIN_PATH" ]; then + # Android dir path + ANDROID_PATH=$PLUGIN_PATH/android + GRADLE_FILE=$ANDROID_PATH/build.gradle + + # Only try to publish if the directory contains a package.json and android package + if test -f "$PLUGIN_PATH/package.json" && test -d "$ANDROID_PATH" && test -f "$GRADLE_FILE"; then + PLUGIN_VERSION=$(grep '"version": ' "$PLUGIN_PATH"/package.json | awk '{print $2}' | tr -d '",') + PLUGIN_NAME=$(grep '"name": ' "$PLUGIN_PATH"/package.json | awk '{print $2}' | tr -d '",') + PLUGIN_NAME=${PLUGIN_NAME#@capacitor/} + LOG_OUTPUT=./tmp/$PLUGIN_NAME.txt + + # Get latest plugin info from MavenCentral + PLUGIN_PUBLISHED_URL="https://repo1.maven.org/maven2/com/capacitorjs/$PLUGIN_NAME/maven-metadata.xml" + PLUGIN_PUBLISHED_DATA=$(curl -s $PLUGIN_PUBLISHED_URL) + PLUGIN_PUBLISHED_VERSION="$(perl -ne 'print and last if s/.*(.*)<\/latest>.*/\1/;' <<< $PLUGIN_PUBLISHED_DATA)" + + if [[ $PLUGIN_VERSION == $PLUGIN_PUBLISHED_VERSION ]]; then + printf %"s\n\n" "Duplicate: a published plugin $PLUGIN_NAME exists for version $PLUGIN_VERSION, skipping..." + else + # Make log dir if doesnt exist + mkdir -p ./tmp + + printf %"s\n" "Attempting to build and publish plugin $PLUGIN_NAME for version $PLUGIN_VERSION to production..." + + # Export ENV variables used by Gradle for the plugin + export PLUGIN_NAME + export PLUGIN_VERSION + export CAPACITOR_VERSION + export CAP_PLUGIN_PUBLISH=true + export PLUGIN_REPO="https://github.com/ionic-team/capacitor-keyboard" + export PLUGIN_SCM="github.com:ionic-team/capacitor-keyboard" + + # Build and publish + "$ANDROID_PATH"/gradlew clean build publishReleasePublicationToSonatypeRepository closeAndReleaseSonatypeStagingRepository --no-daemon --max-workers 1 -b "$ANDROID_PATH"/build.gradle -Pandroid.useAndroidX=true > $LOG_OUTPUT 2>&1 + + if grep --quiet "BUILD SUCCESSFUL" $LOG_OUTPUT; then + printf %"s\n\n" "Success: $PLUGIN_NAME published to MavenCentral." + else + printf %"s\n\n" "Error publishing $PLUGIN_NAME, check $LOG_OUTPUT for more info! Manually review and release from the Central Portal may be necessary https://central.sonatype.com/publishing/deployments/" + cat $LOG_OUTPUT + exit 1 + fi + fi + else + printf %"s\n\n" "$PLUGIN_PATH does not appear to be a plugin (has no package.json file or Android package), skipping..." + fi + fi +} + +# Get latest com.capacitorjs:core XML version info +CAPACITOR_PUBLISHED_URL="https://repo1.maven.org/maven2/com/capacitorjs/core/maven-metadata.xml" +CAPACITOR_PUBLISHED_DATA=$(curl -s $CAPACITOR_PUBLISHED_URL) +CAPACITOR_PUBLISHED_VERSION="$(perl -ne 'print and last if s/.*(.*)<\/latest>.*/\1/;' <<< $CAPACITOR_PUBLISHED_DATA)" + +printf %"s\n" "The latest published Android library version of Capacitor Core is $CAPACITOR_PUBLISHED_VERSION in MavenCentral." + +# Determine Capacitor Version to use as gradle dependency. +STABLE_PART=$(echo "$CAPACITOR_PUBLISHED_VERSION" | cut -d'-' -f1) +IFS='.' read -r MAJOR MINOR PATCH <<< "$STABLE_PART" +if [[ "$CAPACITOR_PUBLISHED_VERSION" == *"-"* ]]; then + # prerelease - go one major lower (latest stable major), but also allow next upcoming major + PREV_MAJOR=$((MAJOR - 1)) + NEXT_MAJOR=$((MAJOR + 1)) + CAPACITOR_VERSION="[$PREV_MAJOR.0,$NEXT_MAJOR.0)" +else + # stable - current major range + NEXT_MAJOR=$((MAJOR + 1)) + CAPACITOR_VERSION="[$MAJOR.0,$NEXT_MAJOR.0)" +fi +printf %"s\n" "Publishing plugin with dependency on Capacitor version $CAPACITOR_VERSION" + +publish_plugin_android '.' diff --git a/scripts/publish-ios.sh b/scripts/publish-ios.sh new file mode 100755 index 0000000..80d4e7f --- /dev/null +++ b/scripts/publish-ios.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash + +publish_plugin_ios () { + PLUGIN_PATH=$1 + # Only try to publish if the directory contains a package.json podspec file + if ! test -f "$PLUGIN_PATH/package.json"; then + printf %"s\n\n" "$PLUGIN_PATH does not appear to be a plugin (has no package.json), skipping..." + return + fi + + PLUGIN_VERSION=$(grep '"version": ' "$PLUGIN_PATH"/package.json | awk '{print $2}' | tr -d '",') + PLUGIN_NAME=$(grep '"name": ' "$PLUGIN_PATH"/package.json | awk '{print $2}' | tr -d '",') + PLUGIN_NAME=${PLUGIN_NAME#@capacitor/} + # capitalize the name, because .podspec file name is capitalized + first_char=$(printf '%s' "$PLUGIN_NAME" | cut -c1 | tr '[:lower:]' '[:upper:]') + rest=$(printf '%s' "$PLUGIN_NAME" | cut -c2-) + PLUGIN_NAME="${first_char}${rest}" + POD_NAME="Capacitor$PLUGIN_NAME" + PODSPEC_FILE_PATH="$PLUGIN_PATH/$POD_NAME.podspec" + if ! test -f $PODSPEC_FILE_PATH; then + printf %"s\n\n" "Was looking for podspec file $PODSPEC_FILE_PATH, but does not seem to exist, skipping..." + return + fi + + # check if version already exists in Trunk + if pod trunk info "$POD_NAME" 2>/dev/null | grep -q " - $PLUGIN_VERSION"; then + printf %"s\n\n" "Duplicate: a published plugin $PLUGIN_NAME exists for version $PLUGIN_VERSION, skipping..." + return + fi + + LOG_OUTPUT=./tmp/$PLUGIN_NAME.txt + # Make log dir if doesnt exist + mkdir -p ./tmp + # publish to Trunk + printf %"s\n" "Attempting to build and publish plugin $PLUGIN_NAME for version $PLUGIN_VERSION to production..." + pod trunk push $PODSPEC_FILE_PATH --allow-warnings > $LOG_OUTPUT 2>&1 + if grep -q "passed" $LOG_OUTPUT; then + printf %"s\n\n" "Success: $PLUGIN_NAME published to CocoaPods Trunk." + else + printf %"s\n\n" "Error publishing $PLUGIN_NAME, check $LOG_OUTPUT for more info!" + cat $LOG_OUTPUT + exit 1 + fi +} + +publish_plugin_ios '.' \ No newline at end of file