Deployment

Automatic deploys

GitHub Actions deploy via Serverless Framework:

Branch / trigger Workflow Stage
develop push .github/workflows/deploy-dev.js.yml dev
main push .github/workflows/deploy-prod.js.yml prod
repository_dispatch dep_update_dev deploy-dev dev
repository_dispatch dep_update_prod deploy-prod prod

Downstream repos (e.g. gameslib) can trigger backend redeploys after package publishes.

Each deploy runs two Serverless stacks (API first, then crons):

Stack Service name Deploy (CI)
API / WebSocket abstract-play bash bin/serverless-deploy.sh <stage> <profile>
Scheduled jobs abstract-play-backend-crons bash crons/scripts/serverless-deploy.sh <stage>

The former backend-crons repository is archived; deploy and develop only from this repo’s crons/ workspace.

Crons source lives in crons/. See Crons deployment.

AP dependency pins (ci-deps.*.json)

Canonical pins live in ci-deps.dev.json and ci-deps.prod.json. CI runs npm ci → manifest validation → ap-install-deps --stage dev|prod → strict lockfile check → build/test.

After merging develop into main (or another branch that changes dependency files), run npm run sync-deps:prod on main or npm run sync-deps on develop, then commit if anything changed. Do not hand-merge AP version strings in package.json.

npm run sync-deps runs ap-install-deps at the repo root, copies AP pins into crons/package.json from the root manifest, then prunes any stale crons/node_modules/@abstractplay tree. Deploy/Test workflows run scripts/sync-crons-ap-deps.mjs before npm ci so a stale crons/package.json (for example after a cross-branch merge with merge=ours) cannot break install. The gameslib relay auto-commit on dep_update_dev / dep_update_prod also stages crons/package.json with the root manifests. postinstall re-runs sync after install; npm run lint runs check-crons-ap-pins so committed pins cannot drift from root. Locally after a merge, prefer npm run ci:install (or ci:install:prod on main) instead of bare npm ci if install fails on workspace pin mismatch.

.gitattributes uses merge=ours for ci-deps.prod.json on main, ci-deps.dev.json on develop, and for package.json, package-lock.json, and crons/package.json on any cross-branch merge — the branch you merge into keeps its pins until you run sync-deps. Do not resolve AP version conflicts by picking lines from the other branch.

Prod deploys may fail at build when code on main uses a gameslib API not yet in the prod pin — wait for dep_update_prod or bump ci-deps.prod.json when releasing.

Lambda module-load checks (CI)

Deploy workflows run npm test after npm run build. pretest runs npm run build:lambda-bundles, which esbuilds every Lambda handler entry with the same options as serverless-esbuild (ESM .mjs, same external list). vitest run executes unit tests from test/**/*.test.ts. test/lambdaInit.test.mjs dynamically imports those bundles plus @abstractplay/gameslib — matching Lambda cold-start module loading.

If vitest fails with exports is not defined loading lib/*.js, delete stale local tsc sidecars (lib/*.js, utils/*.js) left over from before the ESM flip — they are gitignored and must not remain on disk beside the .ts sources.

Handlers are packaged with serverless-esbuild (ESM .mjs bundles). Heavy @abstractplay/* dependencies live in a shared Lambda layer built by npm run build:layers before serverless package / deploy.

CJS packages that use dynamic require() (e.g. web-push, @sunknudsen/totp, i18next) must stay in the esbuild external list so they load from node_modules at runtime — bundling them into ESM output causes init errors like Dynamic require of "crypto" is not supported.

Manual deploy

With AWS profiles configured:

npm run build
npx serverless deploy              # dev (default stage)
npx serverless --stage prod deploy # prod

Or use npm scripts: npm run deploy-dev, npm run deploy-prod, npm run full-dev, npm run full-prod (they invoke the pinned serverless devDependency).

Serverless v4 and secrets

Serverless Framework v4 resolves provider.environment when you run serverless package, print, or deploy. CI and local deploys use the pinned serverless@4.42.0 devDependency via npx serverless (no global Serverless install). The Dashboard org is abstractplay (SERVERLESS_ORG in CI; org: in serverless.yml).

Expected keys in ../apsecrets.yml: totp_key, vapid_private_key, vapid_public_key, openssh_private_key, announcements_discord_webhook_url (YAML snake_case; openssh_private_key is OpenSSH PEM text for bot webhook signing). For CI prod deploy, also set GitHub secret ANNOUNCEMENTS_DISCORD_WEBHOOK_URL (same webhook URL).

Stage configuration

Per-stage settings live in serverless.yml under custom.stageConfig:

Table name: abstract-play-${stage}.

gameProjector stream (automatic in CI)

The DynamoDB table must have streams enabled before CloudFormation can reference StreamArn. The stream Lambda trigger is gated by the enableGameProjectorStream deploy parameter.

CI (bin/serverless-deploy.sh) checks LatestStreamArn on the stage table before deploy:

So the first deploy to a new stage only enables streams. The next CI run on that stage attaches the mapping. No manual flags.

Local manual deploy (same two-step logic):

npm run build
bash bin/serverless-deploy.sh dev AbstractPlayDev
# After first run succeeds, any later run passes enableGameProjectorStream=true automatically.

Once a stage has a stream, always deploy with true (or use the script) so CloudFormation does not remove the event source mapping.

Ops alerts (email)

When OPS_ALERT_EMAIL is set at deploy time, CloudFormation creates an SNS topic (abstractplay-ops-alerts-${stage}) and wires gameProjector alarms to it:

Alarm Signal
abstractplay-game-projector-errors-${stage} Lambda Errors ≥ 1 in 1 minute (catches init crashes)
abstractplay-game-projector-dlq-${stage} DLQ depth ≥ 1 message
abstractplay-game-projector-iterator-age-${stage} Stream IteratorAge > 5 minutes for 10 minutes

First deploy: SNS sends a subscription confirmation email — you must click Confirm subscription once or alarms will not arrive.

Local / manual deploy:

export OPS_ALERT_EMAIL=you@example.com
bash bin/serverless-deploy.sh prod AbstractPlayProd

Omit OPS_ALERT_EMAIL to skip the topic and alarm actions (dev deploys by default).

Required GitHub secrets

Cognito setup (essentials)

Each stage needs a Cognito user pool with an app client for the front end:

  1. Create a user pool (defaults are fine).
  2. Add an app client — do not generate a client secret.
  3. Copy the pool ARN into serverless.yml (custom.stageConfig.{stage}.userpool) for the authQuery authorizer.
  4. App client settings: enable identity providers; set callback/sign-out URLs (http://localhost:3000 for local dev; https://play.dev.abstractplay.com / https://play.abstractplay.com for deployed front ends).
  5. OAuth: Authorization code grant, Implicit grant, openid scope; enable aws.cognito.signin.user.admin and Email.

Bot pools are separate per stage — see Bots.

Documentation deploys

When a push to develop or main includes changes under docs/ or crons/docs/, the deploy workflow dispatches dep_update_dev / dep_update_prod to the docs repository so the site rebuilds (after the docs repo vendors this monorepo and syncs /crons/ — see crons/docs/_docs-repo-integration.md).

Related