Cloudflare terminal delivery #7
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
| name: Cloudflare terminal delivery | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| confirm_account: | |
| description: Exact approved general-purpose test Cloudflare account ID | |
| type: string | |
| required: true | |
| phase: | |
| description: Retained proof phase | |
| type: choice | |
| required: true | |
| options: | |
| - deploy | |
| - verify-and-cleanup | |
| deployment_name: | |
| description: Stable isolated deployment name used by both phases | |
| type: string | |
| required: true | |
| default: terminal-delivery | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: hayasend-cloudflare-terminal-delivery | |
| cancel-in-progress: false | |
| jobs: | |
| retained-terminal-proof: | |
| name: ${{ inputs.phase }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| environment: cloudflare-integration | |
| env: | |
| CLOUDFLARE_TEST_ACCOUNT_ID: ${{ vars.CLOUDFLARE_TEST_ACCOUNT_ID }} | |
| CLOUDFLARE_TEST_ACCOUNT_KIND: ${{ vars.CLOUDFLARE_TEST_ACCOUNT_KIND }} | |
| CLOUDFLARE_TEST_WORKERS_SUBDOMAIN: ${{ vars.CLOUDFLARE_TEST_WORKERS_SUBDOMAIN }} | |
| CLOUDFLARE_TEST_EMAIL_DOMAIN: ${{ vars.CLOUDFLARE_TEST_EMAIL_DOMAIN }} | |
| CLOUDFLARE_TEST_FROM: ${{ vars.CLOUDFLARE_TEST_FROM }} | |
| CLOUDFLARE_TEST_TO: ${{ vars.CLOUDFLARE_TEST_TO }} | |
| CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} | |
| HAYASEND_CLOUDFLARE_API_KEY: ${{ secrets.HAYASEND_CLOUDFLARE_API_KEY }} | |
| DEPLOYMENT_NAME: ${{ inputs.deployment_name }} | |
| DEPLOYMENT_ID: terminal-${{ inputs.deployment_name }} | |
| WRANGLER_VERSION: 4.118.0 | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - name: Validate retained-proof boundary | |
| id: guard | |
| shell: bash | |
| env: | |
| CONFIRM_ACCOUNT: ${{ inputs.confirm_account }} | |
| run: | | |
| set -euo pipefail | |
| test "$GITHUB_REF" = "refs/heads/main" | |
| test "${GITHUB_REF_PROTECTED:-false}" = "true" | |
| test "$CLOUDFLARE_TEST_ACCOUNT_KIND" = "general-purpose-test" | |
| test "$CONFIRM_ACCOUNT" = "$CLOUDFLARE_TEST_ACCOUNT_ID" | |
| test "$CLOUDFLARE_TEST_ACCOUNT_ID" != "" | |
| test "$CLOUDFLARE_TEST_WORKERS_SUBDOMAIN" != "" | |
| test "$CLOUDFLARE_TEST_EMAIL_DOMAIN" != "" | |
| test "$CLOUDFLARE_TEST_FROM" != "" | |
| test "$CLOUDFLARE_TEST_TO" != "" | |
| test "$CLOUDFLARE_API_TOKEN" != "" | |
| test "$HAYASEND_CLOUDFLARE_API_KEY" != "" | |
| if [[ ! "$CLOUDFLARE_TEST_ACCOUNT_ID" =~ ^[a-f0-9]{32}$ ]]; then | |
| echo "::error::CLOUDFLARE_TEST_ACCOUNT_ID is invalid." | |
| exit 1 | |
| fi | |
| if [[ ! "$DEPLOYMENT_NAME" =~ ^[a-z0-9]([a-z0-9-]{0,28}[a-z0-9])?$ ]]; then | |
| echo "::error::deployment_name must be 1-30 lowercase letters, digits, or internal hyphens." | |
| exit 1 | |
| fi | |
| echo "validated=true" >> "$GITHUB_OUTPUT" | |
| - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | |
| with: | |
| node-version: 26.5.0 | |
| cache: npm | |
| - name: Install current pinned toolchain | |
| run: | | |
| npm install --global --ignore-scripts npm@12.0.2 | |
| npm ci | |
| { | |
| node --version | |
| npm --version | |
| npx --yes "wrangler@$WRANGLER_VERSION" --version | |
| } | tee "$RUNNER_TEMP/cloudflare-terminal-tool-versions.txt" | |
| - name: Verify retained namespace is unused | |
| if: inputs.phase == 'deploy' | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| prefix="hayasend-$DEPLOYMENT_NAME" | |
| d1_json="$(npx --yes "wrangler@$WRANGLER_VERSION" d1 list --json)" | |
| # shellcheck disable=SC2016 | |
| if node -e 'const a=JSON.parse(process.argv[1]);const p=process.argv[2];process.exit(a.some(x=>x.name===`${p}-d1`)?0:1)' "$d1_json" "$prefix"; then | |
| echo "::error::The retained D1 name already exists." | |
| exit 1 | |
| fi | |
| if npx --yes "wrangler@$WRANGLER_VERSION" r2 bucket list | grep -F "$prefix-payloads"; then | |
| echo "::error::The retained R2 name already exists." | |
| exit 1 | |
| fi | |
| if npx --yes "wrangler@$WRANGLER_VERSION" queues list | grep -F "$prefix-"; then | |
| echo "::error::A retained Queue name already exists." | |
| exit 1 | |
| fi | |
| set +e | |
| worker_check="$( | |
| npx --yes "wrangler@$WRANGLER_VERSION" versions list \ | |
| --name "$prefix" \ | |
| --json 2>&1 | |
| )" | |
| worker_status="$?" | |
| set -e | |
| if [[ "$worker_status" -eq 0 ]] || ! grep -Fq "code: 10007" <<< "$worker_check"; then | |
| echo "::error::Worker absence could not be verified." | |
| exit 1 | |
| fi | |
| - name: Deploy retained terminal-proof namespace | |
| if: inputs.phase == 'deploy' | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| npm run --silent cli -- doctor cloudflare-sending-domain \ | |
| --account "$CLOUDFLARE_TEST_ACCOUNT_ID" \ | |
| --email-domain "$CLOUDFLARE_TEST_EMAIL_DOMAIN" \ | |
| | tee "$RUNNER_TEMP/cloudflare-terminal-sending-domain.json" | |
| npm run --silent cli -- doctor cloudflare-delivery-recipient \ | |
| --account "$CLOUDFLARE_TEST_ACCOUNT_ID" \ | |
| --recipient "$CLOUDFLARE_TEST_TO" \ | |
| | tee "$RUNNER_TEMP/cloudflare-terminal-recipient.json" | |
| npm run --silent cli -- deploy cloudflare \ | |
| --account "$CLOUDFLARE_TEST_ACCOUNT_ID" \ | |
| --name "$DEPLOYMENT_NAME" \ | |
| --email-domain "$CLOUDFLARE_TEST_EMAIL_DOMAIN" \ | |
| --deployment-id "$DEPLOYMENT_ID" \ | |
| --allowed-recipient "$CLOUDFLARE_TEST_TO" \ | |
| > "$RUNNER_TEMP/cloudflare-terminal-plan.json" | |
| npm run --silent cli -- deploy cloudflare \ | |
| --account "$CLOUDFLARE_TEST_ACCOUNT_ID" \ | |
| --name "$DEPLOYMENT_NAME" \ | |
| --email-domain "$CLOUDFLARE_TEST_EMAIL_DOMAIN" \ | |
| --deployment-id "$DEPLOYMENT_ID" \ | |
| --allowed-recipient "$CLOUDFLARE_TEST_TO" \ | |
| --confirm-account "$CLOUDFLARE_TEST_ACCOUNT_ID" \ | |
| --apply \ | |
| | tee "$RUNNER_TEMP/cloudflare-terminal-deploy.txt" | |
| endpoint="https://hayasend-$DEPLOYMENT_NAME.$CLOUDFLARE_TEST_WORKERS_SUBDOMAIN.workers.dev" | |
| npm run --silent cli -- doctor cloudflare \ | |
| --endpoint "$endpoint" \ | |
| --deployment-id "$DEPLOYMENT_ID" \ | |
| | tee "$RUNNER_TEMP/cloudflare-terminal-doctor.json" | |
| # shellcheck disable=SC2016 | |
| node --input-type=module -e ' | |
| import { writeFileSync } from "node:fs"; | |
| const value = { | |
| object: "cloudflare_terminal_subscription_handoff", | |
| queue: `hayasend-${process.env.DEPLOYMENT_NAME}-email-events`, | |
| source: "Email Sending", | |
| domain: process.env.CLOUDFLARE_TEST_EMAIL_DOMAIN, | |
| events: [ | |
| "message.delivered", | |
| "message.deferred", | |
| "message.bounced", | |
| "message.failed", | |
| "message.rejected", | |
| "message.complained", | |
| ], | |
| next_phase: "verify-and-cleanup", | |
| }; | |
| writeFileSync( | |
| `${process.env.RUNNER_TEMP}/cloudflare-terminal-handoff.json`, | |
| `${JSON.stringify(value, null, 2)}\n`, | |
| { mode: 0o600 }, | |
| ); | |
| ' | |
| - name: Verify subscription and recipient delivery | |
| if: inputs.phase == 'verify-and-cleanup' | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| endpoint="https://hayasend-$DEPLOYMENT_NAME.$CLOUDFLARE_TEST_WORKERS_SUBDOMAIN.workers.dev" | |
| npm run --silent cli -- doctor cloudflare \ | |
| --endpoint "$endpoint" \ | |
| --deployment-id "$DEPLOYMENT_ID" \ | |
| | tee "$RUNNER_TEMP/cloudflare-terminal-doctor.json" | |
| npm run --silent cli -- doctor cloudflare-sending-domain \ | |
| --account "$CLOUDFLARE_TEST_ACCOUNT_ID" \ | |
| --email-domain "$CLOUDFLARE_TEST_EMAIL_DOMAIN" \ | |
| | tee "$RUNNER_TEMP/cloudflare-terminal-sending-domain.json" | |
| npm run --silent cli -- doctor cloudflare-delivery-recipient \ | |
| --account "$CLOUDFLARE_TEST_ACCOUNT_ID" \ | |
| --recipient "$CLOUDFLARE_TEST_TO" \ | |
| | tee "$RUNNER_TEMP/cloudflare-terminal-recipient.json" | |
| npm run --silent cli -- doctor cloudflare-events \ | |
| --account "$CLOUDFLARE_TEST_ACCOUNT_ID" \ | |
| --name "$DEPLOYMENT_NAME" \ | |
| --email-domain "$CLOUDFLARE_TEST_EMAIL_DOMAIN" \ | |
| | tee "$RUNNER_TEMP/cloudflare-terminal-subscription.json" | |
| CF_ENDPOINT="$endpoint" \ | |
| node scripts/cloudflare-terminal-delivery.mjs \ | |
| | tee "$RUNNER_TEMP/cloudflare-terminal-observations.jsonl" | |
| tail -n 1 "$RUNNER_TEMP/cloudflare-terminal-observations.jsonl" \ | |
| > "$RUNNER_TEMP/cloudflare-terminal-delivery.json" | |
| email_id="$( | |
| node -e 'const fs=require("node:fs");const x=JSON.parse(fs.readFileSync(process.argv[1],"utf8"));process.stdout.write(x.email_id)' \ | |
| "$RUNNER_TEMP/cloudflare-terminal-delivery.json" | |
| )" | |
| if [[ ! "$email_id" =~ ^email_[a-f0-9]{32}$ ]]; then | |
| echo "::error::The terminal proof returned an invalid HayaSend email ID." | |
| exit 1 | |
| fi | |
| npx --yes "wrangler@$WRANGLER_VERSION" d1 execute \ | |
| "hayasend-$DEPLOYMENT_NAME-d1" \ | |
| --remote \ | |
| --json \ | |
| --yes \ | |
| --command \ | |
| "SELECT json_extract(entity, '$.id') AS event_id, json_extract(entity, '$.provider_message_id') AS provider_message_id, json_extract(entity, '$.type') AS provider_type, json_extract(entity, '$.terminal') AS terminal FROM provider_events WHERE message_id = '$email_id' ORDER BY received_at, id" \ | |
| > "$RUNNER_TEMP/cloudflare-terminal-ledger.json" | |
| node -e ' | |
| const fs = require("node:fs"); | |
| const rows = JSON.parse(fs.readFileSync(process.argv[1], "utf8")) | |
| .flatMap((page) => page.results ?? []); | |
| if (rows.length !== 1 || rows[0].provider_type !== "delivered" || rows[0].terminal !== 1) { | |
| throw new Error("Expected exactly one terminal delivered provider event."); | |
| } | |
| ' "$RUNNER_TEMP/cloudflare-terminal-ledger.json" | |
| - name: Cleanup after a failed deploy phase | |
| if: failure() && inputs.phase == 'deploy' && steps.guard.outputs.validated == 'true' | |
| shell: bash | |
| run: | | |
| npm run --silent cli -- cleanup cloudflare \ | |
| --account "$CLOUDFLARE_TEST_ACCOUNT_ID" \ | |
| --name "$DEPLOYMENT_NAME" \ | |
| --confirm-account "$CLOUDFLARE_TEST_ACCOUNT_ID" \ | |
| --apply \ | |
| | tee "$RUNNER_TEMP/cloudflare-terminal-cleanup.json" | |
| - name: Cleanup verified retained namespace | |
| if: always() && inputs.phase == 'verify-and-cleanup' && steps.guard.outputs.validated == 'true' | |
| shell: bash | |
| run: | | |
| npm run --silent cli -- cleanup cloudflare \ | |
| --account "$CLOUDFLARE_TEST_ACCOUNT_ID" \ | |
| --name "$DEPLOYMENT_NAME" \ | |
| --confirm-account "$CLOUDFLARE_TEST_ACCOUNT_ID" \ | |
| --apply \ | |
| | tee "$RUNNER_TEMP/cloudflare-terminal-cleanup.json" | |
| - name: Verify cleanup residue | |
| if: always() && inputs.phase == 'verify-and-cleanup' && steps.guard.outputs.validated == 'true' | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| prefix="hayasend-$DEPLOYMENT_NAME" | |
| d1_json="$(npx --yes "wrangler@$WRANGLER_VERSION" d1 list --json)" | |
| # shellcheck disable=SC2016 | |
| if node -e 'const a=JSON.parse(process.argv[1]);const p=process.argv[2];process.exit(a.some(x=>x.name===`${p}-d1`)?0:1)' "$d1_json" "$prefix"; then | |
| echo "::error::D1 database remains after cleanup." | |
| exit 1 | |
| fi | |
| if npx --yes "wrangler@$WRANGLER_VERSION" r2 bucket list | grep -F "$prefix-payloads"; then | |
| echo "::error::R2 bucket remains after cleanup." | |
| exit 1 | |
| fi | |
| if npx --yes "wrangler@$WRANGLER_VERSION" queues list | grep -F "$prefix-"; then | |
| echo "::error::Queue remains after cleanup." | |
| exit 1 | |
| fi | |
| set +e | |
| worker_check="$( | |
| npx --yes "wrangler@$WRANGLER_VERSION" versions list \ | |
| --name "$prefix" \ | |
| --json 2>&1 | |
| )" | |
| worker_status="$?" | |
| set -e | |
| if [[ "$worker_status" -eq 0 ]] || ! grep -Fq "code: 10007" <<< "$worker_check"; then | |
| echo "::error::Worker absence could not be verified after cleanup." | |
| exit 1 | |
| fi | |
| - name: Upload terminal-delivery evidence | |
| if: always() && steps.guard.outputs.validated == 'true' | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: cloudflare-terminal-${{ inputs.phase }}-${{ github.run_id }}-${{ github.run_attempt }} | |
| path: | | |
| ${{ runner.temp }}/cloudflare-terminal-*.json | |
| ${{ runner.temp }}/cloudflare-terminal-*.jsonl | |
| ${{ runner.temp }}/cloudflare-terminal-*.txt | |
| if-no-files-found: error | |
| retention-days: 30 |