Skip to content

Commit 13d7119

Browse files
committed
refactor(ci): split CI and deploy into separate workflows
Move Cloudflare deployment (R2 sync, Worker deploy, cache purge) out of ci.yml into a new deploy.yml workflow that triggers only when a GitHub Release is published. Previously, deployment was gated on commit message matching (chore(release): ...), which was fragile and required np to be configured with a custom message format. Now the flow is: pnpm release → np bumps, tags, pushes, publishes GitHub Release → deploy.yml triggers → build → R2 → Worker → cache purge CI (ci.yml) now only runs lint, format, tests, and build on push/PR. Also update np config to publish a full GitHub Release instead of a draft, and update README and deployment checklist documentation.
1 parent ed92d1c commit 13d7119

5 files changed

Lines changed: 144 additions & 125 deletions

File tree

.github/workflows/ci.yml

Lines changed: 1 addition & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,9 @@ on:
77

88
env:
99
NODE_VERSION: '20'
10-
R2_BUCKET: ${{ vars.R2_BUCKET }}
1110

1211
jobs:
13-
build-test-deploy:
12+
build-test:
1413
runs-on: ubuntu-latest
1514

1615
steps:
@@ -19,16 +18,6 @@ jobs:
1918
with:
2019
fetch-depth: 0
2120

22-
- name: Check if release commit
23-
id: commit_check
24-
run: |
25-
MSG=$(git log -1 --pretty=%s)
26-
if echo "$MSG" | grep -qE '^chore\(release\):'; then
27-
echo "is_release=true" >> "$GITHUB_OUTPUT"
28-
else
29-
echo "is_release=false" >> "$GITHUB_OUTPUT"
30-
fi
31-
3221
- name: Check clear-manifest impact
3322
if: github.event_name == 'pull_request'
3423
id: clear_check
@@ -105,36 +94,3 @@ jobs:
10594
- name: E2E tests
10695
if: github.event_name == 'pull_request'
10796
run: pnpm test:e2e
108-
109-
- name: Deploy to Cloudflare R2
110-
if: github.event_name == 'push' && github.ref == 'refs/heads/main' && steps.commit_check.outputs.is_release == 'true'
111-
env:
112-
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
113-
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
114-
R2_ACCOUNT_ID: ${{ secrets.R2_ACCOUNT_ID }}
115-
run: |
116-
ENDPOINT="https://${R2_ACCOUNT_ID}.r2.cloudflarestorage.com"
117-
aws --endpoint-url "$ENDPOINT" s3 sync dist/ "s3://${R2_BUCKET}/" --delete --acl private
118-
119-
- name: Deploy Worker
120-
if: github.event_name == 'push' && github.ref == 'refs/heads/main' && steps.commit_check.outputs.is_release == 'true'
121-
env:
122-
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
123-
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.R2_ACCOUNT_ID }}
124-
run: |
125-
# Update bucket name in wrangler.toml from R2_BUCKET variable
126-
sed -i "s/bucket_name = \"myselfhosted-webmail-test-1\"/bucket_name = \"${R2_BUCKET}\"/" worker/wrangler.toml
127-
cd worker
128-
pnpm install
129-
npx wrangler deploy
130-
131-
- name: Purge Cloudflare Cache
132-
if: github.event_name == 'push' && github.ref == 'refs/heads/main' && steps.commit_check.outputs.is_release == 'true'
133-
env:
134-
CLOUDFLARE_ZONE_ID: ${{ secrets.CLOUDFLARE_ZONE_ID }}
135-
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
136-
run: |
137-
curl -X POST "https://api.cloudflare.com/client/v4/zones/${CLOUDFLARE_ZONE_ID}/purge_cache" \
138-
-H "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}" \
139-
-H "Content-Type: application/json" \
140-
--data '{"purge_everything":true}'

.github/workflows/deploy.yml

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
name: Deploy
2+
3+
on:
4+
release:
5+
types: [published]
6+
7+
env:
8+
NODE_VERSION: '20'
9+
R2_BUCKET: ${{ vars.R2_BUCKET }}
10+
11+
jobs:
12+
deploy:
13+
runs-on: ubuntu-latest
14+
15+
steps:
16+
- name: Checkout
17+
uses: actions/checkout@v4
18+
19+
- name: Setup pnpm
20+
uses: pnpm/action-setup@v3
21+
with:
22+
run_install: false
23+
24+
- name: Setup Node
25+
uses: actions/setup-node@v4
26+
with:
27+
node-version: ${{ env.NODE_VERSION }}
28+
cache: pnpm
29+
30+
- name: Install dependencies
31+
run: pnpm install --frozen-lockfile
32+
33+
- name: Build
34+
run: pnpm build
35+
36+
- name: Strip source maps
37+
run: find dist -name '*.map' -delete
38+
39+
- name: Deploy to Cloudflare R2
40+
env:
41+
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
42+
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
43+
R2_ACCOUNT_ID: ${{ secrets.R2_ACCOUNT_ID }}
44+
run: |
45+
ENDPOINT="https://${R2_ACCOUNT_ID}.r2.cloudflarestorage.com"
46+
aws --endpoint-url "$ENDPOINT" s3 sync dist/ "s3://${R2_BUCKET}/" --delete --acl private
47+
48+
- name: Deploy Worker
49+
env:
50+
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
51+
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.R2_ACCOUNT_ID }}
52+
run: |
53+
# Update bucket name in wrangler.toml from R2_BUCKET variable
54+
sed -i "s/bucket_name = \"myselfhosted-webmail-test-1\"/bucket_name = \"${R2_BUCKET}\"/" worker/wrangler.toml
55+
cd worker
56+
pnpm install
57+
npx wrangler deploy
58+
59+
- name: Purge Cloudflare Cache
60+
env:
61+
CLOUDFLARE_ZONE_ID: ${{ secrets.CLOUDFLARE_ZONE_ID }}
62+
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
63+
run: |
64+
curl -X POST "https://api.cloudflare.com/client/v4/zones/${CLOUDFLARE_ZONE_ID}/purge_cache" \
65+
-H "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}" \
66+
-H "Content-Type: application/json" \
67+
--data '{"purge_everything":true}'

README.md

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -174,14 +174,22 @@ BREAKING CHANGE: settings store schema changed, requires cache clear
174174

175175
### Releasing
176176

177-
Releases are cut locally using [np](https://github.com/sindresorhus/np). It runs pre-release checks (clean tree, tests, build), bumps `package.json`, creates a git tag, pushes, and drafts a GitHub Release.
178-
179-
Only release commits trigger production deployment — regular pushes to `main` run CI (lint, test, build) but do not deploy.
177+
Releases are managed locally using [np](https://github.com/sindresorhus/np) and deployed automatically via GitHub Actions when a GitHub Release is published.
180178

181179
```bash
182-
pnpm release # interactive version prompt, runs checks, pushes, drafts GitHub release
180+
pnpm release # interactive version prompt, runs checks, pushes, publishes GitHub Release
183181
```
184182

183+
This command will:
184+
185+
1. Verify a clean working tree and up-to-date `main` branch
186+
2. Run lint, format, tests, and build
187+
3. Bump the version in `package.json` and create a git tag
188+
4. Push the commit and tag to GitHub
189+
5. Publish a GitHub Release
190+
191+
Once the GitHub Release is published, the **Deploy** workflow (`.github/workflows/deploy.yml`) automatically triggers and deploys to Cloudflare.
192+
185193
## Configuration
186194

187195
Create a `.env` file to override defaults:
@@ -224,20 +232,25 @@ graph TB
224232

225233
### CI/CD Pipeline
226234

227-
The GitHub Actions workflow (`.github/workflows/ci.yml`) runs on every push to `main` and on pull requests:
235+
CI and deployment are handled by two separate GitHub Actions workflows:
228236

229-
**Every push (CI):**
237+
**CI** (`.github/workflows/ci.yml`) — runs on every push to `main` and on pull requests:
230238

231239
1. **Install**`pnpm install --frozen-lockfile`
232240
2. **Lint**`pnpm lint`
233241
3. **Format**`pnpm format`
234-
4. **Build**`pnpm build` (Vite + Workbox service worker)
242+
4. **Unit tests**`pnpm test -- --run`
243+
5. **Build**`pnpm build` (Vite + Workbox service worker)
244+
6. **E2E tests** — Playwright (pull requests only)
245+
246+
**Deploy** (`.github/workflows/deploy.yml`) — runs only when a GitHub Release is published:
235247

236-
**Release commits only (`chore(release): x.y.z`):**
248+
1. **Build** — Full production build
249+
2. **Deploy to R2** — Sync `dist/` to Cloudflare R2 bucket
250+
3. **Deploy Worker** — Deploy CDN worker for SPA routing + cache headers
251+
4. **Purge Cache** — Clear Cloudflare edge cache
237252

238-
5. **Deploy to R2** — Sync `dist/` to Cloudflare R2 bucket
239-
6. **Deploy Worker** — Deploy CDN worker for SPA routing + cache headers
240-
7. **Purge Cache** — Clear Cloudflare edge cache
253+
Pushing to `main` never triggers a deployment. Only publishing a GitHub Release (via `pnpm release`) triggers the deploy workflow.
241254

242255
### Required Secrets & Variables
243256

docs/deployment-checklist.md

Lines changed: 51 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ Complete setup guide for deploying the webmail app from scratch using Cloudflare
55

66
```mermaid
77
flowchart LR
8-
A["git push main"] --> B["Lint + Format"] --> C["Build + SW gen"] --> D["Deploy R2 + Worker"] --> E["Purge CDN cache"]
9-
E --> F["Result: Static PWA served from Cloudflare edge"]
8+
A["pnpm release"] --> B["np: lint, test, build, bump, tag"] --> C["GitHub Release published"]
9+
C --> D["Deploy workflow: build, R2 sync, Worker deploy, cache purge"]
10+
D --> E["Result: Static PWA served from Cloudflare edge"]
1011
```
1112

1213
## Prerequisites
@@ -182,69 +183,51 @@ None needed — the app is entirely client-side after build.
182183

183184
## 6. CI/CD Pipeline
184185

185-
```yaml
186-
# .github/workflows/ci.yml
187-
name: CI
188-
189-
on:
190-
push:
191-
branches: [main]
192-
pull_request:
193-
194-
env:
195-
NODE_VERSION: '20'
196-
R2_BUCKET: ${{ vars.R2_BUCKET }}
197-
198-
jobs:
199-
build-test-deploy:
200-
runs-on: ubuntu-latest
201-
steps:
202-
# ... checkout, setup pnpm, install deps ...
203-
204-
- name: Lint + Format
205-
run: pnpm lint && pnpm format
206-
207-
- name: Build
208-
run: pnpm build
209-
210-
- name: Deploy to R2
211-
if: github.ref == 'refs/heads/main'
212-
env:
213-
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
214-
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
215-
R2_ACCOUNT_ID: ${{ secrets.R2_ACCOUNT_ID }}
216-
run: |
217-
ENDPOINT="https://${R2_ACCOUNT_ID}.r2.cloudflarestorage.com"
218-
aws --endpoint-url "$ENDPOINT" s3 sync dist/ "s3://${R2_BUCKET}/" --delete
219-
220-
- name: Deploy Worker
221-
if: github.ref == 'refs/heads/main'
222-
env:
223-
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
224-
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.R2_ACCOUNT_ID }}
225-
run: |
226-
sed -i "s/bucket_name = \".*\"/bucket_name = \"${R2_BUCKET}\"/" worker/wrangler.toml
227-
cd worker && pnpm install && npx wrangler deploy
228-
229-
- name: Purge CDN Cache
230-
if: github.ref == 'refs/heads/main'
231-
env:
232-
CLOUDFLARE_ZONE_ID: ${{ secrets.CLOUDFLARE_ZONE_ID }}
233-
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
234-
run: |
235-
curl -X POST "https://api.cloudflare.com/client/v4/zones/${CLOUDFLARE_ZONE_ID}/purge_cache" \
236-
-H "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}" \
237-
-H "Content-Type: application/json" \
238-
--data '{"purge_everything":true}'
186+
There are two separate GitHub Actions workflows:
187+
188+
### CI (`.github/workflows/ci.yml`)
189+
190+
Runs on every push to `main` and on pull requests. Performs lint, format, unit tests, build, and E2E tests (PRs only). **Does not deploy.**
191+
192+
### Deploy (`.github/workflows/deploy.yml`)
193+
194+
Runs only when a GitHub Release is published. Builds the app, syncs to R2, deploys the Worker, and purges the Cloudflare cache.
195+
196+
```mermaid
197+
flowchart TD
198+
subgraph CI ["CI workflow (push / PR)"]
199+
A1["Install"] --> A2["Lint + Format"] --> A3["Unit tests"] --> A4["Build"]
200+
A4 --> A5["E2E tests (PR only)"]
201+
end
202+
subgraph Deploy ["Deploy workflow (release published)"]
203+
B1["Install + Build"] --> B2["Deploy to R2"] --> B3["Deploy Worker"] --> B4["Purge CDN cache"]
204+
end
205+
R["pnpm release → GitHub Release published"] --> Deploy
206+
```
207+
208+
### Releasing
209+
210+
Releases are managed locally using [np](https://github.com/sindresorhus/np):
211+
212+
```bash
213+
pnpm release
239214
```
240215

216+
This will:
217+
218+
1. Verify a clean working tree and up-to-date `main` branch
219+
2. Run lint, format, tests, and build
220+
3. Bump the version in `package.json` and create a git tag
221+
4. Push the commit and tag to GitHub
222+
5. Publish a GitHub Release, which triggers the Deploy workflow
223+
241224
---
242225

243226
## 7. First Deployment
244227

245228
```mermaid
246229
flowchart TD
247-
A["1. Push to main"] --> B["2. Monitor GitHub Actions"] --> C["3. Verify"]
230+
A["1. pnpm release"] --> B["2. Monitor GitHub Actions (Deploy workflow)"] --> C["3. Verify"]
248231
C --> D["R2 bucket has files?"]
249232
C --> E["Worker deployed?<br/>npx wrangler deployments list"]
250233
C --> F["Site loads?<br/>https://mail.yourdomain.com"]
@@ -285,6 +268,7 @@ flowchart TD
285268
| R2 bucket empty | `aws --endpoint-url "$ENDPOINT" s3 ls "s3://${R2_BUCKET}/"` |
286269
| Cache not clearing | Manual purge: `curl -X POST ".../purge_cache" --data '{"purge_everything":true}'` |
287270
| Deploy 403 error | Verify API token has: Workers Scripts: Edit, Workers R2: Edit, Cache Purge: Purge, Workers Routes: Edit |
271+
| Deploy not triggering | Ensure `pnpm release` published a GitHub Release (check the Releases tab), not just a tag |
288272

289273
---
290274

@@ -295,18 +279,19 @@ flowchart TD
295279
A["1. Create R2 bucket: webmail-staging"] --> B["2. Create Worker: update wrangler.toml name"]
296280
B --> C["3. Add route: staging-mail.yourdomain.com/*"]
297281
C --> D["4. Create GitHub environment with separate secrets"]
298-
D --> E["5. Modify workflow: deploy to staging on develop branch"]
282+
D --> E["5. Add environment filter to deploy.yml"]
299283
```
300284

301285
---
302286

303287
## Quick Reference
304288

305-
| Resource | Location |
306-
| ------------- | ----------------------------------------- |
307-
| R2 Bucket | Cloudflare Dashboard → R2 |
308-
| Worker | Cloudflare Dashboard → Workers & Pages |
309-
| DNS | Cloudflare Dashboard → Your Domain → DNS |
310-
| Secrets | GitHub → Settings → Secrets and variables |
311-
| Workflow | `.github/workflows/ci.yml` |
312-
| Worker Config | `worker/wrangler.toml` |
289+
| Resource | Location |
290+
| --------------- | ----------------------------------------- |
291+
| R2 Bucket | Cloudflare Dashboard → R2 |
292+
| Worker | Cloudflare Dashboard → Workers & Pages |
293+
| DNS | Cloudflare Dashboard → Your Domain → DNS |
294+
| Secrets | GitHub → Settings → Secrets and variables |
295+
| CI Workflow | `.github/workflows/ci.yml` |
296+
| Deploy Workflow | `.github/workflows/deploy.yml` |
297+
| Worker Config | `worker/wrangler.toml` |

package.json

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -92,10 +92,8 @@
9292
"*.{json,css,html,md,svelte}": "pnpm format"
9393
},
9494
"np": {
95-
"message": "chore(release): %s",
9695
"branch": "main",
97-
"publish": false,
98-
"releaseDraft": true
96+
"publish": false
9997
},
10098
"packageManager": "pnpm@9.0.0",
10199
"private": true,

0 commit comments

Comments
 (0)