[#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
# GOOGLE_PLAY_CLOUD_PROJECT - Google Cloud project associated with Google Play
# 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
# UPLOAD_KEYSTORE_BASE_64 - The upload signing key for the app
# UPLOAD_KEYSTORE_PASSWORD - The password for UPLOAD_KEYSTORE_BASE_64
@ -54,9 +56,17 @@ jobs:
- id: check_secrets
env:
GOOGLE_PLAY_CLOUD_PROJECT: ${{ secrets.GOOGLE_PLAY_CLOUD_PROJECT }}
GOOGLE_PLAY_SERVICE_ACCOUNT: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT }}
GOOGLE_PLAY_WORKLOAD_IDENTITY_PROVIDER: ${{ secrets.GOOGLE_PLAY_WORKLOAD_IDENTITY_PROVIDER }}
if: "${{ env.GOOGLE_PLAY_CLOUD_PROJECT != '' && env.GOOGLE_PLAY_SERVICE_ACCOUNT != '' && env.GOOGLE_PLAY_WORKLOAD_IDENTITY_PROVIDER != '' }}"
# 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
# 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
build_and_deploy:
@ -71,6 +81,9 @@ jobs:
- name: Checkout
timeout-minutes: 1
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11
with:
ref: main
fetch-depth: 0 # To fetch all commits
- name: Set up Java
uses: actions/setup-java@0ab4596768b603586c0de567f2430c30f5b0d2b0
timeout-minutes: 1
@ -94,6 +107,10 @@ jobs:
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
- 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
uses: google-github-actions/auth@35b0e87d162680511bf346c299f71c9c5c379033
with:
@ -117,14 +134,21 @@ jobs:
timeout-minutes: 25
env:
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_PASSWORD: ${{ secrets.UPLOAD_KEYSTORE_PASSWORD }}
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_GOOGLE_PLAY_DEPLOY_MODE: deploy
run: |
./gradlew :app:assembleDebug :app:publishBundle :app:packageZcashmainnetReleaseUniversalApk
./gradlew :app:publishToGooglePlay
- name: Collect Artifacts
timeout-minutes: 1
env:
@ -133,11 +157,11 @@ jobs:
MAPPINGS_ZIP_PATH: ${{ format('{0}/artifacts/mappings.zip', env.home) }}
run: |
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
- name: Upload Artifacts
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32
timeout-minutes: 1
timeout-minutes: 5
with:
name: Binaries
path: ~/artifacts

View File

@ -105,7 +105,7 @@ jobs:
uses: gradle/gradle-build-action@842c587ad8aa4c68eeba24c396e15af4c2e9f30a
timeout-minutes: 5
- name: Detekt
timeout-minutes: 4
timeout-minutes: 10
run: |
./gradlew detektAll
- name: Collect Artifacts
@ -228,7 +228,7 @@ jobs:
uses: gradle/gradle-build-action@842c587ad8aa4c68eeba24c396e15af4c2e9f30a
timeout-minutes: 5
- name: Test
timeout-minutes: 4
timeout-minutes: 10
run: |
# 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

View File

@ -1,3 +1,4 @@
import co.electriccoin.zcash.Git
import com.android.build.api.variant.BuildConfigField
import com.android.build.api.variant.ResValue
import java.util.Locale
@ -6,10 +7,10 @@ plugins {
id("com.android.application")
kotlin("android")
id("secant.android-build-conventions")
id("com.github.triplet.play")
id("com.osacky.fladle")
id("wtf.emulator.gradle")
id("secant.emulator-wtf-conventions")
id("secant.publish-conventions")
}
val hasFirebaseApiKeys = run {
@ -158,12 +159,6 @@ android {
resValue("string", "support_email_address", supportEmailAddress)
}
playConfigs {
register(testNetFlavorName) {
enabled.set(false)
}
}
testCoverage {
jacocoVersion = project.property("JACOCO_VERSION").toString()
}
@ -202,8 +197,6 @@ dependencies {
}
}
val googlePlayServiceKeyFilePath = project.property("ZCASH_GOOGLE_PLAY_SERVICE_KEY_FILE_PATH").toString()
androidComponents {
onVariants { variant ->
for (output in variant.outputs) {
@ -223,7 +216,9 @@ androidComponents {
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
val versionCodeOffset = 0 // Change this to zero the final digit of the versionName
@ -243,6 +238,9 @@ androidComponents {
}
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 {
// 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`

View File

@ -41,6 +41,10 @@ dependencies {
val rootProperties = getRootProperties()
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("org.jetbrains.kotlin:kotlin-gradle-plugin:${rootProperties.getProperty("KOTLIN_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:zipflinger:8.1.1=compileClasspath,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.auto.value:auto-value-annotations:1.6.2=runtimeClasspath
com.google.code.findbugs:jsr305:3.0.2=runtimeClasspath
com.google.code.gson:gson:2.8.9=runtimeClasspath
com.google.apis:google-api-services-androidpublisher:v3-rev20231030-2.0.0=compileClasspath,runtimeClasspath
com.google.auth:google-auth-library-credentials:1.18.0=compileClasspath,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.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.guava:failureaccess:1.0.1=runtimeClasspath
com.google.guava:guava:31.1-jre=runtimeClasspath
com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=runtimeClasspath
com.google.j2objc:j2objc-annotations:1.3=runtimeClasspath
com.google.guava:failureaccess:1.0.1=compileClasspath,runtimeClasspath
com.google.guava:guava:31.1-jre=compileClasspath,runtimeClasspath
com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=compileClasspath,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.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:3.19.3=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.xml.fastinfoset:FastInfoset:1.2.16=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-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.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-core: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-tcnative-classes:2.0.46.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
jakarta.activation:jakarta.activation-api:1.2.1=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.kxml:kxml2:2.3.0=runtimeClasspath
org.apache.commons:commons-compress:1.21=runtimeClasspath
org.apache.httpcomponents:httpclient:4.5.13=runtimeClasspath
org.apache.httpcomponents:httpcore:4.4.15=runtimeClasspath
org.apache.httpcomponents:httpclient:4.5.14=compileClasspath,runtimeClasspath
org.apache.httpcomponents:httpcore:4.4.16=compileClasspath,runtimeClasspath
org.apache.httpcomponents:httpmime:4.5.6=runtimeClasspath
org.bitbucket.b_c:jose4j:0.7.0=runtimeClasspath
org.bouncycastle:bcpkix-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.glassfish.jaxb:jaxb-runtime: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.util.*
@ -13,7 +14,10 @@ plugins {
val generateBuildConfigTask = tasks.create("buildConfig") {
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()
inputs.property("gitSha", gitInfo.sha)

View File

@ -113,8 +113,12 @@ tasks {
"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_DEPLOY_MODE" to "build",
"ZCASH_GOOGLE_PLAY_DEPLOY_TRACK" to "internal",
"ZCASH_GOOGLE_PLAY_DEPLOY_STATUS" to "draft",
"SDK_INCLUDED_BUILD_PATH" to "",
"BIP_39_INCLUDED_BUILD_PATH" to ""

View File

@ -6,13 +6,17 @@ import java.io.File
object Git {
// 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 repository = git.repository
val head: ObjectId = repository.resolve(HEAD)
val head: ObjectId = repository.resolve(branch)
val count = git.log().call().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.google.android.gms:strict-version-matcher-plugin:1.2.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.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.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.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-gradle:2.9.4=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:guava:31.1-jre=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.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:3.19.3=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.thoughtworks.xstream:xstream:1.4.20=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-logging:commons-logging:1.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-tcnative-classes:2.0.46.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
jakarta.activation:jakarta.activation-api:1.2.1=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.kxml:kxml2:2.3.0=classpath
org.apache.commons:commons-compress:1.21=classpath
org.apache.httpcomponents:httpclient:4.5.13=classpath
org.apache.httpcomponents:httpcore:4.4.15=classpath
org.apache.httpcomponents:httpclient:4.5.14=classpath
org.apache.httpcomponents:httpcore:4.4.16=classpath
org.apache.httpcomponents:httpmime:4.5.6=classpath
org.bitbucket.b_c:jose4j:0.7.0=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.
## Automated Deployment
Automated deployment to Google Play configured with the [Gradle Play Publisher plugin](https://github.com/Triple-T/gradle-play-publisher).
To perform a deployment:
1. Configure a Google Cloud service API key with the correct permissions
Automated deployment to Google Play is configured with custom
[Google Play publishing Gradle task](../build-conventions-secant/src/main/kotlin/secant.publish-conventions.gradle.kts).
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. `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 `deploy`
1. `ZCASH_GOOGLE_PLAY_SERVICE_ACCOUNT_KEY` - Set the Google Play Service Account enabled in the Google Cloud console
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`
To generate a build with a correct version that can be deployed manually later:
1. Configure a Google Cloud service API key with the correct permissions
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_DEPLOY_MODE` - Set to `build` (this is the default value)
1. Run the Gradle tasks `./gradlew :app:processReleaseVersionCodes :app:bundleRelease`
For more information about proper automated deployment setup, see
[Google Play publishing Gradle task](../build-conventions-secant/src/main/kotlin/secant.publish-conventions.gradle.kts)
documentation and related [gradle.properties](../gradle.properties) attributes.
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
# 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.
ZCASH_VERSION_CODE=1
ZCASH_VERSION_NAME=0.2.0
@ -86,12 +86,28 @@ ZCASH_RELEASE_KEY_ALIAS_PASSWORD=
# the default debug key configuration.
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=
# Can be one of {build, deploy}.
# Build can be used to generate a version number for the next release, but does not ultimately create a release on Google Play.
# Deploy commits the build on Google Play, creating a new release
ZCASH_GOOGLE_PLAY_DEPLOY_MODE=build
# Set the Google Play Service Account key to authorize on Google Play
ZCASH_GOOGLE_PLAY_SERVICE_ACCOUNT_KEY=
# Set the Google Play Publisher API key to authorize the publisher on Google Play API
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=
@ -122,7 +138,6 @@ GOOGLE_PLAY_SERVICES_GRADLE_PLUGIN_VERSION=4.3.15
GRADLE_VERSIONS_PLUGIN_VERSION=0.47.0
JGIT_VERSION=6.4.0.202211300538-r
KTLINT_VERSION=0.49.0
PLAY_PUBLISHER_PLUGIN_VERSION=3.8.4
ACCOMPANIST_PERMISSIONS_VERSION=0.32.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
CORE_LIBRARY_DESUGARING_VERSION=2.0.3
FIREBASE_BOM_VERSION_MATCHER=32.0.0
GOOGLE_AUTH_LIB_JAVA_VERSION=1.18.0
JACOCO_VERSION=0.8.9
KOTLIN_VERSION=1.9.10
KOTLINX_COROUTINES_VERSION=1.7.1
@ -160,6 +176,7 @@ KOTLINX_IMMUTABLE_COLLECTIONS_VERSION=0.3.5
KOVER_VERSION=0.7.3
PLAY_APP_UPDATE_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_BIP39_VERSION=1.0.6
ZXING_VERSION=3.5.1

View File

@ -59,7 +59,6 @@ pluginManagement {
id("com.android.library") 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.triplet.play") version (extra["PLAY_PUBLISHER_PLUGIN_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("wtf.emulator.gradle") version (extra["EMULATOR_WTF_GRADLE_PLUGIN_VERSION"].toString()) apply (false)