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
13 changes: 13 additions & 0 deletions .github/scripts/check-release-revision.sh
Original file line number Diff line number Diff line change
@@ -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
20 changes: 20 additions & 0 deletions .github/scripts/check-release-revision.test.cjs
Original file line number Diff line number Diff line change
@@ -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);
});
19 changes: 1 addition & 18 deletions .github/workflows/eas-update.yml
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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:
Expand All @@ -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
17 changes: 17 additions & 0 deletions .github/workflows/release-validation.yml
Original file line number Diff line number Diff line change
@@ -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
23 changes: 16 additions & 7 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -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
Expand Down
3 changes: 2 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
19 changes: 15 additions & 4 deletions RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand All @@ -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
Expand Down
6 changes: 4 additions & 2 deletions docs/CODEBASE_MAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
21 changes: 21 additions & 0 deletions docs/WORK_LOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
25 changes: 12 additions & 13 deletions mobile/DEPLOYMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`).

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