Deployment
Everything is branch-driven. There is no deploy button and no manual promotion: what is on master is production, what is on dev is QA, and a feature branch is checked and nothing else.
Branch to environment
Rendering diagram…
The two environments are genuinely separate down to the database: QA has its own D1 instance, its own Google OAuth client and its own JWT secret. A bad migration on dev cannot reach a real player's league.
→ Deploy Strategy · Dev Branch Deployment
What runs where
Rendering diagram…
The order a deploy happens in
Migrations first, then the Worker, then the frontend. The order is not negotiable: a Worker deployed against a schema it expects but D1 has not received yet is a production error for as long as the gap lasts.
Rendering diagram…
The Worker bundles model/ and dto/ from the repository root, so the deploy installs the root dependencies before running Wrangler, esbuild resolves those imports from the root node_modules, not the backend's.
The target that is not deployed here
The backend runs on either of two stores, and this page describes only one of them. No Cloudflare deployment runs on MongoDB: production and preview are D1, and the MongoDB target runs locally, against a replica set.
Keeping it that way took more than intent. composition.ts names both targets, so the driver is reachable from the Worker's entry point, and making the import dynamic does not help, esbuild follows dynamic imports and inlines them. The dry run measured it: 1.5MB of driver shipped with a Worker that can never use it, and the build failed without nodejs_compat, because the driver imports net, tls and child_process.
So wrangler.jsonc aliases the driver to a stub, which brings the production bundle back to within 8 KiB gzipped of its pre-MongoDB size and leaves the D1 deployments configured exactly as they were. The alias is a top-level key with no per-environment form, which is why the MongoDB run has a wrangler config of its own rather than a fifth environment, and a configuration that can never be deployed does not belong in the file that deploys.
./gradlew devMongo # the frontend, and the Worker on MongoDBThat config also names a different entry module, which is what gives the MongoDB run username/password sign-in and the deployed Worker none of it. → Persistence Targets · Auth Modes
The nightly scoring run
Scoring is the one scheduled job that lives outside Cloudflare, and it runs the published container image rather than the sources.
- It targets production only. QA is not scored.
- It runs the image
publish-images.ymlpushed to GHCR formaster, so the night scores exactly the artefact that was published, no checkout, no JDK. - Concurrency is keyed on the date, so two runs never score the same day at once. Ingest is idempotent anyway, but overlapping runs waste Wikimedia budget.
- A repository variable,
SCORING_RUNNER=external, hands the nightly over to something else without a code change. A manual dispatch always runs regardless , clicking Run is an explicit instruction, and it is how a missed day gets backfilled mid-handover. - An unrecognised value for that variable runs the job anyway, deliberately: scoring twice costs Wikimedia budget, scoring never costs a missing day, so a typo fails toward the cheap mistake.
Secrets, and who holds them
| Secret | Held by | What it opens |
|---|---|---|
CLOUDFLARE_API_TOKEN · CLOUDFLARE_ACCOUNT_ID | The deploy job | Wrangler and Pages |
GOOGLE_CLIENT_ID · GOOGLE_CLIENT_SECRET | The Worker, per environment | Sign-in |
JWT_SECRET | The Worker, per environment | Session signing |
SCORING_INGEST_SECRET | The Worker and the nightly job | /internal/* |
GH_APP_PRIVATE_KEY | The Worker | Filing problem reports as the bot |
CLOUDFLARE_D1_MIGRATION_RUNNER_SECRET | The migration job | Replaying migrations |
GITHUB_TOKEN | Actions, automatically | GHCR, and publishing this site |
The GitHub App key is optional on purpose: a deploy still succeeds before it is set, and the report form answers 502 until it is. A feature that is not configured yet should degrade, not block a release.
The collector holds exactly two credentials, the ingest secret and a Wikimedia user agent, and no database access at all. It is the least privileged thing in the system despite being the one that runs unattended.
Running the whole stack locally
./gradlew noGenie brings up the frontend, the Worker and a local D1 with no credentials to obtain: the compose file wires the dev sign-in route, which mints a normal session and refuses to exist outside the local environment. Three sibling tasks, up, demo, demoNoGenie, add the Article Genie, the seeded demo league, or neither.
→ Running FantasyWiki in Docker · Local Development Setup
Related
- Continuous delivery: the workflows that drive all of this
- Architecture overview
- Setup QA Deploy: the one-time setup
