Add signed Android release pipeline
This commit is contained in:
parent
6e11bb35aa
commit
e72aaf9b73
|
|
@ -55,6 +55,12 @@ WNH_BASE_URL=
|
||||||
# production capacity recommendations.
|
# production capacity recommendations.
|
||||||
WNH_TRACKING_MIN_TIME_MS=5000
|
WNH_TRACKING_MIN_TIME_MS=5000
|
||||||
WNH_TRACKING_HTTP_TIMEOUT_MS=15000
|
WNH_TRACKING_HTTP_TIMEOUT_MS=15000
|
||||||
|
WNH_ANDROID_VERSION_CODE=1
|
||||||
|
WNH_ANDROID_VERSION_NAME=0.1.0
|
||||||
|
# Public identifier of the locally held Google Play upload key. The private
|
||||||
|
# keystore and its randomized password live outside the repository under
|
||||||
|
# ~/.config/who_need_help/android-release/.
|
||||||
|
WNH_ANDROID_SIGNING_KEY_ALIAS=who-need-help-upload
|
||||||
WNH_ANDROID_TEST_API_MATRIX="24 30 34 37.0"
|
WNH_ANDROID_TEST_API_MATRIX="24 30 34 37.0"
|
||||||
WNH_ANDROID_TEST_DATA_PARTITION_SIZE=1G
|
WNH_ANDROID_TEST_DATA_PARTITION_SIZE=1G
|
||||||
# Public raster tile template used by MapLibre. Use a provider whose policy and
|
# Public raster tile template used by MapLibre. Use a provider whose policy and
|
||||||
|
|
|
||||||
|
|
@ -27,3 +27,6 @@ jobs:
|
||||||
env:
|
env:
|
||||||
WNH_ENV_FILE: .env.example
|
WNH_ENV_FILE: .env.example
|
||||||
run: ./scripts/android-build.sh
|
run: ./scripts/android-build.sh
|
||||||
|
|
||||||
|
- name: Exercise signed APK and Play AAB release pipeline
|
||||||
|
run: ./scripts/android-release-ci.sh
|
||||||
|
|
|
||||||
3
.github/workflows/quality.yml
vendored
3
.github/workflows/quality.yml
vendored
|
|
@ -58,3 +58,6 @@ jobs:
|
||||||
env:
|
env:
|
||||||
WNH_ENV_FILE: .env.example
|
WNH_ENV_FILE: .env.example
|
||||||
run: ./scripts/android-build.sh
|
run: ./scripts/android-build.sh
|
||||||
|
|
||||||
|
- name: Exercise signed APK and Play AAB release pipeline
|
||||||
|
run: ./scripts/android-release-ci.sh
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,8 @@ COPY --chown=gradle:gradle . .
|
||||||
ARG WNH_DEBUG_BASE_URL
|
ARG WNH_DEBUG_BASE_URL
|
||||||
ARG WNH_TRACKING_MIN_TIME_MS
|
ARG WNH_TRACKING_MIN_TIME_MS
|
||||||
ARG WNH_TRACKING_HTTP_TIMEOUT_MS
|
ARG WNH_TRACKING_HTTP_TIMEOUT_MS
|
||||||
|
ARG WNH_ANDROID_VERSION_CODE=1
|
||||||
|
ARG WNH_ANDROID_VERSION_NAME=0.1.0
|
||||||
|
|
||||||
RUN --mount=type=cache,target=/home/gradle/.gradle,uid=1000,gid=1000 \
|
RUN --mount=type=cache,target=/home/gradle/.gradle,uid=1000,gid=1000 \
|
||||||
--mount=type=cache,target=/home/gradle/.android,uid=1000,gid=1000 \
|
--mount=type=cache,target=/home/gradle/.android,uid=1000,gid=1000 \
|
||||||
|
|
@ -50,6 +52,8 @@ RUN --mount=type=cache,target=/home/gradle/.gradle,uid=1000,gid=1000 \
|
||||||
"-PWNH_DEBUG_BASE_URL=${WNH_DEBUG_BASE_URL}" \
|
"-PWNH_DEBUG_BASE_URL=${WNH_DEBUG_BASE_URL}" \
|
||||||
"-PWNH_TRACKING_MIN_TIME_MS=${WNH_TRACKING_MIN_TIME_MS}" \
|
"-PWNH_TRACKING_MIN_TIME_MS=${WNH_TRACKING_MIN_TIME_MS}" \
|
||||||
"-PWNH_TRACKING_HTTP_TIMEOUT_MS=${WNH_TRACKING_HTTP_TIMEOUT_MS}" \
|
"-PWNH_TRACKING_HTTP_TIMEOUT_MS=${WNH_TRACKING_HTTP_TIMEOUT_MS}" \
|
||||||
|
"-PWNH_ANDROID_VERSION_CODE=${WNH_ANDROID_VERSION_CODE}" \
|
||||||
|
"-PWNH_ANDROID_VERSION_NAME=${WNH_ANDROID_VERSION_NAME}" \
|
||||||
testDebugUnitTest lintDebug assembleDebug assembleDebugAndroidTest
|
testDebugUnitTest lintDebug assembleDebug assembleDebugAndroidTest
|
||||||
|
|
||||||
FROM android-base AS emulator
|
FROM android-base AS emulator
|
||||||
|
|
@ -144,6 +148,8 @@ COPY --chown=gradle:gradle . .
|
||||||
ARG WNH_BASE_URL
|
ARG WNH_BASE_URL
|
||||||
ARG WNH_TRACKING_MIN_TIME_MS
|
ARG WNH_TRACKING_MIN_TIME_MS
|
||||||
ARG WNH_TRACKING_HTTP_TIMEOUT_MS
|
ARG WNH_TRACKING_HTTP_TIMEOUT_MS
|
||||||
|
ARG WNH_ANDROID_VERSION_CODE=1
|
||||||
|
ARG WNH_ANDROID_VERSION_NAME=0.1.0
|
||||||
|
|
||||||
RUN --mount=type=cache,target=/home/gradle/.gradle,uid=1000,gid=1000 \
|
RUN --mount=type=cache,target=/home/gradle/.gradle,uid=1000,gid=1000 \
|
||||||
--mount=type=cache,target=/home/gradle/.android,uid=1000,gid=1000 \
|
--mount=type=cache,target=/home/gradle/.android,uid=1000,gid=1000 \
|
||||||
|
|
@ -152,6 +158,8 @@ RUN --mount=type=cache,target=/home/gradle/.gradle,uid=1000,gid=1000 \
|
||||||
"-PWNH_DEBUG_BASE_URL=${WNH_BASE_URL}" \
|
"-PWNH_DEBUG_BASE_URL=${WNH_BASE_URL}" \
|
||||||
"-PWNH_TRACKING_MIN_TIME_MS=${WNH_TRACKING_MIN_TIME_MS}" \
|
"-PWNH_TRACKING_MIN_TIME_MS=${WNH_TRACKING_MIN_TIME_MS}" \
|
||||||
"-PWNH_TRACKING_HTTP_TIMEOUT_MS=${WNH_TRACKING_HTTP_TIMEOUT_MS}" \
|
"-PWNH_TRACKING_HTTP_TIMEOUT_MS=${WNH_TRACKING_HTTP_TIMEOUT_MS}" \
|
||||||
|
"-PWNH_ANDROID_VERSION_CODE=${WNH_ANDROID_VERSION_CODE}" \
|
||||||
|
"-PWNH_ANDROID_VERSION_NAME=${WNH_ANDROID_VERSION_NAME}" \
|
||||||
"-PWNH_TEST_BUILD_TYPE=staging" \
|
"-PWNH_TEST_BUILD_TYPE=staging" \
|
||||||
testStagingUnitTest lintStaging assembleStaging assembleStagingAndroidTest
|
testStagingUnitTest lintStaging assembleStaging assembleStagingAndroidTest
|
||||||
|
|
||||||
|
|
@ -168,3 +176,82 @@ COPY --from=android-staging-sdk \
|
||||||
COPY --from=android-staging-sdk \
|
COPY --from=android-staging-sdk \
|
||||||
/workspace/android/app/build/reports/lint-results-staging.html \
|
/workspace/android/app/build/reports/lint-results-staging.html \
|
||||||
/lint-results-staging.html
|
/lint-results-staging.html
|
||||||
|
|
||||||
|
FROM android-base AS android-release-base
|
||||||
|
|
||||||
|
USER root
|
||||||
|
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||||
|
|
||||||
|
ARG BUNDLETOOL_VERSION=1.18.3
|
||||||
|
ARG BUNDLETOOL_SHA256=a099cfa1543f55593bc2ed16a70a7c67fe54b1747bb7301f37fdfd6d91028e29
|
||||||
|
|
||||||
|
RUN curl -fsSL --proto '=https' --tlsv1.2 \
|
||||||
|
"https://github.com/google/bundletool/releases/download/${BUNDLETOOL_VERSION}/bundletool-all-${BUNDLETOOL_VERSION}.jar" \
|
||||||
|
-o /opt/bundletool.jar \
|
||||||
|
&& echo "${BUNDLETOOL_SHA256} /opt/bundletool.jar" | sha256sum -c - \
|
||||||
|
&& chmod 444 /opt/bundletool.jar
|
||||||
|
|
||||||
|
USER gradle
|
||||||
|
|
||||||
|
FROM android-release-base AS android-release-sdk
|
||||||
|
|
||||||
|
USER gradle
|
||||||
|
WORKDIR /workspace/android
|
||||||
|
|
||||||
|
COPY --chown=gradle:gradle . .
|
||||||
|
|
||||||
|
ARG WNH_BASE_URL
|
||||||
|
ARG WNH_TRACKING_MIN_TIME_MS
|
||||||
|
ARG WNH_TRACKING_HTTP_TIMEOUT_MS
|
||||||
|
ARG WNH_ANDROID_VERSION_CODE
|
||||||
|
ARG WNH_ANDROID_VERSION_NAME
|
||||||
|
|
||||||
|
RUN --mount=type=cache,target=/home/gradle/.gradle,uid=1000,gid=1000 \
|
||||||
|
--mount=type=cache,target=/home/gradle/.android,uid=1000,gid=1000 \
|
||||||
|
--mount=type=secret,id=android_upload_keystore,required=true,uid=1000,gid=1000,mode=0400 \
|
||||||
|
--mount=type=secret,id=android_upload_password,required=true,uid=1000,gid=1000,mode=0400 \
|
||||||
|
--mount=type=secret,id=android_upload_alias,required=true,env=WNH_ANDROID_SIGNING_KEY_ALIAS \
|
||||||
|
WNH_ANDROID_SIGNING_STORE_FILE=/run/secrets/android_upload_keystore \
|
||||||
|
WNH_ANDROID_SIGNING_PASSWORD_FILE=/run/secrets/android_upload_password \
|
||||||
|
gradle --no-daemon \
|
||||||
|
"-PWNH_BASE_URL=${WNH_BASE_URL}" \
|
||||||
|
"-PWNH_TRACKING_MIN_TIME_MS=${WNH_TRACKING_MIN_TIME_MS}" \
|
||||||
|
"-PWNH_TRACKING_HTTP_TIMEOUT_MS=${WNH_TRACKING_HTTP_TIMEOUT_MS}" \
|
||||||
|
"-PWNH_ANDROID_VERSION_CODE=${WNH_ANDROID_VERSION_CODE}" \
|
||||||
|
"-PWNH_ANDROID_VERSION_NAME=${WNH_ANDROID_VERSION_NAME}" \
|
||||||
|
testReleaseUnitTest lintRelease assembleRelease bundleRelease \
|
||||||
|
&& "${ANDROID_HOME}/build-tools/37.0.0/apksigner" \
|
||||||
|
verify --verbose --print-certs \
|
||||||
|
app/build/outputs/apk/release/app-release.apk \
|
||||||
|
>app/build/outputs/apk/release/signing-certificate.txt \
|
||||||
|
&& LC_ALL=C jarsigner -verify -verbose -certs \
|
||||||
|
app/build/outputs/bundle/release/app-release.aab \
|
||||||
|
>app/build/outputs/bundle/release/signing-verification.txt \
|
||||||
|
&& grep -Fq "jar verified." \
|
||||||
|
app/build/outputs/bundle/release/signing-verification.txt \
|
||||||
|
&& java -jar /opt/bundletool.jar validate \
|
||||||
|
--bundle=app/build/outputs/bundle/release/app-release.aab \
|
||||||
|
>app/build/outputs/bundle/release/bundletool-validation.txt
|
||||||
|
|
||||||
|
FROM scratch AS release-artifact
|
||||||
|
|
||||||
|
USER 65532:65532
|
||||||
|
|
||||||
|
COPY --from=android-release-sdk \
|
||||||
|
/workspace/android/app/build/outputs/apk/release/app-release.apk \
|
||||||
|
/who-need-help-release.apk
|
||||||
|
COPY --from=android-release-sdk \
|
||||||
|
/workspace/android/app/build/outputs/bundle/release/app-release.aab \
|
||||||
|
/who-need-help-release.aab
|
||||||
|
COPY --from=android-release-sdk \
|
||||||
|
/workspace/android/app/build/outputs/apk/release/signing-certificate.txt \
|
||||||
|
/signing-certificate.txt
|
||||||
|
COPY --from=android-release-sdk \
|
||||||
|
/workspace/android/app/build/outputs/bundle/release/signing-verification.txt \
|
||||||
|
/bundle-signing-verification.txt
|
||||||
|
COPY --from=android-release-sdk \
|
||||||
|
/workspace/android/app/build/outputs/bundle/release/bundletool-validation.txt \
|
||||||
|
/bundletool-validation.txt
|
||||||
|
COPY --from=android-release-sdk \
|
||||||
|
/workspace/android/app/build/reports/lint-results-release.html \
|
||||||
|
/lint-results-release.html
|
||||||
|
|
|
||||||
|
|
@ -34,17 +34,48 @@ The same ignored file supplies `WNH_TRACKING_MIN_TIME_MS` and
|
||||||
local client freshness and timeout behavior; these values are not claimed as
|
local client freshness and timeout behavior; these values are not claimed as
|
||||||
measured production capacity settings.
|
measured production capacity settings.
|
||||||
|
|
||||||
Release builds do not have a default server. Supply the real HTTPS deployment:
|
Release builds do not have a default server and cannot be produced unsigned.
|
||||||
|
The repository reads non-secret version/origin settings from the selected
|
||||||
|
ignored environment file, while the upload key and its randomized password
|
||||||
|
stay outside the repository. Create that key once:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
./gradlew :app:assembleRelease \
|
./scripts/init-android-release-signing.sh
|
||||||
-PWNH_BASE_URL=https://help.your-domain.example \
|
|
||||||
-PWNH_TRACKING_MIN_TIME_MS=5000 \
|
|
||||||
-PWNH_TRACKING_HTTP_TIMEOUT_MS=15000
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The build rejects a missing, HTTP, credentialed, query-bearing, or
|
Back up both reported files before uploading the first bundle. Losing this
|
||||||
fragment-bearing release URL.
|
dedicated upload key is recoverable through Play App Signing, but keeping an
|
||||||
|
offline backup avoids a reset. Do not copy either file into the repository.
|
||||||
|
|
||||||
|
Configure these non-secret values in the deployment's ignored environment
|
||||||
|
file, then produce the signed APK and Play bundle:
|
||||||
|
|
||||||
|
```dotenv
|
||||||
|
WNH_BASE_URL=https://your-final-origin.example
|
||||||
|
WNH_ANDROID_VERSION_CODE=1
|
||||||
|
WNH_ANDROID_VERSION_NAME=0.1.0
|
||||||
|
WNH_ANDROID_SIGNING_KEY_ALIAS=who-need-help-upload
|
||||||
|
```
|
||||||
|
|
||||||
|
```sh
|
||||||
|
WNH_ENV_FILE=.env.production ./scripts/android-release-build.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
The build passes the private files with Docker BuildKit secret mounts, runs
|
||||||
|
release unit tests and lint, enables code/resource shrinking, signs both
|
||||||
|
artifacts, verifies the APK and AAB signatures, and validates the AAB with the
|
||||||
|
same `bundletool` used underneath Android Gradle Plugin and Google Play before
|
||||||
|
exporting anything.
|
||||||
|
It rejects a missing, HTTP, credentialed, query-bearing, or fragment-bearing
|
||||||
|
release URL and rejects missing or partial signing configuration.
|
||||||
|
|
||||||
|
Google Play requires an Android App Bundle for a new app. With Play App
|
||||||
|
Signing, the locally held key signs the uploaded bundle and Google holds the
|
||||||
|
separate app-signing key used for distributed APKs:
|
||||||
|
|
||||||
|
- <https://developer.android.com/studio/publish/app-signing>
|
||||||
|
- <https://support.google.com/googleplay/android-developer/answer/9859152>
|
||||||
|
- <https://docs.docker.com/build/building/secrets/>
|
||||||
|
|
||||||
## Public staging build
|
## Public staging build
|
||||||
|
|
||||||
|
|
@ -147,5 +178,7 @@ docker exec who-need-help-android-emulator adb install \
|
||||||
```
|
```
|
||||||
|
|
||||||
The application ID `org.whoneedhelp.mobile` is provisional until the publishing
|
The application ID `org.whoneedhelp.mobile` is provisional until the publishing
|
||||||
identity and store listing are chosen. Changing it after publication creates a
|
identity and store listing are chosen. Google documents Play package names as
|
||||||
different Android application.
|
unique and permanent, so do not create the Play Console app or publish this
|
||||||
|
identifier until that choice is explicit. Changing it after publication creates
|
||||||
|
a different Android application.
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
|
import java.nio.file.Files
|
||||||
|
import java.nio.file.Path
|
||||||
import org.gradle.api.tasks.compile.JavaCompile
|
import org.gradle.api.tasks.compile.JavaCompile
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
|
|
@ -12,6 +14,43 @@ val trackingHttpTimeoutMs =
|
||||||
providers.gradleProperty("WNH_TRACKING_HTTP_TIMEOUT_MS").orElse("0")
|
providers.gradleProperty("WNH_TRACKING_HTTP_TIMEOUT_MS").orElse("0")
|
||||||
val instrumentationBuildType =
|
val instrumentationBuildType =
|
||||||
providers.gradleProperty("WNH_TEST_BUILD_TYPE").orElse("debug")
|
providers.gradleProperty("WNH_TEST_BUILD_TYPE").orElse("debug")
|
||||||
|
val androidVersionCode = providers.gradleProperty("WNH_ANDROID_VERSION_CODE").orElse("1")
|
||||||
|
val androidVersionName = providers.gradleProperty("WNH_ANDROID_VERSION_NAME").orElse("0.1.0")
|
||||||
|
val releaseSigningStoreFile =
|
||||||
|
providers.environmentVariable("WNH_ANDROID_SIGNING_STORE_FILE").orNull
|
||||||
|
val releaseSigningPasswordFile =
|
||||||
|
providers.environmentVariable("WNH_ANDROID_SIGNING_PASSWORD_FILE").orNull
|
||||||
|
val releaseSigningKeyAlias =
|
||||||
|
providers.environmentVariable("WNH_ANDROID_SIGNING_KEY_ALIAS").orNull
|
||||||
|
|
||||||
|
fun nonBlank(value: String?): String? = value?.trim()?.takeIf(String::isNotEmpty)
|
||||||
|
|
||||||
|
val releaseSigningInputs =
|
||||||
|
listOf(
|
||||||
|
nonBlank(releaseSigningStoreFile),
|
||||||
|
nonBlank(releaseSigningPasswordFile),
|
||||||
|
nonBlank(releaseSigningKeyAlias)
|
||||||
|
)
|
||||||
|
val releaseSigningConfigured = releaseSigningInputs.all { it != null }
|
||||||
|
val releaseSigningPartiallyConfigured = releaseSigningInputs.any { it != null }
|
||||||
|
|
||||||
|
fun readSigningPassword(): String {
|
||||||
|
val passwordPath =
|
||||||
|
nonBlank(releaseSigningPasswordFile)
|
||||||
|
?: throw GradleException("Android release signing password file is not configured")
|
||||||
|
val path = Path.of(passwordPath)
|
||||||
|
|
||||||
|
if (!Files.isRegularFile(path)) {
|
||||||
|
throw GradleException("Android release signing password file does not exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
val password = Files.readString(path).trimEnd('\r', '\n')
|
||||||
|
if (password.isBlank() || password.contains('\n') || password.contains('\r')) {
|
||||||
|
throw GradleException("Android release signing password file is invalid")
|
||||||
|
}
|
||||||
|
|
||||||
|
return password
|
||||||
|
}
|
||||||
|
|
||||||
fun manifestOrigin(value: String): URI? =
|
fun manifestOrigin(value: String): URI? =
|
||||||
runCatching { URI(value) }
|
runCatching { URI(value) }
|
||||||
|
|
@ -36,8 +75,12 @@ android {
|
||||||
applicationId = "org.whoneedhelp.mobile"
|
applicationId = "org.whoneedhelp.mobile"
|
||||||
minSdk = 24
|
minSdk = 24
|
||||||
targetSdk = 37
|
targetSdk = 37
|
||||||
versionCode = 1
|
versionCode =
|
||||||
versionName = "0.1.0"
|
androidVersionCode.get().toIntOrNull()?.takeIf { it > 0 }
|
||||||
|
?: throw GradleException("WNH_ANDROID_VERSION_CODE must be a positive integer")
|
||||||
|
versionName =
|
||||||
|
androidVersionName.get().trim().takeIf(String::isNotEmpty)
|
||||||
|
?: throw GradleException("WNH_ANDROID_VERSION_NAME must not be empty")
|
||||||
buildConfigField("long", "TRACKING_MIN_TIME_MS", "${trackingMinTimeMs.get()}L")
|
buildConfigField("long", "TRACKING_MIN_TIME_MS", "${trackingMinTimeMs.get()}L")
|
||||||
buildConfigField(
|
buildConfigField(
|
||||||
"long",
|
"long",
|
||||||
|
|
@ -48,6 +91,22 @@ android {
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
signingConfigs {
|
||||||
|
if (releaseSigningConfigured) {
|
||||||
|
create("release") {
|
||||||
|
val storePath = Path.of(nonBlank(releaseSigningStoreFile)!!)
|
||||||
|
if (!Files.isRegularFile(storePath)) {
|
||||||
|
throw GradleException("Android release signing keystore does not exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
storeFile = storePath.toFile()
|
||||||
|
storePassword = readSigningPassword()
|
||||||
|
keyAlias = nonBlank(releaseSigningKeyAlias)
|
||||||
|
keyPassword = storePassword
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
debug {
|
debug {
|
||||||
applicationIdSuffix = ".debug"
|
applicationIdSuffix = ".debug"
|
||||||
|
|
@ -82,6 +141,9 @@ android {
|
||||||
release {
|
release {
|
||||||
isMinifyEnabled = true
|
isMinifyEnabled = true
|
||||||
isShrinkResources = true
|
isShrinkResources = true
|
||||||
|
if (releaseSigningConfigured) {
|
||||||
|
signingConfig = signingConfigs.getByName("release")
|
||||||
|
}
|
||||||
buildConfigField(
|
buildConfigField(
|
||||||
"String",
|
"String",
|
||||||
"BASE_URL",
|
"BASE_URL",
|
||||||
|
|
@ -114,6 +176,20 @@ android {
|
||||||
|
|
||||||
tasks.matching { it.name == "preReleaseBuild" || it.name == "preStagingBuild" }.configureEach {
|
tasks.matching { it.name == "preReleaseBuild" || it.name == "preStagingBuild" }.configureEach {
|
||||||
doFirst {
|
doFirst {
|
||||||
|
if (name == "preReleaseBuild" && !releaseSigningConfigured) {
|
||||||
|
val detail =
|
||||||
|
if (releaseSigningPartiallyConfigured) {
|
||||||
|
"Release signing is only partially configured"
|
||||||
|
} else {
|
||||||
|
"Release signing is not configured"
|
||||||
|
}
|
||||||
|
throw GradleException(
|
||||||
|
"$detail; set WNH_ANDROID_SIGNING_STORE_FILE, "
|
||||||
|
+ "WNH_ANDROID_SIGNING_PASSWORD_FILE, and "
|
||||||
|
+ "WNH_ANDROID_SIGNING_KEY_ALIAS"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
val value = releaseBaseUrl.orNull.orEmpty()
|
val value = releaseBaseUrl.orNull.orEmpty()
|
||||||
val uri = runCatching { URI(value) }.getOrNull()
|
val uri = runCatching { URI(value) }.getOrNull()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,3 +2,6 @@ org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||||
org.gradle.parallel=true
|
org.gradle.parallel=true
|
||||||
org.gradle.caching=true
|
org.gradle.caching=true
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
|
# AGP 9 enables unit-test components only for testBuildType by default. Release
|
||||||
|
# is independently minified and signed, so keep its JVM test task available.
|
||||||
|
android.onlyEnableUnitTestForTheTestedBuildType=false
|
||||||
|
|
|
||||||
85
scripts/android-release-build.sh
Executable file
85
scripts/android-release-build.sh
Executable file
|
|
@ -0,0 +1,85 @@
|
||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)
|
||||||
|
ENV_FILE=${WNH_ENV_FILE:-"$ROOT/.env"}
|
||||||
|
config_home=${XDG_CONFIG_HOME:-"$HOME/.config"}
|
||||||
|
SIGNING_DIR=${WNH_ANDROID_SIGNING_DIR:-"$config_home/who_need_help/android-release"}
|
||||||
|
KEYSTORE="$SIGNING_DIR/who-need-help-upload.p12"
|
||||||
|
PASSWORD_FILE="$SIGNING_DIR/who-need-help-upload.password"
|
||||||
|
OUTPUT_DIR=${WNH_ANDROID_RELEASE_OUTPUT_DIR:-"$ROOT/android/dist-release"}
|
||||||
|
|
||||||
|
case "$ENV_FILE" in
|
||||||
|
/*) ;;
|
||||||
|
*) ENV_FILE="$ROOT/$ENV_FILE" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ ! -f "$ENV_FILE" ]; then
|
||||||
|
echo "Missing Android release environment file: $ENV_FILE" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
set -a
|
||||||
|
# shellcheck source=/dev/null
|
||||||
|
. "$ENV_FILE"
|
||||||
|
set +a
|
||||||
|
|
||||||
|
: "${WNH_BASE_URL:?Set WNH_BASE_URL in the selected environment file}"
|
||||||
|
: "${WNH_TRACKING_MIN_TIME_MS:?Set WNH_TRACKING_MIN_TIME_MS in the selected environment file}"
|
||||||
|
: "${WNH_TRACKING_HTTP_TIMEOUT_MS:?Set WNH_TRACKING_HTTP_TIMEOUT_MS in the selected environment file}"
|
||||||
|
: "${WNH_ANDROID_VERSION_CODE:?Set WNH_ANDROID_VERSION_CODE in the selected environment file}"
|
||||||
|
: "${WNH_ANDROID_VERSION_NAME:?Set WNH_ANDROID_VERSION_NAME in the selected environment file}"
|
||||||
|
: "${WNH_ANDROID_SIGNING_KEY_ALIAS:?Set WNH_ANDROID_SIGNING_KEY_ALIAS in the selected environment file}"
|
||||||
|
|
||||||
|
for secret_file in "$KEYSTORE" "$PASSWORD_FILE"; do
|
||||||
|
if [ ! -f "$secret_file" ]; then
|
||||||
|
echo "Missing Android release signing file: $secret_file" >&2
|
||||||
|
echo "Run scripts/init-android-release-signing.sh once." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mode=$(stat -c '%a' "$secret_file")
|
||||||
|
case "$mode" in
|
||||||
|
400|600) ;;
|
||||||
|
*)
|
||||||
|
echo "Android release signing file must have mode 0400 or 0600: $secret_file" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
case "$OUTPUT_DIR" in
|
||||||
|
/*) ;;
|
||||||
|
*) OUTPUT_DIR="$ROOT/$OUTPUT_DIR" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
docker build \
|
||||||
|
--secret "id=android_upload_keystore,src=$KEYSTORE" \
|
||||||
|
--secret "id=android_upload_password,src=$PASSWORD_FILE" \
|
||||||
|
--secret "id=android_upload_alias,env=WNH_ANDROID_SIGNING_KEY_ALIAS" \
|
||||||
|
--build-arg "WNH_BASE_URL=$WNH_BASE_URL" \
|
||||||
|
--build-arg "WNH_TRACKING_MIN_TIME_MS=$WNH_TRACKING_MIN_TIME_MS" \
|
||||||
|
--build-arg "WNH_TRACKING_HTTP_TIMEOUT_MS=$WNH_TRACKING_HTTP_TIMEOUT_MS" \
|
||||||
|
--build-arg "WNH_ANDROID_VERSION_CODE=$WNH_ANDROID_VERSION_CODE" \
|
||||||
|
--build-arg "WNH_ANDROID_VERSION_NAME=$WNH_ANDROID_VERSION_NAME" \
|
||||||
|
--target release-artifact \
|
||||||
|
--output "type=local,dest=$OUTPUT_DIR" \
|
||||||
|
"$ROOT/android"
|
||||||
|
|
||||||
|
for artifact in \
|
||||||
|
"$OUTPUT_DIR/who-need-help-release.apk" \
|
||||||
|
"$OUTPUT_DIR/who-need-help-release.aab" \
|
||||||
|
"$OUTPUT_DIR/signing-certificate.txt" \
|
||||||
|
"$OUTPUT_DIR/bundle-signing-verification.txt" \
|
||||||
|
"$OUTPUT_DIR/bundletool-validation.txt" \
|
||||||
|
"$OUTPUT_DIR/lint-results-release.html"; do
|
||||||
|
if [ ! -s "$artifact" ]; then
|
||||||
|
echo "Android release build did not export the expected artifact: $artifact" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
sha256sum \
|
||||||
|
"$OUTPUT_DIR/who-need-help-release.apk" \
|
||||||
|
"$OUTPUT_DIR/who-need-help-release.aab"
|
||||||
|
echo "Signed Android APK, Play AAB, certificate report, and lint report: $OUTPUT_DIR"
|
||||||
36
scripts/android-release-ci.sh
Executable file
36
scripts/android-release-ci.sh
Executable file
|
|
@ -0,0 +1,36 @@
|
||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
umask 077
|
||||||
|
|
||||||
|
ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)
|
||||||
|
workspace=$(mktemp -d "${TMPDIR:-/tmp}/wnh-android-release-ci.XXXXXX")
|
||||||
|
signing_dir="$workspace/signing"
|
||||||
|
environment_file="$workspace/release.env"
|
||||||
|
output_dir="$workspace/output"
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
rm -rf "$workspace"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT HUP INT TERM
|
||||||
|
|
||||||
|
printf '%s\n' \
|
||||||
|
'WNH_BASE_URL=https://android-release-ci.invalid' \
|
||||||
|
'WNH_TRACKING_MIN_TIME_MS=5000' \
|
||||||
|
'WNH_TRACKING_HTTP_TIMEOUT_MS=15000' \
|
||||||
|
'WNH_ANDROID_VERSION_CODE=1' \
|
||||||
|
'WNH_ANDROID_VERSION_NAME=0.1.0-ci' \
|
||||||
|
'WNH_ANDROID_SIGNING_KEY_ALIAS=who-need-help-ci-upload' \
|
||||||
|
>"$environment_file"
|
||||||
|
chmod 600 "$environment_file"
|
||||||
|
|
||||||
|
WNH_ANDROID_SIGNING_DIR="$signing_dir" \
|
||||||
|
WNH_ANDROID_SIGNING_KEY_ALIAS=who-need-help-ci-upload \
|
||||||
|
"$ROOT/scripts/init-android-release-signing.sh" >/dev/null
|
||||||
|
|
||||||
|
WNH_ANDROID_SIGNING_DIR="$signing_dir" \
|
||||||
|
WNH_ANDROID_RELEASE_OUTPUT_DIR="$output_dir" \
|
||||||
|
WNH_ENV_FILE="$environment_file" \
|
||||||
|
"$ROOT/scripts/android-release-build.sh"
|
||||||
|
|
||||||
|
echo "Ephemeral signed Android release pipeline passed."
|
||||||
84
scripts/init-android-release-signing.sh
Executable file
84
scripts/init-android-release-signing.sh
Executable file
|
|
@ -0,0 +1,84 @@
|
||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
umask 077
|
||||||
|
|
||||||
|
config_home=${XDG_CONFIG_HOME:-"$HOME/.config"}
|
||||||
|
SIGNING_DIR=${WNH_ANDROID_SIGNING_DIR:-"$config_home/who_need_help/android-release"}
|
||||||
|
KEYSTORE="$SIGNING_DIR/who-need-help-upload.p12"
|
||||||
|
PASSWORD_FILE="$SIGNING_DIR/who-need-help-upload.password"
|
||||||
|
KEY_ALIAS=${WNH_ANDROID_SIGNING_KEY_ALIAS:-who-need-help-upload}
|
||||||
|
KEY_IMAGE="gradle:9.6.1-jdk17@sha256:7364ce528f33bb6038672bcef990d524f1ad8fbc292935819c235db886d0fae7"
|
||||||
|
run_id="$$-$(openssl rand -hex 4)"
|
||||||
|
temporary_keystore="$SIGNING_DIR/.who-need-help-upload.$run_id.p12"
|
||||||
|
temporary_password="$SIGNING_DIR/.who-need-help-upload.$run_id.password"
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
if [ -e "$temporary_keystore" ]; then
|
||||||
|
unlink "$temporary_keystore"
|
||||||
|
fi
|
||||||
|
if [ -e "$temporary_password" ]; then
|
||||||
|
unlink "$temporary_password"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
trap cleanup EXIT HUP INT TERM
|
||||||
|
|
||||||
|
case "$KEY_ALIAS" in
|
||||||
|
''|*[!A-Za-z0-9._-]*)
|
||||||
|
echo "WNH_ANDROID_SIGNING_KEY_ALIAS must use only letters, digits, dot, underscore, and dash." >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ -L "$SIGNING_DIR" ]; then
|
||||||
|
echo "Refusing to use a symlink as the Android signing directory: $SIGNING_DIR" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
install -d -m 700 "$SIGNING_DIR"
|
||||||
|
|
||||||
|
if [ -e "$KEYSTORE" ] || [ -e "$PASSWORD_FILE" ]; then
|
||||||
|
echo "Android upload signing material already exists; nothing was overwritten:" >&2
|
||||||
|
echo " $KEYSTORE" >&2
|
||||||
|
echo " $PASSWORD_FILE" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
openssl rand -hex 32 >"$temporary_password"
|
||||||
|
chmod 600 "$temporary_password"
|
||||||
|
|
||||||
|
docker run --rm \
|
||||||
|
--user "$(id -u):$(id -g)" \
|
||||||
|
--mount "type=bind,src=$SIGNING_DIR,dst=/secure" \
|
||||||
|
--entrypoint keytool \
|
||||||
|
"$KEY_IMAGE" \
|
||||||
|
-genkeypair \
|
||||||
|
-keystore "/secure/$(basename "$temporary_keystore")" \
|
||||||
|
-storetype PKCS12 \
|
||||||
|
-storepass:file "/secure/$(basename "$temporary_password")" \
|
||||||
|
-keypass:file "/secure/$(basename "$temporary_password")" \
|
||||||
|
-alias "$KEY_ALIAS" \
|
||||||
|
-keyalg RSA \
|
||||||
|
-keysize 2048 \
|
||||||
|
-validity 10000 \
|
||||||
|
-dname "CN=Who Need Help upload key"
|
||||||
|
|
||||||
|
docker run --rm \
|
||||||
|
--user "$(id -u):$(id -g)" \
|
||||||
|
--mount "type=bind,src=$SIGNING_DIR,dst=/secure,readonly" \
|
||||||
|
--entrypoint keytool \
|
||||||
|
"$KEY_IMAGE" \
|
||||||
|
-list \
|
||||||
|
-keystore "/secure/$(basename "$temporary_keystore")" \
|
||||||
|
-storetype PKCS12 \
|
||||||
|
-storepass:file "/secure/$(basename "$temporary_password")" \
|
||||||
|
-alias "$KEY_ALIAS" >/dev/null
|
||||||
|
|
||||||
|
chmod 600 "$temporary_keystore"
|
||||||
|
mv "$temporary_keystore" "$KEYSTORE"
|
||||||
|
mv "$temporary_password" "$PASSWORD_FILE"
|
||||||
|
|
||||||
|
echo "Generated a dedicated Android upload key without placing secrets in the repository."
|
||||||
|
echo "Private keystore: $KEYSTORE"
|
||||||
|
echo "Password file: $PASSWORD_FILE"
|
||||||
|
echo "Back up both files before the first Play Console upload."
|
||||||
Loading…
Reference in New Issue
Block a user