Skip to content

Instantly share code, notes, and snippets.

@waptik
Last active September 1, 2026 15:54
Show Gist options
  • Select an option

  • Save waptik/c9052d4922fbb1e4c38a8236127802b5 to your computer and use it in GitHub Desktop.

Select an option

Save waptik/c9052d4922fbb1e4c38a8236127802b5 to your computer and use it in GitHub Desktop.
GitHub Actions CI/CD deployment workflow and GitHub secrets stack using Alchemy v2 & Doppler
# ==============================================================================
# GitHub Actions CI/CD Deployment Workflow for Alchemy v2 & Doppler
# ==============================================================================
#
# PURPOSE:
# Automated CI/CD pipeline for deploying applications using Alchemy v2 on Cloudflare,
# with Doppler as the centralized runtime secrets manager.
#
# FEATURES:
# - Automated preview deployments for PRs (stage: pr-<PR_NUMBER>)
# - Production deployments on merge/push to `main` (stage: prod)
# - Automatic preview teardown/cleanup when PR is closed
# - Doppler CLI integration to inject runtime secrets per environment
# - Concurrency grouping by stage to prevent race conditions
# - Non-interactive (`--yes`) and zero-downtime adoption (`--adopt`) flags
#
# PREREQUISITES:
# 1. Doppler Project & Configs:
# - Set up configs in Doppler (e.g. `production` and `dev`).
# 2. GitHub Actions Secrets / Variables:
# - Deploy the companion `stacks-github.ts` stack to sync `DOPPLER_TOKEN`,
# `CLOUDFLARE_API_TOKEN`, and `CLOUDFLARE_ACCOUNT_ID` automatically.
#
# PLACEHOLDERS TO REPLACE:
# - `project`: Your Doppler project name
# - `@repo/location`: Your package name/filter (e.g. `@myorg/infra`)
# ==============================================================================
name: Deploy Application
on:
push:
branches:
- main
pull_request:
types:
- opened
- reopened
- synchronize
- closed
concurrency:
group: deploy-${{ github.event_name == 'pull_request' && format('pr-{0}', github.event.number) || (github.ref == 'refs/heads/main' && 'prod' || github.ref_name) }}
cancel-in-progress: false
env:
STAGE: ${{ github.event_name == 'pull_request' && format('pr-{0}', github.event.number) || (github.ref == 'refs/heads/main' && 'prod' || github.ref_name) }}
jobs:
deploy:
name: Deploy
if: ${{ github.event.action != 'closed' }}
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
run_install: false
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 24
cache: pnpm
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Setup Doppler CLI
uses: dopplerhq/cli-action@v3
- name: Deploy with Alchemy
run: doppler run -c ${{ env.STAGE == 'prod' && 'production' || 'dev' }} --project project -- pnpm --filter @repo/location exec alchemy deploy --stage "$STAGE" --adopt --yes
env:
DOPPLER_TOKEN: ${{ secrets.DOPPLER_TOKEN }}
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
PULL_REQUEST: ${{ github.event.number }}
GITHUB_SHA: ${{ github.sha }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
cleanup:
name: Cleanup
runs-on: ubuntu-latest
if: ${{ github.event_name == 'pull_request' && github.event.action == 'closed' }}
permissions:
contents: read
pull-requests: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
run_install: false
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 24
cache: pnpm
- name: Install dependencies
run: pnpm install --frozen-lockfile --ignore-scripts
- name: Setup Doppler CLI
uses: dopplerhq/cli-action@v3
# Allowlist check: cleanup must only ever destroy pr-N stages
- name: Safety Check
run: |-
case "${{ env.STAGE }}" in
pr-[0-9]*) ;;
*)
echo "ERROR: cleanup only destroys pr-* stages, got '${{ env.STAGE }}'"
exit 1
;;
esac
- name: Destroy Preview Environment
run: doppler run -c dev --project project -- pnpm --filter @repo/location exec alchemy destroy --stage "$STAGE" --yes
env:
DOPPLER_TOKEN: ${{ secrets.DOPPLER_TOKEN }}
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
PULL_REQUEST: ${{ github.event.number }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
/**
* ==============================================================================
* Alchemy v2 Stack: GitHub Actions Secrets & Variables Provisioner
* ==============================================================================
*
* PURPOSE:
* Synchronizes CI/CD secrets (DOPPLER_TOKEN, CLOUDFLARE_API_TOKEN) and variables
* (CLOUDFLARE_ACCOUNT_ID) directly into your GitHub repository settings as code.
*
* WORKFLOW & SETUP INSTRUCTIONS:
* 1. Authenticate with Alchemy (configures Cloudflare & GitHub credentials):
* $ pnpm alchemy login --profile <profile> --configure
*
* 2. Ensure your GITHUB_TOKEN has `secrets:write`, `variables:write`, and `actions:write` permissions.
*
* 3. Preview stack changes (Dry Run):
* $ doppler run -c <config> --project <project> -- alchemy plan path/to/stacks/github.ts --profile <profile>
*
* 4. Deploy the stack to GitHub:
* $ doppler run -c <config> --project <project> -- alchemy deploy path/to/stacks/github.ts --profile <profile>
*
* PLACEHOLDERS TO REPLACE:
* - `owner`: Your GitHub organization or username
* - `repo`: Your GitHub repository name
* - `ProjectGitHubSecrets`: Name of your Alchemy stack
* ==============================================================================
*/
import * as Alchemy from "alchemy";
import * as Cloudflare from "alchemy/Cloudflare";
import * as GitHub from "alchemy/GitHub";
import * as Config from "effect/Config";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
const REPO = { owner: "owner", repository: "repo" } as const;
export default Alchemy.Stack(
"ProjectGitHubSecrets",
{
providers: Layer.mergeAll(
Cloudflare.providers(),
GitHub.providers(),
),
state: Cloudflare.state(),
},
Effect.gen(function* () {
const DOPPLER_TOKEN = yield* Config.redacted("DOPPLER_TOKEN");
const CLOUDFLARE_API_TOKEN = yield* Config.redacted("CLOUDFLARE_API_TOKEN");
const CLOUDFLARE_ACCOUNT_ID = yield* Config.string("CLOUDFLARE_ACCOUNT_ID");
// Sync repository secrets for GitHub Actions CI/CD
yield* GitHub.Secrets({
owner: REPO.owner,
repository: REPO.repository,
secrets: {
DOPPLER_TOKEN,
CLOUDFLARE_API_TOKEN,
},
});
yield* GitHub.Variables({
owner: REPO.owner,
repository: REPO.repository,
variables: {
CLOUDFLARE_ACCOUNT_ID,
},
});
return { repo: `${REPO.owner}/${REPO.repository}` };
}),
);
@waptik

waptik commented Sep 1, 2026

Copy link
Copy Markdown
Author

Setup & Deployment Guide

This gist provides a complete CI/CD deployment pipeline using Alchemy v2 for Cloudflare infrastructure and Doppler as the centralized secrets manager.


Step 1: Authenticate with Alchemy

Before deploying the GitHub stack, configure your Cloudflare and GitHub credentials:

pnpm alchemy login --profile <profile> --configure

Note: Ensure your GITHUB_TOKEN or GitHub OAuth account has secrets:write, variables:write, and actions:write permissions for the target repository.


Step 2: Deploy the GitHub Secrets Stack (stacks-github.ts)

The stacks-github.ts file is an Alchemy stack that synchronizes your repository secrets (DOPPLER_TOKEN, CLOUDFLARE_API_TOKEN) and variables (CLOUDFLARE_ACCOUNT_ID) directly into GitHub Actions as code.

  1. Update Placeholders:

    • Replace owner and repo with your GitHub organization/username and repository.
    • Update ProjectGitHubSecrets to your desired stack name.
  2. Preview Changes (Dry Run):

    doppler run -c <config> --project <project> -- alchemy plan path/to/stacks/github.ts --profile <profile>
  3. Deploy Secrets to GitHub:

    doppler run -c <config> --project <project> -- alchemy deploy path/to/stacks/github.ts --profile <profile>

Step 3: Configure GitHub Actions Workflow

Save github-workflows-deploy-alchemy-v2.yml as .github/workflows/deploy.yml in your repository.

Placeholders to replace:

  • --project project: Set your Doppler project name.
  • --filter @repo/location: Set your monorepo infra package name (e.g. --filter @myorg/infra).

Key Workflow Features & Safety Protections

  • Automated PR Previews: Deploys dynamic preview environments named pr-<PR_NUMBER> on every pull request.
  • Production Deployments: Deploys the prod stage when changes merge into main.
  • Automatic Teardown: Runs alchemy destroy --stage "$STAGE" when a PR is closed.
  • Destruction Safety Guard: The cleanup job uses a shell case allowlist (pr-[0-9]*) to prevent accidental destruction of production or base environments.
  • Non-Interactive Execution: Uses --yes to suppress CLI interactive prompts and --adopt for zero-downtime resource adoption.
  • Stage Concurrency: Concurrency grouping matches the dynamic STAGE expression to eliminate race conditions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment