2 Commits

Author SHA1 Message Date
b260e4e762 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
All checks were successful
Build & Release APK / check (push) Successful in 16s
Build & Release APK / build (push) Successful in 2m27s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-18 00:39:29 -07:00
e02ef05c02 Regenerate package-lock with npm 10 (node 22) so CI's npm ci accepts it — local npm 11 omitted top-level @emnapi peer-dep entries
All checks were successful
Build & Release APK / build (push) Successful in 2m26s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-18 00:26:27 -07:00
14 changed files with 181 additions and 146 deletions

View File

@@ -5,7 +5,36 @@ on:
branches: [main]
jobs:
check:
# Fast fail: lockfile sync, types, lint — catches in ~2 min what would
# otherwise kill the APK build twenty minutes in.
runs-on: desktop
container:
image: node:22
steps:
- name: Checkout
run: |
git init -q .
git fetch -q --depth 1 "https://git.rehbock.xyz/${{ github.repository }}.git" "${{ github.sha }}"
git checkout -q FETCH_HEAD
- name: Lockfile + typecheck + lint
run: |
cd app
npm ci --no-audit --no-fund
npx tsc --noEmit
npx eslint .
- name: Notify failure
if: failure()
env:
NTFY_TOKEN: ${{ secrets.NTFY_TOKEN }}
run: |
[ -n "$NTFY_TOKEN" ] || { echo "NTFY_TOKEN not set — skipping notification"; exit 0; }
curl -s -H "Authorization: Bearer $NTFY_TOKEN" -H "Title: gtd CI: check failed" -H "Priority: high" \
-d "typecheck/lint/lockfile failed at ${{ github.sha }} — https://git.rehbock.xyz/marcus/gtd/actions" \
https://ntfy.rehbock.xyz/gtd-ci
build:
needs: check
# RN/NDK builds starve the 4-core VPS — run on the desktop runner
# (marcusDesktop). Jobs queue while the desktop is off and start when it
# comes online; switch back to ubuntu-latest to build on the VPS.
@@ -81,3 +110,13 @@ jobs:
-H "Authorization: token $TOKEN" \
-F "attachment=@$APK;type=application/vnd.android.package-archive" >/dev/null
echo "published $TAG"
- name: Notify failure
if: failure()
env:
NTFY_TOKEN: ${{ secrets.NTFY_TOKEN }}
run: |
[ -n "$NTFY_TOKEN" ] || { echo "NTFY_TOKEN not set — skipping notification"; exit 0; }
curl -s -H "Authorization: Bearer $NTFY_TOKEN" -H "Title: gtd CI: APK build failed" -H "Priority: high" \
-d "release build failed at ${{ github.sha }} — https://git.rehbock.xyz/marcus/gtd/actions" \
https://ntfy.rehbock.xyz/gtd-ci

12
.githooks/pre-push Executable file
View File

@@ -0,0 +1,12 @@
#!/bin/sh
# Reject pushes whose lockfile CI's npm (node 22) would refuse — a lock
# written by a newer local npm fails `npm ci` on the runner half a build in.
# Enable with: git config core.hooksPath .githooks
set -e
root=$(git rev-parse --show-toplevel)
if git diff --quiet origin/main...HEAD -- app/package.json app/package-lock.json 2>/dev/null; then
exit 0
fi
echo "pre-push: package.json/lock changed — validating with CI's npm (node:22)…"
docker run --rm -v "$root/app:/w" -w /w node:22 npm ci --dry-run --no-audit --no-fund >/dev/null
echo "pre-push: lockfile OK"

View File

@@ -22,6 +22,15 @@ Minimal Getting Things Done app. One Expo/React Native codebase for iPhone, Andr
cd app && npx expo start # scan QR with Expo Go (iPhone or Pixel)
```
- **Node 22 only** (`app/.nvmrc`): CI's npm 10 rejects lockfiles written by newer npm. Never run bare `npm install` to change deps — use `npm run lock` (regenerates the lock via a `node:22` container), or run npm from node 22.
- **Hook**: `git config core.hooksPath .githooks` enables a pre-push check that validates the lockfile against CI's npm before it can break a build.
- **Local Android builds** need JDK 21 (AGP chokes on newer): pinned via `org.gradle.java.home` in `~/.gradle/gradle.properties` → `~/.local/share/java/jdk-21.0.12+8`.
- **`expo prebuild` is safe to re-run**: `app/plugins/withAndroidRelease.js` re-injects the release signing, env-driven versionCode/Name, and gradle.properties tuning into the committed `android/`.
## Release (Android APK → Obtainium)
Every push to `main` runs `.gitea/workflows/release.yml` on the desktop runner: fast check job (lockfile/types/lint), then a signed `assembleRelease`, published as Gitea release `v1.<run>` with the APK attached. Phones track it with Obtainium → source `https://git.rehbock.xyz/marcus/gtd` (Forgejo/Gitea source type). CI failures push to ntfy topic `gtd-ci` when the `NTFY_TOKEN` repo secret is set.
## Deploy
```sh

1
app/.nvmrc Normal file
View File

@@ -0,0 +1 @@
22

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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.

View File

@@ -25,6 +25,7 @@
"favicon": "./assets/images/favicon.png"
},
"plugins": [
"./plugins/withAndroidRelease",
"expo-router",
[
"react-native-android-widget",

70
app/package-lock.json generated
View File

@@ -79,7 +79,6 @@
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz",
"integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.29.7",
"@babel/generator": "^7.29.7",
@@ -964,6 +963,7 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz",
"integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.29.7"
},
@@ -979,6 +979,7 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz",
"integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.29.7"
},
@@ -1010,6 +1011,7 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.8.tgz",
"integrity": "sha512-0UpIXPtdDtMXfnV2OJAVMLpj3H/92vmkA6lpSRakmycJvj3VUy6Xs1dM8tXRugupykr5WB+LpiVl0J8LMVg2mg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.29.7"
},
@@ -1199,6 +1201,31 @@
"node": ">=0.8.0"
}
},
"node_modules/@emnapi/core": {
"version": "1.11.3",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.3.tgz",
"integrity": "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@emnapi/wasi-threads": "1.2.3",
"tslib": "^2.4.0"
}
},
"node_modules/@emnapi/runtime": {
"version": "1.11.3",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz",
"integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@emnapi/wasi-threads": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.3.tgz",
@@ -1206,6 +1233,7 @@
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"tslib": "^2.4.0"
}
@@ -2560,6 +2588,7 @@
"resolved": "https://registry.npmjs.org/@react-native/babel-preset/-/babel-preset-0.86.2.tgz",
"integrity": "sha512-4XKEJ6jKW9lXMB1O5o47gBoGgolde1fbX13gLW/erlcn+1ky+MHHo5UjuM3RWGdPJHIvIzekDDumSUHhB9x5iQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/core": "^7.25.2",
"@babel/plugin-proposal-export-default-from": "^7.24.7",
@@ -2722,6 +2751,7 @@
"resolved": "https://registry.npmjs.org/@react-native/metro-babel-transformer/-/metro-babel-transformer-0.86.2.tgz",
"integrity": "sha512-mX1wgLErdb2hDgXJr9zM9SWLe+ZteZTFTwRWGOQ53yEJwc9DVSnxdTlAtVdMeOj2ntycKh0R8jYdat2Am43cwQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/core": "^7.25.2",
"@react-native/babel-preset": "0.86.2",
@@ -2740,6 +2770,7 @@
"resolved": "https://registry.npmjs.org/@react-native/metro-config/-/metro-config-0.86.2.tgz",
"integrity": "sha512-hJno256j+MS0b3JD1aD3ouTGZVacKNVBuXL2atMQQ8BZ060vl1ptnZ83y569aDW+/rgFSOcqn6ydKeSz4uUKQQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@react-native/js-polyfills": "0.86.2",
"@react-native/metro-babel-transformer": "0.86.2",
@@ -2797,6 +2828,7 @@
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz",
"integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.10.4",
"@babel/runtime": "^7.12.5",
@@ -2816,6 +2848,7 @@
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=10"
},
@@ -2828,6 +2861,7 @@
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"ansi-regex": "^5.0.1",
"ansi-styles": "^5.0.0",
@@ -2841,7 +2875,8 @@
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/@testing-library/jest-dom": {
"version": "6.9.1",
@@ -2896,7 +2931,8 @@
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/@types/estree": {
"version": "1.0.9",
@@ -2963,7 +2999,6 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz",
"integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==",
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.2.2"
}
@@ -3037,7 +3072,6 @@
"integrity": "sha512-fUBfTuuEulWqX6V8+O3PtScV01tzYYRUDTAirHFKoRAt7nOzoGiPt0M/bB47wWNy0coOOcgEwAMUtBpykMxl6w==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.67.0",
"@typescript-eslint/types": "8.67.0",
@@ -3628,7 +3662,6 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz",
"integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==",
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -4228,7 +4261,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.10.44",
"caniuse-lite": "^1.0.30001806",
@@ -4882,7 +4914,8 @@
"version": "0.5.16",
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/dunder-proto": {
"version": "1.0.1",
@@ -5166,7 +5199,6 @@
"integrity": "sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -5387,7 +5419,6 @@
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@rtsao/scc": "^1.1.0",
"array-includes": "^3.1.9",
@@ -5750,7 +5781,6 @@
"resolved": "https://registry.npmjs.org/expo/-/expo-57.0.11.tgz",
"integrity": "sha512-R97257N39Dw0kQFuI4/RvYx95GQ+dmePdo8hxcMOjDxAT4VcCckjILJeAWCE19Jxjb92hZ5NDXAfDPkkV1RB9w==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.20.0",
"@expo/cli": "^57.0.13",
@@ -5828,7 +5858,6 @@
"resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-57.0.9.tgz",
"integrity": "sha512-Y47sGiF+U8fwicUSPdJPjB27PuU+FgLK4Mpai0ksZF5hv8jNN3HBqKuDNxsiu70uHBKf80TaR4gwMPh0pmETiQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@expo/env": "~2.4.2"
},
@@ -5873,7 +5902,6 @@
"resolved": "https://registry.npmjs.org/expo-font/-/expo-font-57.0.1.tgz",
"integrity": "sha512-QyS9L1Kh9sKJg4gfU6rdbpxpmH+DyzBX8z6jVvXMUDoqLr1GqmkO/Wu379KCXjL///kWbhpNlbi7AgBuj4VdIQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"fontfaceobserver": "^2.1.0"
},
@@ -5938,7 +5966,6 @@
"resolved": "https://registry.npmjs.org/expo-linking/-/expo-linking-57.0.5.tgz",
"integrity": "sha512-SmJI3wr0EVfeKPGf+Qgr9gUbrXk8mM0ATqYLvAX/EAzawDjohPzMJ5pTt7TYMX0Wknj4XCtyCQrGhnERMXT6cQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"expo-constants": "~57.0.9",
"invariant": "^2.2.4"
@@ -5998,7 +6025,6 @@
"resolved": "https://registry.npmjs.org/expo-router/-/expo-router-57.0.11.tgz",
"integrity": "sha512-kE2hz4lLkddZ94vN4nmbq5aXeo0t6FaZ8xJn/Hyn8/dQPsGlvDK0j4ZVayEUUdNmkNHsBVihgf1bl/2rCsYEzA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@expo/log-box": "^57.0.2",
"@expo/metro-runtime": "^57.0.8",
@@ -6073,7 +6099,6 @@
"resolved": "https://registry.npmjs.org/expo-server/-/expo-server-57.0.1.tgz",
"integrity": "sha512-sBfVDH6dmKVHZxqUxbfkzS00PZELMZt1IpnHKxcOTMZtR/t7CtRAFrbXcisG+EyzeqHSVDacZT+1tbYfZt5D8w==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=20.16.0"
}
@@ -6337,7 +6362,6 @@
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz",
"integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=10.0.0"
},
@@ -8331,6 +8355,7 @@
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
"license": "MIT",
"peer": true,
"bin": {
"lz-string": "bin/bin.js"
}
@@ -9655,7 +9680,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
"integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -9675,7 +9699,6 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz",
"integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==",
"license": "MIT",
"peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
@@ -9712,7 +9735,6 @@
"resolved": "https://registry.npmjs.org/react-native/-/react-native-0.86.2.tgz",
"integrity": "sha512-zbJXGZpwfZGA79Z9ob6Atvfx4nAQL8yJBa35s58E4Oo+khPykfQP2sTeumkKbjwajFYfVayg8pj7Il9nIfTk7A==",
"license": "MIT",
"peer": true,
"dependencies": {
"@react-native/assets-registry": "0.86.2",
"@react-native/codegen": "0.86.2",
@@ -9818,7 +9840,6 @@
"resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.32.0.tgz",
"integrity": "sha512-uYIMOKlKENORq2SABE+jIjbPU+h5I/sQKcq2v16zRq848nwEp1fWRVwML4QWqijc8UcXJC25o54S8GQd4Mf2OA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@egjs/hammerjs": "^2.0.17",
"@types/react-test-renderer": "^19.1.0",
@@ -9845,7 +9866,6 @@
"resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-4.5.1.tgz",
"integrity": "sha512-RnMvtDuR+68ig864gAvZCOdZehqhC5rFmMo0kn+ARfgVSTvFeF6IFLBVgMPUu0KwihaapEyW24WRi6nEyy1kSA==",
"license": "MIT",
"peer": true,
"dependencies": {
"react-native-is-edge-to-edge": "^1.3.1",
"semver": "^7.7.3"
@@ -9861,7 +9881,6 @@
"resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.7.0.tgz",
"integrity": "sha512-/9/MtQz8ODphjsLdZ+GZAIcC/RtoqW9EeShf7Uvnfgm/pzYrJ75y3PV/J1wuAV1T5Dye5ygq4EAW20RoBq0ABQ==",
"license": "MIT",
"peer": true,
"peerDependencies": {
"react": "*",
"react-native": "*"
@@ -9886,7 +9905,6 @@
"resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.21.2.tgz",
"integrity": "sha512-SO2t9/17zM4iEnFvlu2DA9jqNbzNhoUP+AItkoCOyFmDMOhUnBBznBDCYN92fGdfAkfQlWzPoez6+zLxFNsZEg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.18.6",
"@react-native/normalize-colors": "^0.74.1",
@@ -9919,7 +9937,6 @@
"resolved": "https://registry.npmjs.org/react-native-worklets/-/react-native-worklets-0.10.1.tgz",
"integrity": "sha512-62mRM19bDpfpdI8HLkEErcdOsrAPDtE9lA/sw+5lLRpzBHNhxaoj9QyY2KjXqUmirelxkX4zuPGTC3VdA0feJA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/plugin-transform-arrow-functions": "^7.27.1",
"@babel/plugin-transform-class-properties": "^7.28.6",
@@ -9955,7 +9972,6 @@
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",
"integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -11228,7 +11244,6 @@
"integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
"devOptional": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -11846,7 +11861,6 @@
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}

View File

@@ -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",

View 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;
};