Skip to content

test(mail): E2E invite-ingest harness through real PostalMime + persi… #19

test(mail): E2E invite-ingest harness through real PostalMime + persi…

test(mail): E2E invite-ingest harness through real PostalMime + persi… #19

Workflow file for this run

# Production deploy via the Alchemy stack (infra/alchemy.run.ts).
# Deploys --stage prod → suffixed names (doota-prod, …). The stack NEVER
# touches bare names (doota, doota-mail-inbound, …) — those stay manually
# managed. Stage "production" is retired (see alchemy.run.ts header).
#
# Fires ONLY on a push to main — which is also what a merged PR is. No PR
# previews here; run a stage deploy locally for that (`pnpm infra:deploy`).
# workflow_dispatch exists for the one-time adoption run (see below) and
# manual re-deploys.
#
# Required repository secrets:
# CLOUDFLARE_API_TOKEN — scoped token. Account: Workers Scripts, D1,
# Workers KV Storage, Workers R2 Storage, Queues,
# Secrets Store (all Edit) + Account Settings Read.
# Zone (per ORIGIN/ORIGINS zone): Workers Routes
# Edit, DNS Edit, Zone Read.
# CLOUDFLARE_ACCOUNT_ID — the target account id
# Secrets (MAIL_DEK, MAIL_SEARCH_KEY, BETTER_AUTH_SECRET, VAPID_*) can stay
# unset: the stack mints them into Alchemy state on the first deploy and
# every later run reuses the same values. Provide them as repository secrets
# only to override.
name: Deploy
on:
push:
branches: [main]
workflow_dispatch:
inputs:
adopt:
description: "Adopt pre-existing (wrangler-created) resources — needed once when pointing the stack at an existing deployment"
type: boolean
default: false
concurrency:
group: deploy-prod
cancel-in-progress: false
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
contents: read
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
# Worker secrets/vars — unset repository secrets arrive as "" and the
# stack treats empty as absent (state-minted or feature-off).
# Required
SETUP_TOKEN: ${{ secrets.SETUP_TOKEN }}
CRON_SECRET: ${{ secrets.CRON_SECRET }}
APP_CLOUDFLARE_ACCOUNT_ID: ${{ secrets.APP_CLOUDFLARE_ACCOUNT_ID }}
# We need a different API token because of the requirements are different
# to deploy a worker and to access the dashboard information from cloudflare.
APP_CLOUDFLARE_API_TOKEN: ${{ secrets.APP_CLOUDFLARE_API_TOKEN }}
# Comma-separated full origins WITH protocol; first entry is the
# canonical app URL, every entry is a custom domain + allowed host.
ORIGINS: ${{ vars.ORIGINS }}
UNSUBSCRIBE_URL: ${{ vars.UNSUBSCRIBE_URL }}
LOG_LEVEL: ${{ vars.LOG_LEVEL }}
# Optional
MAIL_DEK: ${{ secrets.MAIL_DEK }}
MAIL_SEARCH_KEY: ${{ secrets.MAIL_SEARCH_KEY }}
BETTER_AUTH_SECRET: ${{ secrets.BETTER_AUTH_SECRET }}
VAPID_PUBLIC_KEY: ${{ secrets.VAPID_PUBLIC_KEY }}
VAPID_PRIVATE_KEY: ${{ secrets.VAPID_PRIVATE_KEY }}
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 10
- uses: actions/setup-node@v4
with:
# >= 22.18 so alchemy's TypeScript entry loads natively (and the
# engine-strict install pins are satisfied).
node-version: 24
cache: pnpm
# infra/ is a standalone package (own lockfile, outside the workspace) so
# alchemy's dependency tree can't perturb the app's resolution — install both.
- run: pnpm install --frozen-lockfile
- run: pnpm -C infra install --frozen-lockfile
# SvelteKit build — the stack deploys .svelte-kit/cloudflare byte-for-byte.
- run: pnpm run build
- name: Deploy (stage prod)
working-directory: infra
run: >
pnpm alchemy deploy --stage prod --yes
${{ inputs.adopt && '--adopt' || '' }}
# env:
# CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
# CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
# # Worker secrets/vars — unset repository secrets arrive as "" and the
# # stack treats empty as absent (state-minted or feature-off).
# # Required
# SETUP_TOKEN: ${{ secrets.SETUP_TOKEN }}
# CRON_SECRET: ${{ secrets.CRON_SECRET }}
# APP_CLOUDFLARE_ACCOUNT_ID: ${{ secrets.APP_CLOUDFLARE_ACCOUNT_ID }}
# # We need a different API token because of the requirements are different
# # to deploy a worker and to access the dashboard information from cloudflare.
# APP_CLOUDFLARE_API_TOKEN: ${{ secrets.APP_CLOUDFLARE_API_TOKEN }}
# # Comma-separated full origins WITH protocol; first entry is the
# # canonical app URL, every entry is a custom domain + allowed host.
# ORIGINS: ${{ vars.ORIGINS }}
# UNSUBSCRIBE_URL: ${{ vars.UNSUBSCRIBE_URL }}
# LOG_LEVEL: ${{ vars.LOG_LEVEL }}
# # Optional
# MAIL_DEK: ${{ secrets.MAIL_DEK }}
# MAIL_SEARCH_KEY: ${{ secrets.MAIL_SEARCH_KEY }}
# BETTER_AUTH_SECRET: ${{ secrets.BETTER_AUTH_SECRET }}
# VAPID_PUBLIC_KEY: ${{ secrets.VAPID_PUBLIC_KEY }}
# VAPID_PRIVATE_KEY: ${{ secrets.VAPID_PRIVATE_KEY }}