Skip to content

Commit b6d938c

Browse files
dfallingclaude
andcommitted
Scaffold React Native Android app with GraphQL, signing, CI
- Bare React Native 0.85 (TS), Android-only, no Expo / Google services - Apollo Client + graphql-codegen (Phoenix schema placeholder for now) - Toolchain pinned via mise: Node 22, Bun 1.2, Zulu JDK 17 - Biome for lint + format (replaces eslint + prettier) - Release signing reads keystore from env vars or gradle.properties - CI workflow: bun install, codegen drift check, tsc, biome, jest, gradle assembleDebug - Release workflow: tag v* -> signed APK -> GitHub Release (Obtainium-compatible) - Application ID: com.culpeos.app, minSdk 26, targetSdk 36 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent d304291 commit b6d938c

48 files changed

Lines changed: 3402 additions & 18 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
name: CI
2+
3+
on:
4+
pull_request:
5+
push:
6+
branches: [main]
7+
8+
jobs:
9+
js:
10+
name: JS checks
11+
runs-on: ubuntu-latest
12+
steps:
13+
- uses: actions/checkout@v4
14+
15+
- uses: actions/setup-node@v4
16+
with:
17+
node-version: '22'
18+
19+
- uses: oven-sh/setup-bun@v2
20+
with:
21+
bun-version: '1.2'
22+
23+
- run: bun install --frozen-lockfile
24+
25+
- name: GraphQL codegen
26+
run: bun run codegen
27+
28+
- name: Verify codegen output is up to date
29+
run: |
30+
if ! git diff --exit-code -- src/graphql/__generated__; then
31+
echo "::error::Generated GraphQL types are stale. Run 'bun run codegen' and commit the result."
32+
exit 1
33+
fi
34+
35+
- name: TypeScript
36+
run: bunx tsc --noEmit
37+
38+
- name: Biome (lint + format check)
39+
run: bun run lint
40+
41+
- name: Test
42+
run: bun run test -- --ci
43+
44+
android:
45+
name: Android assembleDebug
46+
runs-on: ubuntu-latest
47+
steps:
48+
- uses: actions/checkout@v4
49+
50+
- uses: actions/setup-node@v4
51+
with:
52+
node-version: '22'
53+
54+
- uses: oven-sh/setup-bun@v2
55+
with:
56+
bun-version: '1.2'
57+
58+
- uses: actions/setup-java@v4
59+
with:
60+
distribution: 'temurin'
61+
java-version: '17'
62+
63+
- uses: gradle/actions/setup-gradle@v4
64+
65+
- run: bun install --frozen-lockfile
66+
67+
- name: assembleDebug
68+
working-directory: android
69+
run: ./gradlew assembleDebug --no-daemon --stacktrace

.github/workflows/release.yml

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
name: Release APK
2+
3+
on:
4+
push:
5+
tags:
6+
- 'v*'
7+
workflow_dispatch:
8+
9+
permissions:
10+
contents: write
11+
12+
jobs:
13+
build:
14+
runs-on: ubuntu-latest
15+
16+
steps:
17+
- name: Checkout
18+
uses: actions/checkout@v4
19+
with:
20+
# Full history so we can derive a monotonic versionCode from commit count.
21+
fetch-depth: 0
22+
23+
- name: Set up Node
24+
uses: actions/setup-node@v4
25+
with:
26+
node-version: '22'
27+
28+
- name: Set up Bun
29+
uses: oven-sh/setup-bun@v2
30+
with:
31+
bun-version: '1.2'
32+
33+
- name: Set up JDK 17
34+
uses: actions/setup-java@v4
35+
with:
36+
distribution: 'temurin'
37+
java-version: '17'
38+
39+
- name: Set up Gradle
40+
uses: gradle/actions/setup-gradle@v4
41+
42+
- name: Install JS dependencies
43+
run: bun install --frozen-lockfile
44+
45+
- name: Compute version
46+
id: version
47+
run: |
48+
if [[ "$GITHUB_REF" == refs/tags/v* ]]; then
49+
VERSION_NAME="${GITHUB_REF_NAME#v}"
50+
else
51+
VERSION_NAME="0.0.0-dev.${GITHUB_RUN_NUMBER}"
52+
fi
53+
VERSION_CODE=$(git rev-list --count HEAD)
54+
echo "name=$VERSION_NAME" >> "$GITHUB_OUTPUT"
55+
echo "code=$VERSION_CODE" >> "$GITHUB_OUTPUT"
56+
echo "Building $VERSION_NAME (code $VERSION_CODE)"
57+
58+
- name: Decode release keystore
59+
env:
60+
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
61+
run: |
62+
echo "$ANDROID_KEYSTORE_BASE64" | base64 -d > "$RUNNER_TEMP/release.keystore"
63+
64+
- name: Assemble release APK
65+
working-directory: android
66+
env:
67+
RELEASE_STORE_FILE: ${{ runner.temp }}/release.keystore
68+
RELEASE_STORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
69+
RELEASE_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
70+
RELEASE_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
71+
RELEASE_VERSION_NAME: ${{ steps.version.outputs.name }}
72+
RELEASE_VERSION_CODE: ${{ steps.version.outputs.code }}
73+
run: ./gradlew assembleRelease --no-daemon --stacktrace
74+
75+
- name: Rename APK
76+
run: |
77+
mkdir -p artifacts
78+
cp android/app/build/outputs/apk/release/app-release.apk \
79+
"artifacts/culpeos-${{ steps.version.outputs.name }}.apk"
80+
ls -la artifacts
81+
82+
- name: Upload APK as workflow artifact
83+
uses: actions/upload-artifact@v4
84+
with:
85+
name: culpeos-${{ steps.version.outputs.name }}
86+
path: artifacts/*.apk
87+
if-no-files-found: error
88+
89+
- name: Create GitHub Release
90+
if: startsWith(github.ref, 'refs/tags/v')
91+
uses: softprops/action-gh-release@v2
92+
with:
93+
files: artifacts/*.apk
94+
generate_release_notes: true
95+
fail_on_unmatched_files: true

.gitignore

Lines changed: 34 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,50 @@
1-
# Gradle files
2-
.gradle/
3-
build/
1+
# OSX
2+
.DS_Store
43

5-
# Local configuration file (sdk path, etc)
4+
# Android / Gradle
5+
build/
6+
.gradle/
7+
.kotlin/
68
local.properties
7-
8-
# Log/OS Files
9-
*.log
10-
11-
# Android Studio generated files and folders
129
captures/
1310
.externalNativeBuild/
1411
.cxx/
15-
*.aab
16-
*.apk
17-
output-metadata.json
18-
19-
# IntelliJ
2012
*.iml
2113
.idea/
2214
misc.xml
2315
deploymentTargetDropDown.xml
2416
render.experimental.xml
17+
*.hprof
18+
*.log
19+
20+
# Build outputs
21+
*.apk
22+
*.aab
23+
*.jsbundle
24+
output-metadata.json
2525

26-
# Keystore files
26+
# Signing — never commit keystores or passwords
2727
*.jks
2828
*.keystore
29+
!debug.keystore
30+
keystore.properties
31+
release.keystore.b64
2932

30-
# Google Services (e.g. APIs or Firebase)
33+
# Google services (we don't use these on GrapheneOS, but block them just in case)
3134
google-services.json
3235

33-
# Android Profiling
34-
*.hprof
36+
# Node / Bun
37+
node_modules/
38+
npm-debug.log
39+
yarn-error.log
40+
# Lockfile is bun.lock — keep package-lock.json out so it can't drift
41+
package-lock.json
42+
43+
# Metro
44+
.metro-health-check*
45+
46+
# Test coverage
47+
/coverage
48+
49+
# GraphQL — generated TS is committed; ignore introspection dumps
50+
/schema.json

.watchmanconfig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{}

App.tsx

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/**
2+
* Sample React Native App
3+
* https://github.com/facebook/react-native
4+
*
5+
* @format
6+
*/
7+
8+
import {ApolloProvider} from '@apollo/client';
9+
import {NewAppScreen} from '@react-native/new-app-screen';
10+
import {StatusBar, StyleSheet, useColorScheme, View} from 'react-native';
11+
import {
12+
SafeAreaProvider,
13+
useSafeAreaInsets,
14+
} from 'react-native-safe-area-context';
15+
import {apolloClient} from './src/graphql/client';
16+
17+
function App() {
18+
const isDarkMode = useColorScheme() === 'dark';
19+
20+
return (
21+
<ApolloProvider client={apolloClient}>
22+
<SafeAreaProvider>
23+
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
24+
<AppContent />
25+
</SafeAreaProvider>
26+
</ApolloProvider>
27+
);
28+
}
29+
30+
function AppContent() {
31+
const safeAreaInsets = useSafeAreaInsets();
32+
33+
return (
34+
<View style={styles.container}>
35+
<NewAppScreen
36+
templateFileName="App.tsx"
37+
safeAreaInsets={safeAreaInsets}
38+
/>
39+
</View>
40+
);
41+
}
42+
43+
const styles = StyleSheet.create({
44+
container: {
45+
flex: 1,
46+
},
47+
});
48+
49+
export default App;

0 commit comments

Comments
 (0)