Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion .github/workflows/release-mobile-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ on:
workflow_dispatch:
inputs:
version_name:
description: Informational only — the AAB version comes from the product version in build.gradle
description: Informational only — the AAB versionName is <product version>-dev.<run number>, computed in the job
required: false
type: string

Expand Down Expand Up @@ -111,16 +111,37 @@ jobs:
fi
python3 -c 'import json,sys; d=json.load(open("mobile/android/app/google-services.json")); pkgs={c.get("client_info",{}).get("android_client_info",{}).get("package_name") for c in d.get("client",[])}; sys.exit(0 if "com.thedancingdeveloper.vogt.dev" in pkgs else "Firebase config has no Android client for com.thedancingdeveloper.vogt.dev")'

# The dev app is a separate Play record, so its versionCode only has to
# climb among dev uploads: product semver code × 10000 + run number
# (0.6.2 → 60020000+N). Play refuses a re-upload of a code it has
# already seen, and the product version alone moves too rarely for a
# dev channel that republishes between releases.
- name: dev version code
working-directory: mobile
run: |
set -euo pipefail
version="$(python3 -c 'import json; print(json.load(open("package.json"))["version"])')"
IFS=. read -r major minor patch <<<"${version%%-*}"
code=$(( (major * 1000000 + minor * 1000 + patch) * 10000 + GITHUB_RUN_NUMBER ))
{
echo "VOGT_ANDROID_VERSION_CODE=$code"
echo "VOGT_ANDROID_VERSION_NAME=${version}-dev.${GITHUB_RUN_NUMBER}"
} >> "$GITHUB_ENV"
echo "::notice::dev AAB versionCode=$code versionName=${version}-dev.${GITHUB_RUN_NUMBER}"

- name: capacitor sync
working-directory: mobile
env:
VOGT_ANDROID_APP_ID: com.thedancingdeveloper.vogt.dev
VOGT_ANDROID_APP_NAME: Vogt Dev
run: npx cap sync android

- name: bundle and sign the dev AAB
working-directory: mobile/android
env:
VOGT_ANDROID_APP_ID: com.thedancingdeveloper.vogt.dev
VOGT_ANDROID_APP_NAME: Vogt Dev
VOGT_ANDROID_APP_ICON: dev
run: |
set -euo pipefail
# keystore path + creds come from $GITHUB_ENV (set by the resolve step).
Expand Down
19 changes: 19 additions & 0 deletions docs/mobile-release-validation.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,25 @@ for the version being shipped.
(The deterministic native half — clean re-registration, no stale service —
is covered by `VoiceServiceProcessReclaimTest`.)

### Dev app identity (side-by-side with prod)

The dev shell is a separate Play record and a separate app on the device, so
it must be telling apart on the home screen, not only by package name. Three
build inputs carry its identity; `release-mobile-dev.yml` sets all three and
`release.yml` sets none (prod takes the defaults):

| Variable | Read by | Default | Dev workflow |
|---|---|---|---|
| `VOGT_ANDROID_APP_ID` | `build.gradle`, `capacitor.config.ts` | `com.thedancingdeveloper.vogt` | `com.thedancingdeveloper.vogt.dev` |
| `VOGT_ANDROID_APP_NAME` | `build.gradle` (generates `app_name`), `capacitor.config.ts` | `Vogt` | `Vogt Dev` |
| `VOGT_ANDROID_APP_ICON` | `build.gradle` (manifest `icon`/`roundIcon`) | `default` | `dev` (amber launcher set, `res/mipmap-*/ic_launcher_dev*`) |

The dev AAB's `versionCode` is `<product semver code> × 10000 + <run number>`
and its `versionName` is `<product version>-dev.<run number>`, computed in the
workflow, so a dev re-upload between product releases is never refused for a
version code Play has already seen. `tests/test_mobile_identity.py` holds
the three inputs and the workflow to this.

### Play internal-track / pre-launch report

The Play Console pre-launch report and internal-track validation are an external
Expand Down
29 changes: 29 additions & 0 deletions mobile/android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,25 @@ def androidVersionCode = parseVersionCodeOverride(
def androidApplicationId = (
System.getenv('VOGT_ANDROID_APP_ID') ?: 'com.thedancingdeveloper.vogt'
).trim()
// Label and launcher icon are build inputs too, so a dev build is telling
// apart from prod on the home screen and not only by its applicationId.
// VOGT_ANDROID_APP_NAME is the same variable capacitor.config.ts reads for
// `appName`; the string resources are generated here (resValue) rather than
// kept in strings.xml so the two can never disagree. VOGT_ANDROID_APP_ICON
// picks the launcher icon set: `default` (the shipped icon) or `dev` (amber
// variant, res/mipmap-*/ic_launcher_dev*). Anything else fails the build.
def androidAppName = (System.getenv('VOGT_ANDROID_APP_NAME') ?: 'Vogt').trim()
if (androidAppName.isEmpty()) {
throw new GradleException("VOGT_ANDROID_APP_NAME must not be blank")
}
def androidAppIcon = (System.getenv('VOGT_ANDROID_APP_ICON') ?: 'default').trim()
def androidLauncherIcons = [
'default': ['@mipmap/ic_launcher', '@mipmap/ic_launcher_round'],
'dev': ['@mipmap/ic_launcher_dev', '@mipmap/ic_launcher_dev_round'],
]
if (!androidLauncherIcons.containsKey(androidAppIcon)) {
throw new GradleException("VOGT_ANDROID_APP_ICON must be one of ${androidLauncherIcons.keySet()}, got '${androidAppIcon}'")
}

def releaseKeystorePath = System.getenv('VOGT_ANDROID_KEYSTORE_PATH')
def releaseKeystorePassword = System.getenv('VOGT_ANDROID_KEYSTORE_PASSWORD')
Expand All @@ -66,12 +85,22 @@ def hasReleaseSigning = [
android {
namespace "com.thedancingdeveloper.vogt"
compileSdk rootProject.ext.compileSdkVersion
buildFeatures {
// Off by default since AGP 9; the label strings above are resValues.
resValues true
}
defaultConfig {
applicationId androidApplicationId
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode androidVersionCode
versionName androidVersionName
resValue "string", "app_name", androidAppName
resValue "string", "title_activity_main", androidAppName
manifestPlaceholders = [
appIcon: androidLauncherIcons[androidAppIcon][0],
appRoundIcon: androidLauncherIcons[androidAppIcon][1],
]
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
Expand Down
4 changes: 2 additions & 2 deletions mobile/android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
android:allowBackup="false"
android:fullBackupContent="false"
android:dataExtractionRules="@xml/data_extraction_rules"
android:icon="@mipmap/ic_launcher"
android:icon="${appIcon}"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:roundIcon="${appRoundIcon}"
android:supportsRtl="true"
android:theme="@style/AppTheme">

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Dev launcher foreground: the same V glyph as the shipped icon, inked
dark on the amber background, with a "DEV" ribbon across the bottom
corner drawn as paths (VectorDrawable has no text). -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillColor="#0D1117"
android:pathData="M29,32h15l10,32 10,-32h15l-18,46h-14z" />
<path
android:pathData="M30,26h48"
android:strokeColor="#FDE68A"
android:strokeLineCap="round"
android:strokeWidth="5" />
<!-- ribbon -->
<path
android:fillColor="#0D1117"
android:pathData="M60,90 L108,42 L108,58 L76,90 Z" />
<!-- D E V, stroked letters on the ribbon (rotated 45° diagonal baseline) -->
<group android:rotation="-45" android:pivotX="88" android:pivotY="70">
<path
android:pathData="M74,66 v8 h3 a4,4 0 0 0 0,-8 z"
android:strokeColor="#FDE68A"
android:strokeWidth="1.6"
android:fillColor="#00000000" />
<path
android:pathData="M84,66 h-4 v8 h4 M80,70 h3"
android:strokeColor="#FDE68A"
android:strokeWidth="1.6"
android:fillColor="#00000000" />
<path
android:pathData="M88,66 l3,8 l3,-8"
android:strokeColor="#FDE68A"
android:strokeWidth="1.6"
android:fillColor="#00000000" />
</group>
</vector>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_dev_background"/>
<foreground android:drawable="@drawable/ic_launcher_dev_foreground"/>
</adaptive-icon>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_dev_background"/>
<foreground android:drawable="@drawable/ic_launcher_dev_foreground"/>
</adaptive-icon>
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Pre-API-26 fallback for the dev launcher icon (minSdk 23): the adaptive
layers flattened into one drawable. -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="rectangle">
<solid android:color="@color/ic_launcher_dev_background" />
</shape>
</item>
<item android:drawable="@drawable/ic_launcher_dev_foreground" />
</layer-list>
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Pre-API-26 fallback for the dev launcher icon (minSdk 23): the adaptive
layers flattened into one drawable. -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="rectangle">
<solid android:color="@color/ic_launcher_dev_background" />
</shape>
</item>
<item android:drawable="@drawable/ic_launcher_dev_foreground" />
</layer-list>
2 changes: 1 addition & 1 deletion mobile/android/app/src/main/res/raw/keep.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,4 @@
gains a dynamic reference is kept rather than stripped.
-->
<resources xmlns:tools="http://schemas.android.com/tools"
tools:keep="@mipmap/ic_launcher,@mipmap/ic_launcher_round,@drawable/splash,@color/ic_launcher_background" />
tools:keep="@mipmap/ic_launcher,@mipmap/ic_launcher_round,@mipmap/ic_launcher_dev,@mipmap/ic_launcher_dev_round,@drawable/splash,@color/ic_launcher_background,@color/ic_launcher_dev_background" />
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Dev launcher background: amber, so the dev app is telling apart from
prod (near-black) at a glance. Selected by VOGT_ANDROID_APP_ICON=dev. -->
<color name="ic_launcher_dev_background">#B45309</color>
</resources>
4 changes: 2 additions & 2 deletions mobile/android/app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?xml version='1.0' encoding='utf-8'?>
<resources>
<string name="app_name">Vogt</string>
<string name="title_activity_main">Vogt</string>
<!-- app_name and title_activity_main are generated by build.gradle
(resValue) from VOGT_ANDROID_APP_NAME. -->
<string name="package_name">com.thedancingdeveloper.vogt</string>
<string name="custom_url_scheme">com.thedancingdeveloper.vogt</string>
</resources>
37 changes: 37 additions & 0 deletions tests/test_mobile_identity.py
Original file line number Diff line number Diff line change
Expand Up @@ -308,3 +308,40 @@ def test_the_android_shell_does_not_zoom_the_page() -> None:
assert config.count("zoomEnabled: false") == 2, (
"both the top-level and the android block must turn page zoom off"
)


#: The label under the icon and the icon itself are build inputs alongside
#: the id, read by the same two files: `build.gradle` generates the string
#: resources from ``VOGT_ANDROID_APP_NAME`` (resValue) and picks the launcher
#: set from ``VOGT_ANDROID_APP_ICON``; `capacitor.config.ts` reads the name
#: for ``appName``.
APP_NAME_VAR = "VOGT_ANDROID_APP_NAME"
APP_ICON_VAR = "VOGT_ANDROID_APP_ICON"
DEV_WORKFLOW = WORKFLOWS / "release-mobile-dev.yml"


def test_the_dev_app_is_telling_apart_on_the_home_screen() -> None:
"""Two apps with the same name and icon are the same app to a person.

The dev record must build under its own label and launcher icon, or the
operator cannot tell which one is open — the failure reported from a
device carrying both. Asserted on the workflow because that is where the
dev stream is defined, and on the build files because both must read the
variable for the label to reach the manifest and the Capacitor config.
"""
gradle = GRADLE.read_text(encoding="utf-8")
capacitor = CAPACITOR.read_text(encoding="utf-8")
assert APP_NAME_VAR in gradle and APP_NAME_VAR in capacitor
assert APP_ICON_VAR in gradle
workflow = DEV_WORKFLOW.read_text(encoding="utf-8")
names = set(re.findall(rf"{APP_NAME_VAR}:\s*\"?([^\"\n]+?)\"?\s*$", workflow, re.M))
assert names and names != {"Vogt"}, "the dev workflow builds under the prod label"
assert re.search(rf"{APP_ICON_VAR}:\s*dev\s*$", workflow, re.M), (
"the dev workflow builds with the prod launcher icon"
)
# The generated strings must not also be declared statically, or aapt
# rejects the duplicate — and a static copy would silently win a merge.
strings_xml = MOBILE / "android/app/src/main/res/values/strings.xml"
strings = strings_xml.read_text(encoding="utf-8")
assert 'name="app_name"' not in strings
assert 'name="title_activity_main"' not in strings
Loading