[#992][#1025] Custom Google Play publishing

* [#992][#1025] Custom Google Play publishing

- Copy logic from working demo
- Documentation
- Update checkProperties task
- Update release listing
- Status completed
- Switch back to main branch
- Closes #992
- Closes #1025

* Deployment documentation

* Remove deprecated Gradle Publisher plugin

* Improve logging

Improve publishing task logging

* Resolve review comments

* Increase PR actions timeouts
This commit is contained in:
Honza Rychnovský 2023-11-13 11:50:57 +01:00 committed by GitHub
parent 6df3806a1f
commit 70d5721845
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 528 additions and 84 deletions

View File

@ -1,6 +1,8 @@
# Expected secrets # Expected secrets
# GOOGLE_PLAY_CLOUD_PROJECT - Google Cloud project associated with Google Play # GOOGLE_PLAY_CLOUD_PROJECT - Google Cloud project associated with Google Play
# GOOGLE_PLAY_SERVICE_ACCOUNT - Email address of service account # GOOGLE_PLAY_SERVICE_ACCOUNT - Email address of service account
# GOOGLE_PLAY_SERVICE_ACCOUNT_KEY - Google Play Service Account key to authorize on Google Play
# GOOGLE_PLAY_PUBLISHER_API_KEY - Google Play Publisher API key to authorize the publisher on Google Play API
# GOOGLE_PLAY_WORKLOAD_IDENTITY_PROVIDER - Workload identity provider to generate temporary service account key # GOOGLE_PLAY_WORKLOAD_IDENTITY_PROVIDER - Workload identity provider to generate temporary service account key
# UPLOAD_KEYSTORE_BASE_64 - The upload signing key for the app # UPLOAD_KEYSTORE_BASE_64 - The upload signing key for the app
# UPLOAD_KEYSTORE_PASSWORD - The password for UPLOAD_KEYSTORE_BASE_64 # UPLOAD_KEYSTORE_PASSWORD - The password for UPLOAD_KEYSTORE_BASE_64
@ -54,9 +56,17 @@ jobs:
- id: check_secrets - id: check_secrets
env: env:
GOOGLE_PLAY_CLOUD_PROJECT: ${{ secrets.GOOGLE_PLAY_CLOUD_PROJECT }} GOOGLE_PLAY_CLOUD_PROJECT: ${{ secrets.GOOGLE_PLAY_CLOUD_PROJECT }}
GOOGLE_PLAY_SERVICE_ACCOUNT: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT }} # TODO [#1033]: Use token-based authorization on Google Play for automated deployment
GOOGLE_PLAY_WORKLOAD_IDENTITY_PROVIDER: ${{ secrets.GOOGLE_PLAY_WORKLOAD_IDENTITY_PROVIDER }} # TODO [#1033]: https://github.com/zcash/secant-android-wallet/issues/1033
if: "${{ env.GOOGLE_PLAY_CLOUD_PROJECT != '' && env.GOOGLE_PLAY_SERVICE_ACCOUNT != '' && env.GOOGLE_PLAY_WORKLOAD_IDENTITY_PROVIDER != '' }}" # Note that these properties are not currently used due to #1033
# GOOGLE_PLAY_SERVICE_ACCOUNT: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT }}
# GOOGLE_PLAY_WORKLOAD_IDENTITY_PROVIDER: ${{ secrets.GOOGLE_PLAY_WORKLOAD_IDENTITY_PROVIDER }}
GOOGLE_PLAY_SERVICE_ACCOUNT_KEY: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_KEY }}
GOOGLE_PLAY_PUBLISHER_API_KEY: ${{ secrets.GOOGLE_PLAY_PUBLISHER_API_KEY }}
if: "${{ env.GOOGLE_PLAY_CLOUD_PROJECT != '' &&
env.GOOGLE_PLAY_SERVICE_ACCOUNT_KEY != '' &&
env.GOOGLE_PLAY_PUBLISHER_API_KEY != ''
}}"
run: echo "defined=true" >> $GITHUB_OUTPUT run: echo "defined=true" >> $GITHUB_OUTPUT
build_and_deploy: build_and_deploy:
@ -71,6 +81,9 @@ jobs:
- name: Checkout - name: Checkout
timeout-minutes: 1 timeout-minutes: 1
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11
with:
ref: main
fetch-depth: 0 # To fetch all commits
- name: Set up Java - name: Set up Java
uses: actions/setup-java@0ab4596768b603586c0de567f2430c30f5b0d2b0 uses: actions/setup-java@0ab4596768b603586c0de567f2430c30f5b0d2b0
timeout-minutes: 1 timeout-minutes: 1
@ -94,6 +107,10 @@ jobs:
echo ${FIREBASE_DEBUG_JSON_BASE64} | base64 --decode > app/src/debug/google-services.json echo ${FIREBASE_DEBUG_JSON_BASE64} | base64 --decode > app/src/debug/google-services.json
echo ${FIREBASE_RELEASE_JSON_BASE64} | base64 --decode > app/src/release/google-services.json echo ${FIREBASE_RELEASE_JSON_BASE64} | base64 --decode > app/src/release/google-services.json
- name: Authenticate to Google Cloud for Google Play - name: Authenticate to Google Cloud for Google Play
# TODO [#1033]: Use token-based authorization on Google Play for automated deployment
# TODO [#1033]: https://github.com/zcash/secant-android-wallet/issues/1033
# Note that this step is not currently used due to #1033
if: false
id: auth_google_play id: auth_google_play
uses: google-github-actions/auth@35b0e87d162680511bf346c299f71c9c5c379033 uses: google-github-actions/auth@35b0e87d162680511bf346c299f71c9c5c379033
with: with:
@ -117,14 +134,21 @@ jobs:
timeout-minutes: 25 timeout-minutes: 25
env: env:
ORG_GRADLE_PROJECT_ZCASH_SUPPORT_EMAIL_ADDRESS: ${{ vars.SUPPORT_EMAIL_ADDRESS }} ORG_GRADLE_PROJECT_ZCASH_SUPPORT_EMAIL_ADDRESS: ${{ vars.SUPPORT_EMAIL_ADDRESS }}
ORG_GRADLE_PROJECT_ZCASH_GOOGLE_PLAY_SERVICE_KEY_FILE_PATH: ${{ steps.auth_google_play.outputs.credentials_file_path }} # TODO [#1033]: Use token-based authorization on Google Play for automated deployment
# TODO [#1033]: https://github.com/zcash/secant-android-wallet/issues/1033
# Note that these properties are not currently used due to #1033
# ORG_GRADLE_PROJECT_ZCASH_GOOGLE_PLAY_SERVICE_ACCOUNT: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT }}
# ORG_GRADLE_PROJECT_ZCASH_GOOGLE_PLAY_SERVICE_KEY_FILE_PATH: ${{ steps.auth_google_play.outputs.credentials_file_path }}
ORG_GRADLE_PROJECT_ZCASH_GOOGLE_PLAY_SERVICE_ACCOUNT_KEY: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_KEY }}
ORG_GRADLE_PROJECT_ZCASH_GOOGLE_PLAY_PUBLISHER_API_KEY: ${{ secrets.GOOGLE_PLAY_PUBLISHER_API_KEY }}
ORG_GRADLE_PROJECT_ZCASH_GOOGLE_PLAY_DEPLOY_TRACK: internal
ORG_GRADLE_PROJECT_ZCASH_GOOGLE_PLAY_DEPLOY_STATUS: completed
ORG_GRADLE_PROJECT_ZCASH_RELEASE_KEYSTORE_PATH: ${{ format('{0}/release.jks', env.home) }} ORG_GRADLE_PROJECT_ZCASH_RELEASE_KEYSTORE_PATH: ${{ format('{0}/release.jks', env.home) }}
ORG_GRADLE_PROJECT_ZCASH_RELEASE_KEYSTORE_PASSWORD: ${{ secrets.UPLOAD_KEYSTORE_PASSWORD }} ORG_GRADLE_PROJECT_ZCASH_RELEASE_KEYSTORE_PASSWORD: ${{ secrets.UPLOAD_KEYSTORE_PASSWORD }}
ORG_GRADLE_PROJECT_ZCASH_RELEASE_KEY_ALIAS: ${{ secrets.UPLOAD_KEY_ALIAS }} ORG_GRADLE_PROJECT_ZCASH_RELEASE_KEY_ALIAS: ${{ secrets.UPLOAD_KEY_ALIAS }}
ORG_GRADLE_PROJECT_ZCASH_RELEASE_KEY_ALIAS_PASSWORD: ${{ secrets.UPLOAD_KEY_ALIAS_PASSWORD }} ORG_GRADLE_PROJECT_ZCASH_RELEASE_KEY_ALIAS_PASSWORD: ${{ secrets.UPLOAD_KEY_ALIAS_PASSWORD }}
ORG_GRADLE_PROJECT_ZCASH_GOOGLE_PLAY_DEPLOY_MODE: deploy
run: | run: |
./gradlew :app:assembleDebug :app:publishBundle :app:packageZcashmainnetReleaseUniversalApk ./gradlew :app:publishToGooglePlay
- name: Collect Artifacts - name: Collect Artifacts
timeout-minutes: 1 timeout-minutes: 1
env: env:
@ -133,11 +157,11 @@ jobs:
MAPPINGS_ZIP_PATH: ${{ format('{0}/artifacts/mappings.zip', env.home) }} MAPPINGS_ZIP_PATH: ${{ format('{0}/artifacts/mappings.zip', env.home) }}
run: | run: |
mkdir ${ARTIFACTS_DIR_PATH} mkdir ${ARTIFACTS_DIR_PATH}
zip -r ${BINARIES_ZIP_PATH} . -i app/build/outputs/apk/\*/\*.apk app/build/outputs/universal_apk/\*/\*.apk app/build/outputs/bundle/\*/\*.aab zip -r ${BINARIES_ZIP_PATH} . -i app/build/outputs/apk/\*/\*.apk app/build/outputs/bundle/\*/\*.aab
zip -r ${MAPPINGS_ZIP_PATH} . -i app/build/outputs/mapping/\*/mapping.txt zip -r ${MAPPINGS_ZIP_PATH} . -i app/build/outputs/mapping/\*/mapping.txt
- name: Upload Artifacts - name: Upload Artifacts
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32
timeout-minutes: 1 timeout-minutes: 5
with: with:
name: Binaries name: Binaries
path: ~/artifacts path: ~/artifacts

View File

@ -105,7 +105,7 @@ jobs:
uses: gradle/gradle-build-action@842c587ad8aa4c68eeba24c396e15af4c2e9f30a uses: gradle/gradle-build-action@842c587ad8aa4c68eeba24c396e15af4c2e9f30a
timeout-minutes: 5 timeout-minutes: 5
- name: Detekt - name: Detekt
timeout-minutes: 4 timeout-minutes: 10
run: | run: |
./gradlew detektAll ./gradlew detektAll
- name: Collect Artifacts - name: Collect Artifacts
@ -228,7 +228,7 @@ jobs:
uses: gradle/gradle-build-action@842c587ad8aa4c68eeba24c396e15af4c2e9f30a uses: gradle/gradle-build-action@842c587ad8aa4c68eeba24c396e15af4c2e9f30a
timeout-minutes: 5 timeout-minutes: 5
- name: Test - name: Test
timeout-minutes: 4 timeout-minutes: 10
run: | run: |
# Note that we explicitly check just the Kotlin modules, to avoid compiling the Android modules here # Note that we explicitly check just the Kotlin modules, to avoid compiling the Android modules here
./gradlew :configuration-api-lib:check :crash-lib:check :preference-api-lib:check :spackle-lib:check ./gradlew :configuration-api-lib:check :crash-lib:check :preference-api-lib:check :spackle-lib:check

View File

@ -1,3 +1,4 @@
import co.electriccoin.zcash.Git
import com.android.build.api.variant.BuildConfigField import com.android.build.api.variant.BuildConfigField
import com.android.build.api.variant.ResValue import com.android.build.api.variant.ResValue
import java.util.Locale import java.util.Locale
@ -6,10 +7,10 @@ plugins {
id("com.android.application") id("com.android.application")
kotlin("android") kotlin("android")
id("secant.android-build-conventions") id("secant.android-build-conventions")
id("com.github.triplet.play")
id("com.osacky.fladle") id("com.osacky.fladle")
id("wtf.emulator.gradle") id("wtf.emulator.gradle")
id("secant.emulator-wtf-conventions") id("secant.emulator-wtf-conventions")
id("secant.publish-conventions")
} }
val hasFirebaseApiKeys = run { val hasFirebaseApiKeys = run {
@ -158,12 +159,6 @@ android {
resValue("string", "support_email_address", supportEmailAddress) resValue("string", "support_email_address", supportEmailAddress)
} }
playConfigs {
register(testNetFlavorName) {
enabled.set(false)
}
}
testCoverage { testCoverage {
jacocoVersion = project.property("JACOCO_VERSION").toString() jacocoVersion = project.property("JACOCO_VERSION").toString()
} }
@ -202,8 +197,6 @@ dependencies {
} }
} }
val googlePlayServiceKeyFilePath = project.property("ZCASH_GOOGLE_PLAY_SERVICE_KEY_FILE_PATH").toString()
androidComponents { androidComponents {
onVariants { variant -> onVariants { variant ->
for (output in variant.outputs) { for (output in variant.outputs) {
@ -223,7 +216,9 @@ androidComponents {
ResValue(value = hasFirebaseApiKeys.toString()) ResValue(value = hasFirebaseApiKeys.toString())
) )
if (googlePlayServiceKeyFilePath.isNotEmpty()) { if (project.property("ZCASH_GOOGLE_PLAY_SERVICE_ACCOUNT_KEY").toString().isNotEmpty() &&
project.property("ZCASH_GOOGLE_PLAY_PUBLISHER_API_KEY").toString().isNotEmpty()
) {
// Update the versionName to reflect bumps in versionCode // Update the versionName to reflect bumps in versionCode
val versionCodeOffset = 0 // Change this to zero the final digit of the versionName val versionCodeOffset = 0 // Change this to zero the final digit of the versionName
@ -243,6 +238,9 @@ androidComponents {
} }
output.versionName.set(processedVersionCode) output.versionName.set(processedVersionCode)
val gitInfo = Git.newInfo(Git.MAIN, parent!!.projectDir)
output.versionCode.set(gitInfo.commitCount)
} }
} }
@ -276,27 +274,6 @@ androidComponents {
} }
} }
if (googlePlayServiceKeyFilePath.isNotEmpty()) {
configure<com.github.triplet.gradle.play.PlayPublisherExtension> {
serviceAccountCredentials.set(File(googlePlayServiceKeyFilePath))
// For safety, only allow deployment to internal testing track
track.set("internal")
// Automatically manage version incrementing
resolutionStrategy.set(com.github.triplet.gradle.androidpublisher.ResolutionStrategy.AUTO)
val deployMode = project.property("ZCASH_GOOGLE_PLAY_DEPLOY_MODE").toString()
if ("build" == deployMode) {
releaseStatus.set(com.github.triplet.gradle.androidpublisher.ReleaseStatus.DRAFT)
// Prevent upload; only generates a build with the correct version number
commit.set(false)
} else if ("deploy" == deployMode) {
releaseStatus.set(com.github.triplet.gradle.androidpublisher.ReleaseStatus.COMPLETED)
}
}
}
fladle { fladle {
// Firebase Test Lab has min and max values that might differ from our project's // Firebase Test Lab has min and max values that might differ from our project's
// These are determined by `gcloud firebase test android models list` // These are determined by `gcloud firebase test android models list`

View File

@ -41,6 +41,10 @@ dependencies {
val rootProperties = getRootProperties() val rootProperties = getRootProperties()
implementation("com.android.tools.build:gradle:${rootProperties.getProperty("ANDROID_GRADLE_PLUGIN_VERSION")}") implementation("com.android.tools.build:gradle:${rootProperties.getProperty("ANDROID_GRADLE_PLUGIN_VERSION")}")
implementation("com.google.apis:google-api-services-androidpublisher:${rootProperties.getProperty
("PLAY_PUBLISHER_API_VERSION")}")
implementation("com.google.auth:google-auth-library-oauth2-http:${rootProperties.getProperty
("GOOGLE_AUTH_LIB_JAVA_VERSION")}")
implementation("io.gitlab.arturbosch.detekt:detekt-gradle-plugin:${rootProperties.getProperty("DETEKT_VERSION")}") implementation("io.gitlab.arturbosch.detekt:detekt-gradle-plugin:${rootProperties.getProperty("DETEKT_VERSION")}")
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:${rootProperties.getProperty("KOTLIN_VERSION")}") implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:${rootProperties.getProperty("KOTLIN_VERSION")}")
implementation("wtf.emulator:gradle-plugin:${rootProperties.getProperty("EMULATOR_WTF_GRADLE_PLUGIN_VERSION")}") implementation("wtf.emulator:gradle-plugin:${rootProperties.getProperty("EMULATOR_WTF_GRADLE_PLUGIN_VERSION")}")

View File

@ -45,19 +45,27 @@ com.android.tools:sdklib:31.1.1=runtimeClasspath
com.android:signflinger:8.1.1=runtimeClasspath com.android:signflinger:8.1.1=runtimeClasspath
com.android:zipflinger:8.1.1=compileClasspath,runtimeClasspath com.android:zipflinger:8.1.1=compileClasspath,runtimeClasspath
com.google.android:annotations:4.1.1.4=runtimeClasspath com.google.android:annotations:4.1.1.4=runtimeClasspath
com.google.api-client:google-api-client:2.2.0=compileClasspath,runtimeClasspath
com.google.api.grpc:proto-google-common-protos:2.0.1=runtimeClasspath com.google.api.grpc:proto-google-common-protos:2.0.1=runtimeClasspath
com.google.auto.value:auto-value-annotations:1.6.2=runtimeClasspath com.google.apis:google-api-services-androidpublisher:v3-rev20231030-2.0.0=compileClasspath,runtimeClasspath
com.google.code.findbugs:jsr305:3.0.2=runtimeClasspath com.google.auth:google-auth-library-credentials:1.18.0=compileClasspath,runtimeClasspath
com.google.code.gson:gson:2.8.9=runtimeClasspath com.google.auth:google-auth-library-oauth2-http:1.18.0=compileClasspath,runtimeClasspath
com.google.auto.value:auto-value-annotations:1.10.1=compileClasspath,runtimeClasspath
com.google.code.findbugs:jsr305:3.0.2=compileClasspath,runtimeClasspath
com.google.code.gson:gson:2.10=compileClasspath,runtimeClasspath
com.google.crypto.tink:tink:1.7.0=runtimeClasspath com.google.crypto.tink:tink:1.7.0=runtimeClasspath
com.google.dagger:dagger:2.28.3=runtimeClasspath com.google.dagger:dagger:2.28.3=runtimeClasspath
com.google.errorprone:error_prone_annotations:2.11.0=runtimeClasspath com.google.errorprone:error_prone_annotations:2.16=compileClasspath,runtimeClasspath
com.google.flatbuffers:flatbuffers-java:1.12.0=runtimeClasspath com.google.flatbuffers:flatbuffers-java:1.12.0=runtimeClasspath
com.google.guava:failureaccess:1.0.1=runtimeClasspath com.google.guava:failureaccess:1.0.1=compileClasspath,runtimeClasspath
com.google.guava:guava:31.1-jre=runtimeClasspath com.google.guava:guava:31.1-jre=compileClasspath,runtimeClasspath
com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=runtimeClasspath com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=compileClasspath,runtimeClasspath
com.google.j2objc:j2objc-annotations:1.3=runtimeClasspath com.google.http-client:google-http-client-apache-v2:1.42.3=compileClasspath,runtimeClasspath
com.google.http-client:google-http-client-gson:1.42.3=compileClasspath,runtimeClasspath
com.google.http-client:google-http-client:1.42.3=compileClasspath,runtimeClasspath
com.google.j2objc:j2objc-annotations:1.3=compileClasspath,runtimeClasspath
com.google.jimfs:jimfs:1.1=runtimeClasspath com.google.jimfs:jimfs:1.1=runtimeClasspath
com.google.oauth-client:google-oauth-client:1.34.1=compileClasspath,runtimeClasspath
com.google.protobuf:protobuf-java-util:3.19.3=runtimeClasspath com.google.protobuf:protobuf-java-util:3.19.3=runtimeClasspath
com.google.protobuf:protobuf-java:3.19.3=runtimeClasspath com.google.protobuf:protobuf-java:3.19.3=runtimeClasspath
com.google.testing.platform:core-proto:0.0.8-alpha08=runtimeClasspath com.google.testing.platform:core-proto:0.0.8-alpha08=runtimeClasspath
@ -68,11 +76,12 @@ com.sun.activation:javax.activation:1.2.0=runtimeClasspath
com.sun.istack:istack-commons-runtime:3.0.8=runtimeClasspath com.sun.istack:istack-commons-runtime:3.0.8=runtimeClasspath
com.sun.xml.fastinfoset:FastInfoset:1.2.16=runtimeClasspath com.sun.xml.fastinfoset:FastInfoset:1.2.16=runtimeClasspath
com.vdurmont:semver4j:3.1.0=runtimeClasspath com.vdurmont:semver4j:3.1.0=runtimeClasspath
commons-codec:commons-codec:1.11=runtimeClasspath commons-codec:commons-codec:1.15=compileClasspath,runtimeClasspath
commons-io:commons-io:2.12.0=runtimeClasspath commons-io:commons-io:2.12.0=runtimeClasspath
commons-logging:commons-logging:1.2=runtimeClasspath commons-logging:commons-logging:1.2=compileClasspath,runtimeClasspath
io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.23.0=compileClasspath,runtimeClasspath io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.23.0=compileClasspath,runtimeClasspath
io.grpc:grpc-api:1.45.1=runtimeClasspath io.grpc:grpc-api:1.45.1=runtimeClasspath
io.grpc:grpc-context:1.27.2=compileClasspath
io.grpc:grpc-context:1.45.1=runtimeClasspath io.grpc:grpc-context:1.45.1=runtimeClasspath
io.grpc:grpc-core:1.45.1=runtimeClasspath io.grpc:grpc-core:1.45.1=runtimeClasspath
io.grpc:grpc-netty:1.45.1=runtimeClasspath io.grpc:grpc-netty:1.45.1=runtimeClasspath
@ -90,6 +99,8 @@ io.netty:netty-handler:4.1.72.Final=runtimeClasspath
io.netty:netty-resolver:4.1.72.Final=runtimeClasspath io.netty:netty-resolver:4.1.72.Final=runtimeClasspath
io.netty:netty-tcnative-classes:2.0.46.Final=runtimeClasspath io.netty:netty-tcnative-classes:2.0.46.Final=runtimeClasspath
io.netty:netty-transport:4.1.72.Final=runtimeClasspath io.netty:netty-transport:4.1.72.Final=runtimeClasspath
io.opencensus:opencensus-api:0.31.1=compileClasspath,runtimeClasspath
io.opencensus:opencensus-contrib-http-util:0.31.1=compileClasspath,runtimeClasspath
io.perfmark:perfmark-api:0.23.0=runtimeClasspath io.perfmark:perfmark-api:0.23.0=runtimeClasspath
jakarta.activation:jakarta.activation-api:1.2.1=runtimeClasspath jakarta.activation:jakarta.activation-api:1.2.1=runtimeClasspath
jakarta.xml.bind:jakarta.xml.bind-api:2.3.2=runtimeClasspath jakarta.xml.bind:jakarta.xml.bind-api:2.3.2=runtimeClasspath
@ -100,13 +111,13 @@ net.java.dev.jna:jna:5.6.0=runtimeClasspath
net.sf.jopt-simple:jopt-simple:4.9=runtimeClasspath net.sf.jopt-simple:jopt-simple:4.9=runtimeClasspath
net.sf.kxml:kxml2:2.3.0=runtimeClasspath net.sf.kxml:kxml2:2.3.0=runtimeClasspath
org.apache.commons:commons-compress:1.21=runtimeClasspath org.apache.commons:commons-compress:1.21=runtimeClasspath
org.apache.httpcomponents:httpclient:4.5.13=runtimeClasspath org.apache.httpcomponents:httpclient:4.5.14=compileClasspath,runtimeClasspath
org.apache.httpcomponents:httpcore:4.4.15=runtimeClasspath org.apache.httpcomponents:httpcore:4.4.16=compileClasspath,runtimeClasspath
org.apache.httpcomponents:httpmime:4.5.6=runtimeClasspath org.apache.httpcomponents:httpmime:4.5.6=runtimeClasspath
org.bitbucket.b_c:jose4j:0.7.0=runtimeClasspath org.bitbucket.b_c:jose4j:0.7.0=runtimeClasspath
org.bouncycastle:bcpkix-jdk15on:1.67=runtimeClasspath org.bouncycastle:bcpkix-jdk15on:1.67=runtimeClasspath
org.bouncycastle:bcprov-jdk15on:1.67=runtimeClasspath org.bouncycastle:bcprov-jdk15on:1.67=runtimeClasspath
org.checkerframework:checker-qual:3.12.0=runtimeClasspath org.checkerframework:checker-qual:3.12.0=compileClasspath,runtimeClasspath
org.codehaus.mojo:animal-sniffer-annotations:1.19=runtimeClasspath org.codehaus.mojo:animal-sniffer-annotations:1.19=runtimeClasspath
org.glassfish.jaxb:jaxb-runtime:2.3.2=runtimeClasspath org.glassfish.jaxb:jaxb-runtime:2.3.2=runtimeClasspath
org.glassfish.jaxb:txw2:2.3.2=runtimeClasspath org.glassfish.jaxb:txw2:2.3.2=runtimeClasspath

View File

@ -0,0 +1,389 @@
import com.android.build.gradle.internal.tasks.factory.dependsOn
import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport
import com.google.api.client.http.AbstractInputStreamContent
import com.google.api.client.http.FileContent
import com.google.api.client.http.HttpBackOffUnsuccessfulResponseHandler
import com.google.api.client.http.HttpRequest
import com.google.api.client.http.HttpTransport
import com.google.api.client.http.apache.v2.ApacheHttpTransport
import com.google.api.client.http.javanet.NetHttpTransport
import com.google.api.client.json.JsonFactory
import com.google.api.client.json.gson.GsonFactory
import com.google.api.client.util.ExponentialBackOff
import com.google.api.services.androidpublisher.AndroidPublisher
import com.google.api.services.androidpublisher.AndroidPublisherRequestInitializer
import com.google.api.services.androidpublisher.AndroidPublisherScopes
import com.google.api.services.androidpublisher.model.AppEdit
import com.google.api.services.androidpublisher.model.Bundle
import com.google.api.services.androidpublisher.model.Track
import com.google.api.services.androidpublisher.model.TrackRelease
import com.google.auth.http.HttpCredentialsAdapter
import com.google.auth.oauth2.GoogleCredentials
import org.apache.http.auth.AuthScope
import org.apache.http.auth.UsernamePasswordCredentials
import org.apache.http.impl.client.BasicCredentialsProvider
import org.apache.http.impl.client.ProxyAuthenticationStrategy
import java.io.FileInputStream
import java.io.IOException
import java.security.GeneralSecurityException
import java.security.KeyStore
@CacheableTask
abstract class PublishToGooglePlay @Inject constructor(
private val gpServiceAccountKey: String,
private val gpPublisherApiKey: String,
private val track: String,
private val status: String
) : DefaultTask() {
// Note that we need to have all the necessary custom task properties part of the task (i.e. no external
// dependencies allowed) to avoid:
// PublishToGooglePlay is a non-static inner class.
init {
description = "Publish universal Zcash wallet apk to Google Play release channel." // $NON-NLS-1$
group = "publishing" // $NON-NLS-1$
}
private fun log(message: String) {
println("${PublishToGooglePlay::class.java.name}: $message")
}
// Global instance of the JSON factory
private val jsonFactory: JsonFactory by lazy {
GsonFactory.getDefaultInstance()
}
// Global instance of the HTTP transport
@get:Throws(GeneralSecurityException::class, IOException::class)
private val trustedTransport: HttpTransport by lazy {
buildTransport()
}
/**
* Prepares a new trusted [HttpTransport] object to authorize [AndroidPublisher] on Google Play Publish API.
*/
private fun buildTransport(): HttpTransport {
val trustStore: String? = System.getProperty("javax.net.ssl.trustStore", null)
val trustStorePassword: String? =
System.getProperty("javax.net.ssl.trustStorePassword", null)
return if (trustStore == null) {
createHttpTransport()
} else {
val ks = KeyStore.getInstance(KeyStore.getDefaultType())
FileInputStream(trustStore).use { fis ->
ks.load(fis, trustStorePassword?.toCharArray())
}
NetHttpTransport.Builder().trustCertificates(ks).build()
}
}
private fun createHttpTransport(): HttpTransport {
val protocols = arrayOf("https", "http")
for (protocol in protocols) {
val proxyHost = System.getProperty("$protocol.proxyHost")
val proxyUser = System.getProperty("$protocol.proxyUser")
val proxyPassword = System.getProperty("$protocol.proxyPassword")
if (proxyHost != null && proxyUser != null && proxyPassword != null) {
val defaultProxyPort = if (protocol == "http") "80" else "443"
val proxyPort = Integer.parseInt(System.getProperty("$protocol.proxyPort", defaultProxyPort))
val credentials = BasicCredentialsProvider()
credentials.setCredentials(
AuthScope(proxyHost, proxyPort),
UsernamePasswordCredentials(proxyUser, proxyPassword)
)
val httpClient = ApacheHttpTransport.newDefaultHttpClientBuilder()
.setProxyAuthenticationStrategy(ProxyAuthenticationStrategy.INSTANCE)
.setDefaultCredentialsProvider(credentials)
.build()
return ApacheHttpTransport(httpClient)
}
}
return GoogleNetHttpTransport.newTrustedTransport()
}
private class AndroidPublisherAdapter(
credential: GoogleCredentials,
) : HttpCredentialsAdapter(credential) {
override fun initialize(request: HttpRequest) {
val backOffHandler = HttpBackOffUnsuccessfulResponseHandler(
ExponentialBackOff.Builder()
.setMaxElapsedTimeMillis(TimeUnit.MINUTES.toMillis(3).toInt())
.build()
)
super.initialize(
request.setReadTimeout(0)
.setUnsuccessfulResponseHandler(backOffHandler)
)
}
}
/**
* Build service account credential using secret service json key file.
*
* @return OAuth credential for the given service key file path
* @throws IOException in case an incorrect key file path is provided or the credential cannot be created from
* the stream
*/
@Throws(IOException::class)
private fun getCredentialFromServiceKeyFile(serviceKey: String): GoogleCredentials {
log("Authorizing using non-empty service key: ${serviceKey.isNotEmpty()}")
return GoogleCredentials.fromStream(serviceKey.byteInputStream())
.also {
it.createScoped(listOf(AndroidPublisherScopes.ANDROIDPUBLISHER))
}
}
/**
* Prepares API communication service and returns [AndroidPublisher] upon which API requests can be performed. This
* operation performs all the necessary setup steps for running the requests.
*
* @param applicationName The package name of the application, e.g.: com.example.app
* @param serviceAccountKey The service account key for the API communication authorization
* @param publisherApiKey The Google Play Publisher API key for the API communication authorization
* @return The {@Link AndroidPublisher} service
*/
private fun initService(
applicationName: String,
serviceAccountKey: String,
publisherApiKey: String
): AndroidPublisher {
log("Initializing Google Play communication for: $applicationName")
// Running authorization
val credential = getCredentialFromServiceKeyFile(serviceAccountKey)
val httpInitializer = AndroidPublisherAdapter(credential)
// Set up and return API client
return AndroidPublisher.Builder(
trustedTransport,
jsonFactory,
httpInitializer
)
.setApplicationName(applicationName)
.setAndroidPublisherRequestInitializer(AndroidPublisherRequestInitializer(publisherApiKey))
.build()
}
@Throws(IllegalStateException::class, IOException::class, GeneralSecurityException::class)
@Suppress("LongMethod")
private fun runPublish(
track: String,
status: String,
serviceAccountKey: String,
publisherApiKey: String
) {
val packageName = project.property("ZCASH_RELEASE_PACKAGE_NAME").toString()
// Walk through the build directory and find the prepared release aab file
val apkFile = File("app/build/outputs/bundle/").walk()
.filter { it.name.endsWith("release.aab") }
.firstOrNull() ?: error("Universal release apk not found")
log("Publish - APK found: ${apkFile.name}")
val apkFileContent: AbstractInputStreamContent = FileContent(
"application/octet-stream", // APK file type
apkFile
)
// Create the Google Play API service for communication
val service: AndroidPublisher = initService(
packageName,
serviceAccountKey,
publisherApiKey
)
val edits: AndroidPublisher.Edits = service.edits()
// Create a new edit to make changes to the existing listing
val editRequest: AndroidPublisher.Edits.Insert = edits
.insert(
packageName,
null // Intentionally no content provided
)
log("Publish - Edits request: $editRequest")
val edit: AppEdit = editRequest.execute()
log("Publish - Edits excute: $edit")
val editId: String = edit.id
log("Publish - Edit with id: $editId")
val uploadRequest: AndroidPublisher.Edits.Bundles.Upload = edits
.bundles()
.upload(
packageName,
editId,
apkFileContent
)
val bundle: Bundle = uploadRequest.execute()
// Version code
val bundleVersionCodes: MutableList<Long> = ArrayList()
bundleVersionCodes.add(bundle.versionCode.toLong())
// Version name
val gradleVersionName = project.property("ZCASH_VERSION_NAME").toString()
val versionName = "$gradleVersionName (${bundle.versionCode.toLong()}): Automated Internal Testing Release"
log("Publish - Version: $versionName has been uploaded")
// Assign bundle to the selected track
val updateTrackRequest: AndroidPublisher.Edits.Tracks.Update = edits
.tracks()
.update(
packageName,
editId,
track,
Track().setReleases(
listOf(TrackRelease()
.setName(versionName)
.setVersionCodes(bundleVersionCodes)
.setStatus(status)
)
)
)
val updatedTrack: Track = updateTrackRequest.execute()
log("Track ${updatedTrack.track} has been updated")
// Commit changes for edit
val commitRequest: AndroidPublisher.Edits.Commit = edits.commit(
packageName,
editId
)
val appEdit: AppEdit = commitRequest.execute()
log("App edit with id ${appEdit.id} has been committed")
}
@TaskAction
fun runTask() {
log("Publish starting for track: $track and status: $status")
runPublish(
track,
status,
gpServiceAccountKey,
gpPublisherApiKey
)
log("Publishing done")
}
}
/**
* The release track identifier. This class also serves as a type-safe custom task input validation.
*/
enum class PublishTrack {
INTERNAL, // Internal testing track
ALPHA, // Closed testing track
BETA, // Open testing track. Note that use of this track is not supported by this task.
PRODUCTION; // Production track. Note that use of this track is not supported by this task.
companion object {
@Throws(IllegalArgumentException::class)
fun new(identifier: String): PublishTrack {
// Throws IllegalArgumentException if the specified name does not match any of the defined enum constants
return values().find { it.name.lowercase() == identifier }
?: throw IllegalArgumentException("Unsupported enum value: $identifier")
}
}
@Throws(IllegalStateException::class)
fun toGooglePlayIdentifier(): String {
return when (this) {
INTERNAL -> "internal" // $NON-NLS-1$
ALPHA -> "alpha" // $NON-NLS-1$
BETA, PRODUCTION -> error("For security reasons, this script does not support the $this option. Promote " +
"the app manually from a lower testing channel instead.")
}
}
}
/**
* The status of a release. This class also serves as a type-safe custom task input validation.
*/
enum class PublishStatus {
STATUS_UNSPECIFIED, // Unspecified status.
DRAFT, // The release's APKs are not being served to users.
IN_PROGRESS, // The release's APKs are being served to a fraction of users, determined by 'userFraction'.
HALTED, // The release's APKs will no longer be served to users. Users who already have these APKs are unaffected.
COMPLETED; // The release will have no further changes. Its APKs are being served to all users, unless they are
// eligible to APKs of a more recent release.
companion object {
@Throws(IllegalArgumentException::class)
fun new(identifier: String): PublishStatus {
// Throws IllegalArgumentException if the specified name does not match any of the defined enum constants
return values().find { it.name.lowercase() == identifier }
?: throw IllegalArgumentException("Unsupported enum value: $identifier")
}
}
@Throws(IllegalStateException::class)
fun toGooglePlayIdentifier(): String {
return when (this) {
DRAFT -> "draft" // $NON-NLS-1$
COMPLETED -> "completed" // $NON-NLS-1$
STATUS_UNSPECIFIED, IN_PROGRESS, HALTED -> error("Not supported status: $this")
}
}
}
tasks {
// Validate Google Play Service Account KEY input
val googlePlayServiceAccountKey = project.property("ZCASH_GOOGLE_PLAY_SERVICE_ACCOUNT_KEY").toString()
if (googlePlayServiceAccountKey.isEmpty()) {
// The deployment will not run: service account key is empty
return@tasks
}
// Validate Google Play Publisher API KEY input
val googlePlayPublisherApiKey = project.property("ZCASH_GOOGLE_PLAY_PUBLISHER_API_KEY").toString()
if (googlePlayServiceAccountKey.isEmpty()) {
// The deployment will not run: publisher api key is empty
return@tasks
}
// Validate deploy track
val deployTrackString = project.property("ZCASH_GOOGLE_PLAY_DEPLOY_TRACK").toString()
val deployTrack = deployTrackString.let {
if (it.isEmpty()) {
// The deployment will not run: track empty
return@tasks
}
PublishTrack.new(it)
}
// Validate deploy status
val deployStatusString = project.property("ZCASH_GOOGLE_PLAY_DEPLOY_STATUS").toString()
val deployStatus = deployStatusString.let {
if (it.isEmpty()) {
// The deployment will not run: status empty
return@tasks
}
PublishStatus.new(it)
}
// The new task [publishToGooglePlay] runs [assembleDebug] and [bundleZcashmainnetRelease] as its
// dependencies.
// Note that we need to convert these Enums to Strings as enums are not assignable via Kotlin DSL to Gradle
// custom task, although it would be better to work with more type-safe Enums furthermore.
register<PublishToGooglePlay>(
"publishToGooglePlay", // $NON-NLS-1$
googlePlayServiceAccountKey,
googlePlayPublisherApiKey,
deployTrack.toGooglePlayIdentifier(),
deployStatus.toGooglePlayIdentifier()
)
.dependsOn(":app:assembleDebug")
.dependsOn(":app:bundleZcashmainnetRelease")
println("Automated deployment task registered - all the necessary attributes set")
}

View File

@ -1,3 +1,4 @@
import co.electriccoin.zcash.Git
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
@ -13,7 +14,10 @@ plugins {
val generateBuildConfigTask = tasks.create("buildConfig") { val generateBuildConfigTask = tasks.create("buildConfig") {
val generatedDir = layout.buildDirectory.dir("generated").get().asFile val generatedDir = layout.buildDirectory.dir("generated").get().asFile
val gitInfo = co.electriccoin.zcash.Git.newInfo(parent!!.projectDir) val gitInfo = co.electriccoin.zcash.Git.newInfo(
Git.HEAD,
parent!!.projectDir
)
//val buildTimestamp = newIso8601Timestamp() //val buildTimestamp = newIso8601Timestamp()
inputs.property("gitSha", gitInfo.sha) inputs.property("gitSha", gitInfo.sha)

View File

@ -113,8 +113,12 @@ tasks {
"IS_SIGN_RELEASE_BUILD_WITH_DEBUG_KEY" to "false", "IS_SIGN_RELEASE_BUILD_WITH_DEBUG_KEY" to "false",
"ZCASH_GOOGLE_PLAY_SERVICE_ACCOUNT" to "",
"ZCASH_GOOGLE_PLAY_SERVICE_ACCOUNT_KEY" to "",
"ZCASH_GOOGLE_PLAY_PUBLISHER_API_KEY" to "",
"ZCASH_GOOGLE_PLAY_SERVICE_KEY_FILE_PATH" to "", "ZCASH_GOOGLE_PLAY_SERVICE_KEY_FILE_PATH" to "",
"ZCASH_GOOGLE_PLAY_DEPLOY_MODE" to "build", "ZCASH_GOOGLE_PLAY_DEPLOY_TRACK" to "internal",
"ZCASH_GOOGLE_PLAY_DEPLOY_STATUS" to "draft",
"SDK_INCLUDED_BUILD_PATH" to "", "SDK_INCLUDED_BUILD_PATH" to "",
"BIP_39_INCLUDED_BUILD_PATH" to "" "BIP_39_INCLUDED_BUILD_PATH" to ""

View File

@ -6,13 +6,17 @@ import java.io.File
object Git { object Git {
// Get the info for the current branch // Get the info for the current branch
private const val HEAD = "HEAD" const val HEAD = "HEAD" // $NON-NLS-1$
const val MAIN = "main" // $NON-NLS-1$
fun newInfo(workingDirectory: File): GitInfo { fun newInfo(
branch: String,
workingDirectory: File
): GitInfo {
val git = Git.open(workingDirectory) val git = Git.open(workingDirectory)
val repository = git.repository val repository = git.repository
val head: ObjectId = repository.resolve(HEAD) val head: ObjectId = repository.resolve(branch)
val count = git.log().call().count() val count = git.log().call().count()
return GitInfo(ObjectId.toString(head), count) return GitInfo(ObjectId.toString(head), count)

View File

@ -49,13 +49,17 @@ com.github.ben-manes.versions:com.github.ben-manes.versions.gradle.plugin:0.47.0
com.github.ben-manes:gradle-versions-plugin:0.47.0=classpath com.github.ben-manes:gradle-versions-plugin:0.47.0=classpath
com.google.android.gms:strict-version-matcher-plugin:1.2.4=classpath com.google.android.gms:strict-version-matcher-plugin:1.2.4=classpath
com.google.android:annotations:4.1.1.4=classpath com.google.android:annotations:4.1.1.4=classpath
com.google.api-client:google-api-client:2.2.0=classpath
com.google.api.grpc:proto-google-common-protos:2.0.1=classpath com.google.api.grpc:proto-google-common-protos:2.0.1=classpath
com.google.auto.value:auto-value-annotations:1.6.2=classpath com.google.apis:google-api-services-androidpublisher:v3-rev20231030-2.0.0=classpath
com.google.auth:google-auth-library-credentials:1.18.0=classpath
com.google.auth:google-auth-library-oauth2-http:1.18.0=classpath
com.google.auto.value:auto-value-annotations:1.10.1=classpath
com.google.code.findbugs:jsr305:3.0.2=classpath com.google.code.findbugs:jsr305:3.0.2=classpath
com.google.code.gson:gson:2.8.9=classpath com.google.code.gson:gson:2.10=classpath
com.google.crypto.tink:tink:1.7.0=classpath com.google.crypto.tink:tink:1.7.0=classpath
com.google.dagger:dagger:2.28.3=classpath com.google.dagger:dagger:2.28.3=classpath
com.google.errorprone:error_prone_annotations:2.11.0=classpath com.google.errorprone:error_prone_annotations:2.16=classpath
com.google.firebase:firebase-crashlytics-buildtools:2.9.4=classpath com.google.firebase:firebase-crashlytics-buildtools:2.9.4=classpath
com.google.firebase:firebase-crashlytics-gradle:2.9.4=classpath com.google.firebase:firebase-crashlytics-gradle:2.9.4=classpath
com.google.flatbuffers:flatbuffers-java:1.12.0=classpath com.google.flatbuffers:flatbuffers-java:1.12.0=classpath
@ -63,8 +67,12 @@ com.google.gms:google-services:4.3.15=classpath
com.google.guava:failureaccess:1.0.1=classpath com.google.guava:failureaccess:1.0.1=classpath
com.google.guava:guava:31.1-jre=classpath com.google.guava:guava:31.1-jre=classpath
com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=classpath com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=classpath
com.google.http-client:google-http-client-apache-v2:1.42.3=classpath
com.google.http-client:google-http-client-gson:1.42.3=classpath
com.google.http-client:google-http-client:1.42.3=classpath
com.google.j2objc:j2objc-annotations:1.3=classpath com.google.j2objc:j2objc-annotations:1.3=classpath
com.google.jimfs:jimfs:1.1=classpath com.google.jimfs:jimfs:1.1=classpath
com.google.oauth-client:google-oauth-client:1.34.1=classpath
com.google.protobuf:protobuf-java-util:3.19.3=classpath com.google.protobuf:protobuf-java-util:3.19.3=classpath
com.google.protobuf:protobuf-java:3.19.3=classpath com.google.protobuf:protobuf-java:3.19.3=classpath
com.google.testing.platform:core-proto:0.0.8-alpha08=classpath com.google.testing.platform:core-proto:0.0.8-alpha08=classpath
@ -84,7 +92,7 @@ com.sun.istack:istack-commons-runtime:3.0.8=classpath
com.sun.xml.fastinfoset:FastInfoset:1.2.16=classpath com.sun.xml.fastinfoset:FastInfoset:1.2.16=classpath
com.thoughtworks.xstream:xstream:1.4.20=classpath com.thoughtworks.xstream:xstream:1.4.20=classpath
com.vdurmont:semver4j:3.1.0=classpath com.vdurmont:semver4j:3.1.0=classpath
commons-codec:commons-codec:1.11=classpath commons-codec:commons-codec:1.15=classpath
commons-io:commons-io:2.12.0=classpath commons-io:commons-io:2.12.0=classpath
commons-logging:commons-logging:1.2=classpath commons-logging:commons-logging:1.2=classpath
io.github.x-stream:mxparser:1.2.2=classpath io.github.x-stream:mxparser:1.2.2=classpath
@ -109,6 +117,8 @@ io.netty:netty-handler:4.1.72.Final=classpath
io.netty:netty-resolver:4.1.72.Final=classpath io.netty:netty-resolver:4.1.72.Final=classpath
io.netty:netty-tcnative-classes:2.0.46.Final=classpath io.netty:netty-tcnative-classes:2.0.46.Final=classpath
io.netty:netty-transport:4.1.72.Final=classpath io.netty:netty-transport:4.1.72.Final=classpath
io.opencensus:opencensus-api:0.31.1=classpath
io.opencensus:opencensus-contrib-http-util:0.31.1=classpath
io.perfmark:perfmark-api:0.23.0=classpath io.perfmark:perfmark-api:0.23.0=classpath
jakarta.activation:jakarta.activation-api:1.2.1=classpath jakarta.activation:jakarta.activation-api:1.2.1=classpath
jakarta.xml.bind:jakarta.xml.bind-api:2.3.2=classpath jakarta.xml.bind:jakarta.xml.bind-api:2.3.2=classpath
@ -119,8 +129,8 @@ net.java.dev.jna:jna:5.6.0=classpath
net.sf.jopt-simple:jopt-simple:4.9=classpath net.sf.jopt-simple:jopt-simple:4.9=classpath
net.sf.kxml:kxml2:2.3.0=classpath net.sf.kxml:kxml2:2.3.0=classpath
org.apache.commons:commons-compress:1.21=classpath org.apache.commons:commons-compress:1.21=classpath
org.apache.httpcomponents:httpclient:4.5.13=classpath org.apache.httpcomponents:httpclient:4.5.14=classpath
org.apache.httpcomponents:httpcore:4.4.15=classpath org.apache.httpcomponents:httpcore:4.4.16=classpath
org.apache.httpcomponents:httpmime:4.5.6=classpath org.apache.httpcomponents:httpmime:4.5.6=classpath
org.bitbucket.b_c:jose4j:0.7.0=classpath org.bitbucket.b_c:jose4j:0.7.0=classpath
org.bouncycastle:bcpkix-jdk15on:1.67=classpath org.bouncycastle:bcpkix-jdk15on:1.67=classpath

View File

@ -12,20 +12,21 @@ Note that although these are called "release" keys, they may actually be the "up
After signing is configured, it is possible to then configure deployment to Google Play. After signing is configured, it is possible to then configure deployment to Google Play.
## Automated Deployment ## Automated Deployment
Automated deployment to Google Play configured with the [Gradle Play Publisher plugin](https://github.com/Triple-T/gradle-play-publisher). Automated deployment to Google Play is configured with custom
To perform a deployment: [Google Play publishing Gradle task](../build-conventions-secant/src/main/kotlin/secant.publish-conventions.gradle.kts).
1. Configure a Google Cloud service API key with the correct permissions To perform a deployment with this task:
1. Configure a Google Cloud service account and API key with the correct permissions
1. Configure a Google Play Publishing API key in Google Cloud console
1. Configure Gradle properties 1. Configure Gradle properties
1. `ZCASH_GOOGLE_PLAY_SERVICE_KEY_FILE_PATH` - Set to the path of the service key in JSON format 1. `ZCASH_GOOGLE_PLAY_SERVICE_ACCOUNT_KEY` - Set the Google Play Service Account enabled in the Google Cloud console
1. `ZCASH_GOOGLE_PLAY_DEPLOY_MODE` - Set to `deploy` 1. `ZCASH_GOOGLE_PLAY_PUBLISHER_API_KEY` - Set the Google Play Publish API enabled in the Google Cloud console
1. `ZCASH_GOOGLE_PLAY_DEPLOY_TRACK` - Set to `internal` or `alpha`
1. `ZCASH_GOOGLE_PLAY_DEPLOY_STATUS` - Set to `draft` or `completed`
1. Run the Gradle task `./gradlew :app:publishBundle` 1. Run the Gradle task `./gradlew :app:publishBundle`
To generate a build with a correct version that can be deployed manually later: For more information about proper automated deployment setup, see
1. Configure a Google Cloud service API key with the correct permissions [Google Play publishing Gradle task](../build-conventions-secant/src/main/kotlin/secant.publish-conventions.gradle.kts)
1. Configure Gradle properties documentation and related [gradle.properties](../gradle.properties) attributes.
1. `ZCASH_GOOGLE_PLAY_SERVICE_KEY_FILE_PATH` - Set to the path of the service key in JSON format
1. `ZCASH_GOOGLE_PLAY_DEPLOY_MODE` - Set to `build` (this is the default value)
1. Run the Gradle tasks `./gradlew :app:processReleaseVersionCodes :app:bundleRelease`
Note that the above instructions are for repeat deployments. If you do not yet have an app listing, you'll need to create that manually. Note that the above instructions are for repeat deployments. If you do not yet have an app listing, you'll need to create that manually.

View File

@ -48,7 +48,7 @@ ZCASH_FIREBASE_TEST_LAB_PROJECT=
IS_MINIFY_ENABLED=true IS_MINIFY_ENABLED=true
# If ZCASH_GOOGLE_PLAY_SERVICE_KEY_FILE_PATH is set and the deployment task is triggered, then # If ZCASH_GOOGLE_PLAY_SERVICE_KEY_FILE_PATH is set and the deployment task is triggered, then
# VERSION_CODE is effectively ignored VERSION_NAME is suffixed with the version code. # VERSION_CODE is effectively ignored. VERSION_NAME is suffixed with the version code.
# If not using automated Google Play deployment, then these serve as the actual version numbers. # If not using automated Google Play deployment, then these serve as the actual version numbers.
ZCASH_VERSION_CODE=1 ZCASH_VERSION_CODE=1
ZCASH_VERSION_NAME=0.2.0 ZCASH_VERSION_NAME=0.2.0
@ -86,12 +86,28 @@ ZCASH_RELEASE_KEY_ALIAS_PASSWORD=
# the default debug key configuration. # the default debug key configuration.
IS_SIGN_RELEASE_BUILD_WITH_DEBUG_KEY=false IS_SIGN_RELEASE_BUILD_WITH_DEBUG_KEY=false
# Optionally set the Google Play Service Key path to enable deployment # Set the Google Play Service Account email address to enable deployment
# Note that this property is not currently used due to #1033
# TODO [#1033]: Use token-based authorization on Google Play for automated deployment
# TODO [#1033]: https://github.com/zcash/secant-android-wallet/issues/1033
ZCASH_GOOGLE_PLAY_SERVICE_ACCOUNT=
# Also, set the Google Play Service Key path to enable deployment. It's a path to the private key file (only used for
# Service Account auth).
# Note that this property is not currently used due to #1033
# TODO [#1033]: Use token-based authorization on Google Play for automated deployment
# TODO [#1033]: https://github.com/zcash/secant-android-wallet/issues/1033
ZCASH_GOOGLE_PLAY_SERVICE_KEY_FILE_PATH= ZCASH_GOOGLE_PLAY_SERVICE_KEY_FILE_PATH=
# Can be one of {build, deploy}. # Set the Google Play Service Account key to authorize on Google Play
# Build can be used to generate a version number for the next release, but does not ultimately create a release on Google Play. ZCASH_GOOGLE_PLAY_SERVICE_ACCOUNT_KEY=
# Deploy commits the build on Google Play, creating a new release # Set the Google Play Publisher API key to authorize the publisher on Google Play API
ZCASH_GOOGLE_PLAY_DEPLOY_MODE=build ZCASH_GOOGLE_PLAY_PUBLISHER_API_KEY=
# Can be one of {internal, alpha}. There are more of them {beta, production}, which are not supported for security
# reasons. Internal will deploy into the Internal and Alpha into the Closed testing tracks on Google Play.
ZCASH_GOOGLE_PLAY_DEPLOY_TRACK=internal
# Can be one of {draft, completed}.
# Draft can be used to generate a version number for the next release, but does not ultimately create a release on
# Google Play. Completed commits the build on Google Play, creating a new release.
ZCASH_GOOGLE_PLAY_DEPLOY_STATUS=draft
ZCASH_EMULATOR_WTF_API_KEY= ZCASH_EMULATOR_WTF_API_KEY=
@ -122,7 +138,6 @@ GOOGLE_PLAY_SERVICES_GRADLE_PLUGIN_VERSION=4.3.15
GRADLE_VERSIONS_PLUGIN_VERSION=0.47.0 GRADLE_VERSIONS_PLUGIN_VERSION=0.47.0
JGIT_VERSION=6.4.0.202211300538-r JGIT_VERSION=6.4.0.202211300538-r
KTLINT_VERSION=0.49.0 KTLINT_VERSION=0.49.0
PLAY_PUBLISHER_PLUGIN_VERSION=3.8.4
ACCOMPANIST_PERMISSIONS_VERSION=0.32.0 ACCOMPANIST_PERMISSIONS_VERSION=0.32.0
ANDROIDX_ACTIVITY_VERSION=1.8.0 ANDROIDX_ACTIVITY_VERSION=1.8.0
@ -152,6 +167,7 @@ ANDROIDX_UI_AUTOMATOR_VERSION=2.2.0-alpha1
ANDROIDX_WORK_MANAGER_VERSION=2.8.1 ANDROIDX_WORK_MANAGER_VERSION=2.8.1
CORE_LIBRARY_DESUGARING_VERSION=2.0.3 CORE_LIBRARY_DESUGARING_VERSION=2.0.3
FIREBASE_BOM_VERSION_MATCHER=32.0.0 FIREBASE_BOM_VERSION_MATCHER=32.0.0
GOOGLE_AUTH_LIB_JAVA_VERSION=1.18.0
JACOCO_VERSION=0.8.9 JACOCO_VERSION=0.8.9
KOTLIN_VERSION=1.9.10 KOTLIN_VERSION=1.9.10
KOTLINX_COROUTINES_VERSION=1.7.1 KOTLINX_COROUTINES_VERSION=1.7.1
@ -160,6 +176,7 @@ KOTLINX_IMMUTABLE_COLLECTIONS_VERSION=0.3.5
KOVER_VERSION=0.7.3 KOVER_VERSION=0.7.3
PLAY_APP_UPDATE_VERSION=2.0.1 PLAY_APP_UPDATE_VERSION=2.0.1
PLAY_APP_UPDATE_KTX_VERSION=2.0.1 PLAY_APP_UPDATE_KTX_VERSION=2.0.1
PLAY_PUBLISHER_API_VERSION=v3-rev20231030-2.0.0
ZCASH_ANDROID_WALLET_PLUGINS_VERSION=1.0.0 ZCASH_ANDROID_WALLET_PLUGINS_VERSION=1.0.0
ZCASH_BIP39_VERSION=1.0.6 ZCASH_BIP39_VERSION=1.0.6
ZXING_VERSION=3.5.1 ZXING_VERSION=3.5.1

View File

@ -59,7 +59,6 @@ pluginManagement {
id("com.android.library") version (androidGradlePluginVersion) apply (false) id("com.android.library") version (androidGradlePluginVersion) apply (false)
id("com.android.test") version (androidGradlePluginVersion) apply (false) id("com.android.test") version (androidGradlePluginVersion) apply (false)
id("com.github.ben-manes.versions") version (extra["GRADLE_VERSIONS_PLUGIN_VERSION"].toString()) apply (false) id("com.github.ben-manes.versions") version (extra["GRADLE_VERSIONS_PLUGIN_VERSION"].toString()) apply (false)
id("com.github.triplet.play") version (extra["PLAY_PUBLISHER_PLUGIN_VERSION"].toString()) apply (false)
id("com.osacky.fulladle") version (extra["FULLADLE_VERSION"].toString()) apply (false) id("com.osacky.fulladle") version (extra["FULLADLE_VERSION"].toString()) apply (false)
id("org.jetbrains.kotlinx.kover") version (extra["KOVER_VERSION"].toString()) apply (false) id("org.jetbrains.kotlinx.kover") version (extra["KOVER_VERSION"].toString()) apply (false)
id("wtf.emulator.gradle") version (extra["EMULATOR_WTF_GRADLE_PLUGIN_VERSION"].toString()) apply (false) id("wtf.emulator.gradle") version (extra["EMULATOR_WTF_GRADLE_PLUGIN_VERSION"].toString()) apply (false)