Skip to content

Buổi 10: GitHub Actions — 8-Gate Pipeline

Mục tiêu Buổi Học

  • Hiểu rõ 8-Gate Pipeline và tại sao nó QUAN TRỌNG
  • Xây dựng complete GitHub Actions CI workflow cho TaskFlow
  • Thực hành: deploy KHÔNG được chạy khi bất kỳ gate nào fail

PHẦN 1: LÝ THUYẾT (30 phút)

8-Gate Pipeline — Từng Gate Chi Tiết

Từ cm-safe-deploy, đây là 8 gate bắt buộc trước khi deploy:

┌─────────────────────────────────────────────────────┐
│             8-GATE PIPELINE — TASKFLOW              │
├─────────────────────────────────────────────────────┤
│ Gate 0     | Secret Hygiene (<0.5s)                │
│            | Check: không có .env tracked           │
├─────────────────────────────────────────────────────┤
│ Gate 0.5   | Security Scan: Snyk + Aikido         │
│            | Scan: npm packages, code vulnerabilities│
├─────────────────────────────────────────────────────┤
│ Gate 1     | Syntax Check (<1s)                   │
│            | Run: node -c public/static/app.js     │
├─────────────────────────────────────────────────────┤
│ Gate 2     | Test Suite: 5 Test Layers            │
│            | Run: npm run test:gate                 │
├─────────────────────────────────────────────────────┤
│ Gate 3     | i18n Parity                          │
│            | Check: all keys have EN, VI translation│
├─────────────────────────────────────────────────────┤
│ Gate 4     | Build                                 │
│            | Run: npm ci && npm run build          │
├─────────────────────────────────────────────────────┤
│ Gate 5     | Dist Verify                          │
│            | Check: dist/ có đủ assets            │
├─────────────────────────────────────────────────────┤
│ Gate 6     | Deploy + Smoke Test                  │
│            | Deploy + test: GET / returns 200 OK  │
└─────────────────────────────────────────────────────┘

IRON LAW (Luật Sắt):

NO DEPLOY WITHOUT PASSING ALL GATES.
GATES ARE SEQUENTIAL.
EACH MUST PASS BEFORE THE NEXT RUNS.

Tại Sao Cần Sequential Gates?

Code có thể thay đổi sau khi test.

  • Test viết lúc 2:00 PM, deploy lúc 3:00 PM. Trong 1 giờ, ai modify code?
  • Run fresh test trước deploy.

Build không phải lúc nào cũng work.

  • Viết Node.js code OK, nhưng build tool tạo dist/ sai format
  • Syntax check KHÔNG bắt được lỗi build
  • Cần gate riêng cho build + verify output

i18n bugs lặn lẫn đến production.

  • Missing translation key → blank string in UI
  • Tests không catch vì tests chạy English
  • Cần explicit parity check

Deploy là action CUỐI CÙNG, KHÔNG thể undo.

  • CI runs AFTER push (bạn đã commit rồi)
  • Deploy runs AFTER CI (bạn không thể revert từ production)
  • Catch bugs BEFORE push, BEFORE deploy

Rationalization Table — 8 Cái Cớ Sai

Mỗi kỳ lại có engineers muốn skip gates. Đây là 8 cái cớ và tại sao sai:

#Cái CớThực TếHậu QuảGate Bảo Vệ
1"Tests passed earlier"Code changed since thenRun in stale environmentGate 2 (Test Suite)
2"Build always works"Until it doesn't. Ship 0KB assetsUsers get 404 on CSS/JSGate 4, 5
3"It's a one-line change"1 line broke 600 lines of app.jsCascade failuresGate 1, 2
4"CI will catch it"CI runs AFTER push. Too late to revertCommit history pollutedAll (catch before push)
5"Just a hotfix"Hotfixes need MORE testing, not lessBad hotfix breaks mainGate 2 (5 layers)
6"Syntax check is redundant"node -c takes 0.5s, prevented March 2026 disasterProduction down 2 hoursGate 1
7"i18n parity is overkill"Missing key = blank in UIVietnamese users see empty stringsGate 3
8"dist/ is always complete"Build tools silently skip assetsUsers get broken appGate 5

Bài học: Mỗi gate giải quyết một class lỗi cụ thể. Skip 1 gate = 1 class lỗi vào production.


Sequential Execution — Tại Sao?

yaml
job: build
  needs: [test]          # Build chỉ chạy nếu test pass
  
job: deploy
  needs: [build, test]   # Deploy chỉ chạy nếu cả build + test pass

Nếu test fail → build skip → deploy skip. Không bao giờ deploy code có test fail.


PHẦN 2: THỰC HÀNH (55 phút)

BÀI TẬP 1: Viết Complete GitHub Actions CI (25 phút)

Yêu cầu:

  1. Tạo file .github/workflows/ci.yml
  2. Implement tất cả 8 gates
  3. Mỗi gate là một job riêng
  4. Use needs: để enforce sequential

File: .github/workflows/ci.yml

yaml
name: "8-Gate CI Pipeline"

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main, develop]

jobs:
  # ============================================
  # GATE 0: Secret Hygiene (<0.5s)
  # ============================================
  secret-hygiene:
    name: "Gate 0: Secret Hygiene"
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Check for tracked secrets
        run: |
          echo "Checking for tracked .env files..."
          if git ls-files | grep -E '\.env|\.env\.local'; then
            echo "ERROR: .env files are tracked in Git!"
            echo "Run: git rm --cached .env && git commit -m 'Remove .env'"
            exit 1
          fi
          echo "✓ No .env files tracked"

      - name: Check for AWS keys in code
        run: |
          echo "Scanning for hardcoded secrets..."
          if grep -r "AKIA[0-9A-Z]\{16\}" --include="*.js" --include="*.ts" . 2>/dev/null; then
            echo "ERROR: AWS key pattern found!"
            exit 1
          fi
          if grep -r "sk_live_\|sk_test_" --include="*.js" --include="*.ts" . 2>/dev/null; then
            echo "ERROR: Stripe key pattern found!"
            exit 1
          fi
          echo "✓ No hardcoded secrets found"

  # ============================================
  # GATE 0.5: Security Scan (Snyk + Aikido)
  # ============================================
  security-scan:
    name: "Gate 0.5: Security Scan"
    runs-on: ubuntu-latest
    needs: [secret-hygiene]
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: "20"
          cache: "npm"

      - name: Install dependencies
        run: npm ci

      - name: Run npm audit
        run: npm audit --audit-level=high || true

      - name: Check vulnerable packages
        run: |
          echo "Checking for known vulnerabilities..."
          npm audit --json > audit-report.json || true
          VULN_COUNT=$(jq '.metadata.vulnerabilities.total' audit-report.json)
          if [ "$VULN_COUNT" -gt 0 ]; then
            echo "Found $VULN_COUNT vulnerabilities"
            jq '.vulnerabilities[] | select(.severity=="high" or .severity=="critical")' audit-report.json
            exit 1
          fi
          echo "✓ No high/critical vulnerabilities"

  # ============================================
  # GATE 1: Syntax Check (<1s)
  # ============================================
  syntax-check:
    name: "Gate 1: Syntax Check"
    runs-on: ubuntu-latest
    needs: [secret-hygiene]
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: "20"

      - name: Check app.js syntax
        run: |
          echo "Running syntax check on app.js..."
          node -c public/static/app.js
          if [ $? -eq 0 ]; then
            echo "✓ app.js syntax OK"
          else
            echo "✗ app.js has syntax errors"
            exit 1
          fi

      - name: Check package.json
        run: |
          echo "Validating package.json..."
          node -e "JSON.parse(require('fs').readFileSync('package.json', 'utf8'))" && echo "✓ package.json valid"

      - name: ESLint (if configured)
        run: |
          if [ -f ".eslintrc.json" ]; then
            npx eslint public/static/app.js --max-warnings 0 || true
            echo "✓ ESLint check passed"
          fi

  # ============================================
  # GATE 2: Test Suite (5 Test Layers)
  # ============================================
  test-suite:
    name: "Gate 2: Test Suite"
    runs-on: ubuntu-latest
    needs: [syntax-check]
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: "20"
          cache: "npm"

      - name: Install dependencies
        run: npm ci

      - name: Run test:gate (5 layers)
        run: npm run test:gate
        env:
          CI: true
          NODE_ENV: test

      - name: Upload coverage reports
        uses: actions/upload-artifact@v4
        if: always()
        with:
          name: coverage-reports
          path: coverage/

      - name: Comment on PR with coverage
        if: github.event_name == 'pull_request'
        uses: actions/github-script@v7
        with:
          script: |
            const fs = require('fs');
            if (fs.existsSync('coverage/summary.json')) {
              const summary = JSON.parse(fs.readFileSync('coverage/summary.json', 'utf8'));
              const total = summary.total;
              github.rest.issues.createComment({
                issue_number: context.issue.number,
                owner: context.repo.owner,
                repo: context.repo.repo,
                body: `## Test Coverage\n- Lines: ${total.lines.pct}%\n- Functions: ${total.functions.pct}%\n- Statements: ${total.statements.pct}%`
              });
            }

  # ============================================
  # GATE 3: i18n Parity (EN ↔ VI)
  # ============================================
  i18n-check:
    name: "Gate 3: i18n Parity"
    runs-on: ubuntu-latest
    needs: [test-suite]
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: "20"

      - name: Create i18n check script
        run: |
          cat > check-i18n.js << 'EOF'
          const fs = require('fs');
          const path = require('path');

          const localesDir = 'public/locales';
          if (!fs.existsSync(localesDir)) {
            console.log('✓ No i18n files to check');
            process.exit(0);
          }

          const en = JSON.parse(fs.readFileSync(path.join(localesDir, 'en.json'), 'utf8'));
          const vi = JSON.parse(fs.readFileSync(path.join(localesDir, 'vi.json'), 'utf8'));

          let hasError = false;

          Object.keys(en).forEach(key => {
            if (!(key in vi)) {
              console.error(`✗ Missing in VI: "${key}"`);
              hasError = true;
            }
          });

          Object.keys(vi).forEach(key => {
            if (!(key in en)) {
              console.error(`✗ Extra in VI: "${key}"`);
              hasError = true;
            }
          });

          if (!hasError) {
            console.log('✓ i18n Parity OK: EN and VI have same keys');
            process.exit(0);
          } else {
            process.exit(1);
          }
          EOF

          node check-i18n.js
        continue-on-error: true

  # ============================================
  # GATE 4: Build
  # ============================================
  build:
    name: "Gate 4: Build"
    runs-on: ubuntu-latest
    needs: [i18n-check]
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: "20"
          cache: "npm"

      - name: Install dependencies
        run: npm ci

      - name: Build application
        run: npm run build
        env:
          NODE_ENV: production

      - name: Check build output size
        run: |
          if [ -d "dist" ]; then
            du -sh dist/
            echo "✓ Build completed"
          else
            echo "✗ dist/ directory not created"
            exit 1
          fi

      - name: Upload build artifacts
        uses: actions/upload-artifact@v4
        with:
          name: build-dist
          path: dist/
          retention-days: 1

  # ============================================
  # GATE 5: Dist Verify
  # ============================================
  dist-verify:
    name: "Gate 5: Dist Verify"
    runs-on: ubuntu-latest
    needs: [build]
    steps:
      - name: Download build artifacts
        uses: actions/download-artifact@v4
        with:
          name: build-dist
          path: dist/

      - name: Verify dist structure
        run: |
          echo "Verifying dist/ structure..."
          
          REQUIRED_FILES=(
            "dist/index.html"
            "dist/app.js"
            "dist/app.css"
          )
          
          for file in "${REQUIRED_FILES[@]}"; do
            if [ ! -f "$file" ]; then
              echo "✗ Missing required file: $file"
              exit 1
            fi
            SIZE=$(stat -f%z "$file" 2>/dev/null || stat -c%s "$file" 2>/dev/null)
            echo "  ✓ $file ($SIZE bytes)"
          done
          
          echo "✓ All required dist files present"

      - name: Check for incomplete builds
        run: |
          echo "Checking for incomplete/empty files..."
          find dist -type f -size 0 | while read empty; do
            echo "✗ Empty file: $empty"
            exit 1
          done
          echo "✓ No empty files in dist/"

      - name: Verify asset integrity
        run: |
          echo "Verifying HTML syntax..."
          if command -v tidy &> /dev/null; then
            tidy -q dist/index.html || true
          else
            echo "⚠ tidy not installed, skipping HTML validation"
          fi
          echo "✓ dist/ verification passed"

  # ============================================
  # GATE 6: Deploy + Smoke Test
  # ============================================
  smoke-test:
    name: "Gate 6: Deploy + Smoke Test"
    runs-on: ubuntu-latest
    needs: [dist-verify]
    if: github.ref == 'refs/heads/main'
    steps:
      - name: Download build artifacts
        uses: actions/download-artifact@v4
        with:
          name: build-dist
          path: dist/

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: "20"

      - name: Create simple Express server for smoke test
        run: |
          cat > server.js << 'EOF'
          const express = require('express');
          const path = require('path');
          const app = express();

          app.use(express.static('dist'));
          app.get('/', (req, res) => {
            res.sendFile(path.join(__dirname, 'dist/index.html'));
          });

          const server = app.listen(3000, () => {
            console.log('Smoke test server running on port 3000');
          });

          process.on('SIGTERM', () => {
            server.close(() => {
              console.log('Server closed');
              process.exit(0);
            });
          });
          EOF

          echo "✓ Deployment package prepared"

      - name: Run smoke tests
        run: |
          echo "Running smoke tests..."
          
          if [ -f "dist/index.html" ]; then
            echo "✓ index.html exists"
          else
            echo "✗ index.html missing"
            exit 1
          fi

          if [ -f "dist/app.js" ]; then
            if grep -q "function\|const\|let" dist/app.js; then
              echo "✓ app.js contains code"
            else
              echo "✗ app.js is empty"
              exit 1
            fi
          fi

          if [ -f "dist/app.css" ]; then
            echo "✓ app.css exists"
          else
            echo "✗ app.css missing (non-critical)"
          fi

          echo "✓ All smoke tests passed"

  # ============================================
  # FINAL: All Gates Summary
  # ============================================
  all-gates-passed:
    name: "✓ All 8 Gates Passed"
    runs-on: ubuntu-latest
    needs: [smoke-test]
    if: success()
    steps:
      - name: Report success
        run: |
          echo "================================================"
          echo "✓ ALL 8 GATES PASSED"
          echo "================================================"
          echo "Gate 0:   Secret Hygiene ✓"
          echo "Gate 0.5: Security Scan ✓"
          echo "Gate 1:   Syntax Check ✓"
          echo "Gate 2:   Test Suite ✓"
          echo "Gate 3:   i18n Parity ✓"
          echo "Gate 4:   Build ✓"
          echo "Gate 5:   Dist Verify ✓"
          echo "Gate 6:   Smoke Test ✓"
          echo "================================================"
          echo "SAFE TO DEPLOY"
          echo "================================================"

Code Walkthrough:

  1. Gate 0 (secret-hygiene): Check .env không tracked, scan hardcoded AWS/Stripe keys
  2. Gate 0.5 (security-scan): npm audit, check vulnerabilities
  3. Gate 1 (syntax-check): node -c check app.js, validate package.json
  4. Gate 2 (test-suite): npm run test:gate chạy 5 test layers
  5. Gate 3 (i18n-check): Kiểm tra EN ↔ VI key parity
  6. Gate 4 (build): npm ci && npm run build → dist/
  7. Gate 5 (dist-verify): Kiểm tra index.html, app.js, app.css có đủ
  8. Gate 6 (smoke-test): Smoke test: files exist + basic integrity

BÀI TẬP 2: Push to GitHub & Watch Actions Run (15 phút)

Bước 1: Tạo repo trên GitHub

bash
git init
git add .
git commit -m "Initial commit: TaskFlow with CI pipeline"
git branch -M main
git remote add origin https://github.com/YOUR_USERNAME/taskflow.git
git push -u origin main

Bước 2: Vào GitHub → Actions tab

  • Xem workflow run in real-time
  • Watch từng gate execute sequentially
  • Verify all 8 gates PASS

Expected output:

secret-hygiene      ✓ PASSED (0.5s)
security-scan       ✓ PASSED (15s)
syntax-check        ✓ PASSED (2s)
test-suite          ✓ PASSED (30s)
i18n-check          ✓ PASSED (1s)
build               ✓ PASSED (20s)
dist-verify         ✓ PASSED (2s)
smoke-test          ✓ PASSED (5s)
all-gates-passed    ✓ PASSED (1s)

BÀI TẬP 3: Tạo PR với Bug, Xem CI Fail (15 phút)

Bước 1: Thêm bug vào test

javascript
describe('TaskFlow Unit Tests', () => {
  it('should fail intentionally', () => {
    expect(1).toBe(2);
  });
});

Bước 2: Commit + Push lên branch

bash
git checkout -b test/intentional-bug
git add tests/unit/main.test.js
git commit -m "Test: intentional bug to verify CI"
git push origin test/intentional-bug

Bước 3: Tạo PR

  • GitHub tự động chạy CI
  • Gate 2 (test-suite) FAIL
  • PR hiển thị "Checks failed"
  • KHÔNG thể merge

Bước 4: Fix bug + Rerun CI

javascript
expect(1).toBe(1);
git add tests/unit/main.test.js
git commit -m "Fix: correct the test"
git push origin test/intentional-bug
  • CI rerun automatically
  • All gates PASS
  • PR status → "Checks passed"
  • Now can merge

Bài học: CI gates prevent merge khi tests fail. No bypass.


PHẦN 3: QUIZ & HOMEWORK (10 phút)

5 Câu Quiz

Q1: Nếu Gate 2 (test-suite) fail, các gates sau (build, dist-verify, deploy) có chạy không? A. Có, chúng chạy song song
B. Không, vì Gate 2 là needs: [syntax-check] không pass ✓
C. Tuỳ config trong ci.yml
D. Tuỳ CI tool sử dụng

Q2: Gate 1 (syntax-check) với node -c takes bao lâu? A. 30 giây
B. Tuỳ kích thước project
C. <1 giây ✓
D. 5 giây

Q3: Tại sao cần Gate 3 (i18n parity)? A. Vì tests chỉ chạy tiếng Anh, không catch missing VI keys ✓
B. Vì Vietnamese users đọc code
C. Vì i18n là best practice
D. Vì bắt buộc có trong GitHub Actions

Q4: Gate 5 (dist-verify) kiểm tra cái gì? A. Tất cả tests pass
B. index.html, app.js, app.css có đủ trong dist/ ✓
C. Code syntax đúng
D. Package.json valid

Q5: "Just a hotfix, skip tests" là sai vì? A. Hotfix cũng cần qua mọi gate như code bình thường ✓
B. Vì hotfix không phải code thực sự
C. Vì hotfix chỉ fix UI
D. Vì hotfix không ảnh hưởng security


Homework

  1. Thêm Gate 0.5 (Snyk):

    • Cài Snyk CLI: npm install -g snyk
    • Integrate vào ci.yml: snyk test
    • Document cách fix vulnerabilities
  2. Thêm Branch Protection:

    • GitHub Repo Settings → Branches
    • Add rule for main branch
    • Require "ci/github/build" status check
    • Require 1 approved review
    • Test: tạo PR mà không pass CI → không merge được
  3. Optimize workflow timing:

    • Measure hiện tại: tất cả 8 gates chạy bao lâu?
    • Goal: <2 phút total
    • Hints: parallelize independent gates, cache node_modules

TaskFlow Checkpoint

Objectives:

  • [ ] .github/workflows/ci.yml complete với 8 gates
  • [ ] All gates chạy sequential
  • [ ] Gates fail properly block merge
  • [ ] Smoke test passes on main branch

Evidence:

  • Screenshot: GitHub Actions tab showing all ✓
  • Screenshot: PR fail CI → cannot merge
  • Screenshot: Fix PR pass CI → can merge

By end of session: Mọi deploy đều pass 8-gate pipeline.


Tài Liệu Tham Khảo

Powered by CodyMaster × VitePress