CI/CD với Wrangler + GitHub Actions: pipeline, smoke test

Pipeline 4 bước: test → build → deploy → smoke. Scoped API token, smoke test 19 assertion, concurrent lock, preview env, rollback 10 giây. Full workflow file từ blog này.

· 9 phút đọc · Read in English
Pipeline CI/CD Worker 4 bước test → build → wrangler deploy → smoke test trên GitHub Actions, dùng scoped API token, preview environment và rollback 10 giây

TL;DR

CI/CD cho Worker đơn giản hơn phần lớn nền tảng khác: git push → GitHub Actions → wrangler deploy → smoke test. Không container, không artifact storage, không orchestration.

Luận điểm chính:

3 điểm dễ sai nhất: dùng Global API Key thay scoped token (bảo mật), không có smoke test sau khi triển khai (lỗi im lặng), không có concurrent lock trong workflow (race condition khiến hai lần triển khai ghi đè nhau). Pipeline production cần cả 3.

Bài này đi qua: pipeline 4 bước đầy đủ, thiết lập scoped token, pattern smoke test, preview environment, chiến lược rollback, và full workflow file từ blog này.

Bài này đóng Block 3 (Framework). Block 4 bắt đầu với Workers AI ở Part 13.


Dành cho ai

  • Dev đã thiết lập Worker nhưng chưa có CI.
  • Người đang dùng Global API Key trong GitHub secret (cần chuyển ngay).
  • Team đang mở rộng project, cần concurrent lock + smoke test.

Nên đọc trước: Part 4 (Wrangler dev loop), Part 9 (Router), Part 11 (Framework).

Sau bài này bạn sẽ:

  • Thiết lập pipeline GitHub Actions end-to-end.
  • Tạo scoped API token với quyền tối thiểu.
  • Viết smoke test chạy sau khi triển khai.
  • Rollback trong 10 giây khi cần.

Bài này không nói về gì

  • GitLab CI / CircleCI / Jenkins: tương tự, chỉ khác cú pháp. Blog này dùng GitHub Actions.
  • Multi-stage preview environment phức tạp (PR preview, staging, canary): có nhắc nhưng không đi sâu.
  • Blue/green deployment: Workers có rollback tức thời nên pattern này ít cần.

Pipeline 4 bước

Pipeline CI/CD cho Worker: trigger git push main, CI chạy npm ci + test (~40s), build Astro + Pagefind (~20s), wrangler deploy lên 330+ PoP (~30s), smoke test 19 assertion chạy với bản live (~8s). Bất kỳ bước nào fail = CI exit 1. Rollback với wrangler rollback trong 10 giây.

Tổng thời gian: ~100 giây từ push tới live + verified

Thống kê thực tế từ blog này. 100 giây end-to-end là nhanh so với phần lớn pipeline dựa trên container: không build image, không push registry, không roll deployment.


Workflow file đầy đủ

.github/workflows/deploy.yml từ blog cloudsecop.net:

name: Deploy to Cloudflare Workers

on:
  push:
    branches: [main]
  workflow_dispatch:

concurrency:
  group: deploy-${{ github.ref }}
  # cancel-in-progress: true hợp cho blog / low-stakes —
  # push mới huỷ deploy cũ còn đang chạy. Production critical
  # nên dùng `cancel-in-progress: false` để queue, xem mục
  # "Concurrent lock" bên dưới.
  cancel-in-progress: true

permissions:
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    timeout-minutes: 10
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Node
        uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: npm

      - name: Install
        run: npm ci

      - name: Lint posts
        run: npm run lint:posts

      - name: Test
        run: npm test

      - name: Build
        run: npm run build

      - name: Deploy
        run: npx wrangler deploy
        env:
          CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}

      - name: Smoke test
        run: scripts/smoke.sh
        env:
          SITE_URL: https://cloudsecop.net

Giải thích từng phần

concurrency: chỉ 1 lần triển khai chạy tại một thời điểm trên cùng ref. Push 2 lần liên tiếp → lần 2 huỷ lần 1. Tránh race condition khi triển khai.

permissions: contents: read: token GitHub Actions mặc định có quyền write. Hạ xuống read-only. Nguyên tắc least privilege.

timeout-minutes: 10: fail nhanh nếu bước nào treo.

actions/checkout@v4: checkout code. Pin phiên bản để có thể tái lập.

actions/setup-node@v4 + cache: npm: cache node_modules theo package-lock.json. Build sau nhanh hơn 5-10x.

npm ci (không npm install): strict từ lockfile, không cập nhật.

npm run lint:posts: riêng cho blog, kiểm tra schema frontmatter + cặp bản dịch.

npm test: vitest với vitest-pool-workers (Part 4).

npm run build: Astro build + Pagefind index.

npx wrangler deploy: upload Worker. Scoped token (xem phần tiếp).

scripts/smoke.sh: kiểm tra site live sau khi triển khai (xem phần sau).


API token: scoped, không Global

So sánh 3 loại Cloudflare API token: Global API Key (legacy, toàn quyền — KHÔNG DÙNG cho CI), Scoped Account Token (chỉ cấp quyền cần thiết — KHUYẾN NGHỊ), Scoped Zone Token (thêm vào khi có custom domain).

Tạo Scoped Account Token

Cloudflare dashboard → My ProfileAPI TokensCreate Token.

Chọn Custom token với các quyền:

Account permissions:

  • Workers Scripts → Edit
  • Account Settings → Read
  • Workers KV Storage → Edit (nếu dùng KV)
  • D1 → Edit (nếu dùng D1)
  • Workers R2 Storage → Edit (nếu dùng R2)
  • Workers Queues → Edit (nếu dùng Queues)
  • Vectorize → Edit (nếu dùng Vectorize)
  • Workers AI → Read (nếu dùng AI, thường Read đủ)

Zone permissions (nếu có custom domain):

  • Workers Routes → Edit
  • Zone → Read

Account resources: chọn specific account, không All accounts.

Zone resources (nếu có): chọn specific zone, không All zones.

Client IP filtering (tuỳ chọn, paranoid): dải IP public của GitHub Actions. Khó duy trì vì danh sách IP GitHub thay đổi, nhưng an toàn hơn.

TTL (tuỳ chọn): token hết hạn sau N ngày. Tăng an toàn, nhưng cần xoay định kỳ.

Sau khi tạo, Cloudflare hiển thị token một lần duy nhất. Copy ngay.

Lưu vào GitHub secret

Repo → SettingsSecrets and variablesActionsNew repository secret.

  • CLOUDFLARE_API_TOKEN: dán token.
  • CLOUDFLARE_ACCOUNT_ID: lấy ở Cloudflare dashboard, sidebar phải.
  • CF_SUBDOMAIN (chỉ cần cho workflow PR preview bên dưới): subdomain workers.dev của account, ví dụ khavan trong khavan.workers.dev.

Workflow triển khai chính dùng 2 secret đầu:

env:
  CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
  CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}

Tại sao KHÔNG Global API Key

Global API Key (CLOUDFLARE_EMAIL + CLOUDFLARE_API_KEY):

  • Toàn quyền account: xem/đổi/xoá mọi zone, bill, tạo tài nguyên mới.
  • Không thể thu hồi một phần.
  • Một lần lộ = toàn bộ hạ tầng bị compromise.

Scoped token:

  • Chỉ có quyền bạn cấp.
  • Thu hồi dễ: disable token trong dashboard → hết hiệu lực ngay.
  • Nhiều token cho nhiều mục đích (CI triển khai, wrangler cá nhân, monitoring chỉ đọc).

Đổi Global → Scoped ngay nếu chưa.


Smoke test: 19 assertion

Sau khi triển khai, kiểm tra site live. Không phải cứ build pass = triển khai thành công. Cần verify traffic thật.

scripts/smoke.sh

#!/usr/bin/env bash
set -euo pipefail

SITE="${SITE_URL:-https://cloudsecop.net}"
PASS=0
FAIL=0

assert() {
  local name="$1"
  local expected="$2"
  local actual="$3"
  if [[ "$actual" == "$expected" ]]; then
    echo "  ✓ $name"
    PASS=$((PASS + 1))
  else
    echo "  ✗ $name: expected '$expected', got '$actual'"
    FAIL=$((FAIL + 1))
  fi
}

assert_contains() {
  local name="$1"
  local needle="$2"
  local haystack="$3"
  if [[ "$haystack" == *"$needle"* ]]; then
    echo "  ✓ $name"
    PASS=$((PASS + 1))
  else
    echo "  ✗ $name: missing '$needle'"
    FAIL=$((FAIL + 1))
  fi
}

# === Core routes ===
echo "Core routes:"
assert "Home 200" "200" "$(curl -sI "$SITE/" -o /dev/null -w '%{http_code}')"
assert "Blog index 200" "200" "$(curl -sI "$SITE/blog/" -o /dev/null -w '%{http_code}')"
assert "Sample post 200" "200" "$(curl -sI "$SITE/blog/zero-trust-notes/" -o /dev/null -w '%{http_code}')"

# === Real 404 ===
echo "Error handling:"
assert "/nonexistent → 404" "404" "$(curl -sI "$SITE/nonexistent-url-test" -o /dev/null -w '%{http_code}')"

# === Feeds ===
echo "Feeds:"
assert "RSS 200" "200" "$(curl -sI "$SITE/rss.xml" -o /dev/null -w '%{http_code}')"
assert "Sitemap 200" "200" "$(curl -sI "$SITE/sitemap-index.xml" -o /dev/null -w '%{http_code}')"

# === OG image ===
echo "Assets:"
assert "OG default 200" "200" "$(curl -sI "$SITE/og-default.png" -o /dev/null -w '%{http_code}')"
assert "Dynamic OG 200" "200" "$(curl -sI "$SITE/og/zero-trust-notes.png" -o /dev/null -w '%{http_code}')"

# === API ===
echo "API endpoints:"
POPULAR=$(curl -s "$SITE/api/popular")
assert_contains "Popular returns JSON" '"posts"' "$POPULAR"

# === Security headers ===
echo "Security headers:"
HEADERS=$(curl -sI "$SITE/")
assert_contains "HSTS" "strict-transport-security" "$HEADERS"
assert_contains "CSP" "content-security-policy" "$HEADERS"
assert_contains "X-Content-Type-Options" "nosniff" "$HEADERS"
assert_contains "X-Frame-Options" "DENY" "$HEADERS"
assert_contains "Permissions-Policy" "permissions-policy" "$HEADERS"
assert_contains "Referrer-Policy" "strict-origin-when-cross-origin" "$HEADERS"

# === i18n ===
echo "i18n:"
assert "EN home 200" "200" "$(curl -sI "$SITE/en/" -o /dev/null -w '%{http_code}')"
assert_contains "hreflang in home" "hreflang" "$(curl -s "$SITE/")"

# === Summary ===
echo ""
echo "===== $PASS passed, $FAIL failed ====="
[[ $FAIL -eq 0 ]] || exit 1

Chạy ~8 giây, bao phủ 19 assertion.

Tại sao smoke test quan trọng

Build pass != triển khai thành công. Lỗi âm thầm:

  • Thiếu binding trong wrangler.jsonc → Worker crash khi chạy.
  • Env var secret chưa upload → API fail khi user dùng.
  • Sai đường dẫn build asset → 404 mọi trang.
  • Header CSP / HSTS bị tắt nhầm → lùi bước về bảo mật.
  • CDN cache cũ sau khi triển khai → user vẫn thấy bản cũ.

Smoke test bắt được các tình huống này trong < 10 giây, ngay sau khi triển khai.

Mở rộng smoke test

Blog này có 19 assertion. Project của bạn có thể khác:

  • E-commerce: kiểm tra API danh sách sản phẩm, luồng giỏ hàng, redirect checkout.
  • Dashboard: kiểm tra endpoint yêu cầu auth trả 401 khi không có token.
  • Chat app: kiểm tra WebSocket upgrade.
  • RAG endpoint: kiểm tra /api/search trả kết quả hợp lý cho query test.

Nguyên tắc: assertion phải nhanh (< 1s mỗi cái) và quan trọng (nếu fail = ảnh hưởng người dùng).


Concurrent lock: tại sao cần

concurrency:
  group: deploy-${{ github.ref }}
  cancel-in-progress: true

Tình huống không có lock:

  1. Push commit A → Action A chạy (~100s).
  2. 30 giây sau, push commit B → Action B chạy song song.
  3. Action A triển khai xong commit A.
  4. Action B triển khai xong commit B → ghi đè A.
  5. Smoke test A chạy với state của B → fail sai.

Với lock:

  1. Push commit A → Action A chạy.
  2. Push commit B → Action B huỷ Action A (vì cùng group), Action B chạy.
  3. Chỉ 1 lần triển khai thành công, trạng thái nhất quán.

cancel-in-progress: true cho feature branch / PR. Cho production (main) có thể dùng cancel-in-progress: false + queue để không mất lần triển khai.


Preview environment: staging-lite

Cấu hình env trong wrangler.jsonc

{
  "name": "my-app",
  "main": "src/index.ts",
  "compatibility_date": "2026-05-01",
  "d1_databases": [
    { "binding": "DB", "database_name": "my-db-prod", "database_id": "..." }
  ],

  "env": {
    "preview": {
      "name": "my-app-preview",
      "d1_databases": [
        { "binding": "DB", "database_name": "my-db-preview", "database_id": "..." }
      ]
    }
  }
}

Triển khai preview:

npx wrangler deploy --env preview

Chạy tại my-app-preview.<subdomain>.workers.dev, D1 riêng, không ảnh hưởng production.

Workflow cho PR preview

on:
  pull_request:
    branches: [main]

jobs:
  deploy-preview:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 22, cache: npm }
      - run: npm ci
      - run: npm test
      - run: npm run build
      - name: Deploy preview
        run: npx wrangler deploy --env preview
        env:
          CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}

      - name: Comment preview URL on PR
        uses: actions/github-script@v7
        with:
          script: |
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: `Preview deployed: https://my-app-preview.${{ secrets.CF_SUBDOMAIN }}.workers.dev`
            });

PR checker thấy URL preview, kiểm tra trước khi merge main.

Cấu hình multi-environment

{
  "env": {
    "preview": { ... },
    "staging": { ... },
    "prod": { ... }
  }
}

Triển khai staging chỉ khi merge vào branch staging, prod chỉ merge vào main. Pattern:

on:
  push:
    branches: [main, staging]

jobs:
  deploy:
    steps:
      - name: Deploy
        run: |
          if [ "${{ github.ref }}" = "refs/heads/main" ]; then
            npx wrangler deploy --env prod
          elif [ "${{ github.ref }}" = "refs/heads/staging" ]; then
            npx wrangler deploy --env staging
          fi

Rollback trong 10 giây

Via Wrangler

npx wrangler rollback

Revert về lần triển khai trước đó. Hiện danh sách version để chọn:

npx wrangler deployments list
# → version-id: 01234567-abcd-...

npx wrangler rollback --version-id 01234567-abcd-...

Via dashboard

Cloudflare dashboard → Workers → Select Worker → Deployments → Rollback.

Via CI workflow

Workflow riêng cho tình huống khẩn cấp:

# .github/workflows/rollback.yml
name: Emergency rollback

on:
  workflow_dispatch:
    inputs:
      version_id:
        description: 'Version ID to rollback to (leave blank for previous)'
        required: false

jobs:
  rollback:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 22 }
      - run: npm ci
      - name: Rollback
        run: |
          if [ -n "${{ github.event.inputs.version_id }}" ]; then
            npx wrangler rollback --version-id ${{ github.event.inputs.version_id }}
          else
            npx wrangler rollback
          fi
        env:
          CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
      - name: Smoke test
        run: scripts/smoke.sh

Kích hoạt thủ công qua GitHub Actions UI. Rollback + smoke test trong < 30 giây.

Khi nào cần rollback

  • Smoke test fail sau khi triển khai.
  • Khách hàng than phiền sau khi triển khai.
  • Metric tăng đột biến (tỉ lệ lỗi, độ trễ).
  • Lỗ hổng bảo mật được báo cáo.

Workers giữ lại một số deployment gần nhất để rollback (10 bản qua wrangler deployments list). Nếu cần giữ version nào lâu hơn, tag commit tương ứng trong git — đó mới là source of truth rollback thật sự.


Secret rotation pattern

Secret trong Cloudflare:

wrangler secret put API_KEY
# nhập giá trị

Xoay secret:

  1. Tạo secret mới ở provider (Resend, AWS, v.v.).
  2. wrangler secret put API_KEY với giá trị mới.
  3. Không cần triển khai lại — secret được inject khi chạy.
  4. Test qua API endpoint dùng secret.
  5. Thu hồi secret cũ ở provider.

Cloudflare không có tính năng “dual-credential” có sẵn — muốn xoay không downtime, set 2 secret (API_KEY_V1, API_KEY_V2), code thử V1 → phương án dự phòng V2.

Lịch xoay:

  • Hàng năm: secret ít dùng (backup webhook), xoay thủ công mỗi năm.
  • Hàng quý: secret trên đường nóng (API key chính), xoay bằng script + alert.
  • On-demand: khi nghi ngờ bị lộ.

Gotchas

① Wrangler version cache

- run: npx wrangler deploy

npx tải wrangler mới mỗi lần chạy → chậm + có thể breaking change.

Sửa: pin phiên bản trong package.json:

"devDependencies": {
  "wrangler": "4.87.0"
}

Dùng qua npm run:

- run: npm run deploy  # script deploy trong package.json

② Node version mismatch

with:
  node-version: 22

Pin major version. Minor update OK. Wrangler có thể yêu cầu Node tối thiểu nhất định.

③ Secret hiện trong log

- run: echo "Token: ${{ secrets.CLOUDFLARE_API_TOKEN }}"  # GitHub tự che

GitHub Actions tự che secret trong log output. Nhưng đừng pipe secret ra stdout dài dòng — đôi khi việc che fail với multi-line.

④ Smoke test phụ thuộc propagation

# Ngay sau khi triển khai, CDN chưa propagate hết
wrangler deploy
sleep 5  # chờ propagation
scripts/smoke.sh

wrangler deploy return khi Worker đã upload xong. Nhưng CDN cache / DNS propagation có thể chậm vài giây. Smoke test chạy ngay → có thể hit bản cũ.

Blog này chạy smoke test sau khi wrangler deploy return, KHÔNG sleep. Trong ~1.5 năm chưa gặp false positive do propagation. Nếu gặp, thêm sleep 5 trước smoke.

⑤ Lỗi D1 migration không rollback auto

Workflow CI apply D1 migration:

- run: npx wrangler d1 migrations apply my-db --remote

Nếu migration fail giữa chừng, D1 không tự rollback. Worker triển khai thành công nhưng schema không nhất quán.

Sửa: luôn apply migration trước khi triển khai, test local trước khi push.

workflow_dispatch không chạy test

on:
  push:
    branches: [main]
  workflow_dispatch:  # kích hoạt thủ công

Kích hoạt thủ công chạy cùng workflow, cùng step. Nhưng đôi khi team dùng workflow_dispatch để triển khai mà không test → nguy hiểm.

Nếu cần skip test (hotfix khẩn cấp), tạo workflow riêng deploy-hotfix.yml với cờ rõ ràng + approver.


Monitoring sau khi triển khai

CI không thay thế monitoring lúc chạy:

  • Tail Workers: stream log realtime (Part 17).
  • Analytics Engine: event tracking (Part 17).
  • Cloudflare dashboard: requests, errors, CPU time theo Worker.
  • Bên thứ ba: Sentry, Datadog với consumer tail.

Thiết lập alert:

  • Tỉ lệ lỗi > 1% → Slack alert.
  • Độ trễ P95 > 500ms → email alert.
  • Smoke test fail → page oncall.

Chi tiết ở Part 17.


Production checklist

  • Scoped API token (không Global API Key).
  • concurrency.group với cancel-in-progress hoặc queue.
  • permissions: contents: read (quyền tối thiểu cho token GitHub).
  • timeout-minutes đặt hợp lý.
  • npm ci (không npm install).
  • Bước test chạy trước khi triển khai.
  • Smoke test sau khi triển khai với ≥ 5 assertion quan trọng.
  • Workflow rollback có sẵn (kích hoạt thủ công).
  • Preview environment cho PR.
  • Lịch xoay secret (tối thiểu hàng năm / hàng quý).
  • Pin phiên bản Wrangler trong package.json.
  • Pin phiên bản Node (major) trong actions/setup-node.

Kết

Pipeline Worker đơn giản: push → test → build → triển khai → smoke. Mỗi bước đều quan trọng, bỏ một bước là chuẩn bị sẵn chỗ cho sự cố âm thầm.

3 điểm không được bỏ qua: scoped token, concurrent lock, smoke test. 3 cái này phân biệt pipeline “chạy được” và pipeline “production-grade”.

Block 3 (Framework) kết thúc. Block 4 bắt đầu với Part 13: Workers AI và AI Gateway — catalog model inference, giá, pattern cache, khi nào gọi Workers AI vs Bedrock/OpenAI.


Tham khảo