Mobile: standalone signed release APK build (config plugin + script)
CI / policy (push) Successful in 5s
CI / profile (push) Successful in 11s
CI / mobile (push) Successful in 16s
CI / backend (push) Failing after 1m7s

- plugins/withReleaseSigning.js — Expo config plugin that injects a release
  signingConfig into android/app/build.gradle at prebuild, reading credentials
  from credentials/keystore.properties (gitignored). Survives CNG regeneration.
- scripts/build-release-apk.sh — generate keystore (once) → prebuild → build →
  install → emit a shareable cardclaws-demo.apk
- .gitignore: exclude /credentials, *.keystore, *.apk (no secrets/binaries)

Produces a standalone APK with the JS bundle embedded — runs with no Metro, no
laptop, no Wi-Fi. Built, signed, installed, and verified running on a physical
device with Metro stopped + the USB route removed.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
Omar Sobh
2026-06-04 18:11:36 -05:00
co-authored by Claude Opus 4.8
parent bc6f158f43
commit eddca62f23
4 changed files with 90 additions and 0 deletions
+5
View File
@@ -7,6 +7,11 @@ node_modules/
/android /android
/ios /ios
# Signing secrets + build artifacts — NEVER commit
/credentials
*.keystore
*.apk
# Misc # Misc
*.log *.log
.DS_Store .DS_Store
+1
View File
@@ -24,6 +24,7 @@ const config: ExpoConfig = {
cameraPermission: "CardClaws uses the camera to capture your card image.", cameraPermission: "CardClaws uses the camera to capture your card image.",
}, },
], ],
"./plugins/withReleaseSigning.js",
], ],
experiments: { experiments: {
typedRoutes: true, typedRoutes: true,
@@ -0,0 +1,42 @@
// Expo config plugin: inject a release signing config into the generated
// android/app/build.gradle so `assembleRelease` produces a properly-signed,
// standalone APK. Credentials are read at build time from
// credentials/keystore.properties (gitignored) — never baked into source.
//
// This runs during `expo prebuild`, so release signing is reproducible even
// though android/ is regenerated (CNG workflow).
const { withAppBuildGradle } = require("@expo/config-plugins");
const RELEASE_SIGNING_CONFIG = `release {
def kp = new Properties()
def kf = rootProject.file("../credentials/keystore.properties")
if (kf.exists()) {
kp.load(new FileInputStream(kf))
storeFile file(kp["storeFile"])
storePassword kp["storePassword"]
keyAlias kp["keyAlias"]
keyPassword kp["keyPassword"]
}
}
debug {`;
module.exports = function withReleaseSigning(config) {
return withAppBuildGradle(config, (cfg) => {
let src = cfg.modResults.contents;
if (src.includes("signingConfigs.release")) return cfg; // already applied
// 1) Add a `release` signing config alongside `debug`.
src = src.replace(/signingConfigs \{\s*\n\s*debug \{/, `signingConfigs {\n ${RELEASE_SIGNING_CONFIG}`);
// 2) Point the release build type at it (only the release block, anchored on
// the shrinkResources line that follows it in the template).
src = src.replace(
/signingConfig signingConfigs\.debug(\s*\n\s*shrinkResources)/,
"signingConfig signingConfigs.release$1",
);
cfg.modResults.contents = src;
return cfg;
});
};
+42
View File
@@ -0,0 +1,42 @@
#!/usr/bin/env bash
# Build a standalone, signed release APK (JS bundled in — runs with no Metro,
# no laptop). Reproducible: the release signing config is injected at prebuild
# by plugins/withReleaseSigning.js, which reads credentials/keystore.properties.
#
# Usage:
# JAVA_HOME=$(/usr/libexec/java_home -v 17) ./scripts/build-release-apk.sh
#
# Override the (demo) keystore password via KEYSTORE_PASSWORD if you like.
set -euo pipefail
cd "$(dirname "$0")/.."
PASS="${KEYSTORE_PASSWORD:-cardclaws-demo}"
KS="$(pwd)/credentials/cardclaws-release.keystore"
# 1) Create the release keystore once (gitignored).
if [ ! -f "$KS" ]; then
echo "Generating release keystore…"
mkdir -p credentials
keytool -genkeypair -v -keystore "$KS" -alias cardclaws \
-keyalg RSA -keysize 2048 -validity 10000 \
-storepass "$PASS" -keypass "$PASS" \
-dname "CN=CardClaws, O=RedClaw Systems LLC, C=US"
cat > credentials/keystore.properties <<EOF
storeFile=$KS
storePassword=$PASS
keyAlias=cardclaws
keyPassword=$PASS
EOF
fi
# 2) Regenerate native project (applies the signing config plugin).
npx expo prebuild --platform android --clean --no-install
# 3) Build + install the signed release APK to the connected device.
npx expo run:android --variant release
APK="android/app/build/outputs/apk/release/app-release.apk"
if [ -f "$APK" ]; then
cp "$APK" cardclaws-demo.apk
echo "Standalone APK: $(pwd)/cardclaws-demo.apk"
fi