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
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
# Builds the Android app for a deployment, pointed at its server (the APP_URL variable): expo prebuild
# + Gradle on the GitHub runner, signed with the deployment's own key (scripts/keystore.sh). No Expo
# account. The APK is the run's artifact, deleted after 3 days: download and install it before then.
# Self-hosters run it on their copy of this repository (Actions → Build APK → Run workflow); another
# Self-hosters run it on their copy of this repository (Actions → Build phone app → Run workflow); another
# 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.
# `environment` and `ref` work as in deploy.yml: the GitHub environment whose variables (APP_URL) and
# `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
# docker/android.Dockerfile. When React Native needs another SDK platform or NDK, check both.
name: Build APK
name: Build phone app

on:
workflow_dispatch:
Expand Down Expand Up @@ -124,7 +124,7 @@ jobs:

- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
# As in deploy.yml: with a ref, this repository in full (itself, or replacing the submodule).
# As in deploy-server.yml: with a ref, this repository in full (itself, or replacing the submodule).
ref: ${{ (inputs.path || '.') == '.' && inputs.ref || '' }}
fetch-depth: ${{ inputs.ref != '' && '0' || '1' }}
submodules: ${{ inputs.ref == '' }}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
# names the source (SOURCE_D1_DATABASE_NAME, SOURCE_R2_BUCKET) and holds both R2 tokens. `confirm` must
# be the target database's name. Deploy the target right after: the clone recreates its database, and
# its Workers are bound to the old one until then (a caller chains Deploy after this job).
# `path`, `environment` and `ref` work as in deploy.yml. Settings: docs/self-hosting.md.
name: Clone
# `path`, `environment` and `ref` work as in deploy-server.yml. Settings: docs/self-hosting.md.
name: Clone instance

on:
workflow_dispatch:
Expand Down Expand Up @@ -80,7 +80,7 @@ jobs:
SOURCE_R2_SECRET_ACCESS_KEY: ${{ secrets.SOURCE_R2_SECRET_ACCESS_KEY }}
CONFIRM: ${{ inputs.confirm }}
steps:
# As in deploy.yml: with a ref, this repository in full (itself, or replacing the submodule).
# As in deploy-server.yml: with a ref, this repository in full (itself, or replacing the submodule).
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ (inputs.path || '.') == '.' && inputs.ref || '' }}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Deploys OpenClapper to a Cloudflare account: migrates D1, then the app Worker (API + Studio) and,
# once its Access application exists, the admin plane. Self-hosters run it on their copy of this
# repository (Actions → Deploy → Run workflow); another repository can call it with this one as a
# repository (Actions → Deploy server → Run workflow); another repository can call it with this one as a
# submodule (`path`). Every setting comes from the run's GitHub variables and secrets, never from a
# file: docs/self-hosting.md.
#
Expand All @@ -10,7 +10,7 @@
# `ref` deploys another version of this repository than the pinned one (a branch, for staging): it is
# checked out in full instead of the submodule. With `ref`, the steps below come from the ref in the
# caller's `uses:` line and the scripts from `ref`; a staging caller uses `@main` for both.
name: Deploy
name: Deploy server

on:
workflow_dispatch:
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ A **shot** holds the metadata (name, location, INT/EXT, light phases + artificia
- **The Android signing key** lives in the deployment repository's GitHub secrets and the owner's offline backup, never in git or a checkout; a new key cannot update installed apps. Builds use no Expo account (`expo prebuild` + Gradle). Locally only keyless builds: the dev client (`make dev-build`) and OpenClapper Local (`make apk-local`, debug-signed).
- **One product version** (root `package.json`, SemVer 0.x; workspace packages stay `0.0.0`). Releases: `scripts/release.sh X.Y.Z`, only when the owner asks ([releasing](docs/releasing.md)).
- **CI is a gate**: never pass `--admin` to `gh pr merge`; a red check is fixed, not bypassed.
- **Deploys to production are done by the owner**, from the deployment repository `OpenClapper-dev/deploy` (its Deploy and Build APK workflows; its GitHub environments `production` and `staging` hold each instance's settings, see `docs/self-hosting.md`). This repository is for development and is a black box to that one: scripts and reusable workflows read every setting from the environment, never from a settings file or a path outside the checkout, and the Makefile's targets are local-only (no deploy targets, nothing a deployment calls). The tracked `wrangler.jsonc` files are local-dev only: never put an account, database or Access id back into them. You prepare, check and commit. You may apply additive remote migrations when asked. Commit only when asked or when working through an agreed plan; work on a branch.
- **Deploys to production are done by the owner**, from the deployment repository `OpenClapper-dev/deploy` (its "Production · …" and "Staging · …" workflows: Release, Deploy server, Build phone app, Reset from production; its GitHub environments `production` and `staging` hold each instance's settings, see `docs/self-hosting.md`). This repository is for development and is a black box to that one: scripts and reusable workflows read every setting from the environment, never from a settings file or a path outside the checkout, and the Makefile's targets are local-only (no deploy targets, nothing a deployment calls). The tracked `wrangler.jsonc` files are local-dev only: never put an account, database or Access id back into them. You prepare, check and commit. You may apply additive remote migrations when asked. Commit only when asked or when working through an agreed plan; work on a branch.

## Working from a GitHub issue

Expand Down
4 changes: 2 additions & 2 deletions apps/mobile/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ function versionCode(v: string): number {

/**
* The staging app is built from main between releases, and Play takes each version code once, so it
* counts commits instead (GIT_COMMIT_COUNT, set by the Build APK workflow from a full checkout). Without
* counts commits instead (GIT_COMMIT_COUNT, set by the Build phone app workflow from a full checkout). Without
* the count (a local config run) it falls back to the release scheme.
*/
function stagingVersionCode(): number {
Expand Down Expand Up @@ -63,7 +63,7 @@ const withReleaseSigning = (config: ExpoConfig): ExpoConfig =>
* 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.
* - staging: "OpenClapper Staging" for a staging instance, built by the Build APK workflow and signed
* - 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.
Expand Down
16 changes: 8 additions & 8 deletions docs/development.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Development

Node and pnpm versions have one source each:
- **Node**: the root `.nvmrc` (`nvm install`, `nvm use`). The CI, Deploy and Build APK workflows read it (`node-version-file`), and `scripts/apk-local.sh` passes it to the Android image. `engines.node` in the root `package.json` gives the accepted range; bump both together.
- **Node**: the root `.nvmrc` (`nvm install`, `nvm use`). The CI, Deploy server and Build phone app workflows read it (`node-version-file`), and `scripts/apk-local.sh` passes it to the Android image. `engines.node` in the root `package.json` gives the accepted range; bump both together.
- **pnpm**: `packageManager` in the root `package.json`. `pnpm/action-setup` reads it in the workflows, and the Android image runs it through corepack.

pnpm 11 reads its settings only from `pnpm-workspace.yaml` (an `.npmrc` keeps just auth and registry settings, so the repository has none): the isolated linker (pnpm's default, stated explicitly), `engineStrict` (an install on a Node outside `engines.node` fails), the 7-day dependency cooldown (`minimumReleaseAge`) and the packages allowed to run install scripts. `.gitattributes` keeps text files LF in the repository, and `Makefile`, `*.sh`, `*.yml`, `*.yaml` and `*.mjs` LF in the checkout too.
Expand Down Expand Up @@ -93,9 +93,9 @@ make studio # beside it: the Studio on :5173 against the same local Worker
```

- **The development build** ("OpenClapper Dev", `org.openclapper.dev`) is this app with its native modules plus Expo's dev launcher and menu (`expo-dev-client`). It carries no JavaScript: it loads the bundle from Metro, so Fast Refresh works as in Expo Go. Rebuild only when the native side changes: a new native module, a plugin or permission in `app.json`, an SDK upgrade. A bundle that calls a native module the build lacks fails at runtime.
- `APP_VARIANT` switches `app.config.ts` to another name, package id, scheme and icon set: `development` (`make dev-build` and `make mobile`, the green dev icons), `local` (`make apk-local`, also green), `staging` (Build APK, clay); unset is production, any other value fails. Each has its own package id, so it installs next to the production app with its own storage. The Studio and admin dev servers show the green icons too (tab, installed app); builds the real ones ([design](design.md)).
- `APP_VARIANT` switches `app.config.ts` to another name, package id, scheme and icon set: `development` (`make dev-build` and `make mobile`, the green dev icons), `local` (`make apk-local`, also green), `staging` (Build phone app, clay); unset is production, any other value fails. Each has its own package id, so it installs next to the production app with its own storage. The Studio and admin dev servers show the green icons too (tab, installed app); builds the real ones ([design](design.md)).
- `make dev-build` = `VARIANT=dev scripts/apk-local.sh`: `expo prebuild` and `./gradlew assembleDebug` in the Docker toolchain, no account. Signed with the debug key of Expo's prebuild template, the same on every machine. Install the APK from Downloads once (allow installs from the file manager); later builds install over it.
- **On GitHub instead**: Actions → **Build APK** → Run workflow with `variant: dev` (in a deployment repository its Build APK workflow passes the same input). Same runner setup as the release build; the APK `openclapper-dev-<version>-<commit>.apk` is the run's artifact for 3 days. Built from the same template debug key, so it installs over a local `make dev-build` and vice versa.
- **On GitHub instead**: Actions → **Build phone app** → Run workflow with `variant: dev` (in a deployment repository its Build phone app workflow passes the same input). Same runner setup as the release build; the APK `openclapper-dev-<version>-<commit>.apk` is the run's artifact for 3 days. Built from the same template debug key, so it installs over a local `make dev-build` and vice versa.
- In OpenClapper Dev, open the `exp.direct` URL Metro prints (or pick the server in the launcher). Shake or the notification opens the dev menu.

- `scripts/dev-all.sh` (`make all`) opens a tmux session `openclapper`: Worker, Studio (:5173) and admin board (:5174, its Worker on :8788) stacked on the left, `make mobile` on the right (it and the admin pane wait for the Worker: mobile only adds a tunnel, and two wranglers starting together collide on their inspector port), and a **control strip** across the bottom showing which servers answer (● up, ○ down). Click it, then `r` restarts the three servers in their panes, `q` stops everything and closes the session, `d` detaches. Mouse mode is on, panes stay open after a crash so the error is readable, and running `make all` again attaches. Ctrl-C in a server pane stops just that server and leaves a shell to run it again. From another terminal: `make all-down` (Ctrl-C to every server, then it stops anything started from this repo that still listens on :8787, :5173 or Metro's :8081, plus a stray `cloudflared`; other folders' processes are left alone, then the session closes) and `make all-restart` (the same as `r`).
Expand All @@ -110,21 +110,21 @@ make studio # beside it: the Studio on :5173 against the same local Worker

## Deploy and migrations

Not from this checkout: deploying and production APKs belong to a deployment, which runs the GitHub workflows (`deploy.yml`, `build-apk.yml`) with its own variables and secrets ([self-hosting](self-hosting.md)). The reference instances (production and staging) deploy from the repository `OpenClapper-dev/deploy`, which holds this one as a submodule; nothing in this checkout reads its settings, and none of its workflows runs a target of this Makefile. Versions and releases: [releasing](releasing.md). The tracked `wrangler.jsonc` are for local development only.
Not from this checkout: deploying and production APKs belong to a deployment, which runs the GitHub workflows (`deploy-server.yml`, `build-phone-app.yml`) with its own variables and secrets ([self-hosting](self-hosting.md)). The reference instances (production and staging) deploy from the repository `OpenClapper-dev/deploy`, which holds this one as a submodule; nothing in this checkout reads its settings, and none of its workflows runs a target of this Makefile. Versions and releases: [releasing](releasing.md). The tracked `wrangler.jsonc` are for local development only.

- The owner runs production deploys. AI sessions prepare the change and the migration, then ask.
- Migrations are additive: always a new numbered file. Locally: `pnpm -C apps/worker migrate:local` (`make init` runs it).

## APK builds

- **The development build** for the phone loop: `make dev-build`, or Build APK with `variant: dev` (above).
- **The development build** for the phone loop: `make dev-build`, or Build phone app with `variant: dev` (above).
- **OpenClapper Local** (`org.openclapper.local`, the green icon): `API_URL=https://… make apk-local` (a LAN address of `make worker` works too). A complete app with the JavaScript inside, signed with the template's debug key, so it builds from a fresh clone without a secret. It installs next to every other build and updates none of them. A quick-tunnel address changes with every `make mobile`, so for the tunnel the dev build fits better.
- **Production and staging APKs** belong to a deployment: its Build APK workflow builds them for its `APP_URL` and signs them with its key from its GitHub secrets ([self-hosting](self-hosting.md#the-phone)). Nothing here builds them or reads a key.
- **Production and staging APKs** belong to a deployment: its Build phone app workflow builds them for its `APP_URL` and signs them with its key from its GitHub secrets ([self-hosting](self-hosting.md#the-phone)). Nothing here builds them or reads a key.

`scripts/apk-local.sh` builds in a Docker container (`docker/android.Dockerfile`: JDK 17, Android SDK 36, build-tools 36.0.0, NDK 27.1.12297006, cmake 3.22.1; nothing installed in WSL). Node comes from `.nvmrc` and pnpm from `packageManager` (via corepack): the script passes both as build arguments, so the image is rebuilt when either changes. The CI's Build APK uses the GitHub runner's preinstalled SDK instead; both files point at each other, so check the other when an SDK platform, build-tools or NDK version changes.
`scripts/apk-local.sh` builds in a Docker container (`docker/android.Dockerfile`: JDK 17, Android SDK 36, build-tools 36.0.0, NDK 27.1.12297006, cmake 3.22.1; nothing installed in WSL). Node comes from `.nvmrc` and pnpm from `packageManager` (via corepack): the script passes both as build arguments, so the image is rebuilt when either changes. The CI's Build phone app uses the GitHub runner's preinstalled SDK instead; both files point at each other, so check the other when an SDK platform, build-tools or NDK version changes.
- Once: Docker Desktop → Settings → Resources → WSL integration → enable this distro.
- It builds the image (cached after the first run), then in the container with the repo mounted and Gradle's cache in the `openclapper-gradle` volume: `expo prebuild --platform android` (generates the gitignored `apps/mobile/android/` from the template in `node_modules/expo`, removed again afterwards) and Gradle. It writes `~/openclapper-builds/openclapper-dev-<commit>.apk` (the local one `openclapper-local-<version>-<commit>.apk`) and copies it to the Windows Downloads folder (`DOWNLOADS=…` overrides).
- `VARIANT=local` (`make apk-local`) needs `API_URL=…` and nothing else: Gradle's release build type, signed with the debug key because no `ANDROID_KEYSTORE` is set. Release signing comes from a config plugin in `app.config.ts` that reads `ANDROID_KEYSTORE*` at Gradle time; the Build APK workflow fails a release or staging APK that still carries the debug key. The first run downloads Gradle's dependencies (several GB); later builds take about 10 minutes.
- `VARIANT=local` (`make apk-local`) needs `API_URL=…` and nothing else: Gradle's release build type, signed with the debug key because no `ANDROID_KEYSTORE` is set. Release signing comes from a config plugin in `app.config.ts` that reads `ANDROID_KEYSTORE*` at Gradle time; the Build phone app workflow fails a release or staging APK that still carries the debug key. The first run downloads Gradle's dependencies (several GB); later builds take about 10 minutes.
- Native code is compiled only for `arm64-v8a` (`-PreactNativeArchitectures`; `ANDROID_ABIS=arm64-v8a,x86_64` for an emulator, comma-separated). All four architectures would take about four times the native build and the APK size.
- `versionCode` and `versionName` come from the root `package.json` version (`app.config.ts`), the commit from `GIT_COMMIT`.

Expand Down
2 changes: 1 addition & 1 deletion docs/features/phone-app.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Designed in Claude Design (2026-09-28). The IA is organised around what happens

## Builds

One app, four installs side by side, each with its own package id, storage and icon colour (`APP_VARIANT`, `app.config.ts`): **OpenClapper** (production, blue), **OpenClapper Staging** (`org.openclapper.staging`, clay, for a staging instance; built by Build APK, same signing key), **OpenClapper Dev** (the dev client, moss, JavaScript from Metro) and **OpenClapper Local** (`org.openclapper.local`, moss, debug-signed, `make apk-local`). Each talks to the server built into it; the Setup footer names it ([development](../development.md#apk-builds), [self-hosting](../self-hosting.md#the-phone)).
One app, four installs side by side, each with its own package id, storage and icon colour (`APP_VARIANT`, `app.config.ts`): **OpenClapper** (production, blue), **OpenClapper Staging** (`org.openclapper.staging`, clay, for a staging instance; built by Build phone app, same signing key), **OpenClapper Dev** (the dev client, moss, JavaScript from Metro) and **OpenClapper Local** (`org.openclapper.local`, moss, debug-signed, `make apk-local`). Each talks to the server built into it; the Setup footer names it ([development](../development.md#apk-builds), [self-hosting](../self-hosting.md#the-phone)).

## Shell (`apps/mobile/App.tsx`, `src/appState.tsx`)

Expand Down
Loading
Loading