diff --git a/.github/workflows/forum.yml b/.github/workflows/forum.yml new file mode 100644 index 0000000..9c88d50 --- /dev/null +++ b/.github/workflows/forum.yml @@ -0,0 +1,26 @@ +# Tests every pull request that touches the forum app, so a red PR shows +# before merging. Deploys happen in Cloudflare Workers Builds (see web/DEPLOY.md). +name: Forum + +on: + pull_request: + paths: ["web/**", ".github/workflows/forum.yml"] + workflow_dispatch: + +defaults: + run: + working-directory: web + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + cache-dependency-path: web/package-lock.json + - run: npm ci + - run: npm run typecheck + - run: npm test diff --git a/AGENTS.md b/AGENTS.md index 10f593a..89082d5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -12,7 +12,7 @@ Replaces the Tender-hosted NodeBox forum with our own on Cloudflare. `docs/PLAN. - Branch policy: commit straight to `main` and push. No pull requests. - Verify: `npm test && npm run typecheck` in the root, and the same two commands in `web/`. -- Deploy: none yet. The Cloudflare resources do not exist; `web/README.md` lists what to create. +- Deploy: Cloudflare Workers Builds deploys `web/` on every push to `main` (tests, D1 migrations, then deploy). Setup and rollback: `web/DEPLOY.md`. - Secrets come from 1Password: `npm run env` writes `.env` from `.env.template`. ## Rules of the data diff --git a/web/.node-version b/web/.node-version new file mode 100644 index 0000000..2bd5a0a --- /dev/null +++ b/web/.node-version @@ -0,0 +1 @@ +22 diff --git a/web/DEPLOY.md b/web/DEPLOY.md new file mode 100644 index 0000000..64e0ed0 --- /dev/null +++ b/web/DEPLOY.md @@ -0,0 +1,57 @@ +# Deploying the forum + +Cloudflare Workers Builds deploys the forum on every push to `main`. In the +Worker's settings (Workers & Pages → nodebox-forum → Settings → Build): + +| Setting | Value | +| --- | --- | +| Git repository | `nodebox/forum`, production branch `main` | +| Root directory | `web` | +| Build command | `npm run ci:build` (typecheck, tests, client bundle) | +| Deploy command | `npm run ci:deploy` (D1 migrations, then `wrangler deploy`) | +| Non-production branch builds | Off. Preview versions would use the production D1 database and R2 bucket | + +A failing test stops the build, so nothing is deployed. Migrations run before +the new code is live, so every migration must keep the running version +working: add tables and columns, don't drop or rename them in the same +release. Node comes from `web/.node-version` (22). + +The build's API token needs **D1 → Edit** on top of the Workers permissions, +or the migration step fails with an authorisation error. If the token +Cloudflare creates for the build lacks it, edit that token (or pick one that +has it) under the build settings. + +GitHub Actions (`.github/workflows/forum.yml`) only runs the tests on pull +requests; it holds no Cloudflare credentials. + +## One-time setup + +| What | Where | +| --- | --- | +| D1 database | Created; its id is in `wrangler.jsonc` | +| R2 bucket | `npx wrangler r2 bucket create nodebox-forum` | +| Hostname | After the first deploy: add `forum.nodebox.net` as a custom domain (`routes` in `wrangler.jsonc`). Until then the forum runs on its workers.dev URL | +| Email | Onboard `nodebox.net` for Email Sending in the dashboard; sender is `MAIL_FROM` (`forum@nodebox.net`) | +| Images | Uses the account's existing variants `medium` (1000 px wide, in posts) and `thumb` (600 × 600, galleries) | +| Worker secrets | `npm run secrets` copies `CF_ACCOUNT_ID`, `CF_IMAGES_HASH`, `CF_IMAGES_TOKEN` (`op://NodeBox/NodeBox Forum/Cloudflare/…`) and `ANTHROPIC_API_KEY` (spam filter) from 1Password | +| Git connection | The build settings above. Connect it last: the first build deploys | + +## First deploy + +1. Merge this setup to `main` with the database id filled in. Nothing deploys + while the repository isn't connected in Cloudflare. +2. Connect the repository in the build settings. The first build creates the + schema in the empty D1 database and deploys. +3. Load the Tender data from the machine that has `data/` (it contains email + addresses; it never goes to GitHub): + `npx wrangler d1 execute nodebox-forum --remote --file ../data/import.sql` +4. Upload the old attachments to R2 and Images (script still to write). +5. Make the first admin: `npm run admin -- grant --remote`. +6. Check: a sign-in email arrives, an upload works, a screenshot shows its + resized variant, an old Tender URL redirects. + +## Rolling back + +- Code: `npx wrangler rollback` (or pick a version under Workers → Deployments). +- Data: D1 Time Travel, `npx wrangler d1 time-travel restore nodebox-forum --timestamp `. + Migrations only go forward; a bad one is fixed by a new migration or a restore. diff --git a/web/README.md b/web/README.md index 1ca7d03..57fabeb 100644 --- a/web/README.md +++ b/web/README.md @@ -65,17 +65,7 @@ in Node against local D1/R2 (wrangler's platform proxy): sign-in by email link, claiming an imported account, posting with an attachment, reactions, moderation, CSRF and old-URL redirects. -## Deploy (once the accounts exist) - -1. `npx wrangler d1 create nodebox-forum`, paste the id into `wrangler.jsonc`. -2. `npx wrangler r2 bucket create nodebox-forum`. -3. Onboard the sending domain for Cloudflare Email Sending; set `MAILER` to - `cloudflare` and `MAIL_FROM`, `PUBLIC_URL` in `wrangler.jsonc`. -4. In Cloudflare Images, create the variants `nodebox-forum-post` (1200 px wide, - scale down) and `nodebox-forum-thumb` (400 px). -5. Secrets: `wrangler secret put` for `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`, - `CF_ACCOUNT_ID`, `CF_IMAGES_TOKEN`, `CF_IMAGES_HASH`, and `ANTHROPIC_API_KEY` - for the spam filter: - `op read "op://NodeBox/NodeBox Forum/Anthropic/ANTHROPIC_API_KEY" | npx wrangler secret put ANTHROPIC_API_KEY`. - Without that key no post is checked. -6. `npx wrangler d1 migrations apply nodebox-forum --remote`, then `npm run deploy`. +## Deploy + +Pushing to `main` tests, migrates and deploys through GitHub Actions. Setup, +the first deploy and rollback are in [DEPLOY.md](DEPLOY.md). diff --git a/web/package.json b/web/package.json index 09ca697..73ed815 100644 --- a/web/package.json +++ b/web/package.json @@ -7,10 +7,14 @@ "scripts": { "env": "op inject -f -i .env.template -o .env && chmod 600 .env", "build:client": "node scripts/build-client.mjs", - "dev": "npm run build:client && wrangler dev", + "dev": "npm run build:client && wrangler dev --var MAILER:log --var PUBLIC_URL:http://localhost:8787", "db:migrate:local": "wrangler d1 migrations apply nodebox-forum --local", + "db:migrate:remote": "wrangler d1 migrations apply nodebox-forum --remote", "db:seed:local": "wrangler d1 execute nodebox-forum --local --file seed/dev.sql", "deploy": "npm run build:client && wrangler deploy", + "secrets": "sh scripts/put-secrets.sh", + "ci:build": "npm run typecheck && npm test && npm run build:client", + "ci:deploy": "wrangler d1 migrations apply nodebox-forum --remote && wrangler deploy", "types": "wrangler types", "typecheck": "tsc --noEmit && tsc --noEmit -p client", "test": "node --disable-warning=ExperimentalWarning --test test/*.test.ts", diff --git a/web/scripts/put-secrets.sh b/web/scripts/put-secrets.sh new file mode 100755 index 0000000..7fd56dc --- /dev/null +++ b/web/scripts/put-secrets.sh @@ -0,0 +1,11 @@ +#!/bin/sh +# Copies the forum's Cloudflare secrets from 1Password into the Worker. +# Needs the 1Password CLI (op) signed in. Values go from op straight into +# wrangler over a pipe; nothing is printed or written to disk. +set -eu +cd "$(dirname "$0")/.." +for name in CF_ACCOUNT_ID CF_IMAGES_HASH CF_IMAGES_TOKEN; do + op read "op://NodeBox/NodeBox Forum/Cloudflare/$name" | npx wrangler secret put "$name" +done +# The spam filter for new posts; without it no post is checked. +op read "op://NodeBox/NodeBox Forum/Anthropic/ANTHROPIC_API_KEY" | npx wrangler secret put ANTHROPIC_API_KEY diff --git a/web/wrangler.jsonc b/web/wrangler.jsonc index 3f17550..aebb5d6 100644 --- a/web/wrangler.jsonc +++ b/web/wrangler.jsonc @@ -5,9 +5,11 @@ "compatibility_date": "2026-09-01", "compatibility_flags": ["nodejs_compat"], "assets": { "directory": "public", "binding": "ASSETS" }, + // Runs on the workers.dev URL until the forum.nodebox.net custom domain is added (after the first deploy). + "workers_dev": true, + "observability": { "enabled": true }, "d1_databases": [ - // Create with `wrangler d1 create nodebox-forum` and paste the id here. - { "binding": "DB", "database_name": "nodebox-forum", "database_id": "00000000-0000-0000-0000-000000000000", "migrations_dir": "migrations" } + { "binding": "DB", "database_name": "nodebox-forum", "database_id": "c66a199f-225d-46eb-baff-0f16b4696a41", "migrations_dir": "migrations" } ], // Cloudflare Email Sending, for sign-in links. The sending domain must be onboarded in the dashboard. "send_email": [{ "name": "EMAIL" }], @@ -20,17 +22,19 @@ "vars": { "SITE_NAME": "NodeBox Forum", // Absolute origin, used in sign-in emails and the Google redirect URI. - "PUBLIC_URL": "http://localhost:8787", - // Cloudflare Images: ids and variant names are global to the account, so everything is prefixed. + // These are the production values; `npm run dev` overrides PUBLIC_URL and MAILER. + "PUBLIC_URL": "https://forum.nodebox.net", + // Cloudflare Images: image ids are global to the account, so they are prefixed. The variants are the account's shared ones. "CF_IMAGES_PREFIX": "nodebox-forum-", - "CF_IMAGES_VARIANT_POST": "nodebox-forum-post", - "CF_IMAGES_VARIANT_THUMB": "nodebox-forum-thumb", - // "log" prints sign-in links to the console (development); "cloudflare" sends real mail. - "MAILER": "log", + "CF_IMAGES_VARIANT_POST": "medium", + "CF_IMAGES_VARIANT_THUMB": "thumb", + // "cloudflare" sends real mail; "log" prints sign-in links to the console (development). + "MAILER": "cloudflare", "MAIL_FROM": "forum@nodebox.net" } // Secrets (wrangler secret put, or .dev.vars locally): // GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET // CF_ACCOUNT_ID, CF_IMAGES_TOKEN, CF_IMAGES_HASH (the account hash in imagedelivery.net URLs) // ANTHROPIC_API_KEY (spam filter on new posts; without it nothing is checked) + // Deploys run in Cloudflare Workers Builds on every push to main (web/DEPLOY.md). }