From 38d42a800a314a55a40be12a2c0424d01c009585 Mon Sep 17 00:00:00 2001 From: bubulux Date: Fri, 23 Oct 2026 14:58:48 +0200 Subject: [PATCH] Workflows renamed: Deploy server, Build phone app, Clone instance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The reusable workflows' names grew one by one (Deploy, Build APK, Clone). They now say what they act on: deploy-server.yml (D1 migrations, the app Worker with the Studio, the admin plane), build-phone-app.yml (APK, App Bundle, optional Play upload, or the dev client), clone-instance.yml. The deployment repository's callers follow with instance-first names ("Production · Deploy server", …). Docs, scripts and comments use the new names. Co-Authored-By: Claude Opus 5.5 --- .../{build-apk.yml => build-phone-app.yml} | 8 +++---- .../{clone.yml => clone-instance.yml} | 6 ++--- .../{deploy.yml => deploy-server.yml} | 4 ++-- CLAUDE.md | 2 +- apps/mobile/app.config.ts | 4 ++-- docs/development.md | 16 ++++++------- docs/features/phone-app.md | 2 +- docs/infrastructure.md | 2 +- docs/known-issues.md | 6 ++--- docs/releasing.md | 8 +++---- docs/self-hosting.md | 24 +++++++++---------- scripts/apk-local.sh | 2 +- scripts/clone-instance.mjs | 2 +- scripts/deploy-config.mjs | 2 +- scripts/keystore.sh | 4 ++-- scripts/play-upload.mjs | 2 +- scripts/release.sh | 2 +- 17 files changed, 48 insertions(+), 48 deletions(-) rename .github/workflows/{build-apk.yml => build-phone-app.yml} (97%) rename .github/workflows/{clone.yml => clone-instance.yml} (95%) rename .github/workflows/{deploy.yml => deploy-server.yml} (98%) diff --git a/.github/workflows/build-apk.yml b/.github/workflows/build-phone-app.yml similarity index 97% rename from .github/workflows/build-apk.yml rename to .github/workflows/build-phone-app.yml index c02e1d5..7c4a28f 100644 --- a/.github/workflows/build-apk.yml +++ b/.github/workflows/build-phone-app.yml @@ -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: @@ -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 == '' }} diff --git a/.github/workflows/clone.yml b/.github/workflows/clone-instance.yml similarity index 95% rename from .github/workflows/clone.yml rename to .github/workflows/clone-instance.yml index 8ac91da..be41bad 100644 --- a/.github/workflows/clone.yml +++ b/.github/workflows/clone-instance.yml @@ -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: @@ -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 || '' }} diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy-server.yml similarity index 98% rename from .github/workflows/deploy.yml rename to .github/workflows/deploy-server.yml index bb918cb..eaac72a 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy-server.yml @@ -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. # @@ -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: diff --git a/CLAUDE.md b/CLAUDE.md index 2440c67..0912ac8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 diff --git a/apps/mobile/app.config.ts b/apps/mobile/app.config.ts index 2dececd..9bbe162 100644 --- a/apps/mobile/app.config.ts +++ b/apps/mobile/app.config.ts @@ -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 { @@ -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. diff --git a/docs/development.md b/docs/development.md index 8bfc877..24c0231 100644 --- a/docs/development.md +++ b/docs/development.md @@ -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. @@ -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--.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--.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`). @@ -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-.apk` (the local one `openclapper-local--.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`. diff --git a/docs/features/phone-app.md b/docs/features/phone-app.md index 59b278d..da6341b 100644 --- a/docs/features/phone-app.md +++ b/docs/features/phone-app.md @@ -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`) diff --git a/docs/infrastructure.md b/docs/infrastructure.md index e136e25..412a85b 100644 --- a/docs/infrastructure.md +++ b/docs/infrastructure.md @@ -1,6 +1,6 @@ # Infrastructure -Everything runs on one Cloudflare account per instance; nothing else is needed. The tracked `wrangler.jsonc` files are for local development and name no account, database or Access application: each deployment's settings are GitHub variables and secrets (per instance, a GitHub environment), the Deploy workflow exports them, and `scripts/deploy-config.mjs` writes the deploy configs from the environment alone, never from a file ([self-hosting](self-hosting.md)). The reference instances' identifiers live in the deployment repository's environments and notes, not here. +Everything runs on one Cloudflare account per instance; nothing else is needed. The tracked `wrangler.jsonc` files are for local development and name no account, database or Access application: each deployment's settings are GitHub variables and secrets (per instance, a GitHub environment), the Deploy server workflow exports them, and `scripts/deploy-config.mjs` writes the deploy configs from the environment alone, never from a file ([self-hosting](self-hosting.md)). The reference instances' identifiers live in the deployment repository's environments and notes, not here. | Resource | Details | | --- | --- | diff --git a/docs/known-issues.md b/docs/known-issues.md index ee226e7..7c31b85 100644 --- a/docs/known-issues.md +++ b/docs/known-issues.md @@ -4,14 +4,14 @@ Things that may go wrong but are not fixed (or not verified on a device) yet. Ad | Since | Area | What can happen | Why it is left | Where | | --- | --- | --- | --- | --- | -| 2026-10-22 (deploy#5) | Deploy · Clone | The first real clone (production → staging) showed three things. **R2**: rclone's first pass fails every object with `NotImplemented` and its automatic second pass copies them all (`rclone check`: 0 differences); the cause is not found. rclone adds its own `mtime`/`btime` custom metadata on the copies, so staging's objects carry two keys production's do not. Production's objects carry **no** custom metadata at all (no `workspace_id`): likely lost in the bucket copy of the rename on 2026-10-20. **D1**: fixed, a remote import does not keep one transaction for the whole file, so the dump is now ordered parents first (see `prepareDump`); verified on Cloudflare: 29 tables, 456 rows, schema equal, no foreign-key violations. A running export blocks other requests to the source database (seconds at alpha size). | R2 works as is; the metadata keys are not read by the app | `scripts/clone-instance.mjs`, `.github/workflows/clone.yml` | +| 2026-10-22 (deploy#5) | Deploy · Clone | The first real clone (production → staging) showed three things. **R2**: rclone's first pass fails every object with `NotImplemented` and its automatic second pass copies them all (`rclone check`: 0 differences); the cause is not found. rclone adds its own `mtime`/`btime` custom metadata on the copies, so staging's objects carry two keys production's do not. Production's objects carry **no** custom metadata at all (no `workspace_id`): likely lost in the bucket copy of the rename on 2026-10-20. **D1**: fixed, a remote import does not keep one transaction for the whole file, so the dump is now ordered parents first (see `prepareDump`); verified on Cloudflare: 29 tables, 456 rows, schema equal, no foreign-key violations. A running export blocks other requests to the source database (seconds at alpha size). | R2 works as is; the metadata keys are not read by the app | `scripts/clone-instance.mjs`, `.github/workflows/clone-instance.yml` | | 2026-10-21 (#223) | Phone · Expo packages | The Expo packages were aligned with SDK 57 (expo 57.0.28, expo-modules-core 57.0.22, expo-dev-client, expo-image, expo-image-manipulator, expo-location, expo-sqlite, expo-file-system, expo-secure-store and more), and react-native-webview went from 13.17.0 down to the SDK's 13.16.1. These are native modules: the dev build was not rebuilt and nothing was run on a phone (camera and capture, uploads, the WebView maps, offline storage, the dev launcher). `expo-doctor` still fails on expo ~57.0.30 and expo-constants ~57.0.22, which are newer than the 7-day cooldown, so its CI step stays advisory. | Needs `make dev-build` and a phone test. Making expo-doctor blocking waits for the cooldown (expo 57.0.30 clears it on 2026-10-22). | `apps/mobile/package.json`, `.github/workflows/ci.yml` | | 2026-10-21 (#224) | Worker · uploads | A sequence longer than 200 photos is refused (`413`) for the part that crosses the limit: the phone parks it as stuck with its files, nothing is lost, but it cannot be uploaded as one shot. An upload without `content-length` (chunked) is read before the size is known; the per-file 3 MB limit still holds. Not run against the phone: the phone's request is expected to carry `content-length`. | 200 is far above any sequence so far; a split would need phone work. | `shots.ts`, `images.ts`, mobile `uploads.ts` | | 2026-10-21 (#224) | Studio + admin board · CSP | The Content-Security-Policy in `public/_headers` was checked by reading the code and the build (every external origin, no `eval`, no data fonts) and by `curl` against `wrangler dev`, not in a browser: a violation would show as a blocked map tile, weather card, preview websocket, render or icon, and as a CSP error in the DevTools console. The websocket relies on CSP Level 3 matching `wss:` to `'self'` (Chrome does). The bundled MDI CSS also emits its unused `.eot`, `.ttf` and `.woff` (about 3 MB) into `dist/`; browsers fetch only the `.woff2`. | Needs the owner's click-through in both themes, console open. | `apps/studio/public/_headers`, `apps/admin/ui/public/_headers` | | 2026-10-21 (#224) | Worker · sign-in | **No brute-force limit**: the account lockout was dropped (anyone could lock an account out, and its 429 confirmed the email), and nothing limits sign-in or the current-password check by IP yet. One account (the owner's) today, with a strong password. | Rate limiting (a Cloudflare rule or the Workers rate-limit binding, per IP and email) belongs to the sign-in revamp before the app opens up to users. | `authn/first-party.ts` | | 2026-10-21 (#224) | Admin plane · audit | The Audit tab was not clicked through (Sun and Set, Older entries, a narrow window); the route and the rows are tested. | Needs the owner's look after deploy (migration 0025 first). | `apps/admin/ui/src/Audit.tsx` | -| 2026-10-20 (#67) | Admin plane · monitoring | The Cloudflare section's GraphQL queries use the documented field names (October 2026) and were run against a stubbed endpoint only: a real answer may reject a field, which shows as that dataset's error in the section's banner (the others stay). The admin Worker's cron and the Deploy step that sets `CF_ANALYTICS_TOKEN` are untested until the owner deploys with the secret forwarded from the deployment repository. The marks editor (add, remove, save) and the chart tooltip were read, not clicked. | No token here to introspect; no staging. | `apps/admin/src/cloudflare.ts`, `Monitoring.tsx`, `deploy.yml` | -| 2026-10-18 (#117) | Deploy · self-hosting | The workflow has run on the reference instance (from its deploy repository), where `--provision` only found the existing database and bucket: creating them on a fresh account is untested. A checkout made before #117 keeps its local D1 under the old database id: copy `apps/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/.sqlite` over the new file, or re-seed. | First fresh run is a self-hoster's. | `.github/workflows/deploy.yml`, `scripts/deploy-config.mjs` | +| 2026-10-20 (#67) | Admin plane · monitoring | The Cloudflare section's GraphQL queries use the documented field names (October 2026) and were run against a stubbed endpoint only: a real answer may reject a field, which shows as that dataset's error in the section's banner (the others stay). The admin Worker's cron and the Deploy step that sets `CF_ANALYTICS_TOKEN` are untested until the owner deploys with the secret forwarded from the deployment repository. The marks editor (add, remove, save) and the chart tooltip were read, not clicked. | No token here to introspect; no staging. | `apps/admin/src/cloudflare.ts`, `Monitoring.tsx`, `deploy-server.yml` | +| 2026-10-18 (#117) | Deploy · self-hosting | The workflow has run on the reference instance (from its deploy repository), where `--provision` only found the existing database and bucket: creating them on a fresh account is untested. A checkout made before #117 keeps its local D1 under the old database id: copy `apps/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/.sqlite` over the new file, or re-seed. | First fresh run is a self-hoster's. | `.github/workflows/deploy-server.yml`, `scripts/deploy-config.mjs` | | 2026-10-18 (#116) | Admin plane | Deployed behind its Access application and opened by the owner; its actions (create, block, reset, delete) were run locally only. Not tried: the Set theme, narrow windows. Deleting an account with projects is refused: there is no transfer of workspaces between accounts yet. | Needs a look at the board's actions in production. | `apps/admin` | | 2026-10-18 (#114) | Studio · accounts | Clicked through in headless Chrome: sign-in (wrong and right password), the register notice, the account menu, Settings › Account (new workspace, invite, role picker, remove with confirm), sign out, a session killed on the server returning to sign-in. Not tried: Set theme, a narrow window, two tabs signed in as different accounts, the role picker's options as an admin who is not the owner. There is no password change for users yet (an administrator resets it). Inviting by email tells an admin whether that email has an account on the instance. The desktop's virtual desktops are also called workspaces. | Needs the owner's click-through; password change and the naming are follow-ups. | `SignIn.tsx`, `AccountSettings.tsx`, `user.ts` | | 2026-10-18 (#113) | Worker · storage accounting | `photos.size_bytes` is recorded from now on; the photos uploaded before migration 0021 have none, so per-workspace storage counts them as 0 until a backfill reads their sizes from R2. | The backfill needs an R2 listing; one user today. | `shots.ts` | diff --git a/docs/releasing.md b/docs/releasing.md index 8a6e067..86f0098 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -16,11 +16,11 @@ OpenClapper has **one version for the whole product**: the Worker and the Studio - Admin board: its header. - Phone: the Setup footer and the debug log. - **Android**: `versionName` is the version and `versionCode` is derived from it (`major·1 000 000 + minor·1 000 + patch`: 0.1.0 → 1000), so it grows with every release (`apps/mobile/app.config.ts`). Builds between releases carry the same code as the last release; Android installs a same-code update over it. Google Play takes each code once, so production goes to Play once per release. -- **The staging app** (`APP_VARIANT=staging`) counts commits instead (`git rev-list --count HEAD`, Build APK sets it from a full checkout), so every build of `main` is a new version for Play. A branch build has a higher count than `main`: sideloaded, it blocks Play's next update of the staging app (Android refuses a lower code) until it is uninstalled. The same holds once for a staging app sideloaded before Play: its release-based code (`10000` for 0.10.0) is higher than the count. +- **The staging app** (`APP_VARIANT=staging`) counts commits instead (`git rev-list --count HEAD`, Build phone app sets it from a full checkout), so every build of `main` is a new version for Play. A branch build has a higher count than `main`: sideloaded, it blocks Play's next update of the staging app (Android refuses a lower code) until it is uninstalled. The same holds once for a staging app sideloaded before Play: its release-based code (`10000` for 0.10.0) is higher than the count. ## Cutting a release -Try it first where a deployment has a staging instance: deploy `main` there (its Deploy workflow with `ref: main`), click through, then release. +Try it first where a deployment has a staging instance: deploy `main` there (its Deploy server workflow with `ref: main`), click through, then release. From an up-to-date, clean `main`: @@ -39,5 +39,5 @@ The release carries no APK. An APK is built for one server and signed with its d ## After a release, per deployment 1. Move the deployment to the tag. With this repository as a submodule: `scripts/bump.sh v0.2.0`, then commit and push. In a fork: merge the tag. -2. Run **Deploy**. It applies new migrations before the new code goes live. -3. Run **Build APK** and install the APK on the phones. +2. Run **Deploy server**. It applies new migrations before the new code goes live. +3. Run **Build phone app** and install the APK on the phones. diff --git a/docs/self-hosting.md b/docs/self-hosting.md index 4003cc9..c2dc16a 100644 --- a/docs/self-hosting.md +++ b/docs/self-hosting.md @@ -52,7 +52,7 @@ Every setting is a GitHub variable or secret: your copy of the repository → Se ## 3. Deploy -- **GitHub**: Actions → Deploy → Run workflow. The first run: +- **GitHub**: Actions → Deploy server → Run workflow. The first run: 1. creates the D1 database and the R2 bucket; 2. applies every migration; 3. builds the Studio; @@ -95,9 +95,9 @@ Sign in to the Studio with it. There is no public sign-up; every account is made The admin plane's Monitoring tab works without any setup ([monitoring](features/monitoring.md)). Its Cloudflare section, with the account-wide numbers (R2 storage across every bucket, Worker requests and CPU, D1 rows), needs a second API token: 1. My Profile → API Tokens → Create Token → Custom token, with the single permission **Account · Account Analytics · Read** on your account. -2. Set it as the secret `CF_ANALYTICS_TOKEN` and run Deploy again. A deployment repository that calls the Deploy workflow passes it on (`secrets: inherit`, or the one name). +2. Set it as the secret `CF_ANALYTICS_TOKEN` and run Deploy server again. A deployment repository that calls the Deploy server workflow passes it on (`secrets: inherit`, or the one name). -The token is read-only; to rotate it, make a new one and run Deploy again. +The token is read-only; to rotate it, make a new one and run Deploy server again. ## A second instance (staging) @@ -105,15 +105,15 @@ A second instance (a staging copy to try a release on, say) is the same setup ag - Make one **GitHub environment** per instance (Settings → Environments: `production`, `staging`) and give each the variables and secrets of step 2 under the same names; an environment's values win over the repository's, so shared ones (the API token, the signing key) can stay repository secrets. Environments in a private repository need a paid GitHub plan; in a public one they are free. - Give the second one `INSTANCE_LABEL=staging`: the Studio puts it in front of the tab title and shows it as a badge in the taskbar and on sign-in, the admin board in its header. Production has no label and shows nothing. -- Run Deploy and Build APK with the `environment` input set. To try another version than the one the run starts from (a branch), set `ref`. +- Run Deploy server and Build phone app with the `environment` input set. To try another version than the one the run starts from (a branch), set `ref`. - A deployment repository that holds this one as a submodule calls the workflows with `path`, `environment` and `ref` (`secrets: inherit`). The job names the environment itself, since GitHub does not hand environment secrets from a caller to a reusable workflow. With `ref`, the workflow's steps come from the ref in the caller's `uses:` line and the code from `ref`, checked out in full instead of the submodule: a staging caller tracks `@main` for both. -- **Copy one instance into another** (staging ← production, to try a migration or a refactor on real data): the **Clone** workflow, run in the target's environment with `confirm` set to the target's database name. It replaces the target completely: the R2 bucket is synced with deletion (compared by checksum, custom metadata kept), then the D1 database is deleted, created again by name and filled with the source's export (reordered so every table exists before its rows and every row after the rows it references; an optional reference in a cycle or to its own table is set afterwards, since a remote import does not run in one transaction). Sessions are not copied, so everyone signs in again there; accounts, password hashes and all data are production's. The source is only read. Deploy the target right after: it applies newer migrations on top, and until then its Workers point at the deleted database. The target's environment needs: +- **Copy one instance into another** (staging ← production, to try a migration or a refactor on real data): the **Clone instance** workflow, run in the target's environment with `confirm` set to the target's database name. It replaces the target completely: the R2 bucket is synced with deletion (compared by checksum, custom metadata kept), then the D1 database is deleted, created again by name and filled with the source's export (reordered so every table exists before its rows and every row after the rows it references; an optional reference in a cycle or to its own table is set afterwards, since a remote import does not run in one transaction). Sessions are not copied, so everyone signs in again there; accounts, password hashes and all data are production's. The source is only read. Deploy the target right after: it applies newer migrations on top, and until then its Workers point at the deleted database. The target's environment needs: - `SOURCE_D1_DATABASE_NAME`, `SOURCE_R2_BUCKET` (variables), and no `D1_DATABASE_ID` (the recreated database has a new id; the deploy finds it by name); optionally `D1_LOCATION` (`weur`…, the source's location). - Two R2 API tokens (R2 → Manage API tokens), as secrets: `SOURCE_R2_ACCESS_KEY_ID` / `SOURCE_R2_SECRET_ACCESS_KEY` with **Object Read only** on the source bucket, `R2_ACCESS_KEY_ID` / `R2_SECRET_ACCESS_KEY` with Object Read & Write on the target bucket. The deploy token can still edit every D1 database of the account (Cloudflare cannot limit a token to one database): the script's refusal when source and target are the same is the guard there. ## Updating -Releases are tags `vX.Y.Z` with release notes on GitHub ([releasing](releasing.md)). Bring your copy up to the new tag (merge it), then run Deploy, then Build APK. Migrations only ever add (they never delete data), and the workflow applies them before the new code goes live. Settings › General › About in the Studio shows which version runs. +Releases are tags `vX.Y.Z` with release notes on GitHub ([releasing](releasing.md)). Bring your copy up to the new tag (merge it), then run Deploy server, then Build phone app. Migrations only ever add (they never delete data), and the workflow applies them before the new code goes live. Settings › General › About in the Studio shows which version runs. ## The phone @@ -124,22 +124,22 @@ Once: 2. **Back up both files** (a password manager or an encrypted drive). Lose them and nobody can update the app without uninstalling it, which deletes whatever is still queued on the phones. The GitHub secrets and the backup are the key's home: nothing reads it from a checkout. 3. Add the repository variable `APP_URL`: your app's address, `https://` or `https://..workers.dev`. -Then Actions → **Build APK** → Run workflow (`variant: release`, the default; `staging` builds "OpenClapper Staging", `org.openclapper.staging` with the clay icon and the same key, which installs next to the production app, for a [second instance](#a-second-instance-staging); `dev` builds the development client for the phone dev loop instead, [development](development.md)). The build runs on GitHub's runner. It compiles only for `arm64-v8a`, which every current Android phone runs; for older 32-bit phones set the variable `ANDROID_ABIS=arm64-v8a,armeabi-v7a` (each architecture adds build time, disk and APK size). The job first deletes preinstalled tools it does not use, since a standard runner has only about 14 GB free. The APK is the run's artifact and is **deleted after 3 days** (`retention-days`: GitHub removes it, nothing to clean up). Download it, open it on the phone and allow installs from that source. Release and staging runs also make the App Bundle for [Google Play](#play-store). A private repository on GitHub's Free plan has 500 MB of artifact storage; one APK is about 80 MB, a bundle about as much. +Then Actions → **Build phone app** → Run workflow (`variant: release`, the default; `staging` builds "OpenClapper Staging", `org.openclapper.staging` with the clay icon and the same key, which installs next to the production app, for a [second instance](#a-second-instance-staging); `dev` builds the development client for the phone dev loop instead, [development](development.md)). The build runs on GitHub's runner. It compiles only for `arm64-v8a`, which every current Android phone runs; for older 32-bit phones set the variable `ANDROID_ABIS=arm64-v8a,armeabi-v7a` (each architecture adds build time, disk and APK size). The job first deletes preinstalled tools it does not use, since a standard runner has only about 14 GB free. The APK is the run's artifact and is **deleted after 3 days** (`retention-days`: GitHub removes it, nothing to clean up). Download it, open it on the phone and allow installs from that source. Release and staging runs also make the App Bundle for [Google Play](#play-store). A private repository on GitHub's Free plan has 500 MB of artifact storage; one APK is about 80 MB, a bundle about as much. Without GitHub, a release build is your own setup: export the key's four values (with `ANDROID_KEYSTORE` as the path to the `.jks`) and `EXPO_PUBLIC_API_URL`, then run the workflow's build step. `make apk-local` is no release: it builds "OpenClapper Local", debug-signed, for trying the app ([development](development.md#apk-builds)). ## Play Store -Google Play delivers the app instead of sideloading, starting with the **internal testing** track: up to 100 testers by email, no review, no store listing, updates on the phones within minutes. No Expo account and no third-party service is involved: Build APK makes the bundle, and Google's own API takes it (the upload, deploy#11). +Google Play delivers the app instead of sideloading, starting with the **internal testing** track: up to 100 testers by email, no review, no store listing, updates on the phones within minutes. No Expo account and no third-party service is involved: Build phone app makes the bundle, and Google's own API takes it (the upload, deploy#11). -- **The bundle.** Release and staging runs of Build APK produce an Android App Bundle (`.aab`) next to the APK, from the same build and key, as a second artifact. Play takes only bundles; the APK stays for sideloading. `ANDROID_ABIS` limits which devices Play can serve. +- **The bundle.** Release and staging runs of Build phone app produce an Android App Bundle (`.aab`) next to the APK, from the same build and key, as a second artifact. Play takes only bundles; the APK stays for sideloading. `ANDROID_ABIS` limits which devices Play can serve. - **Requirements**, checked by the workflow's "Play requirements" step: - Target API level 36 or higher: Play has rejected new apps and updates below it since 31 August 2026. The step fails below. - 16 KB page size: native libraries' load segments aligned to 16 KB. The step reports any library that is not, as a warning for now. - **Version codes.** Play takes each version code once per app. Production's grows with each release (`0.10.0` → `10000`, [releasing](releasing.md)), so production uploads once per release. The staging app counts commits instead, so every build of `main` is a new version (it needs the full history, which a run with `ref` has). -- **Signing.** Play signs what it delivers (Play App Signing). Let Google generate each app's signing key (the Console's default on the first release); your deployment's key from `scripts/keystore.sh` becomes the app's **upload key**: Build APK signs every bundle with it, and Play accepts only bundles signed with it. A lost upload key can be reset through Google, so no app is ever stuck. The price: an APK from Build APK (signed with your key) and the app from Play (signed with Google's) cannot update each other, so a phone has one or the other; uninstall to switch. (Uploading your own key as the signing key with Google's PEPK tool keeps them interchangeable, but a lost key then means an app that can never be updated.) -- **The first bundle of each app** is uploaded by hand in the Play Console: the API can neither create an app nor choose its signing key. Play Console → Create app → the package (`org.openclapper`, `org.openclapper.staging`) → Testing → Internal testing → Create release → keep "Google-generated key" → the `.aab` from Build APK, which registers its key as the upload key. Then add the testers' emails and open the opt-in link on each phone once. -- **Uploads from the workflow.** Google Cloud Console → a project → IAM → Service accounts → create one and a JSON key for it. Play Console → Users and permissions → invite the service account's email with "Release to testing tracks" on the app (on each app). Store the JSON as the secret `PLAY_SERVICE_ACCOUNT_JSON` (in each GitHub environment that uploads). Then Build APK with `track: internal` uploads the bundle after the build (`scripts/play-upload.mjs`: Google's androidpublisher API with the service account, no dependency). A version code Play already has is a notice, not a failure. An app that was never published takes draft releases only: the run says so, and you roll the release out in the Console once. A newly invited service account may be refused for up to a day. +- **Signing.** Play signs what it delivers (Play App Signing). Let Google generate each app's signing key (the Console's default on the first release); your deployment's key from `scripts/keystore.sh` becomes the app's **upload key**: Build phone app signs every bundle with it, and Play accepts only bundles signed with it. A lost upload key can be reset through Google, so no app is ever stuck. The price: an APK from Build phone app (signed with your key) and the app from Play (signed with Google's) cannot update each other, so a phone has one or the other; uninstall to switch. (Uploading your own key as the signing key with Google's PEPK tool keeps them interchangeable, but a lost key then means an app that can never be updated.) +- **The first bundle of each app** is uploaded by hand in the Play Console: the API can neither create an app nor choose its signing key. Play Console → Create app → the package (`org.openclapper`, `org.openclapper.staging`) → Testing → Internal testing → Create release → keep "Google-generated key" → the `.aab` from Build phone app, which registers its key as the upload key. Then add the testers' emails and open the opt-in link on each phone once. +- **Uploads from the workflow.** Google Cloud Console → a project → IAM → Service accounts → create one and a JSON key for it. Play Console → Users and permissions → invite the service account's email with "Release to testing tracks" on the app (on each app). Store the JSON as the secret `PLAY_SERVICE_ACCOUNT_JSON` (in each GitHub environment that uploads). Then Build phone app with `track: internal` uploads the bundle after the build (`scripts/play-upload.mjs`: Google's androidpublisher API with the service account, no dependency). A version code Play already has is a notice, not a failure. An app that was never published takes draft releases only: the run says so, and you roll the release out in the Console once. A newly invited service account may be refused for up to a day. ## The free plan diff --git a/scripts/apk-local.sh b/scripts/apk-local.sh index c1917eb..a417ab1 100755 --- a/scripts/apk-local.sh +++ b/scripts/apk-local.sh @@ -9,7 +9,7 @@ # green icon, signed with the template's debug key. Needs no secret, so it builds # from a fresh clone; it updates no other build and nothing else updates it. # Production and staging APKs are not built here: they belong to a deployment, signed with its key -# by its Build APK workflow (docs/self-hosting.md#the-phone). +# by its Build phone app workflow (docs/self-hosting.md#the-phone). # Overrides: API_URL, DOWNLOADS (target folder), ANDROID_ABIS (CPU architectures, comma-separated; # default arm64-v8a, what phones run: each one more multiplies the native build and the APK). set -euo pipefail diff --git a/scripts/clone-instance.mjs b/scripts/clone-instance.mjs index 387c40f..0fbdfe5 100644 --- a/scripts/clone-instance.mjs +++ b/scripts/clone-instance.mjs @@ -1,7 +1,7 @@ #!/usr/bin/env node // Replaces one instance with a 1:1 copy of another (staging ← production): the R2 bucket first, then // the D1 database, so a finished run is consistent. Whatever the target held is gone afterwards; the -// source is only read. Run by the Clone workflow (.github/workflows/clone.yml) in the target's GitHub +// source is only read. Run by the Clone instance workflow (.github/workflows/clone-instance.yml) in the target's GitHub // environment; like every script here it reads the environment only (docs/self-hosting.md). // // node scripts/clone-instance.mjs diff --git a/scripts/deploy-config.mjs b/scripts/deploy-config.mjs index 56faa4f..1122c8f 100644 --- a/scripts/deploy-config.mjs +++ b/scripts/deploy-config.mjs @@ -6,7 +6,7 @@ // node scripts/deploy-config.mjs [--provision] // // Settings come from the environment only, never from a file: a deployment sets them as GitHub -// variables and secrets (the Deploy workflow exports them), or exports the same names itself +// variables and secrets (the Deploy server workflow exports them), or exports the same names itself // (docs/self-hosting.md). An empty variable counts as unset. // CLOUDFLARE_ACCOUNT_ID required // D1_DATABASE_ID required, unless --provision finds or creates the database by name diff --git a/scripts/keystore.sh b/scripts/keystore.sh index e5af08a..e5f38bf 100644 --- a/scripts/keystore.sh +++ b/scripts/keystore.sh @@ -6,7 +6,7 @@ # it lives in the deployment's GitHub secrets and in your backup, so the folder must be outside this # repository (default ~/openclapper-android-key). Nothing in this repository reads it from disk. # Uses keytool from a local JDK, else the Docker image of scripts/apk-local.sh. -# bash scripts/keystore.sh [folder] [owner/repo] # owner/repo: whose Build APK workflow gets the secrets +# bash scripts/keystore.sh [folder] [owner/repo] # owner/repo: whose Build phone app workflow gets the secrets set -euo pipefail cd "$(dirname "$0")/.." @@ -45,7 +45,7 @@ Made $jks and $envfile. BACK UP BOTH FILES NOW (a password manager or an encrypted drive). Without them no update installs over the app. -For the Build APK workflow, set these four secrets in $repo (add --env to give them +For the Build phone app workflow, set these four secrets in $repo (add --env to give them to one GitHub environment only): base64 -w0 $jks | gh secret set ANDROID_KEYSTORE_BASE64 -R $repo gh secret set ANDROID_KEYSTORE_PASSWORD -R $repo --body "\$(sed -n 's/^ANDROID_KEYSTORE_PASSWORD=//p' $envfile)" diff --git a/scripts/play-upload.mjs b/scripts/play-upload.mjs index e8b7d02..97de4eb 100644 --- a/scripts/play-upload.mjs +++ b/scripts/play-upload.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env node // Uploads an Android App Bundle to a Google Play track, with Google's own API (androidpublisher v3) -// and nothing else: no Expo account, no third-party action, no dependency. Run by the Build APK +// and nothing else: no Expo account, no third-party action, no dependency. Run by the Build phone app // workflow with `track` set; like every script here it reads its secret from the environment only. // // node scripts/play-upload.mjs --bundle app.aab --package org.openclapper --track internal [--notes "…"] [--status completed|draft] diff --git a/scripts/release.sh b/scripts/release.sh index a9f97c6..4bc0ec2 100644 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -37,5 +37,5 @@ gh release create "v$next" --title "v$next" --generate-notes --verify-tag "${pre cat <