Skip to content

Deploy production

Deploy production #11

Workflow file for this run

# Deploy PRODUCTION — runs scripts/deploy-prod.sh (migrations + both workers to
# the TOP-LEVEL wrangler config: custom domains cloud.parachute.computer +
# u.parachute.computer; NO seeding) then the READ-ONLY prod smoke
# (scripts/smoke-prod.ts — creates no accounts/vaults/notes).
#
# Triggers: manual workflow_dispatch ONLY (deploys the selected ref, default
# branch by default) — see the note above the `on:` block for why the old
# v*-tag trigger was retired.
#
# SECRET GATE: CLOUDFLARE_API_TOKEN lives in the `production` ENVIRONMENT
# secrets (approval-gated); CLOUDFLARE_ACCOUNT_ID + DEV_SECRETS are repo
# secrets. See deploy-staging.yml's header for the exact Cloudflare token
# permissions and the DEV_SECRETS format
# (prod's smoke only reads DEV_USER_EMAIL from it, to exercise the operator
# magic-link path; deploy-prod.sh itself never seeds). When any secret is
# missing the workflow warns and skips, exiting green.
name: Deploy production
# Deliberate-dispatch only. The old `push: tags: v*` trigger predates the
# production environment gate: every rc tag (one per merged PR) would now
# park a pending-approval run in Actions — pure approval fatigue. Prod
# deploys are an explicit `gh workflow run deploy-prod.yml` + the owner's
# approval click, nothing implicit.
on:
workflow_dispatch:
concurrency:
group: deploy-prod
cancel-in-progress: false
jobs:
deploy:
name: deploy + smoke (production)
runs-on: ubuntu-latest
# Deploy credentials live in the `production` GitHub Environment, which is
# protected by a required-reviewer rule: every run PAUSES until the owner
# approves it in the Actions UI. Dispatching ≠ deploying — the human gate
# is enforced by GitHub, not by convention.
environment: production
timeout-minutes: 25
steps:
- name: Check deploy secrets
id: gate
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
DEV_SECRETS: ${{ secrets.DEV_SECRETS }}
run: |
missing=""
if [ -z "$CLOUDFLARE_API_TOKEN" ]; then missing="$missing CLOUDFLARE_API_TOKEN"; fi
if [ -z "$CLOUDFLARE_ACCOUNT_ID" ]; then missing="$missing CLOUDFLARE_ACCOUNT_ID"; fi
if [ -z "$DEV_SECRETS" ]; then missing="$missing DEV_SECRETS"; fi
if [ -n "$missing" ]; then
echo "::notice title=Production deploy skipped::Missing deploy secret(s):$missing. Deploy + smoke skipped (green by design). See the header of .github/workflows/deploy-staging.yml for what to set."
{
echo "## Production deploy skipped"
echo ""
echo "Missing deploy secret(s): \`$missing\`"
echo ""
echo "Set CLOUDFLARE_API_TOKEN under Settings → Environments (production/staging) and the rest under Settings → Secrets and variables → Actions, then re-run via workflow_dispatch. Token permissions and the DEV_SECRETS format are documented in deploy-staging.yml's header."
} >> "$GITHUB_STEP_SUMMARY"
echo "ready=false" >> "$GITHUB_OUTPUT"
else
echo "ready=true" >> "$GITHUB_OUTPUT"
fi
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
if: steps.gate.outputs.ready == 'true'
- name: Clone parachute-vault (sibling file dep for @openparachute/core)
# workers/vault builds against file:../../../parachute-vault/core — a
# sibling checkout outside this repo. Deploys build against
# parachute-vault@main, matching local practice (the deploy script's
# own `bun install` refreshes the copied dep from this clone).
if: steps.gate.outputs.ready == 'true'
run: |
git clone --depth 1 https://github.com/ParachuteComputer/parachute-vault.git "$GITHUB_WORKSPACE/../parachute-vault"
echo "parachute-vault @ $(git -C "$GITHUB_WORKSPACE/../parachute-vault" rev-parse HEAD)"
- name: Fetch pinned Parachute App (SPA served by the identity worker)
# scripts/spa-source.env is the single source pin. Fetching the exact
# commit makes tagged production deploys deterministic.
if: steps.gate.outputs.ready == 'true'
run: |
source scripts/spa-source.env
APP_REPO="$GITHUB_WORKSPACE/../parachute-app"
git init "$APP_REPO"
git -C "$APP_REPO" remote add origin https://github.com/ParachuteComputer/parachute-app.git
git -C "$APP_REPO" fetch --depth 1 origin "$SPA_APP_REF"
git -C "$APP_REPO" checkout --detach FETCH_HEAD
echo "parachute-app v$SPA_APP_VERSION @ $(git -C "$APP_REPO" rev-parse HEAD)"
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
if: steps.gate.outputs.ready == 'true'
with:
bun-version: 1.3.13 # keep consistent with local dev
- name: Materialize pinned Hub door-contract dependency
if: steps.gate.outputs.ready == 'true'
run: bash scripts/materialize-door-contract.sh
- name: Cache bun download cache
if: steps.gate.outputs.ready == 'true'
uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0
with:
path: ~/.bun/install/cache
key: ${{ runner.os }}-bun-${{ hashFiles('package.json', 'workers/*/package.json') }}
restore-keys: |
${{ runner.os }}-bun-
- name: Materialize workers/identity/.dev-secrets
# smoke-prod.ts reads DEV_USER_EMAIL from this file (operator
# magic-link check). The deploy script itself never touches it.
if: steps.gate.outputs.ready == 'true'
env:
DEV_SECRETS: ${{ secrets.DEV_SECRETS }}
run: printf '%s\n' "$DEV_SECRETS" > workers/identity/.dev-secrets
- name: Deploy production (migrations + both workers, NO seed)
if: steps.gate.outputs.ready == 'true'
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
run: bash scripts/deploy-prod.sh
- name: Live smoke (read-only, production)
if: steps.gate.outputs.ready == 'true'
run: bun scripts/smoke-prod.ts