CI hardening: fast-fail check job (lockfile/tsc/eslint) gates the APK build + optional ntfy alerts; config plugin makes 'expo prebuild' preserve release signing/versioning; node 22 pinned with docker lock script + pre-push hook
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1
app/.nvmrc
Normal file
1
app/.nvmrc
Normal file
@@ -0,0 +1 @@
|
||||
22
|
||||
@@ -1,18 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path android:pathData="M0,0h108v108H0z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:type="linear"
|
||||
android:startX="54" android:startY="0"
|
||||
android:endX="54" android:endY="108"
|
||||
android:startColor="#1A1F2B"
|
||||
android:endColor="#0B0D12" />
|
||||
</aapt:attr>
|
||||
</path>
|
||||
</vector>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@color/splashscreen_background"/>
|
||||
<item>
|
||||
<bitmap android:gravity="center" android:src="@drawable/splashscreen_logo"/>
|
||||
</item>
|
||||
</layer-list>
|
||||
@@ -1,63 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- The four GTD quadrants (next/waiting/someday/projects) with a check in
|
||||
"next" — matches the app's home screen. Grid spans 32..76 to stay inside
|
||||
the adaptive-icon safe zone. -->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<!-- top-left: next (green) -->
|
||||
<path android:pathData="M37,32 h11 a5,5 0 0 1 5,5 v11 a5,5 0 0 1 -5,5 h-11 a5,5 0 0 1 -5,-5 v-11 a5,5 0 0 1 5,-5 z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:type="linear"
|
||||
android:startX="34" android:startY="34"
|
||||
android:endX="53" android:endY="53"
|
||||
android:startColor="#4FD48C"
|
||||
android:endColor="#2E9E60" />
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<!-- top-right: waiting (amber) -->
|
||||
<path android:pathData="M60,32 h11 a5,5 0 0 1 5,5 v11 a5,5 0 0 1 -5,5 h-11 a5,5 0 0 1 -5,-5 v-11 a5,5 0 0 1 5,-5 z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:type="linear"
|
||||
android:startX="57" android:startY="34"
|
||||
android:endX="76" android:endY="53"
|
||||
android:startColor="#EDBB55"
|
||||
android:endColor="#C8912B" />
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<!-- bottom-left: someday (violet) -->
|
||||
<path android:pathData="M37,55 h11 a5,5 0 0 1 5,5 v11 a5,5 0 0 1 -5,5 h-11 a5,5 0 0 1 -5,-5 v-11 a5,5 0 0 1 5,-5 z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:type="linear"
|
||||
android:startX="34" android:startY="57"
|
||||
android:endX="53" android:endY="76"
|
||||
android:startColor="#AC9FF0"
|
||||
android:endColor="#7E6DD6" />
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<!-- bottom-right: projects (blue) -->
|
||||
<path android:pathData="M60,55 h11 a5,5 0 0 1 5,5 v11 a5,5 0 0 1 -5,5 h-11 a5,5 0 0 1 -5,-5 v-11 a5,5 0 0 1 5,-5 z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:type="linear"
|
||||
android:startX="57" android:startY="57"
|
||||
android:endX="76" android:endY="76"
|
||||
android:startColor="#6FAAEE"
|
||||
android:endColor="#3F7DC4" />
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<!-- checkmark in the green quadrant -->
|
||||
<path
|
||||
android:pathData="M38,42.8 L42,46.8 L48.5,38.8"
|
||||
android:strokeColor="#FFFFFF"
|
||||
android:strokeWidth="3"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round"
|
||||
android:fillColor="#00000000" />
|
||||
</vector>
|
||||
@@ -1,27 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path
|
||||
android:pathData="M37,33.5 h11 a3.5,3.5 0 0 1 3.5,3.5 v11 a3.5,3.5 0 0 1 -3.5,3.5 h-11 a3.5,3.5 0 0 1 -3.5,-3.5 v-11 a3.5,3.5 0 0 1 3.5,-3.5 z"
|
||||
android:strokeColor="#FFFFFF"
|
||||
android:strokeWidth="3"
|
||||
android:fillColor="#00000000" />
|
||||
<path
|
||||
android:pathData="M60,33.5 h11 a3.5,3.5 0 0 1 3.5,3.5 v11 a3.5,3.5 0 0 1 -3.5,3.5 h-11 a3.5,3.5 0 0 1 -3.5,-3.5 v-11 a3.5,3.5 0 0 1 3.5,-3.5 z"
|
||||
android:strokeColor="#FFFFFF"
|
||||
android:strokeWidth="3"
|
||||
android:fillColor="#00000000" />
|
||||
<path
|
||||
android:pathData="M37,56.5 h11 a3.5,3.5 0 0 1 3.5,3.5 v11 a3.5,3.5 0 0 1 -3.5,3.5 h-11 a3.5,3.5 0 0 1 -3.5,-3.5 v-11 a3.5,3.5 0 0 1 3.5,-3.5 z"
|
||||
android:strokeColor="#FFFFFF"
|
||||
android:strokeWidth="3"
|
||||
android:fillColor="#00000000" />
|
||||
<path
|
||||
android:pathData="M60,56.5 h11 a3.5,3.5 0 0 1 3.5,3.5 v11 a3.5,3.5 0 0 1 -3.5,3.5 h-11 a3.5,3.5 0 0 1 -3.5,-3.5 v-11 a3.5,3.5 0 0 1 3.5,-3.5 z"
|
||||
android:strokeColor="#FFFFFF"
|
||||
android:strokeWidth="3"
|
||||
android:fillColor="#FFFFFF" />
|
||||
</vector>
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
<monochrome android:drawable="@drawable/ic_launcher_monochrome"/>
|
||||
</adaptive-icon>
|
||||
<background android:drawable="@mipmap/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
<monochrome android:drawable="@mipmap/ic_launcher_monochrome"/>
|
||||
</adaptive-icon>
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
<monochrome android:drawable="@drawable/ic_launcher_monochrome"/>
|
||||
</adaptive-icon>
|
||||
<background android:drawable="@mipmap/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
<monochrome android:drawable="@mipmap/ic_launcher_monochrome"/>
|
||||
</adaptive-icon>
|
||||
@@ -28,8 +28,6 @@ android.enablePngCrunchInReleaseBuilds=true
|
||||
# Use this property to specify which architecture you want to build.
|
||||
# You can also override it from the CLI using
|
||||
# ./gradlew <task> -PreactNativeArchitectures=x86_64
|
||||
# arm64 only: Pixel is arm64-v8a; x86/v7a builds are wasted time + APK bloat.
|
||||
# Add back x86_64 temporarily if an emulator build is ever needed.
|
||||
reactNativeArchitectures=arm64-v8a
|
||||
|
||||
# Use this property to enable support to the new architecture.
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
"favicon": "./assets/images/favicon.png"
|
||||
},
|
||||
"plugins": [
|
||||
"./plugins/withAndroidRelease",
|
||||
"expo-router",
|
||||
[
|
||||
"react-native-android-widget",
|
||||
|
||||
@@ -39,7 +39,11 @@
|
||||
"eslint-config-expo": "~57.0.1",
|
||||
"typescript": "~6.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "22.x"
|
||||
},
|
||||
"scripts": {
|
||||
"lock": "docker run --rm -v $PWD:/w -w /w node:22 npm install --package-lock-only --no-audit --no-fund",
|
||||
"start": "expo start",
|
||||
"reset-project": "node ./scripts/reset-project.js",
|
||||
"android": "expo run:android",
|
||||
|
||||
59
app/plugins/withAndroidRelease.js
Normal file
59
app/plugins/withAndroidRelease.js
Normal file
@@ -0,0 +1,59 @@
|
||||
// Makes `expo prebuild` safe to re-run: re-applies the hand-maintained Android
|
||||
// release config that a fresh prebuild would otherwise reset to template
|
||||
// defaults (android/ is committed; CI builds it and relies on all of this).
|
||||
const { withAppBuildGradle, withGradleProperties } = require("expo/config-plugins");
|
||||
|
||||
const RELEASE_SIGNING = ` release {
|
||||
// Keystore path + password come from env (CI) or ~/.android-keys (local).
|
||||
def ksPath = System.getenv("KEYSTORE_FILE") ?: "\${System.properties['user.home']}/.android-keys/gtd-release.jks"
|
||||
def passFile = new File("\${System.properties['user.home']}/.android-keys/gtd-release.password")
|
||||
def ksPass = System.getenv("KEYSTORE_PASSWORD") ?: (passFile.exists() ? passFile.text.trim() : null)
|
||||
if (new File(ksPath).exists() && ksPass != null) {
|
||||
storeFile file(ksPath)
|
||||
storePassword ksPass
|
||||
keyAlias System.getenv("KEY_ALIAS") ?: "gtd"
|
||||
keyPassword System.getenv("KEY_PASSWORD") ?: ksPass
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
function patchBuildGradle(gradle) {
|
||||
gradle = gradle.replace(/versionCode \d+\n/, 'versionCode ((System.getenv("VERSION_CODE") ?: "1").toInteger())\n');
|
||||
gradle = gradle.replace(/versionName "[^"]*"\n/, 'versionName (System.getenv("VERSION_NAME") ?: "1.0-dev")\n');
|
||||
if (!gradle.includes("KEYSTORE_FILE")) {
|
||||
// Insert the release signing config right after the debug one.
|
||||
gradle = gradle.replace(
|
||||
/(signingConfigs \{\n debug \{[\s\S]*?\n \}\n)/,
|
||||
`$1${RELEASE_SIGNING}`,
|
||||
);
|
||||
}
|
||||
// Release builds use the release key when present, debug key otherwise.
|
||||
gradle = gradle.replace(
|
||||
/(release \{\n)(?:\s*\/\/[^\n]*\n)*\s*signingConfig signingConfigs\.debug\n/,
|
||||
"$1 signingConfig signingConfigs.release.storeFile ? signingConfigs.release : signingConfigs.debug\n",
|
||||
);
|
||||
return gradle;
|
||||
}
|
||||
|
||||
const GRADLE_PROPS = [
|
||||
// RN/NDK builds want headroom; the template default is 2 GB.
|
||||
{ key: "org.gradle.jvmargs", value: "-Xmx8192m -XX:MaxMetaspaceSize=1024m" },
|
||||
// arm64 only: Pixel is arm64-v8a; x86/v7a builds are wasted time + APK bloat.
|
||||
{ key: "reactNativeArchitectures", value: "arm64-v8a" },
|
||||
];
|
||||
|
||||
module.exports = function withAndroidRelease(config) {
|
||||
config = withAppBuildGradle(config, (c) => {
|
||||
c.modResults.contents = patchBuildGradle(c.modResults.contents);
|
||||
return c;
|
||||
});
|
||||
config = withGradleProperties(config, (c) => {
|
||||
for (const { key, value } of GRADLE_PROPS) {
|
||||
const existing = c.modResults.find((p) => p.type === "property" && p.key === key);
|
||||
if (existing) existing.value = value;
|
||||
else c.modResults.push({ type: "property", key, value });
|
||||
}
|
||||
return c;
|
||||
});
|
||||
return config;
|
||||
};
|
||||
Reference in New Issue
Block a user