From fe5315b91bd174dd82fdc3b7ebc21c6d5cd725bd Mon Sep 17 00:00:00 2001 From: Muawiya Amir Date: Sun, 13 Sep 2026 22:50:16 +0500 Subject: [PATCH 1/2] fix: publish production OTA only after deployed revision confirmation --- .github/scripts/check-release-revision.sh | 13 +++++++++++ .../scripts/check-release-revision.test.cjs | 20 ++++++++++++++++ .github/workflows/eas-update.yml | 19 +-------------- .github/workflows/release-validation.yml | 17 ++++++++++++++ .github/workflows/release.yml | 23 +++++++++++++------ 5 files changed, 67 insertions(+), 25 deletions(-) create mode 100644 .github/scripts/check-release-revision.sh create mode 100644 .github/scripts/check-release-revision.test.cjs create mode 100644 .github/workflows/release-validation.yml diff --git a/.github/scripts/check-release-revision.sh b/.github/scripts/check-release-revision.sh new file mode 100644 index 0000000..570bf2e --- /dev/null +++ b/.github/scripts/check-release-revision.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +set -euo pipefail +# This validates an operator's deployment attestation, not Railway health itself. +if [[ "${GITHUB_REF:-}" != refs/heads/main ]]; then + echo '::error::Publish releases from main only.' + exit 1 +fi +if [[ ! "${VERIFIED_BACKEND_SHA:-}" =~ ^[0-9a-f]{40}$ ]] || + [[ "$VERIFIED_BACKEND_SHA" != "${GITHUB_SHA:-}" ]] || + [[ "$VERIFIED_BACKEND_SHA" != "${REMOTE_MAIN_SHA:-}" ]]; then + echo '::error::Verify the current main commit on the production API and workers, then provide its full SHA.' + exit 1 +fi diff --git a/.github/scripts/check-release-revision.test.cjs b/.github/scripts/check-release-revision.test.cjs new file mode 100644 index 0000000..f0ea9d3 --- /dev/null +++ b/.github/scripts/check-release-revision.test.cjs @@ -0,0 +1,20 @@ +const {test}=require('node:test'); +const assert=require('node:assert/strict'); +const {spawnSync}=require('node:child_process'); +const path=require('node:path'); +const sha='a'.repeat(40), other='b'.repeat(40); +const baseline={...process.env,GITHUB_REF:'refs/heads/main',GITHUB_SHA:sha,VERIFIED_BACKEND_SHA:sha,REMOTE_MAIN_SHA:sha}; +for(const [name,override,success] of [ + ['matching main deployment',{},true], + ['preview branch',{GITHUB_REF:'refs/heads/develop'},false], + ['tag dispatch',{GITHUB_REF:'refs/tags/v1.9.0'},false], + ['missing attestation',{VERIFIED_BACKEND_SHA:''},false], + ['short SHA',{VERIFIED_BACKEND_SHA:'aaaaaaa'},false], + ['older backend',{VERIFIED_BACKEND_SHA:other},false], + ['main advanced after dispatch',{REMOTE_MAIN_SHA:other},false], + ['remote lookup failed',{REMOTE_MAIN_SHA:''},false], + ['shell characters',{VERIFIED_BACKEND_SHA:'$(exit 0)'},false], +]) test(name,()=>{ + const result=spawnSync('bash',[path.join(__dirname,'check-release-revision.sh')],{env:{...baseline,...override},encoding:'utf8'}); + assert.equal(result.status,success?0:1,result.stderr+result.stdout); +}); diff --git a/.github/workflows/eas-update.yml b/.github/workflows/eas-update.yml index 99aec6d..1ecff76 100644 --- a/.github/workflows/eas-update.yml +++ b/.github/workflows/eas-update.yml @@ -1,5 +1,5 @@ # Preview-channel OTA for the develop branch (the developer's own devices), -# plus a manual dispatch that publishes the chosen channel. Production OTA +# plus a manual preview dispatch. Production OTA # for a release lives in release.yml, so the release tag is only cut after # that publish succeeds. Native changes need a new build — see # mobile/DEPLOYMENT.md. @@ -15,12 +15,6 @@ on: - "mobile/**" - "!mobile/**.md" workflow_dispatch: - inputs: - channel: - description: "Update channel (manual runs only)" - type: choice - default: preview - options: [preview, production] jobs: update: @@ -46,18 +40,7 @@ jobs: # --environment pulls the EXPO_PUBLIC_* variables from EAS into the # bundle; without it the update ships with empty config and crashes. - - name: Publish OTA update (production) - if: github.event_name == 'workflow_dispatch' && github.event.inputs.channel == 'production' - env: - MSG: ${{ github.event.head_commit.message || 'manual dispatch' }} - run: eas update --channel production --environment production --message "$MSG" --non-interactive - - # A push to develop refreshes preview; a manual run publishes only the - # chosen channel. - name: Publish OTA update (preview) - if: >- - (github.event_name == 'push') || - (github.event_name == 'workflow_dispatch' && github.event.inputs.channel == 'preview') env: MSG: ${{ github.event.head_commit.message || 'manual dispatch' }} run: eas update --channel preview --environment preview --message "$MSG" --non-interactive diff --git a/.github/workflows/release-validation.yml b/.github/workflows/release-validation.yml new file mode 100644 index 0000000..97f6bd3 --- /dev/null +++ b/.github/workflows/release-validation.yml @@ -0,0 +1,17 @@ +name: Release guard validation +on: + pull_request: + paths: + - '.github/scripts/check-release-revision*' + - '.github/workflows/release*.yml' + - '.github/workflows/eas-update.yml' +jobs: + validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-node@v7 + with: + node-version: 24 + - run: bash -n .github/scripts/check-release-revision.sh + - run: node --test --test-isolation=none .github/scripts/check-release-revision.test.cjs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4152006..a8901e2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,13 +1,14 @@ -# Merging a release PR into main cuts a versioned release. The production OTA -# publishes FIRST; the tag and GitHub Release are created only if that publish -# succeeds, so a Release can never claim a version that never reached phones. -# (Railway deploys the backend independently on the same push — see -# mobile/DEPLOYMENT.md; it is not gated here.) +# Railway deploys main independently. Publish only after the operator verifies +# the same commit on the API and workers and checks production health. name: Release on: - push: - branches: [main] + workflow_dispatch: + inputs: + backend_revision: + description: 'Full main commit SHA verified on healthy production API and workers' + required: true + type: string # One release at a time: two quick merges must not race the tag check. concurrency: @@ -27,6 +28,14 @@ jobs: steps: - uses: actions/checkout@v7 + - name: Verify deployed revision before publishing + env: + VERIFIED_BACKEND_SHA: ${{ inputs.backend_revision }} + run: | + REMOTE_MAIN_SHA=$(git ls-remote origin refs/heads/main | cut -f1) + export REMOTE_MAIN_SHA + bash ../.github/scripts/check-release-revision.sh + - uses: actions/setup-node@v7 with: node-version: 22 From fd74b15bccfcfe1ea80077842d79a5cb8a7f55ad Mon Sep 17 00:00:00 2001 From: Muawiya Amir Date: Sun, 13 Sep 2026 22:50:40 +0500 Subject: [PATCH 2/2] docs: explain backend-first release and local backup status --- AGENTS.md | 3 ++- RELEASING.md | 19 +++++++++++++++---- docs/CODEBASE_MAP.md | 6 ++++-- docs/WORK_LOG.md | 21 +++++++++++++++++++++ mobile/DEPLOYMENT.md | 25 ++++++++++++------------- 5 files changed, 54 insertions(+), 20 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index ad8690e..3900d82 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -47,7 +47,8 @@ requires the exact Expo SDK 57 documentation before writing mobile code. Use a descriptive `codex/` branch for a new chunk unless the owner specifies a branch. Check the available base revision; do not assume local refs are current. - `main` is production. A production release uses a `develop` to `main` PR and - the release runbook. Merging there triggers deployment and release automation. + the release runbook. Merging deploys the backend; publish the mobile release + separately after verifying the deployed API and worker revision. - Every release PR must include a one-time What's New card. During release preparation, automatically add a nonempty entry in `mobile/src/data/whatsNew.ts` matching `mobile/app.config.js`'s version; do not wait for the owner to remind you. diff --git a/RELEASING.md b/RELEASING.md index 0223f1e..65c25e5 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -7,7 +7,8 @@ Production release flow. Keep it boring and repeatable. (This is our internal flow — the project doesn't take outside PRs; see [CONTRIBUTING.md](CONTRIBUTING.md).) - `main` is production. A release is a single PR **develop → main** (no `release/*` branch). -- Merging to `main` triggers `.github/workflows/release.yml`. +- Merging to `main` triggers Railway backend deployment. Mobile publication is + a separate manual run of `.github/workflows/release.yml` after verification. ## Cutting a release 1. **Bump the version on `develop` first.** Edit `mobile/app.config.js` → `expo.version` @@ -26,9 +27,19 @@ Production release flow. Keep it boring and repeatable. release PR.** 4. Open the release PR **develop → main**. It must pass the required **"Migrations applied check"** and get its approval, then merge. -5. On merge, `release.yml` publishes the production + preview OTA, cuts the `vX.Y.Z` - tag + GitHub Release, and dispatches the APK build (which **skips** unless - runtimeVersion changed). Railway auto-deploys the `api` service from `main`. +5. On merge, Railway auto-deploys the API from `main`. Keep generation paused + during backend/worker transitions. Verify the new main commit is deployed to + the API and workers, `/health` succeeds, and the release-specific smoke checks + pass. Do not resume old generators against the new editorial schema. +6. Open **GitHub Actions → Release → Run workflow**, select **main**, and enter + the full 40-character main commit SHA you verified on production API/workers + in `backend_revision`. This is an operator attestation; the workflow does not + inspect Railway deployments itself. Do not submit it until checks are complete. +7. The workflow rejects another branch, an older deployment or a main revision + that advanced before validation. It then publishes production + preview OTA, + cuts the version tag/GitHub Release, and dispatches the native-gated APK build. + Do not merge another release while publication is running. The standalone + EAS Update workflow publishes preview only; production uses this release path. ## Database migrations — MANUAL, every release The deploy does **not** auto-migrate. Files in `backend/migrations/*.sql` must be run diff --git a/docs/CODEBASE_MAP.md b/docs/CODEBASE_MAP.md index 4860a3a..1f7763c 100644 --- a/docs/CODEBASE_MAP.md +++ b/docs/CODEBASE_MAP.md @@ -265,9 +265,11 @@ and the session pooler. Applied migrations must not be rewritten. on port 55433, applies every migration, and disables live generation. HTTP calls to Gemini/Expo are mocked. Database-dependent tests skip if Podman cannot start. - `.github/workflows/eas-update.yml` publishes preview OTA on qualifying mobile - pushes to `develop`; manual dispatch can select a channel. `eas-build.yml` is + pushes to `develop`; manual dispatch also publishes preview only. `eas-build.yml` is a manual Android build workflow. -- `release.yml` runs on `main`, publishes production then preview OTA, creates a +- `release.yml` is manually dispatched on `main` after operator confirmation of + the deployed backend/worker SHA; its guard rejects missing/mismatched revisions + and non-main dispatches. It publishes production then preview OTA, creates a version tag/GitHub release, and dispatches `release-apk.yml`. APK publication is gated on native `runtimeVersion` changes. Railway deploys the backend independently; follow `RELEASING.md` for migration and release ordering. diff --git a/docs/WORK_LOG.md b/docs/WORK_LOG.md index 1298c1f..6bd6190 100644 --- a/docs/WORK_LOG.md +++ b/docs/WORK_LOG.md @@ -7,6 +7,27 @@ claims as completed work. ## Current status +### Release #200 readiness and deployment ordering + +- Owner requested a merge-ready release, with production merge left for approval. + Railway screenshot confirms main auto-deploy with `/backend` root directory. +- Separating the backend merge/deploy from mobile publication. Release becomes + manual on main with a required full deployed-commit attestation; a guard rejects + wrong branches, missing/mismatched SHAs and a main revision changed since dispatch. + Standalone EAS Update remains preview-only so it cannot bypass the release gate. +- This attestation is not automatic Railway verification. After merging main, + inspect API/worker deployments and health before dispatching Release. Keep + generation paused until compatible workers are confirmed. No production merge, + mobile publication or backend deployment is performed by this preparation. +- Local backup was created by the owner on their computer. Its index was checked; + nothing was uploaded/restored. Production SQL migrations were applied directly + by the owner and independently verified; they did not reload the backup. +- Validation: nine release-guard cases passed, including rejected stale/missing + revisions and non-main branches; shell syntax, all workflow YAML parsing and + production/preview wiring checks passed. No application code changed; retain + prior app test/export evidence. GitHub checks are verified at handoff. + + ### Version 1.9.0 production migration verification - Owner reported generation paused and manually applied migrations 0011–0015 diff --git a/mobile/DEPLOYMENT.md b/mobile/DEPLOYMENT.md index 6d3bbaf..2faa1c5 100644 --- a/mobile/DEPLOYMENT.md +++ b/mobile/DEPLOYMENT.md @@ -61,11 +61,11 @@ The `production` profile builds an AAB for Play Store submission; use ## OTA update (JS, UI, styling, assets — no reinstall) -```bash -cd mobile -npm run update:production # eas update --channel production -npm run update:preview # eas update --channel preview -``` +For production, follow [the release runbook](../RELEASING.md): merge the release, +verify the new backend and workers, then manually run **Release** on `main` with +the verified full commit SHA. Merging alone does not publish a mobile update. +Use **EAS Update (OTA)** for preview updates. Direct local production publishing +bypasses the deployment check and is not the normal release procedure. Installed apps fetch the update on next launch (`checkAutomatically: ON_LOAD`). @@ -80,12 +80,11 @@ OTA cannot ship native code. Instead: ## GitHub automation -Two workflows in [.github/workflows](../.github/workflows), both need the -`EXPO_TOKEN` repository secret (expo.dev → Account settings → Access tokens; -add at GitHub → Settings → Secrets and variables → Actions): +Workflows in [.github/workflows](../.github/workflows) use the `EXPO_TOKEN` +repository secret for EAS: -- **eas-update.yml** — every push to `main` touching `mobile/**` publishes an - OTA update to the **production** channel automatically. Manual dispatch lets - you pick `preview` instead. -- **eas-build.yml** — manual dispatch only (builds cost quota); choose the - profile. +- **release.yml** — manual production release after backend/worker verification, + followed by preview OTA, release tagging and native-gated APK dispatch. +- **eas-update.yml** — qualifying mobile pushes to `develop` and manual runs + publish preview only. +- **eas-build.yml** — manual build; choose the profile.