Separate Android deployment identities
This commit is contained in:
parent
591384f2b7
commit
e5829bcaf2
12
.env.example
12
.env.example
|
|
@ -72,7 +72,7 @@ PHX_CHECK_ORIGINS=
|
||||||
# Android debug builds compile this origin into BuildConfig. The Docker
|
# Android debug builds compile this origin into BuildConfig. The Docker
|
||||||
# emulator uses adb reverse to expose the local Compose proxy on loopback.
|
# emulator uses adb reverse to expose the local Compose proxy on loopback.
|
||||||
WNH_DEBUG_BASE_URL=http://localhost:4010
|
WNH_DEBUG_BASE_URL=http://localhost:4010
|
||||||
# Staging/release builds require a public HTTPS origin. Keep the value
|
# Development/staging/release builds require a public HTTPS origin. Keep the value
|
||||||
# environment-specific; scripts/ensure-local-public-origin.sh can derive it
|
# environment-specific; scripts/ensure-local-public-origin.sh can derive it
|
||||||
# from the three PHX_* values in the ignored .env.
|
# from the three PHX_* values in the ignored .env.
|
||||||
WNH_BASE_URL=
|
WNH_BASE_URL=
|
||||||
|
|
@ -91,8 +91,9 @@ WNH_FIREBASE_API_KEY=
|
||||||
WNH_FIREBASE_PROJECT_ID=
|
WNH_FIREBASE_PROJECT_ID=
|
||||||
WNH_FIREBASE_GCM_SENDER_ID=
|
WNH_FIREBASE_GCM_SENDER_ID=
|
||||||
# Verified Android App Links are configured by the web deployment rather than
|
# Verified Android App Links are configured by the web deployment rather than
|
||||||
# embedded as secrets in the application. Use org.whoneedhelp.mobile.staging
|
# embedded as secrets in the application. Use
|
||||||
# with the staging signing certificate on the dev checkout and
|
# org.whoneedhelp.mobile.development with the development certificate on DEV,
|
||||||
|
# org.whoneedhelp.mobile.staging with the staging certificate on test, and
|
||||||
# org.whoneedhelp.mobile with every active Play signing certificate on
|
# org.whoneedhelp.mobile with every active Play signing certificate on
|
||||||
# production. Keep both empty until the matching signed APK/AAB is available.
|
# production. Keep both empty until the matching signed APK/AAB is available.
|
||||||
ANDROID_APP_LINKS_PACKAGE_NAME=
|
ANDROID_APP_LINKS_PACKAGE_NAME=
|
||||||
|
|
@ -101,7 +102,10 @@ ANDROID_APP_LINKS_SHA256_CERT_FINGERPRINTS=
|
||||||
# keystore and its randomized password live outside the repository under
|
# keystore and its randomized password live outside the repository under
|
||||||
# ~/.config/who_need_help/android-release/.
|
# ~/.config/who_need_help/android-release/.
|
||||||
WNH_ANDROID_SIGNING_KEY_ALIAS=who-need-help-upload
|
WNH_ANDROID_SIGNING_KEY_ALIAS=who-need-help-upload
|
||||||
# The dev-domain staging APK uses a different stable signing identity under
|
# The DEV-domain APK uses a stable signing identity under
|
||||||
|
# ~/.config/who_need_help/android-development/.
|
||||||
|
WNH_ANDROID_DEVELOPMENT_SIGNING_KEY_ALIAS=who-need-help-development
|
||||||
|
# The test-domain staging APK uses a different stable signing identity under
|
||||||
# ~/.config/who_need_help/android-staging/. This keeps App Link verification
|
# ~/.config/who_need_help/android-staging/. This keeps App Link verification
|
||||||
# reproducible without reusing the future production upload key.
|
# reproducible without reusing the future production upload key.
|
||||||
WNH_ANDROID_STAGING_SIGNING_KEY_ALIAS=who-need-help-staging
|
WNH_ANDROID_STAGING_SIGNING_KEY_ALIAS=who-need-help-staging
|
||||||
|
|
|
||||||
|
|
@ -30,3 +30,6 @@ jobs:
|
||||||
|
|
||||||
- name: Exercise signed APK and Play AAB release pipeline
|
- name: Exercise signed APK and Play AAB release pipeline
|
||||||
run: ./scripts/android-release-ci.sh
|
run: ./scripts/android-release-ci.sh
|
||||||
|
|
||||||
|
- name: Exercise isolated signed development APK pipeline
|
||||||
|
run: ./scripts/android-public-ci.sh
|
||||||
|
|
|
||||||
3
.github/workflows/quality.yml
vendored
3
.github/workflows/quality.yml
vendored
|
|
@ -61,3 +61,6 @@ jobs:
|
||||||
|
|
||||||
- name: Exercise signed APK and Play AAB release pipeline
|
- name: Exercise signed APK and Play AAB release pipeline
|
||||||
run: ./scripts/android-release-ci.sh
|
run: ./scripts/android-release-ci.sh
|
||||||
|
|
||||||
|
- name: Exercise isolated signed development APK pipeline
|
||||||
|
run: ./scripts/android-public-ci.sh
|
||||||
|
|
|
||||||
15
README.md
15
README.md
|
|
@ -517,14 +517,21 @@ Results and failure diagnostics are retained by API under ignored
|
||||||
`output/android-instrumentation/`; the exact emulator container and one-run
|
`output/android-instrumentation/`; the exact emulator container and one-run
|
||||||
image are removed automatically.
|
image are removed automatically.
|
||||||
|
|
||||||
The public-staging variant and its instrumentation APK use the explicit HTTPS
|
The public development and staging variants and their instrumentation APKs use
|
||||||
origin from the ignored `.env`. The smoke probe checks rendered WebView DOM on
|
the explicit HTTPS origin from each checkout's ignored `.env`. Development uses
|
||||||
|
`org.whoneedhelp.mobile.development`; the independent test checkout uses
|
||||||
|
`org.whoneedhelp.mobile.staging`. The smoke probe checks rendered WebView DOM on
|
||||||
the home and Safety routes. The cross-client probe uses run-scoped users and a
|
the home and Safety routes. The cross-client probe uses run-scoped users and a
|
||||||
matched medicine request to verify Android login, private chat in both
|
matched medicine request to verify Android login, private chat in both
|
||||||
directions, foreground location sharing, browser marker appearance and
|
directions, foreground location sharing, browser marker appearance and removal,
|
||||||
removal, and exact database cleanup:
|
and exact database cleanup:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
./scripts/android-development-build.sh
|
||||||
|
./scripts/android-development-smoke.sh
|
||||||
|
./scripts/android-browser-development-e2e.sh
|
||||||
|
|
||||||
|
# Run these only from the independent test checkout.
|
||||||
./scripts/android-staging-build.sh
|
./scripts/android-staging-build.sh
|
||||||
./scripts/android-staging-smoke.sh
|
./scripts/android-staging-smoke.sh
|
||||||
./scripts/android-browser-staging-e2e.sh
|
./scripts/android-browser-staging-e2e.sh
|
||||||
|
|
|
||||||
|
|
@ -146,7 +146,7 @@ COPY --from=android-sdk \
|
||||||
/workspace/android/app/build/reports/lint-results-debug.html \
|
/workspace/android/app/build/reports/lint-results-debug.html \
|
||||||
/lint-results-debug.html
|
/lint-results-debug.html
|
||||||
|
|
||||||
FROM android-base AS android-staging-sdk
|
FROM android-base AS android-public-sdk
|
||||||
|
|
||||||
USER gradle
|
USER gradle
|
||||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||||
|
|
@ -163,14 +163,22 @@ ARG WNH_FIREBASE_APPLICATION_ID
|
||||||
ARG WNH_FIREBASE_CLIENT_VALUE
|
ARG WNH_FIREBASE_CLIENT_VALUE
|
||||||
ARG WNH_FIREBASE_PROJECT_ID
|
ARG WNH_FIREBASE_PROJECT_ID
|
||||||
ARG WNH_FIREBASE_GCM_SENDER_ID
|
ARG WNH_FIREBASE_GCM_SENDER_ID
|
||||||
|
ARG WNH_PUBLIC_BUILD_TYPE
|
||||||
|
ARG WNH_EXPECTED_APPLICATION_ID
|
||||||
|
|
||||||
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 \
|
||||||
--mount=type=secret,id=android_staging_keystore,required=true,uid=1000,gid=1000,mode=0400 \
|
--mount=type=secret,id=android_nonproduction_keystore,required=true,uid=1000,gid=1000,mode=0400 \
|
||||||
--mount=type=secret,id=android_staging_password,required=true,uid=1000,gid=1000,mode=0400 \
|
--mount=type=secret,id=android_nonproduction_password,required=true,uid=1000,gid=1000,mode=0400 \
|
||||||
--mount=type=secret,id=android_staging_alias,required=true,env=WNH_ANDROID_SIGNING_KEY_ALIAS \
|
--mount=type=secret,id=android_nonproduction_alias,required=true,env=WNH_ANDROID_SIGNING_KEY_ALIAS \
|
||||||
WNH_ANDROID_SIGNING_STORE_FILE=/run/secrets/android_staging_keystore \
|
case "${WNH_PUBLIC_BUILD_TYPE}" in \
|
||||||
WNH_ANDROID_SIGNING_PASSWORD_FILE=/run/secrets/android_staging_password \
|
development) task_name=Development ;; \
|
||||||
|
staging) task_name=Staging ;; \
|
||||||
|
*) echo "WNH_PUBLIC_BUILD_TYPE must be development or staging" >&2; exit 1 ;; \
|
||||||
|
esac \
|
||||||
|
&& test -n "${WNH_EXPECTED_APPLICATION_ID}" \
|
||||||
|
&& WNH_ANDROID_SIGNING_STORE_FILE=/run/secrets/android_nonproduction_keystore \
|
||||||
|
WNH_ANDROID_SIGNING_PASSWORD_FILE=/run/secrets/android_nonproduction_password \
|
||||||
gradle --no-daemon \
|
gradle --no-daemon \
|
||||||
"-PWNH_BASE_URL=${WNH_BASE_URL}" \
|
"-PWNH_BASE_URL=${WNH_BASE_URL}" \
|
||||||
"-PWNH_DEBUG_BASE_URL=${WNH_BASE_URL}" \
|
"-PWNH_DEBUG_BASE_URL=${WNH_BASE_URL}" \
|
||||||
|
|
@ -182,38 +190,38 @@ RUN --mount=type=cache,target=/home/gradle/.gradle,uid=1000,gid=1000 \
|
||||||
"-PWNH_FIREBASE_API_KEY=${WNH_FIREBASE_CLIENT_VALUE}" \
|
"-PWNH_FIREBASE_API_KEY=${WNH_FIREBASE_CLIENT_VALUE}" \
|
||||||
"-PWNH_FIREBASE_PROJECT_ID=${WNH_FIREBASE_PROJECT_ID}" \
|
"-PWNH_FIREBASE_PROJECT_ID=${WNH_FIREBASE_PROJECT_ID}" \
|
||||||
"-PWNH_FIREBASE_GCM_SENDER_ID=${WNH_FIREBASE_GCM_SENDER_ID}" \
|
"-PWNH_FIREBASE_GCM_SENDER_ID=${WNH_FIREBASE_GCM_SENDER_ID}" \
|
||||||
"-PWNH_TEST_BUILD_TYPE=staging" \
|
"-PWNH_TEST_BUILD_TYPE=${WNH_PUBLIC_BUILD_TYPE}" \
|
||||||
testStagingUnitTest lintStaging assembleStaging assembleStagingAndroidTest \
|
"test${task_name}UnitTest" \
|
||||||
|
"lint${task_name}" \
|
||||||
|
"assemble${task_name}" \
|
||||||
|
"assemble${task_name}AndroidTest" \
|
||||||
&& "${ANDROID_HOME}/build-tools/37.0.0/apksigner" \
|
&& "${ANDROID_HOME}/build-tools/37.0.0/apksigner" \
|
||||||
verify --verbose --print-certs \
|
verify --verbose --print-certs \
|
||||||
app/build/outputs/apk/staging/app-staging.apk \
|
"app/build/outputs/apk/${WNH_PUBLIC_BUILD_TYPE}/app-${WNH_PUBLIC_BUILD_TYPE}.apk" \
|
||||||
>app/build/outputs/apk/staging/signing-certificate.txt \
|
>"/tmp/signing-certificate.txt" \
|
||||||
&& "${ANDROID_HOME}/build-tools/37.0.0/aapt2" dump badging \
|
&& "${ANDROID_HOME}/build-tools/37.0.0/aapt2" dump badging \
|
||||||
app/build/outputs/apk/staging/app-staging.apk \
|
"app/build/outputs/apk/${WNH_PUBLIC_BUILD_TYPE}/app-${WNH_PUBLIC_BUILD_TYPE}.apk" \
|
||||||
| sed -n "s/^package: name='\\([^']*\\)'.*/\\1/p" \
|
| sed -n "s/^package: name='\\([^']*\\)'.*/\\1/p" \
|
||||||
>app/build/outputs/apk/staging/package-name.txt \
|
>"/tmp/package-name.txt" \
|
||||||
&& grep -Fx "org.whoneedhelp.mobile.staging" \
|
&& grep -Fx "${WNH_EXPECTED_APPLICATION_ID}" "/tmp/package-name.txt" \
|
||||||
app/build/outputs/apk/staging/package-name.txt
|
&& mkdir -p /workspace/export \
|
||||||
|
&& cp \
|
||||||
|
"app/build/outputs/apk/${WNH_PUBLIC_BUILD_TYPE}/app-${WNH_PUBLIC_BUILD_TYPE}.apk" \
|
||||||
|
"/workspace/export/who-need-help-${WNH_PUBLIC_BUILD_TYPE}.apk" \
|
||||||
|
&& cp \
|
||||||
|
"app/build/outputs/apk/androidTest/${WNH_PUBLIC_BUILD_TYPE}/app-${WNH_PUBLIC_BUILD_TYPE}-androidTest.apk" \
|
||||||
|
"/workspace/export/who-need-help-${WNH_PUBLIC_BUILD_TYPE}-androidTest.apk" \
|
||||||
|
&& cp \
|
||||||
|
"app/build/reports/lint-results-${WNH_PUBLIC_BUILD_TYPE}.html" \
|
||||||
|
"/workspace/export/lint-results-${WNH_PUBLIC_BUILD_TYPE}.html" \
|
||||||
|
&& cp /tmp/signing-certificate.txt /workspace/export/signing-certificate.txt \
|
||||||
|
&& cp /tmp/package-name.txt /workspace/export/package-name.txt
|
||||||
|
|
||||||
FROM scratch AS staging-artifact
|
FROM scratch AS public-artifact
|
||||||
|
|
||||||
USER 65532:65532
|
USER 65532:65532
|
||||||
|
|
||||||
COPY --from=android-staging-sdk \
|
COPY --from=android-public-sdk /workspace/export/ /
|
||||||
/workspace/android/app/build/outputs/apk/staging/app-staging.apk \
|
|
||||||
/who-need-help-staging.apk
|
|
||||||
COPY --from=android-staging-sdk \
|
|
||||||
/workspace/android/app/build/outputs/apk/androidTest/staging/app-staging-androidTest.apk \
|
|
||||||
/who-need-help-staging-androidTest.apk
|
|
||||||
COPY --from=android-staging-sdk \
|
|
||||||
/workspace/android/app/build/reports/lint-results-staging.html \
|
|
||||||
/lint-results-staging.html
|
|
||||||
COPY --from=android-staging-sdk \
|
|
||||||
/workspace/android/app/build/outputs/apk/staging/signing-certificate.txt \
|
|
||||||
/signing-certificate.txt
|
|
||||||
COPY --from=android-staging-sdk \
|
|
||||||
/workspace/android/app/build/outputs/apk/staging/package-name.txt \
|
|
||||||
/package-name.txt
|
|
||||||
|
|
||||||
FROM android-base AS android-release-base
|
FROM android-base AS android-release-base
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -87,8 +87,11 @@ build.
|
||||||
|
|
||||||
Google/Firebase setup is environment-specific as well:
|
Google/Firebase setup is environment-specific as well:
|
||||||
|
|
||||||
- dev/staging uses package `org.whoneedhelp.mobile.staging`, its stable staging
|
- development uses package `org.whoneedhelp.mobile.development`, its stable
|
||||||
signing certificate, the dev Web OAuth client, and the dev Firebase project;
|
development certificate, the development Web OAuth client, and the
|
||||||
|
development Firebase project;
|
||||||
|
- test/staging uses package `org.whoneedhelp.mobile.staging`, its independent
|
||||||
|
staging certificate, and the test environment's provider configuration;
|
||||||
- production uses package `org.whoneedhelp.mobile`, the Play-distributed signing
|
- production uses package `org.whoneedhelp.mobile`, the Play-distributed signing
|
||||||
certificate, the production Web OAuth client, and the production Firebase
|
certificate, the production Web OAuth client, and the production Firebase
|
||||||
project;
|
project;
|
||||||
|
|
@ -119,38 +122,50 @@ separate app-signing key used for distributed APKs:
|
||||||
- <https://support.google.com/googleplay/android-developer/answer/9859152>
|
- <https://support.google.com/googleplay/android-developer/answer/9859152>
|
||||||
- <https://docs.docker.com/build/building/secrets/>
|
- <https://docs.docker.com/build/building/secrets/>
|
||||||
|
|
||||||
## Public staging build
|
## Public development and staging builds
|
||||||
|
|
||||||
The installable `staging` build type uses the explicit public HTTPS
|
The installable `development` and `staging` build types use the explicit public
|
||||||
`WNH_BASE_URL`, disables cleartext traffic, and has its own
|
HTTPS `WNH_BASE_URL` and disable cleartext traffic. Development is the
|
||||||
`org.whoneedhelp.mobile.staging` application ID. Configure a missing local value
|
`org.whoneedhelp.mobile.development` application connected to the development
|
||||||
from the existing `PHX_HOST`, `PHX_SCHEME`, and `PHX_URL_PORT`, then build:
|
origin. Generate its dedicated signing identity once, synchronize the public
|
||||||
|
identity into the checkout's single ignored `.env`, and build:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
./scripts/ensure-local-public-origin.sh
|
./scripts/init-android-development-signing.sh
|
||||||
|
./scripts/configure-android-development-env.sh
|
||||||
|
./scripts/android-development-build.sh
|
||||||
|
sha256sum android/dist-development/who-need-help-development.apk
|
||||||
|
```
|
||||||
|
|
||||||
|
The separate test checkout uses `org.whoneedhelp.mobile.staging`, its own
|
||||||
|
staging key, and the same workflow with test-specific inputs:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./scripts/init-android-staging-signing.sh
|
||||||
./scripts/android-staging-build.sh
|
./scripts/android-staging-build.sh
|
||||||
sha256sum android/dist-staging/who-need-help-staging.apk
|
sha256sum android/dist-staging/who-need-help-staging.apk
|
||||||
```
|
```
|
||||||
|
|
||||||
This variant uses the dedicated stable staging key below
|
The two identities live below
|
||||||
`~/.config/who_need_help/android-staging/`. It is not the production upload
|
`~/.config/who_need_help/android-development/` and
|
||||||
identity and must not be published as a production release. The manifest
|
`~/.config/who_need_help/android-staging/`. Neither is the production upload
|
||||||
accepts same-origin HTTPS deep links, while verified Android App Links require
|
identity or suitable for publication as the production application. Each
|
||||||
this staging certificate fingerprint in the dev deployment's
|
deployment's `/.well-known/assetlinks.json` must contain the package and
|
||||||
`/.well-known/assetlinks.json`.
|
certificate fingerprint of the APK connected to that exact origin.
|
||||||
|
|
||||||
With the temporary public origin reachable, run the API 37 emulator smoke test:
|
With the matching public origin reachable, run the API 37 emulator smoke test:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
./scripts/android-development-smoke.sh
|
||||||
./scripts/android-staging-smoke.sh
|
./scripts/android-staging-smoke.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
The script installs the exported staging APK into a fresh project-scoped
|
Each script installs the corresponding APK into a fresh project-scoped emulator
|
||||||
emulator container, loads the configured HTTPS home page, follows a
|
container, loads the configured HTTPS home page, follows a
|
||||||
same-origin `/safety` deep link, verifies that the package does not claim an
|
same-origin `/safety` deep link, verifies that the package does not claim an
|
||||||
external HTTPS origin, and retains UI dumps, screenshots, package metadata,
|
external HTTPS origin, and retains UI dumps, screenshots, package metadata,
|
||||||
and logcat diagnostics under ignored `output/android-staging-smoke/`. The
|
and logcat diagnostics under its ignored `output/android-*-smoke/` directory.
|
||||||
one-run container and image are removed on success or failure.
|
The one-run container and image are removed on success or failure.
|
||||||
|
|
||||||
## Reproducible Docker build
|
## Reproducible Docker build
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -161,15 +161,34 @@ android {
|
||||||
"\"${debugBaseUrl.get().replace("\\", "\\\\").replace("\"", "\\\"")}\""
|
"\"${debugBaseUrl.get().replace("\\", "\\\\").replace("\"", "\\\"")}\""
|
||||||
)
|
)
|
||||||
manifestPlaceholders["usesCleartextTraffic"] = "true"
|
manifestPlaceholders["usesCleartextTraffic"] = "true"
|
||||||
manifestPlaceholders["deepLinkScheme"] = debugManifestOrigin?.scheme ?: "https"
|
|
||||||
manifestPlaceholders["deepLinkHost"] =
|
manifestPlaceholders["deepLinkHost"] =
|
||||||
debugManifestOrigin?.host ?: "invalid.whoneedhelp.local"
|
debugManifestOrigin?.host ?: "invalid.whoneedhelp.local"
|
||||||
}
|
}
|
||||||
|
|
||||||
create("staging") {
|
create("development") {
|
||||||
initWith(getByName("debug"))
|
initWith(getByName("debug"))
|
||||||
applicationIdSuffix = ".staging"
|
applicationIdSuffix = ".development"
|
||||||
versionNameSuffix = "-staging"
|
versionNameSuffix = "-development"
|
||||||
|
isDebuggable = false
|
||||||
|
if (releaseSigningConfigured) {
|
||||||
|
signingConfig = signingConfigs.getByName("release")
|
||||||
|
}
|
||||||
|
buildConfigField(
|
||||||
|
"String",
|
||||||
|
"BASE_URL",
|
||||||
|
"\"${releaseBaseUrl.get().replace("\\", "\\\\").replace("\"", "\\\"")}\""
|
||||||
|
)
|
||||||
|
manifestPlaceholders["usesCleartextTraffic"] = "false"
|
||||||
|
manifestPlaceholders["deepLinkHost"] =
|
||||||
|
releaseManifestOrigin?.host ?: "invalid.whoneedhelp.local"
|
||||||
|
matchingFallbacks += listOf("debug")
|
||||||
|
}
|
||||||
|
|
||||||
|
create("staging") {
|
||||||
|
initWith(getByName("debug"))
|
||||||
|
applicationIdSuffix = ".staging"
|
||||||
|
versionNameSuffix = "-staging"
|
||||||
|
isDebuggable = false
|
||||||
if (releaseSigningConfigured) {
|
if (releaseSigningConfigured) {
|
||||||
signingConfig = signingConfigs.getByName("release")
|
signingConfig = signingConfigs.getByName("release")
|
||||||
}
|
}
|
||||||
|
|
@ -179,7 +198,6 @@ android {
|
||||||
"\"${releaseBaseUrl.get().replace("\\", "\\\\").replace("\"", "\\\"")}\""
|
"\"${releaseBaseUrl.get().replace("\\", "\\\\").replace("\"", "\\\"")}\""
|
||||||
)
|
)
|
||||||
manifestPlaceholders["usesCleartextTraffic"] = "false"
|
manifestPlaceholders["usesCleartextTraffic"] = "false"
|
||||||
manifestPlaceholders["deepLinkScheme"] = releaseManifestOrigin?.scheme ?: "https"
|
|
||||||
manifestPlaceholders["deepLinkHost"] =
|
manifestPlaceholders["deepLinkHost"] =
|
||||||
releaseManifestOrigin?.host ?: "invalid.whoneedhelp.local"
|
releaseManifestOrigin?.host ?: "invalid.whoneedhelp.local"
|
||||||
matchingFallbacks += listOf("debug")
|
matchingFallbacks += listOf("debug")
|
||||||
|
|
@ -197,7 +215,6 @@ android {
|
||||||
"\"${releaseBaseUrl.get().replace("\\", "\\\\").replace("\"", "\\\"")}\""
|
"\"${releaseBaseUrl.get().replace("\\", "\\\\").replace("\"", "\\\"")}\""
|
||||||
)
|
)
|
||||||
manifestPlaceholders["usesCleartextTraffic"] = "false"
|
manifestPlaceholders["usesCleartextTraffic"] = "false"
|
||||||
manifestPlaceholders["deepLinkScheme"] = releaseManifestOrigin?.scheme ?: "https"
|
|
||||||
manifestPlaceholders["deepLinkHost"] =
|
manifestPlaceholders["deepLinkHost"] =
|
||||||
releaseManifestOrigin?.host ?: "invalid.whoneedhelp.local"
|
releaseManifestOrigin?.host ?: "invalid.whoneedhelp.local"
|
||||||
proguardFiles(
|
proguardFiles(
|
||||||
|
|
@ -221,7 +238,11 @@ android {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.matching { it.name == "preReleaseBuild" || it.name == "preStagingBuild" }.configureEach {
|
tasks.matching {
|
||||||
|
it.name == "preDevelopmentBuild" ||
|
||||||
|
it.name == "preStagingBuild" ||
|
||||||
|
it.name == "preReleaseBuild"
|
||||||
|
}.configureEach {
|
||||||
doFirst {
|
doFirst {
|
||||||
validateFirebaseConfiguration()
|
validateFirebaseConfiguration()
|
||||||
if (!releaseSigningConfigured) {
|
if (!releaseSigningConfigured) {
|
||||||
|
|
|
||||||
|
|
@ -40,9 +40,9 @@
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
<data
|
<data android:scheme="http" />
|
||||||
android:host="${deepLinkHost}"
|
<data android:scheme="https" />
|
||||||
android:scheme="${deepLinkScheme}" />
|
<data android:host="${deepLinkHost}" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<service
|
<service
|
||||||
|
|
|
||||||
|
|
@ -25,8 +25,11 @@ final class LaunchUrlResolver {
|
||||||
String debugInitialUrl,
|
String debugInitialUrl,
|
||||||
boolean debugBuild
|
boolean debugBuild
|
||||||
) {
|
) {
|
||||||
if (deepLinkUrl != null && trustedOrigin.matches(deepLinkUrl)) {
|
if (deepLinkUrl != null) {
|
||||||
return deepLinkUrl;
|
String appLink = trustedOrigin.canonicalAppLink(deepLinkUrl);
|
||||||
|
if (appLink != null) {
|
||||||
|
return appLink;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|
|
||||||
|
|
@ -86,6 +86,41 @@ final class TrustedOrigin {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String canonicalAppLink(String value) {
|
||||||
|
if (matches(value)) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
URI candidate = new URI(value);
|
||||||
|
|
||||||
|
if (
|
||||||
|
!"https".equals(normalized(base.getScheme())) ||
|
||||||
|
!"http".equals(normalized(candidate.getScheme())) ||
|
||||||
|
!normalized(base.getHost()).equals(normalized(candidate.getHost())) ||
|
||||||
|
candidate.getUserInfo() != null ||
|
||||||
|
(candidate.getPort() != -1 && candidate.getPort() != 80)
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
StringBuilder canonical = new StringBuilder(originRule());
|
||||||
|
if (candidate.getRawPath() != null) {
|
||||||
|
canonical.append(candidate.getRawPath());
|
||||||
|
}
|
||||||
|
if (candidate.getRawQuery() != null) {
|
||||||
|
canonical.append('?').append(candidate.getRawQuery());
|
||||||
|
}
|
||||||
|
if (candidate.getRawFragment() != null) {
|
||||||
|
canonical.append('#').append(candidate.getRawFragment());
|
||||||
|
}
|
||||||
|
|
||||||
|
return canonical.toString();
|
||||||
|
} catch (URISyntaxException exception) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static boolean pathIsOrigin(String path) {
|
private static boolean pathIsOrigin(String path) {
|
||||||
return path == null || path.isEmpty() || "/".equals(path);
|
return path == null || path.isEmpty() || "/".equals(path);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,27 @@ public final class LaunchUrlResolverTest {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void releaseUpgradesAssociatedHttpLinkToTrustedHttpsOrigin() {
|
||||||
|
assertEquals(
|
||||||
|
"https://help.example/requests/123?from=email#handover",
|
||||||
|
LaunchUrlResolver.incomingUrl(
|
||||||
|
origin,
|
||||||
|
"http://help.example/requests/123?from=email#handover",
|
||||||
|
null,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
);
|
||||||
|
assertNull(
|
||||||
|
LaunchUrlResolver.incomingUrl(
|
||||||
|
origin,
|
||||||
|
"http://attacker.example/requests/123",
|
||||||
|
null,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void debugCanUseValidatedInitialUrlExtra() {
|
public void debugCanUseValidatedInitialUrlExtra() {
|
||||||
assertEquals(
|
assertEquals(
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package org.whoneedhelp.mobile;
|
||||||
|
|
||||||
import static org.junit.Assert.assertFalse;
|
import static org.junit.Assert.assertFalse;
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertNull;
|
||||||
import static org.junit.Assert.assertThrows;
|
import static org.junit.Assert.assertThrows;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
|
@ -18,6 +19,13 @@ public final class TrustedOriginTest {
|
||||||
assertFalse(origin.matches("http://help.example/"));
|
assertFalse(origin.matches("http://help.example/"));
|
||||||
assertFalse(origin.matches("https://help.example:444/"));
|
assertFalse(origin.matches("https://help.example:444/"));
|
||||||
assertEquals("https://help.example", origin.originRule());
|
assertEquals("https://help.example", origin.originRule());
|
||||||
|
assertEquals(
|
||||||
|
"https://help.example/requests/123?from=web#message",
|
||||||
|
origin.canonicalAppLink(
|
||||||
|
"http://help.example/requests/123?from=web#message"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
assertNull(origin.canonicalAppLink("http://help.example.evil.test/"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
||||||
|
|
@ -126,24 +126,33 @@ checkout they target. Do not create `.env.android-release`,
|
||||||
WNH_BASE_URL=https://dev.example.com
|
WNH_BASE_URL=https://dev.example.com
|
||||||
WNH_ANDROID_VERSION_CODE=1
|
WNH_ANDROID_VERSION_CODE=1
|
||||||
WNH_ANDROID_VERSION_NAME=0.1.0
|
WNH_ANDROID_VERSION_NAME=0.1.0
|
||||||
WNH_ANDROID_STAGING_SIGNING_KEY_ALIAS=who-need-help-staging
|
WNH_ANDROID_DEVELOPMENT_SIGNING_KEY_ALIAS=who-need-help-development
|
||||||
ANDROID_APP_LINKS_PACKAGE_NAME=org.whoneedhelp.mobile.staging
|
ANDROID_APP_LINKS_PACKAGE_NAME=org.whoneedhelp.mobile.development
|
||||||
ANDROID_APP_LINKS_SHA256_CERT_FINGERPRINTS=AA:BB:...
|
ANDROID_APP_LINKS_SHA256_CERT_FINGERPRINTS=AA:BB:...
|
||||||
```
|
```
|
||||||
|
|
||||||
The dev checkout uses package `org.whoneedhelp.mobile.staging` and a dedicated
|
The development checkout uses package
|
||||||
stable key under `~/.config/who_need_help/android-staging/`. Generate it once:
|
`org.whoneedhelp.mobile.development` and a dedicated stable key under
|
||||||
|
`~/.config/who_need_help/android-development/`. Generate it once and place only
|
||||||
|
its public identity into the existing ignored `.env`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./scripts/init-android-staging-signing.sh
|
./scripts/init-android-development-signing.sh
|
||||||
./scripts/android-staging-build.sh
|
./scripts/configure-android-development-env.sh
|
||||||
|
./scripts/android-development-build.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
The build exports the package and certificate reports, verifies that both match
|
The build exports the package and certificate reports, verifies that both match
|
||||||
the checkout `.env`, and checks the HTTPS
|
the checkout `.env`, and checks the HTTPS
|
||||||
`/.well-known/assetlinks.json` response. Losing this key changes the staging
|
`/.well-known/assetlinks.json` response. Losing this key changes the development
|
||||||
certificate and breaks previously installed App Links, so back it up.
|
certificate and breaks previously installed App Links, so back it up.
|
||||||
|
|
||||||
|
The independent test checkout uses package
|
||||||
|
`org.whoneedhelp.mobile.staging`, its own key under
|
||||||
|
`~/.config/who_need_help/android-staging/`, and
|
||||||
|
`scripts/android-staging-build.sh`. Do not copy either ignored `.env` or signing
|
||||||
|
material between development and test.
|
||||||
|
|
||||||
The production checkout instead uses package `org.whoneedhelp.mobile`, the
|
The production checkout instead uses package `org.whoneedhelp.mobile`, the
|
||||||
separate upload material under
|
separate upload material under
|
||||||
`~/.config/who_need_help/android-release/`, and its own `.env`:
|
`~/.config/who_need_help/android-release/`, and its own `.env`:
|
||||||
|
|
@ -187,7 +196,9 @@ For Android Google sign-in, each environment's `GOOGLE_OAUTH_CLIENT_ID` is also
|
||||||
the public server client ID supplied at runtime to Credential Manager.
|
the public server client ID supplied at runtime to Credential Manager.
|
||||||
`GOOGLE_OAUTH_CLIENT_SECRET` never leaves Phoenix. The Android package and
|
`GOOGLE_OAUTH_CLIENT_SECRET` never leaves Phoenix. The Android package and
|
||||||
signing-certificate identity must be registered in the matching Google project:
|
signing-certificate identity must be registered in the matching Google project:
|
||||||
`org.whoneedhelp.mobile.staging` plus the stable staging certificate for dev,
|
`org.whoneedhelp.mobile.development` plus the stable development certificate
|
||||||
|
for development,
|
||||||
|
`org.whoneedhelp.mobile.staging` plus the stable staging certificate for test,
|
||||||
and `org.whoneedhelp.mobile` plus the Play-distributed certificate for
|
and `org.whoneedhelp.mobile` plus the Play-distributed certificate for
|
||||||
production. Do not add a second Google secret file or Gradle property.
|
production. Do not add a second Google secret file or Gradle property.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -191,7 +191,7 @@ this audit.
|
||||||
| Social profiles | Manual links implemented; optional GitHub verification implemented and automated-tested | Manual links cannot set verification fields. The optional GitHub flow uses state, PKCE, a user-bound one-time session, unique provider ownership, and an audit record. The local protocol drill also performs real HTTP token/user exchanges without returning an access token to the application. | GitHub OAuth credentials are intentionally absent and are not required for registration or the help flow. The real external provider redirect/callback remains disabled and unverified; other providers remain manual/unverified. |
|
| Social profiles | Manual links implemented; optional GitHub verification implemented and automated-tested | Manual links cannot set verification fields. The optional GitHub flow uses state, PKCE, a user-bound one-time session, unique provider ownership, and an audit record. The local protocol drill also performs real HTTP token/user exchanges without returning an access token to the application. | GitHub OAuth credentials are intentionally absent and are not required for registration or the help flow. The real external provider redirect/callback remains disabled and unverified; other providers remain manual/unverified. |
|
||||||
| Support and content removal | Implemented and browser-verified | Public support, account deletion, general removal, and TAKE IT DOWN forms create separate audited queues; private email status links verify public contacts; authenticated submissions use the account email; moderator-only operations can update status and notify verified contacts. TAKE IT DOWN accepts URLs/text only and records a 48-hour review due time. Authenticated users can download an allowlisted JSON export, and moderators can run a read-only deletion relationship preflight. | The current product hosts no user media and does not claim TAKE IT DOWN coverage. Staffing, jurisdiction-specific legal classification, final retention rules, destructive account erasure/anonymisation, and identical-media-copy handling remain operational/legal work. |
|
| Support and content removal | Implemented and browser-verified | Public support, account deletion, general removal, and TAKE IT DOWN forms create separate audited queues; private email status links verify public contacts; authenticated submissions use the account email; moderator-only operations can update status and notify verified contacts. TAKE IT DOWN accepts URLs/text only and records a 48-hour review due time. Authenticated users can download an allowlisted JSON export, and moderators can run a read-only deletion relationship preflight. | The current product hosts no user media and does not claim TAKE IT DOWN coverage. Staffing, jurisdiction-specific legal classification, final retention rules, destructive account erasure/anonymisation, and identical-media-copy handling remain operational/legal work. |
|
||||||
| Voluntary thanks | Implemented as an external optional link | A helper can expose an optional link after completion; the UI states that the platform does not process the payment. | The platform does not provide payments, escrow, refunds, tax reporting, or payment guarantees. |
|
| Voluntary thanks | Implemented as an external optional link | A helper can expose an optional link after completion; the UI states that the platform does not process the payment. | The platform does not provide payments, escrow, refunds, tax reporting, or payment guarantees. |
|
||||||
| Android client | Local and public-staging clients implemented and emulator-verified | The native packages `org.whoneedhelp.mobile.debug` and `org.whoneedhelp.mobile.staging` launch the same authenticated LiveView app. Existing lifecycle, permission, deep-link, foreground tracking, recoverable main-page failure, notification-Stop, and Activity-destruction tests remain in place. A stable staging certificate now signs the dev APK, the HTTPS deployment publishes the matching App Links statement, and the build checks its package and SHA-256 certificate. A separate upload key produces a signed production APK and Play AAB. | Play registration/App Signing, on-device domain-verification observation, physical-device FCM delivery, unattended/background-permission tracking, and iOS are not complete. |
|
| Android client | Local, development, test/staging, and production build identities implemented | The native packages `org.whoneedhelp.mobile.debug`, `org.whoneedhelp.mobile.development`, `org.whoneedhelp.mobile.staging`, and `org.whoneedhelp.mobile` are separated by build type and signing identity. Lifecycle, permission, deep-link, foreground tracking, recoverable main-page failure, notification-Stop, and Activity-destruction tests remain in place. Ephemeral signed development and production pipelines verify package IDs, certificates, unit tests, lint, APKs and instrumentation artifacts; production also verifies the signed AAB with Bundletool. | The new development package still needs an online App Links/device observation after the controlled development rebuild. Play registration/App Signing, physical-device FCM delivery, unattended/background-permission tracking, and iOS are not complete. |
|
||||||
| Multiple web/worker instances | Implemented and locally failure/rollout-verified | The final isolated Compose drill passed BEAM crashes and sequential replacement with 3 web/2 worker replicas: all five nodes joined, PubSub passed, and 744/744 readiness requests succeeded. The project-owned kind cluster replaced all 2 web/2 worker pod UIDs under `maxUnavailable=0`; all four replacement pods joined, PubSub passed, and 363/363 samples ultimately succeeded. | Local PostGIS is a single instance. Production database HA, backups, and recovery are operator work and are not claimed complete. |
|
| Multiple web/worker instances | Implemented and locally failure/rollout-verified | The final isolated Compose drill passed BEAM crashes and sequential replacement with 3 web/2 worker replicas: all five nodes joined, PubSub passed, and 744/744 readiness requests succeeded. The project-owned kind cluster replaced all 2 web/2 worker pod UIDs under `maxUnavailable=0`; all four replacement pods joined, PubSub passed, and 363/363 samples ultimately succeeded. | Local PostGIS is a single instance. Production database HA, backups, and recovery are operator work and are not claimed complete. |
|
||||||
| Local observability | Implemented and protocol-verified | Pinned Prometheus scraped the exact 3 web and 2 worker targets with a file Bearer credential; Grafana provisioned a healthy datasource and ten-panel web/worker/BEAM/Ecto/Oban dashboard; Alertmanager delivered firing and resolved webhooks for an induced scoped replica stop. | Local delivery does not establish production retention, notification-provider reliability, on-call policy, or measured alert thresholds. |
|
| Local observability | Implemented and protocol-verified | Pinned Prometheus scraped the exact 3 web and 2 worker targets with a file Bearer credential; Grafana provisioned a healthy datasource and ten-panel web/worker/BEAM/Ecto/Oban dashboard; Alertmanager delivered firing and resolved webhooks for an induced scoped replica stop. | Local delivery does not establish production retention, notification-provider reliability, on-call policy, or measured alert thresholds. |
|
||||||
| Encrypted local backup | Implemented and failure-verified | Pinned Restic streamed PostgreSQL custom format into pinned local MinIO with no host plaintext dump, passed full-data checking and a fresh-database restore, rejected a corrupted repository, and published no snapshot for an interrupted upload. The one-run MinIO project and volume were removed after retaining the non-secret evidence. | The drill proves the local mechanism, not off-site durability, database HA, or a production RPO/RTO/retention policy. |
|
| Encrypted local backup | Implemented and failure-verified | Pinned Restic streamed PostgreSQL custom format into pinned local MinIO with no host plaintext dump, passed full-data checking and a fresh-database restore, rejected a corrupted repository, and published no snapshot for an interrupted upload. The one-run MinIO project and volume were removed after retaining the non-secret evidence. | The drill proves the local mechanism, not off-site durability, database HA, or a production RPO/RTO/retention policy. |
|
||||||
|
|
@ -1365,6 +1365,34 @@ None of the observations below describe the current delivery path.
|
||||||
unsubscribe removal remain unverified until the provider enables them and a
|
unsubscribe removal remain unverified until the provider enables them and a
|
||||||
new delivered MIME is inspected.
|
new delivered MIME is inspected.
|
||||||
|
|
||||||
|
## Development Android and provider isolation on 2026-07-23
|
||||||
|
|
||||||
|
- The development checkout now owns
|
||||||
|
`org.whoneedhelp.mobile.development`, an independent mode-`0600` signing
|
||||||
|
identity outside the repository, and matching App Links values in its one
|
||||||
|
ignored mode-`0600` `.env`. The test/staging package remains
|
||||||
|
`org.whoneedhelp.mobile.staging`; production remains
|
||||||
|
`org.whoneedhelp.mobile`.
|
||||||
|
- The signed ephemeral development pipeline passed development unit tests,
|
||||||
|
Android lint, APK and instrumentation-APK assembly, package/certificate
|
||||||
|
verification, and offline App Links identity validation. The signed
|
||||||
|
production pipeline separately passed release tests, lint, R8/resource
|
||||||
|
shrinking, APK/AAB signing checks, Bundletool validation, and production App
|
||||||
|
Links identity validation.
|
||||||
|
- The complete isolated quality/security gate passed with 352 ExUnit tests,
|
||||||
|
ShellCheck, Hadolint, actionlint, Compose/Helm validation, format/compiler,
|
||||||
|
xref, Credo, Sobelow, Dialyzer, Hex/npm audits, source scanning, and all
|
||||||
|
configured runtime image scans. Its unique Compose project, volume, and
|
||||||
|
temporary image tags were absent after cleanup.
|
||||||
|
- The development Google Web OAuth client is configured for the exact
|
||||||
|
`https://whoneedhelp.imalto.site` origin and callback, and its ID/secret are
|
||||||
|
present only in the ignored development `.env`. The currently running
|
||||||
|
development containers have not yet been rebuilt with that configuration.
|
||||||
|
- Firebase Android and server FCM credentials are intentionally still absent.
|
||||||
|
Adding Firebase to the existing Google Cloud development project is pending
|
||||||
|
explicit acceptance of the separate Firebase terms. No test or production
|
||||||
|
provider configuration was changed.
|
||||||
|
|
||||||
## Known work before a public production launch
|
## Known work before a public production launch
|
||||||
|
|
||||||
- Promote the tested release from `test.whoneedhelp.com` to the independent
|
- Promote the tested release from `test.whoneedhelp.com` to the independent
|
||||||
|
|
@ -1373,13 +1401,14 @@ None of the observations below describe the current delivery path.
|
||||||
after that promotion; the current test origin still depends on its configured
|
after that promotion; the current test origin still depends on its configured
|
||||||
workstation/VPN/gateway path.
|
workstation/VPN/gateway path.
|
||||||
- The final Android application ID is `org.whoneedhelp.mobile`. The application
|
- The final Android application ID is `org.whoneedhelp.mobile`. The application
|
||||||
now publishes environment-specific `/.well-known/assetlinks.json`, and the
|
publishes environment-specific `/.well-known/assetlinks.json`. The previous
|
||||||
stable-signed dev APK was checked against its HTTPS response. Before a Play
|
staging identity was checked against its HTTPS response; repeat that online
|
||||||
release, register the application, add the Play App Signing certificate
|
check for the new `org.whoneedhelp.mobile.development` identity after the
|
||||||
fingerprint alongside any sideload/upload fingerprint, repeat Android's
|
controlled development rebuild. Before a Play release, register the
|
||||||
domain verification on a device, and complete store policy/release work. A
|
application, add the Play App Signing certificate fingerprint alongside any
|
||||||
dedicated upload key and signed APK/AAB exist, but no Play application has
|
sideload/upload fingerprint, repeat Android's domain verification on a
|
||||||
been registered.
|
device, and complete store policy/release work. A dedicated upload key and
|
||||||
|
signed APK/AAB exist, but no Play application has been registered.
|
||||||
- Operate PostgreSQL/PostGIS with off-site backups, recovery testing, monitoring,
|
- Operate PostgreSQL/PostGIS with off-site backups, recovery testing, monitoring,
|
||||||
and the availability model selected for real usage.
|
and the availability model selected for real usage.
|
||||||
- The development Brevo SMTP transport and sender have completed an external
|
- The development Brevo SMTP transport and sender have completed an external
|
||||||
|
|
@ -1391,8 +1420,9 @@ None of the observations below describe the current delivery path.
|
||||||
Google OAuth client on its exact HTTPS callback origin after the tested
|
Google OAuth client on its exact HTTPS callback origin after the tested
|
||||||
release is explicitly promoted. The test client and callback have already
|
release is explicitly promoted. The test client and callback have already
|
||||||
completed real registration and returning-user login.
|
completed real registration and returning-user login.
|
||||||
- Configure environment-specific VAPID and Firebase credentials, then verify a
|
- Development VAPID is configured. Configure isolated Firebase/FCM credentials,
|
||||||
real browser subscription and Android device against each deployed origin.
|
then verify a real browser subscription and Android device against each
|
||||||
|
deployed origin.
|
||||||
Direct Web Push/FCM adapters, registration lifecycle, private payload shape,
|
Direct Web Push/FCM adapters, registration lifecycle, private payload shape,
|
||||||
retries, invalid-device cleanup, and Android deep-link handling are
|
retries, invalid-device cleanup, and Android deep-link handling are
|
||||||
implemented and locally tested; real provider/device delivery is not yet
|
implemented and locally tested; real provider/device delivery is not yet
|
||||||
|
|
|
||||||
7
scripts/android-browser-development-e2e.sh
Executable file
7
scripts/android-browser-development-e2e.sh
Executable file
|
|
@ -0,0 +1,7 @@
|
||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
WNH_ANDROID_PUBLIC_VARIANT=development
|
||||||
|
export WNH_ANDROID_PUBLIC_VARIANT
|
||||||
|
|
||||||
|
exec "$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd)/android-browser-staging-e2e.sh" "$@"
|
||||||
|
|
@ -6,8 +6,27 @@ ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)
|
||||||
cd "$ROOT"
|
cd "$ROOT"
|
||||||
|
|
||||||
ENV_FILE="$ROOT/.env"
|
ENV_FILE="$ROOT/.env"
|
||||||
APK="$ROOT/android/dist-staging/who-need-help-staging.apk"
|
variant=${WNH_ANDROID_PUBLIC_VARIANT:-staging}
|
||||||
TEST_APK="$ROOT/android/dist-staging/who-need-help-staging-androidTest.apk"
|
|
||||||
|
case "$variant" in
|
||||||
|
development)
|
||||||
|
package=org.whoneedhelp.mobile.development
|
||||||
|
expected_deployment_env=development
|
||||||
|
build_script=scripts/android-development-build.sh
|
||||||
|
;;
|
||||||
|
staging)
|
||||||
|
package=org.whoneedhelp.mobile.staging
|
||||||
|
expected_deployment_env="test"
|
||||||
|
build_script=scripts/android-staging-build.sh
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "WNH_ANDROID_PUBLIC_VARIANT must be development or staging." >&2
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
APK="$ROOT/android/dist-$variant/who-need-help-$variant.apk"
|
||||||
|
TEST_APK="$ROOT/android/dist-$variant/who-need-help-$variant-androidTest.apk"
|
||||||
remote_target=
|
remote_target=
|
||||||
remote_host=
|
remote_host=
|
||||||
remote_root=
|
remote_root=
|
||||||
|
|
@ -15,13 +34,13 @@ remote_output_dir=
|
||||||
remote_tools_image=
|
remote_tools_image=
|
||||||
ssh_tunnel_pid=
|
ssh_tunnel_pid=
|
||||||
run_id="$(date -u +%Y%m%d%H%M%S)-$$"
|
run_id="$(date -u +%Y%m%d%H%M%S)-$$"
|
||||||
output_dir="$ROOT/output/android-browser-staging-e2e/$run_id"
|
output_root="$ROOT/output/android-browser-$variant-e2e"
|
||||||
|
output_dir="$output_root/$run_id"
|
||||||
tools_image="who-need-help:android-e2e-tools-$run_id"
|
tools_image="who-need-help:android-e2e-tools-$run_id"
|
||||||
browser_image="who-need-help-e2e-tests:android-$run_id"
|
browser_image="who-need-help-e2e-tests:android-$run_id"
|
||||||
android_image="who-need-help-android:cross-client-$run_id"
|
android_image="who-need-help-android:$variant-cross-client-$run_id"
|
||||||
container="who-need-help-android-cross-client-$run_id"
|
container="who-need-help-android-$variant-cross-client-$run_id"
|
||||||
avd_volume="who-need-help-android-avd-cross-client-$run_id"
|
avd_volume="who-need-help-android-avd-$variant-cross-client-$run_id"
|
||||||
package=org.whoneedhelp.mobile.staging
|
|
||||||
service_class=org.whoneedhelp.mobile.TrackingService
|
service_class=org.whoneedhelp.mobile.TrackingService
|
||||||
fixture_password="$(openssl rand -hex 24)"
|
fixture_password="$(openssl rand -hex 24)"
|
||||||
android_message="android-$run_id"
|
android_message="android-$run_id"
|
||||||
|
|
@ -46,6 +65,11 @@ case "$#" in
|
||||||
esac
|
esac
|
||||||
|
|
||||||
if [[ -n "$remote_target" ]]; then
|
if [[ -n "$remote_target" ]]; then
|
||||||
|
if [[ "$variant" != staging ]]; then
|
||||||
|
echo "Remote cross-client E2E is restricted to the isolated test/staging deployment." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
case "$remote_target" in
|
case "$remote_target" in
|
||||||
*:/*)
|
*:/*)
|
||||||
remote_host=${remote_target%%:*}
|
remote_host=${remote_target%%:*}
|
||||||
|
|
@ -85,13 +109,17 @@ if [[ ! -f "$ENV_FILE" ]]; then
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
"$ROOT/scripts/validate-android-environment.sh" \
|
||||||
|
"$ENV_FILE" \
|
||||||
|
"$expected_deployment_env"
|
||||||
|
|
||||||
if [[ ! -s "$APK" ]]; then
|
if [[ ! -s "$APK" ]]; then
|
||||||
echo "Missing staging APK: $APK. Run scripts/android-staging-build.sh first." >&2
|
echo "Missing $variant APK: $APK. Run $build_script first." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ ! -s "$TEST_APK" ]]; then
|
if [[ ! -s "$TEST_APK" ]]; then
|
||||||
echo "Missing staging test APK: $TEST_APK. Run scripts/android-staging-build.sh first." >&2
|
echo "Missing $variant test APK: $TEST_APK. Run $build_script first." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
@ -100,6 +128,11 @@ set -a
|
||||||
. "$ENV_FILE"
|
. "$ENV_FILE"
|
||||||
set +a
|
set +a
|
||||||
|
|
||||||
|
if [[ -z "$remote_target" && "${DEPLOYMENT_ENV:-}" != "$expected_deployment_env" ]]; then
|
||||||
|
echo "The $variant cross-client E2E requires DEPLOYMENT_ENV=$expected_deployment_env." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ -z "$remote_target" && "${DATABASE_MODE:-container}" != container ]]; then
|
if [[ -z "$remote_target" && "${DATABASE_MODE:-container}" != container ]]; then
|
||||||
echo "This rollback-based Android/browser drill requires DATABASE_MODE=container." >&2
|
echo "This rollback-based Android/browser drill requires DATABASE_MODE=container." >&2
|
||||||
exit 1
|
exit 1
|
||||||
|
|
@ -202,7 +235,7 @@ esac
|
||||||
mailpit_url="http://${mailpit_host}:${MAILPIT_PORT}"
|
mailpit_url="http://${mailpit_host}:${MAILPIT_PORT}"
|
||||||
|
|
||||||
mkdir -p "$output_dir"
|
mkdir -p "$output_dir"
|
||||||
chmod 700 "$ROOT/output" "$ROOT/output/android-browser-staging-e2e" "$output_dir"
|
chmod 700 "$ROOT/output" "$output_root" "$output_dir"
|
||||||
|
|
||||||
if [[ -n "$remote_target" ]]; then
|
if [[ -n "$remote_target" ]]; then
|
||||||
remote_runtime=$(
|
remote_runtime=$(
|
||||||
|
|
@ -424,7 +457,7 @@ start_android_phase() {
|
||||||
-e request_path "$request_path" \
|
-e request_path "$request_path" \
|
||||||
-e android_message "$android_message" \
|
-e android_message "$android_message" \
|
||||||
-e browser_reply "$browser_reply" \
|
-e browser_reply "$browser_reply" \
|
||||||
org.whoneedhelp.mobile.staging.test/androidx.test.runner.AndroidJUnitRunner \
|
"${package}.test/androidx.test.runner.AndroidJUnitRunner" \
|
||||||
>"$android_runner_output" 2>&1 &
|
>"$android_runner_output" 2>&1 &
|
||||||
android_runner_pid=$!
|
android_runner_pid=$!
|
||||||
}
|
}
|
||||||
|
|
@ -462,7 +495,7 @@ cleanup() {
|
||||||
|
|
||||||
if [[ "$prepared" -eq 1 ]]; then
|
if [[ "$prepared" -eq 1 ]]; then
|
||||||
if ! run_fixture_tool cleanup >"$output_dir/fixture-cleanup.log" 2>&1; then
|
if ! run_fixture_tool cleanup >"$output_dir/fixture-cleanup.log" 2>&1; then
|
||||||
echo "Exact Android/browser staging cleanup failed; inspect $output_dir." >&2
|
echo "Exact Android/browser $variant cleanup failed; inspect $output_dir." >&2
|
||||||
status=1
|
status=1
|
||||||
else
|
else
|
||||||
snapshot_database "$output_dir/database-after.txt"
|
snapshot_database "$output_dir/database-after.txt"
|
||||||
|
|
@ -670,10 +703,10 @@ adb shell settings put global window_animation_scale 0
|
||||||
adb shell settings put global transition_animation_scale 0
|
adb shell settings put global transition_animation_scale 0
|
||||||
adb shell settings put global animator_duration_scale 0
|
adb shell settings put global animator_duration_scale 0
|
||||||
adb shell cmd location set-location-enabled true
|
adb shell cmd location set-location-enabled true
|
||||||
docker cp "$APK" "$container:/tmp/who-need-help-staging.apk"
|
docker cp "$APK" "$container:/tmp/who-need-help-public.apk"
|
||||||
docker cp "$TEST_APK" "$container:/tmp/who-need-help-staging-androidTest.apk"
|
docker cp "$TEST_APK" "$container:/tmp/who-need-help-public-androidTest.apk"
|
||||||
adb install -r /tmp/who-need-help-staging.apk >"$output_dir/install.txt"
|
adb install -r /tmp/who-need-help-public.apk >"$output_dir/install.txt"
|
||||||
adb install -r /tmp/who-need-help-staging-androidTest.apk \
|
adb install -r /tmp/who-need-help-public-androidTest.apk \
|
||||||
>"$output_dir/test-install.txt"
|
>"$output_dir/test-install.txt"
|
||||||
adb shell pm list instrumentation >"$output_dir/instrumentation.txt"
|
adb shell pm list instrumentation >"$output_dir/instrumentation.txt"
|
||||||
adb shell pm grant "$package" android.permission.ACCESS_FINE_LOCATION
|
adb shell pm grant "$package" android.permission.ACCESS_FINE_LOCATION
|
||||||
|
|
@ -691,7 +724,7 @@ for _attempt in $(seq 1 45); do
|
||||||
done
|
done
|
||||||
|
|
||||||
if [[ "$network_ready" -ne 1 ]]; then
|
if [[ "$network_ready" -ne 1 ]]; then
|
||||||
echo "The Android emulator could not resolve and reach the staging host." >&2
|
echo "The Android emulator could not resolve and reach the $variant host." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
@ -787,5 +820,5 @@ fi
|
||||||
printf 'load_tls_browser_or_fatal_errors=0\n'
|
printf 'load_tls_browser_or_fatal_errors=0\n'
|
||||||
} >"$output_dir/summary.txt"
|
} >"$output_dir/summary.txt"
|
||||||
|
|
||||||
echo "Android/browser public staging E2E passed."
|
echo "Android/browser public $variant E2E passed."
|
||||||
echo "Evidence: $output_dir"
|
echo "Evidence: $output_dir"
|
||||||
|
|
|
||||||
64
scripts/android-development-build.sh
Executable file
64
scripts/android-development-build.sh
Executable file
|
|
@ -0,0 +1,64 @@
|
||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)
|
||||||
|
ENV_FILE="$ROOT/.env"
|
||||||
|
config_home=${XDG_CONFIG_HOME:-"$HOME/.config"}
|
||||||
|
SIGNING_DIR=${WNH_ANDROID_DEVELOPMENT_SIGNING_DIR:-"$config_home/who_need_help/android-development"}
|
||||||
|
KEYSTORE="$SIGNING_DIR/who-need-help-development.p12"
|
||||||
|
PASSWORD_FILE="$SIGNING_DIR/who-need-help-development.password"
|
||||||
|
|
||||||
|
"$ROOT/scripts/ensure-local-public-origin.sh"
|
||||||
|
"$ROOT/scripts/validate-android-environment.sh" "$ENV_FILE" development
|
||||||
|
|
||||||
|
set -a
|
||||||
|
# shellcheck source=/dev/null
|
||||||
|
. "$ENV_FILE"
|
||||||
|
set +a
|
||||||
|
|
||||||
|
: "${WNH_TRACKING_MIN_TIME_MS:?Set WNH_TRACKING_MIN_TIME_MS in .env}"
|
||||||
|
: "${WNH_TRACKING_HTTP_TIMEOUT_MS:?Set WNH_TRACKING_HTTP_TIMEOUT_MS in .env}"
|
||||||
|
: "${WNH_ANDROID_VERSION_CODE:?Set WNH_ANDROID_VERSION_CODE in .env}"
|
||||||
|
: "${WNH_ANDROID_VERSION_NAME:?Set WNH_ANDROID_VERSION_NAME in .env}"
|
||||||
|
: "${WNH_ANDROID_DEVELOPMENT_SIGNING_KEY_ALIAS:?Set WNH_ANDROID_DEVELOPMENT_SIGNING_KEY_ALIAS in .env}"
|
||||||
|
|
||||||
|
for secret_file in "$KEYSTORE" "$PASSWORD_FILE"; do
|
||||||
|
if [ ! -f "$secret_file" ]; then
|
||||||
|
echo "Missing Android development signing file: $secret_file" >&2
|
||||||
|
echo "Run scripts/init-android-development-signing.sh once." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mode=$(stat -c '%a' "$secret_file")
|
||||||
|
case "$mode" in
|
||||||
|
400|600) ;;
|
||||||
|
*)
|
||||||
|
echo "Android development signing file must have mode 0400 or 0600: $secret_file" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
docker build \
|
||||||
|
--secret "id=android_nonproduction_keystore,src=$KEYSTORE" \
|
||||||
|
--secret "id=android_nonproduction_password,src=$PASSWORD_FILE" \
|
||||||
|
--secret "id=android_nonproduction_alias,env=WNH_ANDROID_DEVELOPMENT_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" \
|
||||||
|
--build-arg "WNH_FIREBASE_APPLICATION_ID=${WNH_FIREBASE_APPLICATION_ID:-}" \
|
||||||
|
--build-arg "WNH_FIREBASE_CLIENT_VALUE=${WNH_FIREBASE_API_KEY:-}" \
|
||||||
|
--build-arg "WNH_FIREBASE_PROJECT_ID=${WNH_FIREBASE_PROJECT_ID:-}" \
|
||||||
|
--build-arg "WNH_FIREBASE_GCM_SENDER_ID=${WNH_FIREBASE_GCM_SENDER_ID:-}" \
|
||||||
|
--build-arg "WNH_PUBLIC_BUILD_TYPE=development" \
|
||||||
|
--build-arg "WNH_EXPECTED_APPLICATION_ID=org.whoneedhelp.mobile.development" \
|
||||||
|
--target public-artifact \
|
||||||
|
--output "type=local,dest=$ROOT/android/dist-development" \
|
||||||
|
"$ROOT/android"
|
||||||
|
|
||||||
|
"$ROOT/scripts/android-app-links-verify.sh" \
|
||||||
|
"$ENV_FILE" \
|
||||||
|
"$ROOT/android/dist-development" \
|
||||||
|
--online
|
||||||
7
scripts/android-development-smoke.sh
Executable file
7
scripts/android-development-smoke.sh
Executable file
|
|
@ -0,0 +1,7 @@
|
||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
WNH_ANDROID_PUBLIC_VARIANT=development
|
||||||
|
export WNH_ANDROID_PUBLIC_VARIANT
|
||||||
|
|
||||||
|
exec "$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd)/android-staging-smoke.sh" "$@"
|
||||||
78
scripts/android-public-ci.sh
Executable file
78
scripts/android-public-ci.sh
Executable file
|
|
@ -0,0 +1,78 @@
|
||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
umask 077
|
||||||
|
|
||||||
|
ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)
|
||||||
|
workspace=$(mktemp -d "${TMPDIR:-/tmp}/wnh-android-public-ci.XXXXXX")
|
||||||
|
signing_dir="$workspace/signing"
|
||||||
|
environment_file="$workspace/development.env"
|
||||||
|
output_dir="$workspace/output"
|
||||||
|
key_alias=who-need-help-ci-development
|
||||||
|
base_url=https://android-development-ci.invalid
|
||||||
|
WNH_ANDROID_SIGNING_KEY_ALIAS=$key_alias
|
||||||
|
export WNH_ANDROID_SIGNING_KEY_ALIAS
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
rm -rf "$workspace"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT HUP INT TERM
|
||||||
|
|
||||||
|
WNH_ANDROID_SIGNING_DIR="$signing_dir" \
|
||||||
|
WNH_ANDROID_SIGNING_BASENAME=who-need-help-development \
|
||||||
|
WNH_ANDROID_SIGNING_KEY_ALIAS="$key_alias" \
|
||||||
|
WNH_ANDROID_SIGNING_SUBJECT="CN=Who Need Help CI development key" \
|
||||||
|
"$ROOT/scripts/init-android-release-signing.sh" >/dev/null
|
||||||
|
|
||||||
|
fingerprint=$(
|
||||||
|
"$ROOT/scripts/android-signing-fingerprint.sh" \
|
||||||
|
"$signing_dir/who-need-help-development.p12" \
|
||||||
|
"$signing_dir/who-need-help-development.password" \
|
||||||
|
"$key_alias"
|
||||||
|
)
|
||||||
|
|
||||||
|
printf '%s\n' \
|
||||||
|
'DEPLOYMENT_ENV=development' \
|
||||||
|
'PHX_HOST=android-development-ci.invalid' \
|
||||||
|
'PHX_SCHEME=https' \
|
||||||
|
'PHX_URL_PORT=443' \
|
||||||
|
"WNH_BASE_URL=$base_url" \
|
||||||
|
"WNH_DEBUG_BASE_URL=$base_url" \
|
||||||
|
'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_DEVELOPMENT_SIGNING_KEY_ALIAS=$key_alias" \
|
||||||
|
'ANDROID_APP_LINKS_PACKAGE_NAME=org.whoneedhelp.mobile.development' \
|
||||||
|
"ANDROID_APP_LINKS_SHA256_CERT_FINGERPRINTS=$fingerprint" \
|
||||||
|
>"$environment_file"
|
||||||
|
chmod 600 "$environment_file"
|
||||||
|
|
||||||
|
"$ROOT/scripts/validate-android-environment.sh" \
|
||||||
|
"$environment_file" \
|
||||||
|
development
|
||||||
|
|
||||||
|
docker build \
|
||||||
|
--secret "id=android_nonproduction_keystore,src=$signing_dir/who-need-help-development.p12" \
|
||||||
|
--secret "id=android_nonproduction_password,src=$signing_dir/who-need-help-development.password" \
|
||||||
|
--secret "id=android_nonproduction_alias,env=WNH_ANDROID_SIGNING_KEY_ALIAS" \
|
||||||
|
--build-arg "WNH_BASE_URL=$base_url" \
|
||||||
|
--build-arg WNH_TRACKING_MIN_TIME_MS=5000 \
|
||||||
|
--build-arg WNH_TRACKING_HTTP_TIMEOUT_MS=15000 \
|
||||||
|
--build-arg WNH_ANDROID_VERSION_CODE=1 \
|
||||||
|
--build-arg WNH_ANDROID_VERSION_NAME=0.1.0-ci \
|
||||||
|
--build-arg WNH_PUBLIC_BUILD_TYPE=development \
|
||||||
|
--build-arg WNH_EXPECTED_APPLICATION_ID=org.whoneedhelp.mobile.development \
|
||||||
|
--target public-artifact \
|
||||||
|
--output "type=local,dest=$output_dir" \
|
||||||
|
"$ROOT/android"
|
||||||
|
|
||||||
|
"$ROOT/scripts/android-app-links-verify.sh" \
|
||||||
|
"$environment_file" \
|
||||||
|
"$output_dir"
|
||||||
|
|
||||||
|
test -s "$output_dir/who-need-help-development.apk"
|
||||||
|
test -s "$output_dir/who-need-help-development-androidTest.apk"
|
||||||
|
test -s "$output_dir/lint-results-development.html"
|
||||||
|
|
||||||
|
echo "Ephemeral signed Android development pipeline passed."
|
||||||
|
|
@ -24,6 +24,8 @@ set -a
|
||||||
. "$ENV_FILE"
|
. "$ENV_FILE"
|
||||||
set +a
|
set +a
|
||||||
|
|
||||||
|
"$ROOT/scripts/validate-android-environment.sh" "$ENV_FILE" production
|
||||||
|
|
||||||
: "${WNH_BASE_URL:?Set WNH_BASE_URL in the selected environment file}"
|
: "${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_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_TRACKING_HTTP_TIMEOUT_MS:?Set WNH_TRACKING_HTTP_TIMEOUT_MS in the selected environment file}"
|
||||||
|
|
@ -84,10 +86,7 @@ for artifact in \
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
if [ -n "${ANDROID_APP_LINKS_PACKAGE_NAME:-}" ] ||
|
"$ROOT/scripts/android-app-links-verify.sh" "$ENV_FILE" "$OUTPUT_DIR"
|
||||||
[ -n "${ANDROID_APP_LINKS_SHA256_CERT_FINGERPRINTS:-}" ]; then
|
|
||||||
"$ROOT/scripts/android-app-links-verify.sh" "$ENV_FILE" "$OUTPUT_DIR"
|
|
||||||
fi
|
|
||||||
|
|
||||||
sha256sum \
|
sha256sum \
|
||||||
"$OUTPUT_DIR/who-need-help-release.apk" \
|
"$OUTPUT_DIR/who-need-help-release.apk" \
|
||||||
|
|
|
||||||
|
|
@ -8,26 +8,42 @@ workspace=$(mktemp -d "${TMPDIR:-/tmp}/wnh-android-release-ci.XXXXXX")
|
||||||
signing_dir="$workspace/signing"
|
signing_dir="$workspace/signing"
|
||||||
environment_file="$workspace/release.env"
|
environment_file="$workspace/release.env"
|
||||||
output_dir="$workspace/output"
|
output_dir="$workspace/output"
|
||||||
|
key_alias=who-need-help-ci-upload
|
||||||
|
base_url=https://android-release-ci.invalid
|
||||||
|
|
||||||
cleanup() {
|
cleanup() {
|
||||||
rm -rf "$workspace"
|
rm -rf "$workspace"
|
||||||
}
|
}
|
||||||
trap cleanup EXIT HUP INT TERM
|
trap cleanup EXIT HUP INT TERM
|
||||||
|
|
||||||
|
WNH_ANDROID_SIGNING_DIR="$signing_dir" \
|
||||||
|
WNH_ANDROID_SIGNING_KEY_ALIAS="$key_alias" \
|
||||||
|
"$ROOT/scripts/init-android-release-signing.sh" >/dev/null
|
||||||
|
|
||||||
|
fingerprint=$(
|
||||||
|
"$ROOT/scripts/android-signing-fingerprint.sh" \
|
||||||
|
"$signing_dir/who-need-help-upload.p12" \
|
||||||
|
"$signing_dir/who-need-help-upload.password" \
|
||||||
|
"$key_alias"
|
||||||
|
)
|
||||||
|
|
||||||
printf '%s\n' \
|
printf '%s\n' \
|
||||||
'WNH_BASE_URL=https://android-release-ci.invalid' \
|
'DEPLOYMENT_ENV=production' \
|
||||||
|
'PHX_HOST=android-release-ci.invalid' \
|
||||||
|
'PHX_SCHEME=https' \
|
||||||
|
'PHX_URL_PORT=443' \
|
||||||
|
"WNH_BASE_URL=$base_url" \
|
||||||
|
"WNH_DEBUG_BASE_URL=$base_url" \
|
||||||
'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_CODE=1' \
|
||||||
'WNH_ANDROID_VERSION_NAME=0.1.0-ci' \
|
'WNH_ANDROID_VERSION_NAME=0.1.0-ci' \
|
||||||
'WNH_ANDROID_SIGNING_KEY_ALIAS=who-need-help-ci-upload' \
|
"WNH_ANDROID_SIGNING_KEY_ALIAS=$key_alias" \
|
||||||
|
'ANDROID_APP_LINKS_PACKAGE_NAME=org.whoneedhelp.mobile' \
|
||||||
|
"ANDROID_APP_LINKS_SHA256_CERT_FINGERPRINTS=$fingerprint" \
|
||||||
>"$environment_file"
|
>"$environment_file"
|
||||||
chmod 600 "$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_SIGNING_DIR="$signing_dir" \
|
||||||
WNH_ANDROID_RELEASE_OUTPUT_DIR="$output_dir" \
|
WNH_ANDROID_RELEASE_OUTPUT_DIR="$output_dir" \
|
||||||
WNH_ENV_FILE="$environment_file" \
|
WNH_ENV_FILE="$environment_file" \
|
||||||
|
|
|
||||||
82
scripts/android-signing-fingerprint.sh
Executable file
82
scripts/android-signing-fingerprint.sh
Executable file
|
|
@ -0,0 +1,82 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
keystore=${1:-}
|
||||||
|
password_file=${2:-}
|
||||||
|
key_alias=${3:-}
|
||||||
|
key_image="gradle:9.6.1-jdk17@sha256:7364ce528f33bb6038672bcef990d524f1ad8fbc292935819c235db886d0fae7"
|
||||||
|
|
||||||
|
if [[ -z "$keystore" || -z "$password_file" || -z "$key_alias" ]]; then
|
||||||
|
echo "Usage: $0 KEYSTORE PASSWORD_FILE KEY_ALIAS" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
keystore=$(realpath "$keystore")
|
||||||
|
password_file=$(realpath "$password_file")
|
||||||
|
keystore_dir=$(dirname "$keystore")
|
||||||
|
password_dir=$(dirname "$password_file")
|
||||||
|
|
||||||
|
for secret_file in "$keystore" "$password_file"; do
|
||||||
|
[[ -f "$secret_file" && ! -L "$secret_file" ]] || {
|
||||||
|
echo "Android signing input must be a regular non-symlink file: $secret_file" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
case "$(stat -c '%a' "$secret_file")" in
|
||||||
|
400 | 600) ;;
|
||||||
|
*)
|
||||||
|
echo "Android signing input must have mode 0400 or 0600: $secret_file" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
case "$key_alias" in
|
||||||
|
'' | *[!A-Za-z0-9._-]*)
|
||||||
|
echo "Android signing alias contains unsupported characters." >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
mounts=(
|
||||||
|
--mount "type=bind,src=$keystore_dir,dst=/keystore,readonly"
|
||||||
|
)
|
||||||
|
password_container_dir=/password
|
||||||
|
if [[ "$password_dir" == "$keystore_dir" ]]; then
|
||||||
|
password_container_dir=/keystore
|
||||||
|
else
|
||||||
|
mounts+=(--mount "type=bind,src=$password_dir,dst=/password,readonly")
|
||||||
|
fi
|
||||||
|
|
||||||
|
report=$(
|
||||||
|
docker run --rm \
|
||||||
|
--user "$(id -u):$(id -g)" \
|
||||||
|
"${mounts[@]}" \
|
||||||
|
--entrypoint keytool \
|
||||||
|
"$key_image" \
|
||||||
|
-list -v \
|
||||||
|
-keystore "/keystore/$(basename "$keystore")" \
|
||||||
|
-storetype PKCS12 \
|
||||||
|
-storepass:file "$password_container_dir/$(basename "$password_file")" \
|
||||||
|
-alias "$key_alias"
|
||||||
|
)
|
||||||
|
|
||||||
|
fingerprint=$(
|
||||||
|
awk -F': ' '
|
||||||
|
/^[[:space:]]*SHA256:/ {
|
||||||
|
print $2
|
||||||
|
found = 1
|
||||||
|
exit
|
||||||
|
}
|
||||||
|
END { if (!found) exit 1 }
|
||||||
|
' <<<"$report"
|
||||||
|
) || {
|
||||||
|
echo "The signing certificate SHA-256 fingerprint was not found." >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
[[ "${fingerprint//:/}" =~ ^[0-9A-Fa-f]{64}$ ]] || {
|
||||||
|
echo "The signing certificate SHA-256 fingerprint is malformed." >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
printf '%s\n' "$(tr '[:lower:]' '[:upper:]' <<<"$fingerprint")"
|
||||||
|
|
@ -9,6 +9,7 @@ KEYSTORE="$SIGNING_DIR/who-need-help-staging.p12"
|
||||||
PASSWORD_FILE="$SIGNING_DIR/who-need-help-staging.password"
|
PASSWORD_FILE="$SIGNING_DIR/who-need-help-staging.password"
|
||||||
|
|
||||||
"$ROOT/scripts/ensure-local-public-origin.sh"
|
"$ROOT/scripts/ensure-local-public-origin.sh"
|
||||||
|
"$ROOT/scripts/validate-android-environment.sh" "$ENV_FILE" test
|
||||||
|
|
||||||
set -a
|
set -a
|
||||||
# shellcheck source=/dev/null
|
# shellcheck source=/dev/null
|
||||||
|
|
@ -40,9 +41,9 @@ for secret_file in "$KEYSTORE" "$PASSWORD_FILE"; do
|
||||||
done
|
done
|
||||||
|
|
||||||
docker build \
|
docker build \
|
||||||
--secret "id=android_staging_keystore,src=$KEYSTORE" \
|
--secret "id=android_nonproduction_keystore,src=$KEYSTORE" \
|
||||||
--secret "id=android_staging_password,src=$PASSWORD_FILE" \
|
--secret "id=android_nonproduction_password,src=$PASSWORD_FILE" \
|
||||||
--secret "id=android_staging_alias,env=WNH_ANDROID_STAGING_SIGNING_KEY_ALIAS" \
|
--secret "id=android_nonproduction_alias,env=WNH_ANDROID_STAGING_SIGNING_KEY_ALIAS" \
|
||||||
--build-arg "WNH_BASE_URL=$WNH_BASE_URL" \
|
--build-arg "WNH_BASE_URL=$WNH_BASE_URL" \
|
||||||
--build-arg "WNH_TRACKING_MIN_TIME_MS=$WNH_TRACKING_MIN_TIME_MS" \
|
--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_TRACKING_HTTP_TIMEOUT_MS=$WNH_TRACKING_HTTP_TIMEOUT_MS" \
|
||||||
|
|
@ -52,7 +53,9 @@ docker build \
|
||||||
--build-arg "WNH_FIREBASE_CLIENT_VALUE=${WNH_FIREBASE_API_KEY:-}" \
|
--build-arg "WNH_FIREBASE_CLIENT_VALUE=${WNH_FIREBASE_API_KEY:-}" \
|
||||||
--build-arg "WNH_FIREBASE_PROJECT_ID=${WNH_FIREBASE_PROJECT_ID:-}" \
|
--build-arg "WNH_FIREBASE_PROJECT_ID=${WNH_FIREBASE_PROJECT_ID:-}" \
|
||||||
--build-arg "WNH_FIREBASE_GCM_SENDER_ID=${WNH_FIREBASE_GCM_SENDER_ID:-}" \
|
--build-arg "WNH_FIREBASE_GCM_SENDER_ID=${WNH_FIREBASE_GCM_SENDER_ID:-}" \
|
||||||
--target staging-artifact \
|
--build-arg "WNH_PUBLIC_BUILD_TYPE=staging" \
|
||||||
|
--build-arg "WNH_EXPECTED_APPLICATION_ID=org.whoneedhelp.mobile.staging" \
|
||||||
|
--target public-artifact \
|
||||||
--output "type=local,dest=$ROOT/android/dist-staging" \
|
--output "type=local,dest=$ROOT/android/dist-staging" \
|
||||||
"$ROOT/android"
|
"$ROOT/android"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,14 +4,33 @@ umask 077
|
||||||
|
|
||||||
ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)
|
ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)
|
||||||
ENV_FILE="$ROOT/.env"
|
ENV_FILE="$ROOT/.env"
|
||||||
APK="$ROOT/android/dist-staging/who-need-help-staging.apk"
|
variant=${WNH_ANDROID_PUBLIC_VARIANT:-staging}
|
||||||
TEST_APK="$ROOT/android/dist-staging/who-need-help-staging-androidTest.apk"
|
|
||||||
|
case "$variant" in
|
||||||
|
development)
|
||||||
|
package=org.whoneedhelp.mobile.development
|
||||||
|
validation_environment=development
|
||||||
|
build_script=scripts/android-development-build.sh
|
||||||
|
;;
|
||||||
|
staging)
|
||||||
|
package=org.whoneedhelp.mobile.staging
|
||||||
|
validation_environment="test"
|
||||||
|
build_script=scripts/android-staging-build.sh
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "WNH_ANDROID_PUBLIC_VARIANT must be development or staging." >&2
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
APK="$ROOT/android/dist-$variant/who-need-help-$variant.apk"
|
||||||
|
TEST_APK="$ROOT/android/dist-$variant/who-need-help-$variant-androidTest.apk"
|
||||||
run_id=$(date -u +%Y%m%d%H%M%S)-$$
|
run_id=$(date -u +%Y%m%d%H%M%S)-$$
|
||||||
image="who-need-help-android:staging-smoke-$run_id"
|
image="who-need-help-android:$variant-smoke-$run_id"
|
||||||
container="who-need-help-android-staging-smoke-$run_id"
|
container="who-need-help-android-$variant-smoke-$run_id"
|
||||||
avd_volume="who-need-help-android-avd-staging-smoke-$run_id"
|
avd_volume="who-need-help-android-avd-$variant-smoke-$run_id"
|
||||||
output="$ROOT/output/android-staging-smoke/$run_id"
|
output_root="$ROOT/output/android-$variant-smoke"
|
||||||
package=org.whoneedhelp.mobile.staging
|
output="$output_root/$run_id"
|
||||||
activity=org.whoneedhelp.mobile.MainActivity
|
activity=org.whoneedhelp.mobile.MainActivity
|
||||||
|
|
||||||
if [ ! -e /dev/kvm ]; then
|
if [ ! -e /dev/kvm ]; then
|
||||||
|
|
@ -24,13 +43,17 @@ if [ ! -f "$ENV_FILE" ]; then
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
"$ROOT/scripts/validate-android-environment.sh" \
|
||||||
|
"$ENV_FILE" \
|
||||||
|
"$validation_environment"
|
||||||
|
|
||||||
if [ ! -s "$APK" ]; then
|
if [ ! -s "$APK" ]; then
|
||||||
echo "Missing staging APK: $APK. Run scripts/android-staging-build.sh first." >&2
|
echo "Missing $variant APK: $APK. Run $build_script first." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ ! -s "$TEST_APK" ]; then
|
if [ ! -s "$TEST_APK" ]; then
|
||||||
echo "Missing staging test APK: $TEST_APK. Run scripts/android-staging-build.sh first." >&2
|
echo "Missing $variant test APK: $TEST_APK. Run $build_script first." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
@ -42,6 +65,7 @@ set +a
|
||||||
: "${WNH_BASE_URL:?Set WNH_BASE_URL in .env}"
|
: "${WNH_BASE_URL:?Set WNH_BASE_URL in .env}"
|
||||||
: "${WNH_TRACKING_MIN_TIME_MS:?Set WNH_TRACKING_MIN_TIME_MS in .env}"
|
: "${WNH_TRACKING_MIN_TIME_MS:?Set WNH_TRACKING_MIN_TIME_MS in .env}"
|
||||||
: "${WNH_TRACKING_HTTP_TIMEOUT_MS:?Set WNH_TRACKING_HTTP_TIMEOUT_MS in .env}"
|
: "${WNH_TRACKING_HTTP_TIMEOUT_MS:?Set WNH_TRACKING_HTTP_TIMEOUT_MS in .env}"
|
||||||
|
: "${WNH_ANDROID_VERSION_NAME:?Set WNH_ANDROID_VERSION_NAME in .env}"
|
||||||
|
|
||||||
case "$WNH_BASE_URL" in
|
case "$WNH_BASE_URL" in
|
||||||
https://*/* | https://*) ;;
|
https://*/* | https://*) ;;
|
||||||
|
|
@ -63,7 +87,7 @@ esac
|
||||||
expected_host=${origin_without_scheme%%:*}
|
expected_host=${origin_without_scheme%%:*}
|
||||||
|
|
||||||
mkdir -p "$output"
|
mkdir -p "$output"
|
||||||
chmod 700 "$ROOT/output" "$ROOT/output/android-staging-smoke" "$output"
|
chmod 700 "$ROOT/output" "$output_root" "$output"
|
||||||
|
|
||||||
cleanup() {
|
cleanup() {
|
||||||
status=$?
|
status=$?
|
||||||
|
|
@ -133,7 +157,7 @@ docker run -d \
|
||||||
|
|
||||||
if [ "$(docker inspect --format '{{.State.Running}}' "$container")" != "true" ]; then
|
if [ "$(docker inspect --format '{{.State.Running}}' "$container")" != "true" ]; then
|
||||||
docker logs "$container" >"$output/emulator.log" 2>&1 || true
|
docker logs "$container" >"$output/emulator.log" 2>&1 || true
|
||||||
echo "Android staging emulator exited before ADB became available; inspect $output/emulator.log." >&2
|
echo "Android $variant emulator exited before ADB became available; inspect $output/emulator.log." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
@ -156,7 +180,7 @@ while [ "$attempt" -lt 90 ]; do
|
||||||
done
|
done
|
||||||
|
|
||||||
if [ "$booted" != "1" ]; then
|
if [ "$booted" != "1" ]; then
|
||||||
echo "Android staging emulator did not finish booting." >&2
|
echo "Android $variant emulator did not finish booting." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
@ -164,11 +188,11 @@ docker exec "$container" adb shell input keyevent 82
|
||||||
docker exec "$container" adb shell settings put global window_animation_scale 0
|
docker exec "$container" adb shell settings put global window_animation_scale 0
|
||||||
docker exec "$container" adb shell settings put global transition_animation_scale 0
|
docker exec "$container" adb shell settings put global transition_animation_scale 0
|
||||||
docker exec "$container" adb shell settings put global animator_duration_scale 0
|
docker exec "$container" adb shell settings put global animator_duration_scale 0
|
||||||
docker cp "$APK" "$container:/tmp/who-need-help-staging.apk"
|
docker cp "$APK" "$container:/tmp/who-need-help-public.apk"
|
||||||
docker cp "$TEST_APK" "$container:/tmp/who-need-help-staging-androidTest.apk"
|
docker cp "$TEST_APK" "$container:/tmp/who-need-help-public-androidTest.apk"
|
||||||
docker exec "$container" adb install -r /tmp/who-need-help-staging.apk \
|
docker exec "$container" adb install -r /tmp/who-need-help-public.apk \
|
||||||
>"$output/install.txt"
|
>"$output/install.txt"
|
||||||
docker exec "$container" adb install -r /tmp/who-need-help-staging-androidTest.apk \
|
docker exec "$container" adb install -r /tmp/who-need-help-public-androidTest.apk \
|
||||||
>"$output/test-install.txt"
|
>"$output/test-install.txt"
|
||||||
docker exec "$container" adb shell pm list instrumentation \
|
docker exec "$container" adb shell pm list instrumentation \
|
||||||
>"$output/instrumentation.txt"
|
>"$output/instrumentation.txt"
|
||||||
|
|
@ -189,12 +213,12 @@ while [ "$attempt" -lt 45 ]; do
|
||||||
done
|
done
|
||||||
|
|
||||||
if [ "$network_ready" != true ]; then
|
if [ "$network_ready" != true ]; then
|
||||||
echo "The Android emulator could not resolve and reach the staging host." >&2
|
echo "The Android emulator could not resolve and reach the $variant host." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if ! grep -Fq "versionName=0.1.0-staging" "$output/package.txt"; then
|
if ! grep -Fq "versionName=$WNH_ANDROID_VERSION_NAME-$variant" "$output/package.txt"; then
|
||||||
echo "The installed package is not the expected staging variant." >&2
|
echo "The installed package is not the expected $variant variant." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
@ -202,12 +226,12 @@ same_origin="$WNH_BASE_URL/safety"
|
||||||
external_origin=https://example.com/
|
external_origin=https://example.com/
|
||||||
|
|
||||||
if ! grep -Fq "Authority: \"$expected_host\"" "$output/package.txt"; then
|
if ! grep -Fq "Authority: \"$expected_host\"" "$output/package.txt"; then
|
||||||
echo "The staging APK does not declare its exact HTTPS host." >&2
|
echo "The $variant APK does not declare its exact HTTPS host." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if grep -Fq 'Authority: "example.com"' "$output/package.txt"; then
|
if grep -Fq 'Authority: "example.com"' "$output/package.txt"; then
|
||||||
echo "The staging APK incorrectly declares an external HTTPS host." >&2
|
echo "The $variant APK incorrectly declares an external HTTPS host." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
@ -222,7 +246,7 @@ docker exec "$container" adb shell cmd package resolve-activity --brief \
|
||||||
-d "$external_origin" >"$output/external-origin-resolver.txt"
|
-d "$external_origin" >"$output/external-origin-resolver.txt"
|
||||||
|
|
||||||
if grep -Fq "$package/" "$output/external-origin-resolver.txt"; then
|
if grep -Fq "$package/" "$output/external-origin-resolver.txt"; then
|
||||||
echo "The staging APK incorrectly claimed an external HTTPS origin." >&2
|
echo "The $variant APK incorrectly claimed an external HTTPS origin." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
@ -249,7 +273,7 @@ while [ "$attempt" -lt 45 ]; do
|
||||||
done
|
done
|
||||||
|
|
||||||
if [ "$home_loaded" != true ]; then
|
if [ "$home_loaded" != true ]; then
|
||||||
echo "The staging WebView did not finish loading the public home page." >&2
|
echo "The $variant WebView did not finish loading the public home page." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
@ -294,7 +318,7 @@ docker exec "$container" adb exec-out screencap -p >"$output/safety.png"
|
||||||
set +e
|
set +e
|
||||||
docker exec "$container" adb shell am instrument -w -r \
|
docker exec "$container" adb shell am instrument -w -r \
|
||||||
-e class org.whoneedhelp.mobile.PublicStagingInstrumentedTest \
|
-e class org.whoneedhelp.mobile.PublicStagingInstrumentedTest \
|
||||||
org.whoneedhelp.mobile.staging.test/androidx.test.runner.AndroidJUnitRunner \
|
"${package}.test/androidx.test.runner.AndroidJUnitRunner" \
|
||||||
>"$output/dom-results.txt" 2>&1
|
>"$output/dom-results.txt" 2>&1
|
||||||
dom_status=$?
|
dom_status=$?
|
||||||
set -e
|
set -e
|
||||||
|
|
@ -304,7 +328,7 @@ if [ "$dom_status" -ne 0 ] \
|
||||||
|| grep -Eq 'FAILURES!!!|INSTRUMENTATION_FAILED|Process crashed' \
|
|| grep -Eq 'FAILURES!!!|INSTRUMENTATION_FAILED|Process crashed' \
|
||||||
"$output/dom-results.txt"; then
|
"$output/dom-results.txt"; then
|
||||||
cat "$output/dom-results.txt" >&2
|
cat "$output/dom-results.txt" >&2
|
||||||
echo "The staging WebView DOM assertions failed." >&2
|
echo "The $variant WebView DOM assertions failed." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
@ -316,12 +340,12 @@ docker exec "$container" adb shell dumpsys activity activities \
|
||||||
if grep -Eqi \
|
if grep -Eqi \
|
||||||
'Main-frame load failed|net::ERR_|ERR_CERT|SSL handshake failed|chromium.*crash' \
|
'Main-frame load failed|net::ERR_|ERR_CERT|SSL handshake failed|chromium.*crash' \
|
||||||
"$output/webview-after-dom.txt"; then
|
"$output/webview-after-dom.txt"; then
|
||||||
echo "Android staging logcat contains a public-page load or TLS failure." >&2
|
echo "Android $variant logcat contains a public-page load or TLS failure." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if ! grep -Fq 'INSTRUMENTATION_CODE: -1' "$output/dom-results.txt"; then
|
if ! grep -Fq 'INSTRUMENTATION_CODE: -1' "$output/dom-results.txt"; then
|
||||||
echo "The staging DOM runner did not finish normally." >&2
|
echo "The $variant DOM runner did not finish normally." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
@ -339,5 +363,5 @@ fi
|
||||||
printf 'load_or_tls_errors=0\n'
|
printf 'load_or_tls_errors=0\n'
|
||||||
} >"$output/summary.txt"
|
} >"$output/summary.txt"
|
||||||
|
|
||||||
echo "Android public staging smoke passed."
|
echo "Android public $variant smoke passed."
|
||||||
echo "Evidence: $output"
|
echo "Evidence: $output"
|
||||||
|
|
|
||||||
|
|
@ -210,11 +210,30 @@ else
|
||||||
partial "Android App Links" "package and signing fingerprints must be configured together"
|
partial "Android App Links" "package and signing fingerprints must be configured together"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if all_set WNH_ANDROID_VERSION_CODE WNH_ANDROID_VERSION_NAME \
|
android_signing_alias=
|
||||||
WNH_ANDROID_SIGNING_KEY_ALIAS WNH_ANDROID_STAGING_SIGNING_KEY_ALIAS; then
|
android_signing_label=
|
||||||
ready "Android release inputs" "version and separate production/staging signing aliases are present"
|
case "$deployment_env" in
|
||||||
|
development)
|
||||||
|
android_signing_alias=WNH_ANDROID_DEVELOPMENT_SIGNING_KEY_ALIAS
|
||||||
|
android_signing_label=development
|
||||||
|
;;
|
||||||
|
test)
|
||||||
|
android_signing_alias=WNH_ANDROID_STAGING_SIGNING_KEY_ALIAS
|
||||||
|
android_signing_label=staging
|
||||||
|
;;
|
||||||
|
production)
|
||||||
|
android_signing_alias=WNH_ANDROID_SIGNING_KEY_ALIAS
|
||||||
|
android_signing_label=production
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [[ -n "$android_signing_alias" ]] &&
|
||||||
|
all_set WNH_ANDROID_VERSION_CODE WNH_ANDROID_VERSION_NAME "$android_signing_alias"; then
|
||||||
|
ready "Android release inputs" \
|
||||||
|
"version and $android_signing_label signing alias are present"
|
||||||
else
|
else
|
||||||
missing "Android release inputs" "version code/name and both signing aliases"
|
missing "Android release inputs" \
|
||||||
|
"version code/name and the signing alias for DEPLOYMENT_ENV=$deployment_env"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
printf '\nEnvironment readiness: %d blocking item(s), %d local-only warning(s).\n' \
|
printf '\nEnvironment readiness: %d blocking item(s), %d local-only warning(s).\n' \
|
||||||
|
|
|
||||||
50
scripts/configure-android-development-env.sh
Executable file
50
scripts/configure-android-development-env.sh
Executable file
|
|
@ -0,0 +1,50 @@
|
||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
umask 077
|
||||||
|
|
||||||
|
ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)
|
||||||
|
env_file=${1:-"$ROOT/.env"}
|
||||||
|
config_home=${XDG_CONFIG_HOME:-"$HOME/.config"}
|
||||||
|
signing_dir=${WNH_ANDROID_DEVELOPMENT_SIGNING_DIR:-"$config_home/who_need_help/android-development"}
|
||||||
|
key_alias=${WNH_ANDROID_DEVELOPMENT_SIGNING_KEY_ALIAS:-who-need-help-development}
|
||||||
|
keystore="$signing_dir/who-need-help-development.p12"
|
||||||
|
password_file="$signing_dir/who-need-help-development.password"
|
||||||
|
values_file=$(mktemp "${TMPDIR:-/tmp}/wnh-android-development-env.XXXXXX")
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
if [ -e "$values_file" ]; then
|
||||||
|
unlink "$values_file"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
trap cleanup EXIT HUP INT TERM
|
||||||
|
|
||||||
|
case "$env_file" in
|
||||||
|
/*) ;;
|
||||||
|
*) env_file="$ROOT/$env_file" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ ! -f "$env_file" ]; then
|
||||||
|
echo "Development environment file does not exist: $env_file" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
fingerprint=$(
|
||||||
|
"$ROOT/scripts/android-signing-fingerprint.sh" \
|
||||||
|
"$keystore" \
|
||||||
|
"$password_file" \
|
||||||
|
"$key_alias"
|
||||||
|
)
|
||||||
|
|
||||||
|
printf '%s\n' \
|
||||||
|
'DEPLOYMENT_ENV=development' \
|
||||||
|
'ANDROID_APP_LINKS_PACKAGE_NAME=org.whoneedhelp.mobile.development' \
|
||||||
|
"ANDROID_APP_LINKS_SHA256_CERT_FINGERPRINTS=$fingerprint" \
|
||||||
|
"WNH_ANDROID_DEVELOPMENT_SIGNING_KEY_ALIAS=$key_alias" \
|
||||||
|
>"$values_file"
|
||||||
|
chmod 600 "$values_file"
|
||||||
|
|
||||||
|
"$ROOT/scripts/set-env-values.sh" "$env_file" "$values_file" >/dev/null
|
||||||
|
"$ROOT/scripts/validate-android-environment.sh" "$env_file" development
|
||||||
|
|
||||||
|
echo "Configured the ignored development environment with the public Android identity."
|
||||||
10
scripts/init-android-development-signing.sh
Executable file
10
scripts/init-android-development-signing.sh
Executable file
|
|
@ -0,0 +1,10 @@
|
||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
config_home=${XDG_CONFIG_HOME:-"$HOME/.config"}
|
||||||
|
|
||||||
|
WNH_ANDROID_SIGNING_DIR=${WNH_ANDROID_DEVELOPMENT_SIGNING_DIR:-"$config_home/who_need_help/android-development"} \
|
||||||
|
WNH_ANDROID_SIGNING_BASENAME=who-need-help-development \
|
||||||
|
WNH_ANDROID_SIGNING_KEY_ALIAS=${WNH_ANDROID_DEVELOPMENT_SIGNING_KEY_ALIAS:-who-need-help-development} \
|
||||||
|
WNH_ANDROID_SIGNING_SUBJECT="CN=Who Need Help development key" \
|
||||||
|
exec "$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd)/init-android-release-signing.sh"
|
||||||
|
|
@ -322,6 +322,9 @@ TEST_UPSTREAM_VALUE=$test_upstream \
|
||||||
replacement["FCM_SERVICE_ACCOUNT_JSON_BASE64"] = ENVIRON["FCM_SERVICE_ACCOUNT_JSON_BASE64_VALUE"]
|
replacement["FCM_SERVICE_ACCOUNT_JSON_BASE64"] = ENVIRON["FCM_SERVICE_ACCOUNT_JSON_BASE64_VALUE"]
|
||||||
replacement["ANDROID_APP_LINKS_PACKAGE_NAME"] = ENVIRON["ANDROID_APP_LINKS_PACKAGE_NAME_VALUE"]
|
replacement["ANDROID_APP_LINKS_PACKAGE_NAME"] = ENVIRON["ANDROID_APP_LINKS_PACKAGE_NAME_VALUE"]
|
||||||
replacement["ANDROID_APP_LINKS_SHA256_CERT_FINGERPRINTS"] = ENVIRON["ANDROID_APP_LINKS_FINGERPRINTS_VALUE"]
|
replacement["ANDROID_APP_LINKS_SHA256_CERT_FINGERPRINTS"] = ENVIRON["ANDROID_APP_LINKS_FINGERPRINTS_VALUE"]
|
||||||
|
replacement["WNH_ANDROID_SIGNING_KEY_ALIAS"] = "who-need-help-upload"
|
||||||
|
replacement["WNH_ANDROID_DEVELOPMENT_SIGNING_KEY_ALIAS"] = ""
|
||||||
|
replacement["WNH_ANDROID_STAGING_SIGNING_KEY_ALIAS"] = ""
|
||||||
replacement["CODEX_SESSION_ID"] = ENVIRON["CODEX_SESSION_ID_VALUE"]
|
replacement["CODEX_SESSION_ID"] = ENVIRON["CODEX_SESSION_ID_VALUE"]
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -263,6 +263,9 @@ CODEX_SESSION_ID_VALUE=$codex_session_id \
|
||||||
replacement["FCM_SERVICE_ACCOUNT_JSON_BASE64"] = ENVIRON["FCM_SERVICE_ACCOUNT_JSON_BASE64_VALUE"]
|
replacement["FCM_SERVICE_ACCOUNT_JSON_BASE64"] = ENVIRON["FCM_SERVICE_ACCOUNT_JSON_BASE64_VALUE"]
|
||||||
replacement["ANDROID_APP_LINKS_PACKAGE_NAME"] = ENVIRON["ANDROID_APP_LINKS_PACKAGE_NAME_VALUE"]
|
replacement["ANDROID_APP_LINKS_PACKAGE_NAME"] = ENVIRON["ANDROID_APP_LINKS_PACKAGE_NAME_VALUE"]
|
||||||
replacement["ANDROID_APP_LINKS_SHA256_CERT_FINGERPRINTS"] = ENVIRON["ANDROID_APP_LINKS_FINGERPRINTS_VALUE"]
|
replacement["ANDROID_APP_LINKS_SHA256_CERT_FINGERPRINTS"] = ENVIRON["ANDROID_APP_LINKS_FINGERPRINTS_VALUE"]
|
||||||
|
replacement["WNH_ANDROID_SIGNING_KEY_ALIAS"] = ""
|
||||||
|
replacement["WNH_ANDROID_DEVELOPMENT_SIGNING_KEY_ALIAS"] = ""
|
||||||
|
replacement["WNH_ANDROID_STAGING_SIGNING_KEY_ALIAS"] = "who-need-help-staging"
|
||||||
replacement["CODEX_SESSION_ID"] = ENVIRON["CODEX_SESSION_ID_VALUE"]
|
replacement["CODEX_SESSION_ID"] = ENVIRON["CODEX_SESSION_ID_VALUE"]
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -127,6 +127,51 @@ if ./scripts/set-env-values.sh \
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
echo "Checking single-file environment template synchronization"
|
||||||
|
sync_template="$scan_dir/sync-template.env.example"
|
||||||
|
sync_env="$scan_dir/sync.env"
|
||||||
|
printf '%s\n' \
|
||||||
|
'# Template comment' \
|
||||||
|
'FIRST_VALUE=template-default' \
|
||||||
|
'SECOND_VALUE=' \
|
||||||
|
>"$sync_template"
|
||||||
|
printf '%s\n' \
|
||||||
|
'SECOND_VALUE=preserve-this-value' \
|
||||||
|
'LOCAL_ONLY_VALUE=preserve-local-key' \
|
||||||
|
'FIRST_VALUE=preserve-first-value' \
|
||||||
|
>"$sync_env"
|
||||||
|
chmod 600 "$sync_env"
|
||||||
|
sync_output=$(
|
||||||
|
./scripts/sync-env-template.sh "$sync_env" "$sync_template"
|
||||||
|
)
|
||||||
|
if printf '%s' "$sync_output" | grep -F 'preserve-this-value' >/dev/null; then
|
||||||
|
echo "Environment synchronizer printed an environment value." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
test "$(stat -c '%a' "$sync_env")" = 600
|
||||||
|
grep -Fx '# Template comment' "$sync_env" >/dev/null
|
||||||
|
grep -Fx 'FIRST_VALUE=preserve-first-value' "$sync_env" >/dev/null
|
||||||
|
grep -Fx 'SECOND_VALUE=preserve-this-value' "$sync_env" >/dev/null
|
||||||
|
grep -Fx 'LOCAL_ONLY_VALUE=preserve-local-key' "$sync_env" >/dev/null
|
||||||
|
test "$(grep -Fc 'FIRST_VALUE=' "$sync_env")" = 1
|
||||||
|
test "$(grep -Fc 'SECOND_VALUE=' "$sync_env")" = 1
|
||||||
|
test "$(grep -Fc 'LOCAL_ONLY_VALUE=' "$sync_env")" = 1
|
||||||
|
sync_hash=$(sha256sum "$sync_env" | awk '{print $1}')
|
||||||
|
./scripts/sync-env-template.sh "$sync_env" "$sync_template" >/dev/null
|
||||||
|
test "$(sha256sum "$sync_env" | awk '{print $1}')" = "$sync_hash"
|
||||||
|
|
||||||
|
sync_duplicate="$scan_dir/sync-duplicate.env"
|
||||||
|
printf '%s\n' \
|
||||||
|
'FIRST_VALUE=one' \
|
||||||
|
'FIRST_VALUE=two' \
|
||||||
|
>"$sync_duplicate"
|
||||||
|
chmod 600 "$sync_duplicate"
|
||||||
|
if ./scripts/sync-env-template.sh \
|
||||||
|
"$sync_duplicate" "$sync_template" >/dev/null 2>&1; then
|
||||||
|
echo "Environment synchronizer accepted duplicate source keys." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
google_client="$scan_dir/google-oauth-client.json"
|
google_client="$scan_dir/google-oauth-client.json"
|
||||||
printf '%s\n' \
|
printf '%s\n' \
|
||||||
'{"web":{"client_id":"quality-google-client","project_id":"quality-development","client_secret":"quality-google-secret","redirect_uris":["https://dev.help.test/auth/google/callback"]}}' \
|
'{"web":{"client_id":"quality-google-client","project_id":"quality-development","client_secret":"quality-google-secret","redirect_uris":["https://dev.help.test/auth/google/callback"]}}' \
|
||||||
|
|
@ -157,6 +202,42 @@ grep -Fx 'WNH_FIREBASE_API_KEY=quality-firebase-api-key' "$credential_env" >/dev
|
||||||
grep -Fx 'WNH_FIREBASE_PROJECT_ID=quality-development' "$credential_env" >/dev/null
|
grep -Fx 'WNH_FIREBASE_PROJECT_ID=quality-development' "$credential_env" >/dev/null
|
||||||
grep -Fx 'WNH_FIREBASE_GCM_SENDER_ID=123456789' "$credential_env" >/dev/null
|
grep -Fx 'WNH_FIREBASE_GCM_SENDER_ID=123456789' "$credential_env" >/dev/null
|
||||||
|
|
||||||
|
echo "Checking Android environment isolation"
|
||||||
|
android_fingerprint=D7:C4:F1:12:4D:F4:68:E5:B3:54:DE:D8:96:E8:01:51:29:41:F1:8A:71:0C:18:B0:E7:98:AA:2B:81:DA:11:DF
|
||||||
|
android_env="$scan_dir/android-development.env"
|
||||||
|
printf '%s\n' \
|
||||||
|
'DEPLOYMENT_ENV=development' \
|
||||||
|
'PHX_HOST=dev.help.test' \
|
||||||
|
'PHX_SCHEME=https' \
|
||||||
|
'PHX_URL_PORT=443' \
|
||||||
|
'WNH_BASE_URL=https://dev.help.test' \
|
||||||
|
'ANDROID_APP_LINKS_PACKAGE_NAME=org.whoneedhelp.mobile.development' \
|
||||||
|
"ANDROID_APP_LINKS_SHA256_CERT_FINGERPRINTS=$android_fingerprint" \
|
||||||
|
>"$android_env"
|
||||||
|
chmod 600 "$android_env"
|
||||||
|
./scripts/validate-android-environment.sh \
|
||||||
|
"$android_env" development >/dev/null
|
||||||
|
|
||||||
|
sed -i \
|
||||||
|
's|^ANDROID_APP_LINKS_PACKAGE_NAME=.*|ANDROID_APP_LINKS_PACKAGE_NAME=org.whoneedhelp.mobile.staging|' \
|
||||||
|
"$android_env"
|
||||||
|
if ./scripts/validate-android-environment.sh \
|
||||||
|
"$android_env" development >/dev/null 2>&1; then
|
||||||
|
echo "Development Android validation accepted the test package." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
sed -i \
|
||||||
|
's|^DEPLOYMENT_ENV=.*|DEPLOYMENT_ENV=test|; s|^ANDROID_APP_LINKS_PACKAGE_NAME=.*|ANDROID_APP_LINKS_PACKAGE_NAME=org.whoneedhelp.mobile.staging|' \
|
||||||
|
"$android_env"
|
||||||
|
./scripts/validate-android-environment.sh "$android_env" test >/dev/null
|
||||||
|
|
||||||
|
sed -i \
|
||||||
|
's|^DEPLOYMENT_ENV=.*|DEPLOYMENT_ENV=production|; s|^ANDROID_APP_LINKS_PACKAGE_NAME=.*|ANDROID_APP_LINKS_PACKAGE_NAME=org.whoneedhelp.mobile|' \
|
||||||
|
"$android_env"
|
||||||
|
./scripts/validate-android-environment.sh \
|
||||||
|
"$android_env" production >/dev/null
|
||||||
|
|
||||||
fcm_service_account="$scan_dir/fcm-service-account.json"
|
fcm_service_account="$scan_dir/fcm-service-account.json"
|
||||||
printf '%s\n' \
|
printf '%s\n' \
|
||||||
'{"type":"service_account","project_id":"quality-development","client_email":"quality-fcm@quality-development.iam.gserviceaccount.com","private_key":"quality-private-key"}' \
|
'{"type":"service_account","project_id":"quality-development","client_email":"quality-fcm@quality-development.iam.gserviceaccount.com","private_key":"quality-private-key"}' \
|
||||||
|
|
|
||||||
120
scripts/sync-env-template.sh
Executable file
120
scripts/sync-env-template.sh
Executable file
|
|
@ -0,0 +1,120 @@
|
||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)
|
||||||
|
env_file=${1:-"$ROOT/.env"}
|
||||||
|
template_file=${2:-"$ROOT/.env.example"}
|
||||||
|
|
||||||
|
case "$env_file" in
|
||||||
|
/*) ;;
|
||||||
|
*) env_file="$ROOT/$env_file" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
case "$template_file" in
|
||||||
|
/*) ;;
|
||||||
|
*) template_file="$ROOT/$template_file" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ ! -f "$env_file" ]; then
|
||||||
|
echo "Environment file does not exist: $env_file" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f "$template_file" ]; then
|
||||||
|
echo "Environment template does not exist: $template_file" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$(stat -c '%a' "$env_file")" != 600 ]; then
|
||||||
|
echo "Environment file must have mode 0600: $env_file" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
env_dir=$(CDPATH='' cd -- "$(dirname -- "$env_file")" && pwd)
|
||||||
|
env_name=$(basename -- "$env_file")
|
||||||
|
temporary_env=$(mktemp "$env_dir/$env_name.tmp.XXXXXX")
|
||||||
|
trap 'rm -f "$temporary_env"' EXIT HUP INT TERM
|
||||||
|
chmod 600 "$temporary_env"
|
||||||
|
|
||||||
|
if ! awk '
|
||||||
|
BEGIN {
|
||||||
|
current_file = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
FNR == 1 && NR != 1 {
|
||||||
|
current_file = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
current_file {
|
||||||
|
separator = index($0, "=")
|
||||||
|
|
||||||
|
if (separator > 1) {
|
||||||
|
key = substr($0, 1, separator - 1)
|
||||||
|
|
||||||
|
if (key ~ /^[A-Z][A-Z0-9_]*$/) {
|
||||||
|
if (key in current) {
|
||||||
|
exit 40
|
||||||
|
}
|
||||||
|
|
||||||
|
current[key] = substr($0, separator + 1)
|
||||||
|
current_order[++current_count] = key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
next
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
separator = index($0, "=")
|
||||||
|
|
||||||
|
if (separator > 1) {
|
||||||
|
key = substr($0, 1, separator - 1)
|
||||||
|
|
||||||
|
if (key ~ /^[A-Z][A-Z0-9_]*$/) {
|
||||||
|
if (key in template_seen) {
|
||||||
|
exit 41
|
||||||
|
}
|
||||||
|
|
||||||
|
template_seen[key] = 1
|
||||||
|
|
||||||
|
if (key in current) {
|
||||||
|
print key "=" current[key]
|
||||||
|
emitted[key] = 1
|
||||||
|
next
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
print
|
||||||
|
}
|
||||||
|
|
||||||
|
END {
|
||||||
|
unknown_count = 0
|
||||||
|
|
||||||
|
for (position = 1; position <= current_count; position++) {
|
||||||
|
key = current_order[position]
|
||||||
|
|
||||||
|
if (!(key in emitted) && !(key in template_seen)) {
|
||||||
|
unknown[++unknown_count] = key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unknown_count > 0) {
|
||||||
|
print ""
|
||||||
|
print "# Local keys not present in the current template."
|
||||||
|
|
||||||
|
for (position = 1; position <= unknown_count; position++) {
|
||||||
|
key = unknown[position]
|
||||||
|
print key "=" current[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
' "$env_file" "$template_file" >"$temporary_env"; then
|
||||||
|
echo "Refusing to synchronize the environment: invalid or duplicate keys were found." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mv "$temporary_env" "$env_file"
|
||||||
|
trap - EXIT HUP INT TERM
|
||||||
|
|
||||||
|
echo "Environment synchronized with the template without printing values: $env_file"
|
||||||
110
scripts/validate-android-environment.sh
Executable file
110
scripts/validate-android-environment.sh
Executable file
|
|
@ -0,0 +1,110 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)
|
||||||
|
env_file=${1:-"$ROOT/.env"}
|
||||||
|
expected_environment=${2:-}
|
||||||
|
|
||||||
|
if [[ "$env_file" != /* ]]; then
|
||||||
|
env_file="$ROOT/$env_file"
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$expected_environment" in
|
||||||
|
development) expected_package=org.whoneedhelp.mobile.development ;;
|
||||||
|
test) expected_package=org.whoneedhelp.mobile.staging ;;
|
||||||
|
production) expected_package=org.whoneedhelp.mobile ;;
|
||||||
|
*)
|
||||||
|
echo "Usage: $0 ENV_FILE development|test|production" >&2
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
[[ -f "$env_file" ]] || {
|
||||||
|
echo "Android build environment does not exist: $env_file" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
[[ "$(stat -c '%a' "$env_file")" == 600 ]] || {
|
||||||
|
echo "Android build environment must have mode 0600: $env_file" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
read_unique() {
|
||||||
|
local key=$1
|
||||||
|
local output
|
||||||
|
|
||||||
|
output=$(
|
||||||
|
awk -v key="$key" '
|
||||||
|
index($0, key "=") == 1 {
|
||||||
|
count += 1
|
||||||
|
value = substr($0, length(key) + 2)
|
||||||
|
}
|
||||||
|
END {
|
||||||
|
if (count != 1) exit 1
|
||||||
|
if (value ~ /^".*"$/ || value ~ /^'\''.*'\''$/) {
|
||||||
|
value = substr(value, 2, length(value) - 2)
|
||||||
|
}
|
||||||
|
print value
|
||||||
|
}
|
||||||
|
' "$env_file"
|
||||||
|
) || {
|
||||||
|
echo "$key must occur exactly once in $env_file." >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
[[ -n "$output" ]] || {
|
||||||
|
echo "$key must not be empty in $env_file." >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
printf '%s' "$output"
|
||||||
|
}
|
||||||
|
|
||||||
|
deployment_environment=$(read_unique DEPLOYMENT_ENV)
|
||||||
|
phx_host=$(read_unique PHX_HOST)
|
||||||
|
phx_scheme=$(read_unique PHX_SCHEME)
|
||||||
|
phx_port=$(read_unique PHX_URL_PORT)
|
||||||
|
base_url=$(read_unique WNH_BASE_URL)
|
||||||
|
app_links_package=$(read_unique ANDROID_APP_LINKS_PACKAGE_NAME)
|
||||||
|
app_links_fingerprints=$(read_unique ANDROID_APP_LINKS_SHA256_CERT_FINGERPRINTS)
|
||||||
|
|
||||||
|
[[ "$deployment_environment" == "$expected_environment" ]] || {
|
||||||
|
echo "Android build requires DEPLOYMENT_ENV=$expected_environment." >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
[[ "$app_links_package" == "$expected_package" ]] || {
|
||||||
|
echo "Android build for $expected_environment requires package $expected_package." >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
[[ "$phx_scheme" == https ]] || {
|
||||||
|
echo "Public Android builds require PHX_SCHEME=https." >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
[[ "$phx_host" =~ ^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$ ]] || {
|
||||||
|
echo "PHX_HOST must be a lowercase public DNS hostname." >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if ! [[ "$phx_port" =~ ^[1-9][0-9]{0,4}$ ]] ||
|
||||||
|
((phx_port > 65535)); then
|
||||||
|
echo "PHX_URL_PORT must be a valid TCP port." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
expected_origin="https://$phx_host"
|
||||||
|
if [[ "$phx_port" != 443 ]]; then
|
||||||
|
expected_origin="$expected_origin:$phx_port"
|
||||||
|
fi
|
||||||
|
[[ "$base_url" == "$expected_origin" ]] || {
|
||||||
|
echo "WNH_BASE_URL must equal the canonical PHX origin: $expected_origin" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
IFS=',' read -r -a fingerprints <<<"$app_links_fingerprints"
|
||||||
|
for fingerprint in "${fingerprints[@]}"; do
|
||||||
|
compact=${fingerprint//:/}
|
||||||
|
compact=${compact//[[:space:]]/}
|
||||||
|
[[ "$compact" =~ ^[0-9A-Fa-f]{64}$ ]] || {
|
||||||
|
echo "ANDROID_APP_LINKS_SHA256_CERT_FINGERPRINTS contains an invalid value." >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Verified $expected_environment Android origin, application ID, and App Links identity."
|
||||||
Loading…
Reference in New Issue
Block a user