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
60 changes: 37 additions & 23 deletions .github/workflows/build-phone-app.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@
# repository can call it with this one as a submodule (`path`). Setup: docs/self-hosting.md#the-phone.
# variant=staging builds "OpenClapper Staging" (its own package id and clay icon, so it sits next to the
# production app; signed with the same key) for a staging instance's APP_URL. variant=dev builds the
# development client "OpenClapper Dev" instead (debug-signed, its own package id, loads the JavaScript
# from Metro: make mobile), on the same runner setup.
# development client instead (its own package id, loads the JavaScript from Metro: make mobile), on the
# same runner setup: with the environment's key "OpenClapper Dev", a release build for Google Play's
# internal track (deploy#22); without a key "OpenClapper Dev Local", debug-signed like make dev-build.
# `environment` and `ref` work as in deploy-server.yml: the GitHub environment whose variables (APP_URL) and
# secrets (the signing key) to use, and another version of this repository to build than the pinned one.
# The Android SDK, build-tools and NDK are the runner's; local builds (scripts/apk-local.sh) pin theirs in
Expand All @@ -22,7 +23,7 @@ on:
options: [release, staging, dev]
default: release
track:
description: Upload the bundle to this Google Play track (release and staging; needs PLAY_SERVICE_ACCOUNT_JSON)
description: Upload the bundle to this Google Play track (needs PLAY_SERVICE_ACCOUNT_JSON, and for dev the key)
type: choice
options: [none, internal]
default: none
Expand All @@ -41,7 +42,7 @@ on:
type: string
default: release
track:
description: Upload the bundle to this Google Play track (none, internal); release and staging only
description: Upload the bundle to this Google Play track (none, internal); dev only with the key
type: string
default: none
path:
Expand All @@ -57,7 +58,7 @@ on:
type: string
default: ""
secrets:
# Only the release and staging variants use them.
# The release and staging variants need them; the dev variant is signed with them when they are set.
ANDROID_KEYSTORE_BASE64:
required: false
ANDROID_KEYSTORE_PASSWORD:
Expand Down Expand Up @@ -103,11 +104,18 @@ jobs:
run: |
case "$VARIANT" in release|staging|dev) ;; *) echo "::error::variant must be release, staging or dev"; exit 1 ;; esac
case "$TRACK" in none) ;; internal)
[ "$VARIANT" != dev ] || { echo "::error::The dev client does not go to Google Play"; exit 1; }
[ -n "$PLAY_SERVICE_ACCOUNT_JSON" ] || { echo "::error::Set the PLAY_SERVICE_ACCOUNT_JSON secret to upload to Google Play (docs/self-hosting.md)"; exit 1; } ;;
*) echo "::error::track must be none or internal"; exit 1 ;; esac
[ "$VARIANT" != dev ] || exit 0 # the development client needs neither a server nor the key
[ -n "$EXPO_PUBLIC_API_URL" ] || { echo "::error::Set the APP_URL variable to your server, e.g. https://openclapper-api.<subdomain>.workers.dev"; exit 1; }
if [ "$VARIANT" = dev ]; then
# The development client needs no server (Metro gives it one); without the key it is the
# debug-signed OpenClapper Dev Local, which Google Play does not take.
if [ -z "$ANDROID_KEYSTORE_BASE64" ]; then
[ "$TRACK" = none ] || { echo "::error::The dev client goes to Google Play signed: set the ANDROID_KEYSTORE_BASE64 secret and the other three (scripts/keystore.sh)"; exit 1; }
exit 0
fi
else
[ -n "$EXPO_PUBLIC_API_URL" ] || { echo "::error::Set the APP_URL variable to your server, e.g. https://openclapper-api.<subdomain>.workers.dev"; exit 1; }
fi
for v in ANDROID_KEYSTORE_BASE64 ANDROID_KEYSTORE_PASSWORD ANDROID_KEY_ALIAS ANDROID_KEY_PASSWORD; do
[ -n "${!v}" ] || { echo "::error::Set the $v secret (scripts/keystore.sh prints all four)"; exit 1; }
done
Expand Down Expand Up @@ -178,31 +186,37 @@ jobs:
run: |
export GIT_COMMIT=$(git rev-parse --short HEAD)
version=$(node -p 'require("../../package.json").version')
if [ "$VARIANT" = dev ]; then
# The development client: its own name, package id and icon (app.config.ts), debug-signed with
# the template's fixed debug key (so one dev build updates the last), no bundle inside.
export APP_VARIANT=development
task=assembleDebug out=debug name="openclapper-dev-$version-$GIT_COMMIT.apk"
if [ "$VARIANT" = dev ] && [ -z "$ANDROID_KEYSTORE_BASE64" ]; then
# The development client without a key, OpenClapper Dev Local: its own name, package id and icon
# (app.config.ts), debug-signed with the template's fixed debug key (so it installs over a
# make dev-build and the other way round), no bundle inside.
export APP_VARIANT=development-local
task=assembleDebug out=debug name="openclapper-dev-local-$version-$GIT_COMMIT.apk"
else
echo "$ANDROID_KEYSTORE_BASE64" | base64 -d > "$RUNNER_TEMP/release.jks"
export ANDROID_KEYSTORE="$RUNNER_TEMP/release.jks"
task=assembleRelease out=release name="openclapper-$version-$GIT_COMMIT.apk"
# The staging app: its own name, package id and icon (app.config.ts). Its version code is the
# build time, minutes since 2020-01-01 UTC: every build is higher than the one before, whatever
# ref it comes from, so Play installs an older tag as an ordinary update (a rollback keeps the
# phone's data). Builds in the same minute share a code; Play then keeps the first.
if [ "$VARIANT" = staging ]; then
export APP_VARIANT=staging name="openclapper-staging-$version-$GIT_COMMIT.apk"
export STAGING_VERSION_CODE=$(( ($(date -u +%s) - $(date -u -d 2020-01-01 +%s)) / 60 ))
echo "Version code: $STAGING_VERSION_CODE (minutes since 2020-01-01 UTC)"
# The staging app and the dev client for Play: their own name, package id and icon
# (app.config.ts). Their version code is the build time, minutes since 2020-01-01 UTC: every
# build is higher than the one before, whatever ref it comes from, so Play installs an older
# one as an ordinary update (a rollback keeps the phone's data). Builds in the same minute
# share a code; Play then keeps the first. The dev client keeps its launcher and menu in this
# release build (app.config.ts, withDevClientInRelease).
case "$VARIANT" in
staging) export APP_VARIANT=staging name="openclapper-staging-$version-$GIT_COMMIT.apk" ;;
dev) export APP_VARIANT=development name="openclapper-dev-$version-$GIT_COMMIT.apk" ;;
esac
if [ "$VARIANT" != release ]; then
export BUILD_TIME_VERSION_CODE=$(( ($(date -u +%s) - $(date -u -d 2020-01-01 +%s)) / 60 ))
echo "Version code: $BUILD_TIME_VERSION_CODE (minutes since 2020-01-01 UTC)"
fi
# The App Bundle for Play next to the APK for sideloading, from the same build and key.
task="$task bundleRelease"
fi
pnpm exec expo prebuild --platform android --no-install
(cd android && ./gradlew $task --no-daemon -PreactNativeArchitectures="$ANDROID_ABIS")
mv "android/app/build/outputs/apk/$out/app-$out.apk" "$RUNNER_TEMP/$name"
if [ "$VARIANT" != dev ]; then
if [ "$out" = release ]; then
aab="${name%.apk}.aab"
mv android/app/build/outputs/bundle/release/app-release.aab "$RUNNER_TEMP/$aab"
# Never hand out a release signed with the debug key (it would not update a real install).
Expand Down Expand Up @@ -267,7 +281,7 @@ jobs:
retention-days: 3
compression-level: 0

# The App Bundle (release and staging): what goes to Google Play (scripts/play-upload.mjs, deploy#11).
# The App Bundle (every signed build): what goes to Google Play (scripts/play-upload.mjs, deploy#11).
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
if: env.AAB_NAME != ''
with:
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ admin: ## Admin board -> http://localhost:5174 with its Worker on :8788 (local D
mobile: ## Local Worker behind a quick tunnel + Metro for the dev build (OpenClapper Dev); LOCAL data
pnpm dev:mobile

dev-build: ## Development build (OpenClapper Dev) in Docker, to the Windows Downloads folder; rebuild after native changes
dev-build: ## Development build (OpenClapper Dev Local) in Docker, to the Windows Downloads folder; rebuild after native changes
VARIANT=dev bash scripts/apk-local.sh

apk-local: ## OpenClapper Local in Docker, a complete app for one server, no secrets: API_URL=https://… make apk-local
Expand Down
40 changes: 30 additions & 10 deletions apps/mobile/app.config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { ConfigContext, ExpoConfig } from "expo/config";
import { withAndroidManifest, withAppBuildGradle } from "expo/config-plugins";
import { withAndroidManifest, withAppBuildGradle, withGradleProperties } from "expo/config-plugins";
import { version } from "../../package.json";

/**
Expand All @@ -12,13 +12,13 @@ function versionCode(v: string): number {
}

/**
* The staging app is built from any ref between releases, and Android only installs a higher version
* code, so it uses the build time instead (STAGING_VERSION_CODE, minutes since 2020, set by the Build
* phone app workflow): a build of an older tag still installs as an update. Without it (a local config
* run) it falls back to the release scheme.
* The staging app and the dev client on Google Play are built from any ref between releases, and Android
* only installs a higher version code, so they use the build time instead (BUILD_TIME_VERSION_CODE,
* minutes since 2020, set by the Build phone app workflow): a build of an older ref still installs as an
* update. Without it (a local config run) it falls back to the release scheme.
*/
function stagingVersionCode(): number {
const code = Number.parseInt(process.env.STAGING_VERSION_CODE ?? "", 10);
function buildTimeVersionCode(): number {
const code = Number.parseInt(process.env.BUILD_TIME_VERSION_CODE ?? "", 10);
return code > 0 ? code : versionCode(version);
}

Expand Down Expand Up @@ -75,21 +75,40 @@ const withUncappedLocation = (config: ExpoConfig): ExpoConfig =>
return c;
}) as ExpoConfig;

/**
* expo-dev-launcher and expo-dev-menu compile themselves out of release builds unless these Gradle
* properties say otherwise (their android/build.gradle). The dev client on Google Play must be a release
* build (Play rejects debuggable bundles), so it keeps them in.
*/
const withDevClientInRelease = (config: ExpoConfig): ExpoConfig =>
withGradleProperties(config, (c) => {
for (const key of ["expo.devlauncher.configureInRelease", "expo.devmenu.configureInRelease"]) {
c.modResults = c.modResults.filter((p) => !(p.type === "property" && p.key === key));
c.modResults.push({ type: "property", key, value: "true" });
}
return c;
}) as ExpoConfig;

/** Every build's config plugins of our own. */
const withOwnPlugins = (config: ExpoConfig): ExpoConfig => withUncappedLocation(withReleaseSigning(config));

/**
* The builds besides production (app.json), by APP_VARIANT. Each has its own package id, so it sits next
* to the production app with its own storage instead of replacing it, and its own icon colour tells them
* apart in the drawer (assets/icons, #249):
* - development: the dev client "OpenClapper Dev" (make dev-build, make mobile), loads the JavaScript from Metro.
* - development: the dev client "OpenClapper Dev", loads the JavaScript from Metro (make mobile). A release
* build signed with the deployment's key, from Google Play's internal track (Build phone app, variant dev).
* - development-local: the same dev client built without a key (make dev-build, or Build phone app with
* variant dev and no key), debug-signed. Android installs no update signed with another key, so it has
* its own package id and sits next to the Play one; both share the scheme, so Metro opens either.
* - staging: "OpenClapper Staging" for a staging instance, built by the Build phone app workflow and signed
* with the deployment's key like production.
* - local: "OpenClapper Local" (make apk-local), the JavaScript inside and signed with the template's
* debug key, so anyone can build it from a fresh clone without a secret; it updates no other build.
*/
const VARIANTS: Record<string, { name: string; slug: string; package: string; icons: string; background: string }> = {
development: { name: "OpenClapper Dev", slug: "dev", package: "org.openclapper.dev", icons: "dev", background: "#3B7A1A" },
"development-local": { name: "OpenClapper Dev Local", slug: "dev", package: "org.openclapper.dev.local", icons: "dev", background: "#3B7A1A" },
staging: { name: "OpenClapper Staging", slug: "staging", package: "org.openclapper.staging", icons: "staging", background: "#B8471F" },
local: { name: "OpenClapper Local", slug: "local", package: "org.openclapper.local", icons: "dev", background: "#3B7A1A" },
};
Expand All @@ -112,14 +131,14 @@ export default ({ config }: ConfigContext): ExpoConfig => {
// A typo must not silently build the production app.
if (!v) throw new Error(`APP_VARIANT=${name}: expected one of ${Object.keys(VARIANTS).join(", ")}, or unset for production`);
const icons = `./assets/icons/${v.icons}`;
return withOwnPlugins({
const variant = withOwnPlugins({
...app,
name: v.name,
scheme: `openclapper-${v.slug}`,
icon: `${icons}/app-icon-1024.png`,
android: {
...app.android,
...(name === "staging" ? { versionCode: stagingVersionCode() } : {}),
...(name === "staging" || name === "development" ? { versionCode: buildTimeVersionCode() } : {}),
package: v.package,
adaptiveIcon: {
backgroundColor: v.background,
Expand All @@ -128,4 +147,5 @@ export default ({ config }: ConfigContext): ExpoConfig => {
},
},
});
return name === "development" ? withDevClientInRelease(variant) : variant;
};
Loading
Loading