Continuous Integration
Set up CI/CD pipelines for Alchemy projects with GitHub Actions, automated deployments, and PR previews.
Set up preview deployments and continuous integration for your Alchemy projects using GitHub Actions.
As part of this guide, we’ll:
- Add a Github Workflow to deploy your
prod
stage from themain
branch - Deploy a preview
pr-<number>
stage for each Pull Request - Update your
alchemy.run.ts
script to add a Github Comment to the PR with the preview URL
-
Configure environment variables
Set up required secrets in your GitHub repository settings (Settings → Secrets and variables → Actions):
Terminal window ALCHEMY_PASSWORD=your-encryption-passwordALCHEMY_STATE_TOKEN=your-state-tokenCLOUDFLARE_API_TOKEN=your-cloudflare-api-tokenCLOUDFLARE_EMAIL=your-cloudflare-email -
Set up preview environments in your Alchemy script
Update your
alchemy.run.ts
to support multiple stages, use the CloudflareStateStore and add a GithubComment to the PR with the preview URL:import alchemy from "alchemy";import { Worker, Vite } from "alchemy/cloudflare";import { GitHubComment } from "alchemy/github";import { CloudflareStateStore } from "alchemy/state";const app = await alchemy("my-app", {stateStore: (scope) => new CloudflareStateStore(scope),});// your website may be different, we use Vite for illustration purposesconst website = await Vite("website");console.log(`🚀 Deployed to: https://${website.url}`);if (process.env.PULL_REQUEST) {// if this is a PR, add a comment to the PR with the preview URL// it will auto-update with each pushawait GitHubComment("preview-comment", {owner: "your-username",repository: "your-repo",issueNumber: Number(process.env.PULL_REQUEST),body: `## 🚀 Preview DeployedYour changes have been deployed to a preview environment:**🌐 Website:** ${website.url}Built from commit ${process.env.GITHUB_SHA?.slice(0, 7)}---<sub>🤖 This comment updates automatically with each push.</sub>`,});}await app.finalize(); -
Create deployment workflow
Create
.github/workflows/deploy.yml
with a workflow for deploying yourprod
stage from themain
branch and a previewpr-<number>
stage for each Pull Request:name: Deploy Applicationon:push:branches:- mainpull_request:types:- opened- reopened- synchronize- closedconcurrency:group: deploy-${{ github.ref }}cancel-in-progress: falseenv:STAGE: ${{ github.ref == 'refs/heads/main' && 'prod' || format('pr-{0}',github.event.number) }}jobs:deploy:if: ${{ github.event.action != 'closed' }}runs-on: ubuntu-latestpermissions:contents: readpull-requests: writesteps:- uses: actions/checkout@v4- name: Setup Bunuses: oven-sh/setup-bun@v2- name: Install dependenciesrun: bun install- name: Deployrun: bun alchemy deploy --stage ${{ env.STAGE }}env:ALCHEMY_PASSWORD: ${{ secrets.ALCHEMY_PASSWORD }}ALCHEMY_STATE_TOKEN: ${{ secrets.ALCHEMY_STATE_TOKEN }}CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}CLOUDFLARE_EMAIL: ${{ secrets.CLOUDFLARE_EMAIL }}PULL_REQUEST: ${{ github.event.number }}GITHUB_SHA: ${{ github.sha }}GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}cleanup:runs-on: ubuntu-latestif: ${{ github.event_name == 'pull_request' && github.event.action == 'closed'}}permissions:id-token: writecontents: readpull-requests: writesteps:- uses: actions/checkout@v4- name: Setup Bunuses: oven-sh/setup-bun@v2- name: Install dependenciesrun: bun install- name: Destroy Preview Environmentrun: bun alchemy destroy --stage ${{ env.STAGE }}env:ALCHEMY_PASSWORD: ${{ secrets.ALCHEMY_PASSWORD }}ALCHEMY_STATE_TOKEN: ${{ secrets.ALCHEMY_STATE_TOKEN }}CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}CLOUDFLARE_EMAIL: ${{ secrets.CLOUDFLARE_EMAIL }}PULL_REQUEST: ${{ github.event.number }}name: Deploy Applicationon:push:branches:- mainpull_request:types:- opened- reopened- synchronize- closedconcurrency:group: deploy-${{ github.ref }}cancel-in-progress: falseenv:STAGE: ${{ github.ref == 'refs/heads/main' && 'prod' || format('pr-{0}',github.event.number) }}jobs:deploy:if: ${{ github.event.action != 'closed' }}runs-on: ubuntu-latestpermissions:contents: readpull-requests: writesteps:- uses: actions/checkout@v4- name: Setup Node.jsuses: actions/setup-node@v4with:node-version: "24"- name: Install dependenciesrun: npm ci- name: Deployrun: npx alchemy deploy --stage ${{ env.STAGE }}env:ALCHEMY_PASSWORD: ${{ secrets.ALCHEMY_PASSWORD }}ALCHEMY_STATE_TOKEN: ${{ secrets.ALCHEMY_STATE_TOKEN }}CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}CLOUDFLARE_EMAIL: ${{ secrets.CLOUDFLARE_EMAIL }}PULL_REQUEST: ${{ github.event.number }}GITHUB_SHA: ${{ github.sha }}GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}cleanup:runs-on: ubuntu-latestif: ${{ github.event_name == 'pull_request' && github.event.action == 'closed'}}permissions:id-token: writecontents: readpull-requests: writesteps:- uses: actions/checkout@v4- name: Setup Node.jsuses: actions/setup-node@v4with:node-version: "24"- name: Install dependenciesrun: npm ci- name: Destroy Preview Environmentrun: npx alchemy destroy --stage ${{ env.STAGE }}env:ALCHEMY_PASSWORD: ${{ secrets.ALCHEMY_PASSWORD }}ALCHEMY_STATE_TOKEN: ${{ secrets.ALCHEMY_STATE_TOKEN }}CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}CLOUDFLARE_EMAIL: ${{ secrets.CLOUDFLARE_EMAIL }}PULL_REQUEST: ${{ github.event.number }}name: Deploy Applicationon:push:branches:- mainpull_request:types:- opened- reopened- synchronize- closedconcurrency:group: deploy-${{ github.ref }}cancel-in-progress: falseenv:STAGE: ${{ github.ref == 'refs/heads/main' && 'prod' || format('pr-{0}',github.event.number) }}jobs:deploy:if: ${{ github.event.action != 'closed' }}runs-on: ubuntu-latestpermissions:contents: readpull-requests: writesteps:- uses: actions/checkout@v4- name: Setup pnpmuses: pnpm/action-setup@v4with:version: "10"run_install: false- name: Setup Node.jsuses: actions/setup-node@v4with:node-version: "24"cache: pnpm- name: Install dependenciesrun: pnpm install- name: Deployrun: pnpm dlx alchemy deploy --stage ${{ env.STAGE }}env:ALCHEMY_PASSWORD: ${{ secrets.ALCHEMY_PASSWORD }}ALCHEMY_STATE_TOKEN: ${{ secrets.ALCHEMY_STATE_TOKEN }}CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}CLOUDFLARE_EMAIL: ${{ secrets.CLOUDFLARE_EMAIL }}PULL_REQUEST: ${{ github.event.number }}GITHUB_SHA: ${{ github.sha }}GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}cleanup:runs-on: ubuntu-latestif: ${{ github.event_name == 'pull_request' && github.event.action == 'closed'}}permissions:id-token: writecontents: readpull-requests: writesteps:- uses: actions/checkout@v4- name: Setup pnpmuses: pnpm/action-setup@v4with:version: "10"run_install: false- name: Setup Node.jsuses: actions/setup-node@v4with:node-version: "24"cache: pnpm- name: Install dependenciesrun: pnpm install- name: Destroy Preview Environmentrun: pnpm dlx alchemy destroy --stage ${{ env.STAGE }}env:ALCHEMY_PASSWORD: ${{ secrets.ALCHEMY_PASSWORD }}ALCHEMY_STATE_TOKEN: ${{ secrets.ALCHEMY_STATE_TOKEN }}CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}CLOUDFLARE_EMAIL: ${{ secrets.CLOUDFLARE_EMAIL }}PULL_REQUEST: ${{ github.event.number }}name: Deploy Applicationon:push:branches:- mainpull_request:types:- opened- reopened- synchronize- closedconcurrency:group: deploy-${{ github.ref }}cancel-in-progress: falseenv:STAGE: ${{ github.ref == 'refs/heads/main' && 'prod' || format('pr-{0}',github.event.number) }}jobs:deploy:if: ${{ github.event.action != 'closed' }}runs-on: ubuntu-latestpermissions:contents: readpull-requests: writesteps:- uses: actions/checkout@v4- name: Setup Node.jsuses: actions/setup-node@v4with:node-version: "24"cache: yarn- name: Install yarnrun: npm install -g yarn- name: Install dependenciesrun: yarn install- name: Deployrun: yarn dlx alchemy deploy --stage ${{ env.STAGE }}env:ALCHEMY_PASSWORD: ${{ secrets.ALCHEMY_PASSWORD }}ALCHEMY_STATE_TOKEN: ${{ secrets.ALCHEMY_STATE_TOKEN }}CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}CLOUDFLARE_EMAIL: ${{ secrets.CLOUDFLARE_EMAIL }}PULL_REQUEST: ${{ github.event.number }}GITHUB_SHA: ${{ github.sha }}GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}cleanup:runs-on: ubuntu-latestif: ${{ github.event_name == 'pull_request' && github.event.action == 'closed'}}permissions:id-token: writecontents: readpull-requests: writesteps:- uses: actions/checkout@v4- name: Setup Node.jsuses: actions/setup-node@v4with:node-version: "24"cache: yarn- name: Install yarnrun: npm install -g yarn- name: Install dependenciesrun: yarn install- name: Destroy Preview Environmentrun: yarn dlx alchemy destroy --stage ${{ env.STAGE }}env:ALCHEMY_PASSWORD: ${{ secrets.ALCHEMY_PASSWORD }}ALCHEMY_STATE_TOKEN: ${{ secrets.ALCHEMY_STATE_TOKEN }}CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}CLOUDFLARE_EMAIL: ${{ secrets.CLOUDFLARE_EMAIL }}PULL_REQUEST: ${{ github.event.number }}