Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 3 additions & 37 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ on:
push:
branches: [main]
pull_request:
types: [opened, reopened, synchronize, closed]
types: [opened, reopened, synchronize]

concurrency:
# Grouped by PR number, not github.ref: a merged PR's `closed` run has
# github.ref = refs/heads/main and would otherwise cancel main's push run.
group: ci-${{ github.event.pull_request.number && format('pr-{0}', github.event.pull_request.number) || github.ref }}
# A new push to a PR supersedes its in-progress run; main runs in order.
# Closing a PR does not cancel anything: cleanup.yml waits for the PR's
# last run to finish (and tear down its test stages), then destroys.
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

env:
Expand All @@ -22,7 +22,6 @@ env:

jobs:
check:
if: github.event.action != 'closed'
runs-on: ubuntu-latest
permissions:
contents: read
Expand Down Expand Up @@ -151,36 +150,3 @@ jobs:
PULL_REQUEST: ${{ github.event.number }}
APP_URL: ${{ needs.deploy.outputs.url }}
DEMO_SHA: ${{ github.event.pull_request.head.sha }}

cleanup:
if: github.event_name == 'pull_request' && github.event.action == 'closed'
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PULL_REQUEST: ${{ github.event.number }}
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 24
cache: pnpm
- run: pnpm install --frozen-lockfile
- name: Safety check
run: |
if [ "${{ env.STAGE }}" = "prod" ]; then
echo "ERROR: Cannot destroy prod environment in cleanup job"
exit 1
fi
- name: Destroy preview
run: pnpm exec alchemy destroy --stage ${{ env.STAGE }} --yes
# Normally removed by the demo job itself; this catches interrupted runs.
- name: Destroy demo stages
run: |
pnpm exec alchemy destroy --stage demo-${{ env.STAGE }} --yes
pnpm exec alchemy destroy --stage dev-${{ env.STAGE }} --yes
57 changes: 57 additions & 0 deletions .github/workflows/cleanup.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
name: Cleanup

# Destroys a PR's stages when it closes. Kept out of ci.yml so that no CI run
# can cancel it and leave resources behind.
on:
pull_request:
types: [closed]

concurrency:
group: cleanup-pr-${{ github.event.number }}
cancel-in-progress: false

env:
STAGE: pr-${{ github.event.number }}

jobs:
cleanup:
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
pull-requests: write
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PULL_REQUEST: ${{ github.event.number }}
steps:
# A CI run still in progress could deploy after we destroy, and its
# test suites tear down their own stages when they finish.
- name: Wait for the PR's CI runs to finish
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
HEAD_REF: ${{ github.event.pull_request.head.ref }}
run: |
for attempt in $(seq 1 120); do
active=$(gh api "repos/$GITHUB_REPOSITORY/actions/workflows/ci.yml/runs?event=pull_request&branch=$HEAD_REF" \
--jq '[.workflow_runs[] | select(.status != "completed")] | length')
[ "$active" = "0" ] && exit 0
echo "waiting for $active CI run(s)…"
sleep 30
done
echo "CI runs still active after 60 minutes; destroying anyway"
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 24
cache: pnpm
- run: pnpm install --frozen-lockfile
- name: Destroy preview
run: pnpm exec alchemy destroy --stage ${{ env.STAGE }} --yes
# Normally removed by the demo job itself; this catches interrupted runs.
- name: Destroy demo stages
run: |
pnpm exec alchemy destroy --stage demo-${{ env.STAGE }} --yes
pnpm exec alchemy destroy --stage dev-${{ env.STAGE }} --yes
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ AGENTS.md how we build: TDD with Alchemy-deployed tests, demos,
alchemy.run.ts stack: `@cafe/backend` Worker + Foldkit Website (VITE_API_URL -> Api url), PR preview comment in CI
stacks/
github.ts stack: CI infra: Cloudflare credentials as Actions secrets, public R2 bucket for demos
.github/workflows/ci.yml check → live tests → deploy, one stage per PR, cleanup on close
.github/workflows/
ci.yml check → live tests → deploy → demo, one stage per PR
cleanup.yml destroys a PR's stages when it closes
packages/
api/ @cafe/api: HTTP API + RPC schemas, safe to import from the browser
src/
Expand Down Expand Up @@ -94,7 +96,7 @@ The e2e tests need Playwright's Chromium: `pnpm --filter @cafe/frontend exec pla
2. **test**: backend API tests and Playwright e2e tests against real Cloudflare, in stage `test-pr-<number>` (each suite destroys what it deployed).
3. **deploy**: `alchemy deploy` to stage `pr-<number>` (or `prod` on `main`), with a PR comment linking the preview.
4. **demo**: records the demo (macOS, tcut) and uploads it to the demos bucket: `pr-<number>/<sha>/` goes into the PR's demo comment, `main/` is the README demo. PR demos expire after 30 days.
5. **cleanup**: when a PR closes, `alchemy destroy` its `pr-<number>` stage.
5. **cleanup** (`cleanup.yml`): when a PR closes, waits for the PR's CI runs to finish, then `alchemy destroy`s its `pr-<number>` and demo stages. It is a separate workflow so no CI run can cancel it.

CI authenticates with the `CLOUDFLARE_API_TOKEN` and `CLOUDFLARE_ACCOUNT_ID` repository secrets. `stacks/github.ts` writes them from the profile it is deployed with, which must use a Cloudflare API token (requires admin on the repository):

Expand Down
Loading