Skip to content

Commit 2642f89

Browse files
authored
Merge pull request #46 from MaxLinCode/codex/db-rollout-hardening
Harden DB rollout workflow
2 parents acfae7c + a6b9ab1 commit 2642f89

8 files changed

Lines changed: 102 additions & 38 deletions

File tree

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
name: Apply DB Migrations
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
workflow_dispatch:
8+
9+
concurrency:
10+
group: apply-db-migrations-main
11+
cancel-in-progress: false
12+
13+
jobs:
14+
apply-db-migrations:
15+
runs-on: ubuntu-latest
16+
timeout-minutes: 10
17+
permissions:
18+
contents: read
19+
env:
20+
DATABASE_URL: ${{ secrets.DATABASE_URL_PRODUCTION }}
21+
steps:
22+
- name: Check required secret
23+
run: test -n "$DATABASE_URL"
24+
25+
- name: Check out repository
26+
uses: actions/checkout@v4
27+
28+
- name: Set up pnpm
29+
uses: pnpm/action-setup@v4
30+
with:
31+
version: 9.15.4
32+
33+
- name: Set up Node.js
34+
uses: actions/setup-node@v4
35+
with:
36+
node-version: 22
37+
cache: pnpm
38+
39+
- name: Install dependencies
40+
run: pnpm install --frozen-lockfile
41+
42+
- name: Check migrations
43+
run: pnpm --filter @atlas/db db:check
44+
45+
- name: Apply migrations
46+
run: pnpm --filter @atlas/db db:migrate

README.md

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,19 @@ Atlas is a chat-first brain-dump scheduler MVP. A user sends freeform text, the
2525
- `pnpm typecheck`: run TypeScript checks across the repo
2626
- `pnpm test`: run the test suite
2727
- `pnpm telegram:webhook:set`: register the Telegram webhook and print Telegram's current webhook info
28-
- `pnpm db:generate`: generate a new Drizzle migration from schema changes
29-
- `pnpm db:migrate`: apply existing Drizzle migrations to the configured database
30-
- `pnpm db:studio`: open the Drizzle Studio workflow
3128
- `pnpm db:test:start`: start the local Homebrew Postgres test service and create `atlas_test`
3229
- `pnpm db:test:reset`: reset the local `atlas_test` database schema for integration reruns
3330
- `pnpm db:test:stop`: stop the local Homebrew Postgres test service
3431

32+
Database schema commands are owned by `packages/db`:
33+
34+
- `cd packages/db && DATABASE_URL=... pnpm db:generate`
35+
- `cd packages/db && DATABASE_URL=... pnpm db:check`
36+
- `cd packages/db && DATABASE_URL=... pnpm db:migrate`
37+
- `pnpm --filter @atlas/db db:generate` with `DATABASE_URL` already set
38+
- `pnpm --filter @atlas/db db:check` with `DATABASE_URL` already set
39+
- `pnpm --filter @atlas/db db:migrate` with `DATABASE_URL` already set
40+
3541
## Prompt Improvement Loop
3642

3743
Atlas treats prompt changes as product behavior changes. Use the live eval harness to iterate on prompts deliberately instead of editing blind.
@@ -79,7 +85,7 @@ Notes:
7985
## Environment
8086

8187
Copy `.env.example` into `apps/web/.env.local` for local development and provide real values through your deployment environment for hosted runs.
82-
For local-only test credentials, prefer `apps/web/.env.test.local`, which is gitignored and loaded by the Next app, Drizzle config, and integration test runner.
88+
For local-only test credentials, prefer `apps/web/.env.test.local`, which is gitignored and loaded by the Next app and integration test runner.
8389

8490
- `DATABASE_URL`: Postgres connection string
8591
- `APP_BASE_URL`: canonical deployed app origin used when generating Google Calendar connect links
@@ -98,6 +104,8 @@ To register the production Telegram webhook once those values are set, also expo
98104

99105
For hosted rollout steps, use [`docs/workflows/production-deploy-checklist.md`](docs/workflows/production-deploy-checklist.md) as the main production deploy runbook and [`docs/workflows/vercel-telegram-webhook.md`](docs/workflows/vercel-telegram-webhook.md) for the narrower webhook-only setup flow.
100106

107+
Production schema changes are not applied from local laptops. Merge to `main`, let the GitHub Actions migration workflow apply `packages/db/drizzle/` against the production database, and only release after that workflow succeeds.
108+
101109
Atlas no longer exposes public planner/debug mutation routes. The intended externally reachable surfaces are:
102110

103111
- `POST /api/telegram/webhook`

docs/current-work.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
Atlas is a schedule-forward, Google-calendar-gated product with a working mutation pipeline.
66
Current implementation focus: replace transcript-heavy conversation memory with an explicit conversation-state layer. Atlas now persists a user-scoped conversation snapshot with transcript, summary, entity registry, and discourse state so reference resolution can anchor on known objects instead of reconstructing intent from recent turns alone. Recent work still includes the prompt-asset cleanup, expanded live eval coverage around ambiguous routing and confirmed-mutation recovery, a consolidated `pnpm eval:all` loop plus suite-specific eval reports and prompt-improvement briefs for prompt iteration, chat-first prompt/docs framing, and consistent `referenceTime` threading through scheduling.
77

8+
DB rollout hardening is now part of the active release path: `packages/db` owns all Drizzle commands and config, root-level DB command aliases are removed, and production migrations are expected to run from GitHub Actions before Vercel production release.
9+
810
## Near-term milestones
911

1012
- Finish the prompt-asset cleanup and eval loop:

docs/workflows/production-deploy-checklist.md

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,16 @@ Set these in the Vercel project for the target environment before deploying:
4343
- `GOOGLE_LINK_TOKEN_SECRET`, `GOOGLE_CALENDAR_TOKEN_ENCRYPTION_KEY`, and `CRON_SECRET` should be unique per environment.
4444
- `GOOGLE_CALENDAR_TOKEN_ENCRYPTION_KEY` must be a base64-encoded 32-byte key.
4545
- `TELEGRAM_ALLOWED_USER_IDS` is required in all environments. The app should not boot without it.
46+
- Vercel Preview and Vercel Production must use different `DATABASE_URL` values. Preview must not point at the production database.
47+
48+
## GitHub Actions migration secret
49+
50+
Set the production database connection string in GitHub Actions secrets as `DATABASE_URL_PRODUCTION`.
51+
52+
- This secret is for the hosted migration workflow, not for Vercel runtime configuration.
53+
- The migration workflow maps `DATABASE_URL_PRODUCTION` to `DATABASE_URL` only inside the job that runs `pnpm --filter @atlas/db db:check` and `pnpm --filter @atlas/db db:migrate`.
54+
- Keep the production secret scoped to the repo or environment that owns production releases.
55+
- In GitHub branch protection or rulesets, mark `Apply DB Migrations / apply-db-migrations` as a required status check for the production release path.
4656

4757
## Pre-deploy checks
4858

@@ -55,18 +65,25 @@ Set these in the Vercel project for the target environment before deploying:
5565

5666
1. Review the pending migration files under `packages/db/drizzle/`.
5767
2. Confirm the migration set matches the code being deployed.
58-
3. Apply migrations to the target database before or alongside the app deploy, but do not leave the database halfway between expected schema versions.
59-
4. If the environment has known partial-state risk, verify the release migration is safe to rerun or has an explicit recovery plan.
60-
61-
Atlas does not yet have a fully documented hosted migration runner. For now, production releases must include an explicit operator-applied migration step.
68+
3. Confirm the database commands are run only through `packages/db`:
69+
- `cd packages/db && DATABASE_URL=... pnpm db:generate`
70+
- `cd packages/db && DATABASE_URL=... pnpm db:check`
71+
- `cd packages/db && DATABASE_URL=... pnpm db:migrate`
72+
- or the equivalent `pnpm --filter @atlas/db ...` form with `DATABASE_URL` already set
73+
4. Merge the release to `main` and let [`.github/workflows/apply-db-migrations.yml`](/Users/maxlin/Code/Atlas/.github/workflows/apply-db-migrations.yml) run against `DATABASE_URL_PRODUCTION`.
74+
5. Do not apply production migrations from a local shell. The GitHub Actions migration workflow is the only supported production migration runner.
75+
6. Confirm the migration workflow succeeds before starting or approving the production Vercel release.
76+
7. If the environment has known partial-state risk, verify the release migration is safe to rerun or has an explicit recovery plan.
6277

6378
## Deploy
6479

6580
1. Push the release branch and confirm the intended commit SHA.
66-
2. Verify the Vercel project uses `apps/web` as the Root Directory and can access workspace files outside that directory.
67-
3. Confirm the target Vercel environment has all required env vars.
68-
4. Deploy the intended commit to Vercel.
69-
5. Confirm the deployment is healthy before switching any production traffic assumptions to it.
81+
2. Merge the intended commit to `main`.
82+
3. Verify [`.github/workflows/apply-db-migrations.yml`](/Users/maxlin/Code/Atlas/.github/workflows/apply-db-migrations.yml) passed on that `main` commit before promoting any production deploy.
83+
4. Verify the Vercel project uses `apps/web` as the Root Directory and can access workspace files outside that directory.
84+
5. Confirm the target Vercel environment has all required env vars, including separate Preview and Production `DATABASE_URL` values.
85+
6. Deploy the intended `main` commit to Vercel Production.
86+
7. Confirm the deployment is healthy before switching any production traffic assumptions to it.
7087

7188
## Register or verify the Telegram webhook
7289

@@ -122,11 +139,13 @@ curl "https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/getWebhookInfo"
122139
1. Confirm removed public planner/debug routes are still unavailable.
123140
2. Confirm linked-account reads do not expose raw Google access or refresh tokens.
124141
3. Confirm Vercel logs do not expose webhook secrets, cron secrets, link tokens, or decrypted Google credentials.
142+
4. Confirm the production deploy commit has a successful `apply-db-migrations` GitHub Actions run attached to it.
125143

126144
## Rollback notes
127145

128146
- If the app deploy is bad but the schema is still compatible, roll traffic back to the last known good deployment.
129147
- If a migration is not backward-compatible, do not assume app rollback alone is sufficient.
148+
- If the migration workflow fails, do not continue with production deploy until the workflow is fixed or the migration issue is explicitly recovered.
130149
- If Google OAuth is failing after deploy, first verify `APP_BASE_URL`, `GOOGLE_OAUTH_REDIRECT_URI`, and the Google Cloud OAuth client configuration all match exactly.
131150
- If Telegram delivery fails after deploy, verify webhook registration, secret-token alignment, and Vercel route health before changing app code.
132151

docs/workflows/vercel-telegram-webhook.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ Set these in the Vercel project for the target environment before deploying:
2727
- `GOOGLE_CALENDAR_TOKEN_ENCRYPTION_KEY`
2828
- `CRON_SECRET`
2929

30+
Vercel Preview and Vercel Production must use different `DATABASE_URL` values. Preview must not target the production database.
31+
3032
## Route expectations
3133

3234
- The Telegram webhook endpoint is `POST /api/telegram/webhook`.
@@ -39,7 +41,7 @@ Set these in the Vercel project for the target environment before deploying:
3941
2. Set the framework to Next.js.
4042
3. Set the Root Directory to `apps/web`.
4143
4. Enable the Vercel setting that allows the build to access workspace files outside the Root Directory if Vercel does not detect it automatically.
42-
5. Add the required environment variables in Vercel for Preview or Production.
44+
5. Add the required environment variables in Vercel for Preview or Production, including separate Preview and Production `DATABASE_URL` values.
4345
6. Deploy the latest `main` commit.
4446

4547
## Register the Telegram webhook
@@ -73,7 +75,7 @@ curl "https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/getWebhookInfo"
7375
## Notes
7476

7577
- Vercel only hosts the protected cron endpoints. Scheduled follow-up dispatch and Google Calendar reconciliation now come from GitHub Actions via [`.github/workflows/send-followups.yml`](/Users/maxlin/Code/Atlas/.github/workflows/send-followups.yml) and [`.github/workflows/reconcile-google-calendar.yml`](/Users/maxlin/Code/Atlas/.github/workflows/reconcile-google-calendar.yml), not from Vercel Cron Jobs.
78+
- Production database migrations are owned by `packages/db` and are applied by [`.github/workflows/apply-db-migrations.yml`](/Users/maxlin/Code/Atlas/.github/workflows/apply-db-migrations.yml), not by ad hoc local commands or Vercel deploy hooks.
7679
- Do not log the webhook secret in Vercel function logs or error output.
7780
- Keep the route handler thin and continue moving persistence and planning behavior into workspace packages.
7881
- After the first Vercel smoke test, the next backend milestone is replacing the `processInboxItem` stub with planner-owned persistence that reads canonical `inbox_items`, creates validated `tasks`, and records `planner_runs` as operational audit state.
79-
- End-to-end deployability is not fully hardened yet. Follow-up work should add a hosted migration-apply workflow, rename or replace any misleading generate-only migration commands, and document a repeatable production deploy checklist that covers schema application, deployment, webhook registration, and post-deploy verification.

package.json

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,6 @@
1515
"typecheck": "pnpm -r typecheck",
1616
"test": "pnpm -r test",
1717
"telegram:webhook:set": "bash scripts/set-telegram-webhook.sh",
18-
"db:generate": "pnpm --filter @atlas/db db:generate",
19-
"db:migrate": "pnpm --filter @atlas/db db:migrate",
20-
"db:studio": "pnpm --filter @atlas/db db:studio",
2118
"db:test:start": "bash scripts/db-test-start.sh",
2219
"db:test:stop": "bash scripts/db-test-stop.sh",
2320
"db:test:reset": "bash scripts/db-test-reset.sh"

packages/db/drizzle.config.ts

Lines changed: 7 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,17 @@
1-
import { existsSync } from "node:fs";
2-
import path from "node:path";
3-
import { loadEnvFile } from "node:process";
4-
import { fileURLToPath } from "node:url";
5-
61
import { defineConfig } from "drizzle-kit";
2+
const databaseUrl = process.env.DATABASE_URL;
73

8-
const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
9-
10-
loadLocalEnv(path.join(rootDir, "apps/web/.env.local"));
11-
loadLocalEnv(path.join(rootDir, "apps/web/.env.test.local"));
12-
loadLocalEnv(path.join(rootDir, ".env.local"));
13-
loadLocalEnv(path.join(rootDir, ".env.test.local"));
4+
if (!databaseUrl) {
5+
throw new Error(
6+
"packages/db/drizzle.config.ts requires DATABASE_URL in the environment. Run Drizzle commands from packages/db or via `pnpm --filter @atlas/db ...` with DATABASE_URL already set."
7+
);
8+
}
149

1510
export default defineConfig({
1611
schema: "./src/schema.ts",
1712
out: "./drizzle",
1813
dialect: "postgresql",
1914
dbCredentials: {
20-
url: process.env.DATABASE_URL ?? "postgresql://postgres@localhost:5432/atlas_test"
15+
url: databaseUrl
2116
}
2217
});
23-
24-
function loadLocalEnv(filePath: string) {
25-
if (existsSync(filePath)) {
26-
loadEnvFile(filePath);
27-
}
28-
}

packages/db/package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@
1111
"lint": "tsc --noEmit -p tsconfig.json",
1212
"typecheck": "tsc --noEmit -p tsconfig.json",
1313
"test": "vitest run",
14-
"db:generate": "drizzle-kit generate",
15-
"db:migrate": "drizzle-kit migrate",
16-
"db:studio": "drizzle-kit studio"
14+
"db:generate": "drizzle-kit generate --config=./drizzle.config.ts",
15+
"db:check": "drizzle-kit check --config=./drizzle.config.ts",
16+
"db:migrate": "drizzle-kit migrate --config=./drizzle.config.ts",
17+
"db:studio": "drizzle-kit studio --config=./drizzle.config.ts"
1718
},
1819
"dependencies": {
1920
"@atlas/core": "workspace:*",

0 commit comments

Comments
 (0)