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 then | Run in stale environment | Gate 2 (Test Suite) |
| 2 | "Build always works" | Until it doesn't. Ship 0KB assets | Users get 404 on CSS/JS | Gate 4, 5 |
| 3 | "It's a one-line change" | 1 line broke 600 lines of app.js | Cascade failures | Gate 1, 2 |
| 4 | "CI will catch it" | CI runs AFTER push. Too late to revert | Commit history polluted | All (catch before push) |
| 5 | "Just a hotfix" | Hotfixes need MORE testing, not less | Bad hotfix breaks main | Gate 2 (5 layers) |
| 6 | "Syntax check is redundant" | node -c takes 0.5s, prevented March 2026 disaster | Production down 2 hours | Gate 1 |
| 7 | "i18n parity is overkill" | Missing key = blank in UI | Vietnamese users see empty strings | Gate 3 |
| 8 | "dist/ is always complete" | Build tools silently skip assets | Users get broken app | Gate 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?
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 passNế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:
- Tạo file
.github/workflows/ci.yml - Implement tất cả 8 gates
- Mỗi gate là một job riêng
- Use
needs:để enforce sequential
File: .github/workflows/ci.yml
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:
- Gate 0 (secret-hygiene): Check .env không tracked, scan hardcoded AWS/Stripe keys
- Gate 0.5 (security-scan): npm audit, check vulnerabilities
- Gate 1 (syntax-check): node -c check app.js, validate package.json
- Gate 2 (test-suite): npm run test:gate chạy 5 test layers
- Gate 3 (i18n-check): Kiểm tra EN ↔ VI key parity
- Gate 4 (build): npm ci && npm run build → dist/
- Gate 5 (dist-verify): Kiểm tra index.html, app.js, app.css có đủ
- 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
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 mainBướ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
describe('TaskFlow Unit Tests', () => {
it('should fail intentionally', () => {
expect(1).toBe(2);
});
});Bước 2: Commit + Push lên branch
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-bugBướ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
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
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
- Cài Snyk CLI:
Thêm Branch Protection:
- GitHub Repo Settings → Branches
- Add rule for
mainbranch - Require "ci/github/build" status check
- Require 1 approved review
- Test: tạo PR mà không pass CI → không merge được
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.ymlcomplete 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
- GitHub Actions Docs: https://docs.github.com/en/actions
- cm-safe-deploy (trong course materials)
- Workflow syntax: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions