CI Workflows
Copy these GitHub Actions workflows into your repo's .github/workflows/ directory. Adjust branch names, env names, and secrets as needed.
Prerequisites
GitHub secrets / variables to configure:
| Secret / Variable | Type | Purpose |
|---|---|---|
CLOUDFLARE_API_TOKEN | Secret | CF API token (scoped to your account) |
CLOUDFLARE_ACCOUNT_ID | Variable | CF account ID |
TAMER_SECRETS_KEY_dev | Secret | Vault master key for dev env |
TAMER_SECRETS_KEY_prod | Secret | Vault master key for prod env |
Dev deploy (push to dev branch)
yaml
# .github/workflows/dev-deploy.yml
name: Dev Deploy
on:
push:
branches: [dev]
jobs:
deploy:
timeout-minutes: 10
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v1
with:
bun-version: 1.3.10
- run: bun install --no-save --linker hoisted --backend copyfile
- name: Verify secrets
run: bunx tamer secrets verify --env dev
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID }}
TAMER_SECRETS_KEY_dev: ${{ secrets.TAMER_SECRETS_KEY_DEV }}
- name: Apply
run: bunx tamer apply --env dev
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID }}
TAMER_SECRETS_KEY_dev: ${{ secrets.TAMER_SECRETS_KEY_DEV }}
- name: Migrate
run: bunx tamer migrate --env dev
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID }}
- name: Deploy
run: bunx tamer deploy --env dev
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID }}
TAMER_SECRETS_KEY_dev: ${{ secrets.TAMER_SECRETS_KEY_DEV }}
DO_NOT_TRACK: "1"Prod deploy (merge to main)
yaml
# .github/workflows/prod-deploy.yml
name: Prod Deploy
on:
push:
branches: [main]
jobs:
deploy:
timeout-minutes: 10
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v1
with:
bun-version: 1.3.10
- run: bun install --no-save --linker hoisted --backend copyfile
- name: Verify secrets
run: bunx tamer secrets verify --env prod
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID }}
TAMER_SECRETS_KEY_prod: ${{ secrets.TAMER_SECRETS_KEY_PROD }}
- name: Apply
run: bunx tamer apply --env prod
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID }}
TAMER_SECRETS_KEY_prod: ${{ secrets.TAMER_SECRETS_KEY_PROD }}
- name: Migrate
run: bunx tamer migrate --env prod
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID }}
- name: Deploy
run: bunx tamer deploy --env prod
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID }}
TAMER_SECRETS_KEY_prod: ${{ secrets.TAMER_SECRETS_KEY_PROD }}
DO_NOT_TRACK: "1"PR preview deploy
yaml
# .github/workflows/pr-preview.yml
name: PR Preview
on:
pull_request:
types: [opened, synchronize, reopened]
concurrency:
group: pr-preview-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
preview:
timeout-minutes: 15
runs-on: ubuntu-latest
if: github.event.pull_request.head.repo.full_name == github.repository
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v1
with:
bun-version: 1.3.10
- run: bun install --no-save --linker hoisted --backend copyfile
- name: Configure env
run: |
echo "PR_ENV=pr-${{ github.event.pull_request.number }}" >> $GITHUB_ENV
echo "TAMER_SECRETS_KEY_pr-${{ github.event.pull_request.number }}=${{ secrets.TAMER_SECRETS_KEY_DEV }}" >> $GITHUB_ENV
- name: Copy secrets
run: bunx tamer secrets copy --from dev --to ${{ env.PR_ENV }}
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID }}
- name: Apply
run: bunx tamer apply --env ${{ env.PR_ENV }}
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID }}
- name: Deploy
run: bunx tamer deploy --env ${{ env.PR_ENV }}
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID }}
DO_NOT_TRACK: "1"
- name: Comment preview URL
uses: actions/github-script@v7
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `Preview deployed: https://${process.env.PR_ENV}.yourdomain.com`
});
env:
PR_ENV: ${{ env.PR_ENV }}PR cleanup (PR closed)
yaml
# .github/workflows/pr-cleanup.yml
name: PR Cleanup
on:
pull_request:
types: [closed]
jobs:
cleanup:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v1
with:
bun-version: 1.3.10
- run: bun install --no-save --linker hoisted --backend copyfile
- name: Destroy ephemeral env
run: |
PR_ENV=pr-${{ github.event.pull_request.number }}
bunx tamer destroy --env $PR_ENV --confirm-env $PR_ENV --wipe-metadata --force
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID }}Scheduled garbage collection
yaml
# .github/workflows/env-gc.yml
name: Env GC
on:
schedule:
- cron: '0 * * * *' # hourly
jobs:
gc:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v1
with:
bun-version: 1.3.10
- run: bun install --no-save --linker hoisted --backend copyfile
- name: Garbage collect stale envs
run: bunx tamer env gc --max-age 72h --force
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID }}Order matters
The correct CI command sequence for any env:
tamer secrets verify → tamer apply → tamer migrate → tamer deployverifyfails if a declared secret is missing from the vaultapplycreates Cloudflare resources + generates wrangler.jsonmigrateruns D1 migrations (needs resources to exist)deploybuilds SPA + deploys workers + pushes secrets + registers workflows
For PR previews, migrate is needed only if your stack declares stack-level D1 (e.g. the API worker's app-db); tenant shard groups are migrated during wfp tenant provision. Skip migrate for tenant-only stacks; otherwise run it on fresh pr-<n> envs too. For prod, always run all four.