test(mail): E2E invite-ingest harness through real PostalMime + persi… #19
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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 }} |