Skip to content

Part 5: CI/CD

CI runs start from an empty disk, so store state where every run can read it:

alchemy.run.ts
{
providers: Layer.mergeAll(Cloudflare.providers(), E2B.providers()),
state: Alchemy.localState(),
state: Cloudflare.state(),
},

Add your E2B API key to the repository’s secrets as E2B_API_KEY. Alchemy reads it instead of a profile.

.github/workflows/deploy.yml
name: Deploy
on:
pull_request:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
env:
E2B_API_KEY: ${{ secrets.E2B_API_KEY }}
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
STAGE: ${{ github.event_name == 'pull_request' && format('pr-{0}', github.event.number) || 'prod' }}
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- run: bun install
- run: bun alchemy deploy --stage "$STAGE" --yes

Each pull request gets its own stage. E2B has no keyless CI login, so the workflow uses the API key.

.github/workflows/cleanup.yml
name: Cleanup
on:
pull_request:
types: [closed]
jobs:
destroy:
runs-on: ubuntu-latest
env:
E2B_API_KEY: ${{ secrets.E2B_API_KEY }}
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- run: bun install
- run: bun alchemy destroy --stage "pr-${{ github.event.number }}" --yes

Destroying the stage kills the sandboxes its Worker created.