Part 6: Stages and CI
Deploy another stage
Section titled “Deploy another stage”pnpm alchemy deploy --stage stagingbun alchemy deploy --stage stagingnpx alchemy deploy --stage stagingyarn alchemy deploy --stage stagingThe MyEnv environment’s name includes the stage, so staging gets its own environment, app, images and volumes, and changing the image in one stage never reaches another. Your own stage is untouched.
Limit what a stage can spend
Section titled “Limit what a stage can spend”export const MyEnv = Modal.Environment("MyEnv", {});export const MyEnv = Modal.Environment("MyEnv", { maxConcurrentTasks: 20, budget: 50,});budget is a monthly limit in US dollars for the environment. Modal stops starting containers when it’s reached.
Keep state in Cloudflare
Section titled “Keep state in Cloudflare”CI runs start from an empty disk, so store state where every run can read it:
{ providers: Layer.mergeAll(Cloudflare.providers(), Modal.providers()), state: Alchemy.localState(), state: Cloudflare.state(), },Store the token
Section titled “Store the token”Modal has no OIDC federation, so CI uses a token. Create one for a workspace you use only for CI, and add MODAL_TOKEN_ID and MODAL_TOKEN_SECRET to the repository’s secrets.
Deploy per pull request
Section titled “Deploy per pull request”name: Deployon: pull_request: push: branches: [main]
jobs: deploy: runs-on: ubuntu-latest env: MODAL_TOKEN_ID: ${{ secrets.MODAL_TOKEN_ID }} MODAL_TOKEN_SECRET: ${{ secrets.MODAL_TOKEN_SECRET }} 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" --yesEach pull request deploys its own stage, in its own Modal environment, and so builds its own base: a pull request that changes Warm rebuilds it for that stage only.
Clean up closed pull requests
Section titled “Clean up closed pull requests”name: Cleanupon: pull_request: types: [closed]
jobs: destroy: runs-on: ubuntu-latest env: MODAL_TOKEN_ID: ${{ secrets.MODAL_TOKEN_ID }} MODAL_TOKEN_SECRET: ${{ secrets.MODAL_TOKEN_SECRET }} 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 }}" --yesDestroying the stage removes its environment, with its images and the volumes your Worker created in it.