Continuous Integration
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 }}cleanup:runs-on: ubuntu-latestif: ${{ github.event.action == 'closed' && github.ref != 'refs/heads/main' }}permissions:id-token: writecontents: readsteps:- 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 }}cleanup:runs-on: ubuntu-latestif: ${{ github.event.action == 'closed' && github.ref != 'refs/heads/main' }}permissions:id-token: writecontents: readsteps:- 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 }}cleanup:runs-on: ubuntu-latestif: ${{ github.event.action == 'closed' && github.ref != 'refs/heads/main' }}permissions:id-token: writecontents: readsteps:- 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 }}cleanup:runs-on: ubuntu-latestif: ${{ github.event.action == 'closed' && github.ref != 'refs/heads/main' }}permissions:id-token: writecontents: readsteps:- 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 }}