2026-06-30 · 6 min
GitHub Actions Deploy Pipeline ke Cloudflare: Setup 90 Detik
Tiga bulan lalu, deploy SaaS klien Tangerang masih 6-8 menit dari git push sampai live di Cloudflare Pages. Saya tahu bisa lebih cepat, tapi rewrite pipeline berarti potensi outage. Setelah satu weekend rewrite + 2 minggu monitoring, pipeline saya sekarang: 87 detik p50, 134 detik p95, zero downtime selama 12 minggu.
Share setup dan failure mode yang harus diantisipasi.
Konteks app
- Stack: Astro 6 (static front), Cloudflare Workers (API), Postgres (Hetzner).
- Deploy target: Cloudflare Pages (front) + Workers (API) + D1 migration runner.
- Repo size: 22k LOC, 380 file.
- Test suite: 247 test (unit + integration), runtime 2 menit 40 detik.
- Deploy cadence: 4-7 deploy/hari ke production.
Pipeline lama:
- Total: 6m 40s p50
- Install deps: 1m 50s
- Lint + typecheck: 1m 10s
- Test: 2m 40s
- Build: 35s
- Deploy: 25s
Saya tidak terima 6 menit. Tim mulai batch-deploy (kumpulkan 3-4 PR, deploy sekali) yang violate continuous deployment principle.
Strategi optimasi
Tiga area:
- Cache dependency install agresif
- Parallel test sharding
- Conditional build per changed path
1. Dependency cache
Sebelumnya saya pakai actions/setup-node@v4 default cache. Itu cache node_modules cache tapi tidak full restore.
Setelah migrate ke Bun (lihat Bun vs Node Astro install):
- uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.12
- name: Cache Bun deps
uses: actions/cache@v4
with:
path: |
~/.bun/install/cache
node_modules
key: bun-${{ runner.os }}-${{ hashFiles('bun.lockb') }}
restore-keys: |
bun-${{ runner.os }}-
- run: bun install --frozen-lockfile
Cold cache: 38 detik (vs 1m 50s sebelumnya). Warm cache: 4 detik (vs 22 detik sebelumnya).
2. Test sharding
247 test serial = 2m 40s. Pakai matrix shard:
jobs:
test:
strategy:
matrix:
shard: [1, 2, 3, 4]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- run: bun install --frozen-lockfile
- run: bun test --shard ${{ matrix.shard }}/4
4 shard parallel: 42 detik total (limited by slowest shard). Speedup 3.8x.
Catatan: GitHub Actions free tier kasih 20 concurrent job. Untuk tim Pro plan (kami subscribe $4/user/mo), unlimited concurrency dalam limit minute. Total cost CI: ~$60/bulan untuk tim 6.
3. Conditional build
Front-end (Astro) dan API (Workers) di monorepo sama. Tidak perlu build keduanya kalau cuma satu yang berubah.
- name: Detect changes
id: changes
uses: dorny/paths-filter@v3
with:
filters: |
frontend:
- 'apps/web/**'
- 'packages/ui/**'
api:
- 'apps/api/**'
- 'packages/db/**'
- name: Build frontend
if: steps.changes.outputs.frontend == 'true'
run: bun run build:web
- name: Build api
if: steps.changes.outputs.api == 'true'
run: bun run build:api
Average build time saved: 18-22 detik per deploy.
Pipeline lengkap
name: Deploy Production
on:
push:
branches: [main]
concurrency:
group: deploy-production
cancel-in-progress: false
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
shard: [1, 2, 3, 4]
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- uses: actions/cache@v4
with:
path: ~/.bun/install/cache
key: bun-${{ hashFiles('bun.lockb') }}
- run: bun install --frozen-lockfile
- run: bun test --shard ${{ matrix.shard }}/4
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- run: bun install --frozen-lockfile
- run: bun run lint
- run: bun run typecheck
deploy:
needs: [test, lint]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- run: bun install --frozen-lockfile
- uses: dorny/paths-filter@v3
id: changes
with:
filters: |
frontend: ['apps/web/**']
api: ['apps/api/**']
- name: Build & deploy frontend
if: steps.changes.outputs.frontend == 'true'
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }}
run: |
bun run build:web
bunx wrangler pages deploy apps/web/dist --project-name=kami-web
- name: Build & deploy API
if: steps.changes.outputs.api == 'true'
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
run: |
bun run build:api
bunx wrangler deploy --env production
- name: Smoke test
run: |
sleep 8
curl -sf https://api.kami.id/health || exit 1
curl -sf https://app.kami.id/ | grep -q "<title>Kami" || exit 1
- name: Rollback on failure
if: failure() && steps.changes.outputs.api == 'true'
run: bunx wrangler rollback --env production
Timing breakdown
| Step | Time |
|---|---|
| Checkout + Bun setup | 4s |
| Install (warm cache) | 4s |
| Test (shard 4, parallel) | 42s |
| Lint + typecheck (parallel) | 28s |
| Build frontend | 14s |
| Deploy Pages | 9s |
| Smoke test | 12s |
| Total p50 | 87s |
P95 134 detik karena occasional cold cache atau Cloudflare deploy queue delay.
Yang break
1. Concurrent deploy race
Tanpa concurrency block, 2 PR merge cepat bisa trigger 2 deploy parallel. Kalau deploy A masih running saat B mulai, Pages bisa kasih state inconsistent (asset versi B tapi html versi A di edge cache).
Fix: concurrency: { group: deploy-production, cancel-in-progress: false }. Queue serial, no overwrite.
2. Cloudflare API rate limit
Bulan kedua, kami push 12 deploy dalam 2 jam (refactor sprint). API token kena rate limit Cloudflare (1.200 req/5 menit untuk Pages API). Deploy ke-7 gagal dengan 429 Too Many Requests.
Fix: bukan ganti API, tapi consolidate deploy. Batch PR dengan label deploy-after-merge, automation merge batch tiap 30 menit.
3. Smoke test false negative
Smoke test saya nge-test <title> di response. Suatu hari saya rename app dari “Kami” ke “Kami Cloud” di production tag. Smoke test gagal, auto-rollback trigger. Production sebenarnya OK, tapi rollback bikin downtime 90 detik karena DNS cache.
Fix: smoke test jangan check string yang bisa berubah. Sekarang saya cek HTTP status 200 + presence of meta tag name="generator" (lebih stable).
4. Secret rotation
CF_API_TOKEN saya pakai expire 90 hari (Cloudflare policy our team). Bulan ke-3 expired, deploy gagal jam 2 pagi (saat ada hotfix). Tidak ada notification.
Fix: workflow scheduled tiap Senin pagi yang cek token validity:
on:
schedule:
- cron: '0 1 * * 1' # Monday 08:00 WIB
jobs:
check-token:
runs-on: ubuntu-latest
steps:
- run: |
response=$(curl -s -o /dev/null -w "%{http_code}" \
-H "Authorization: Bearer ${{ secrets.CF_API_TOKEN }}" \
https://api.cloudflare.com/client/v4/user/tokens/verify)
if [ "$response" != "200" ]; then
echo "Token invalid or expired"
exit 1
fi
Plus integration ke Slack: kalau gagal, notify channel #ops.
Verdict
90 detik deploy itu titik manis untuk SaaS ukuran SMB. Lebih cepat dari ini butuh trade-off: skip test (no), atau skip smoke test (risky). Konfigurasi di atas balance speed vs safety.
Saya tidak akan rekomendasi pipeline ini untuk app yang lebih dari 50k LOC atau test > 500 — di skala itu, sharding tidak linear scale lagi dan ada overhead orchestration matrix. Untuk SMB Indonesia: cukup. Lihat juga Cloudflare Pages vs Vercel untuk konteks platform.
Ditulis oleh Reza Pradipta