Buổi 11: Multi-Platform CI + Branch Protection Rules
Mục tiêu Buổi Học
- Hiểu sự khác biệt giữa GitHub Actions, GitLab CI, Bitbucket Pipelines
- Implement branch protection rules trên GitHub
- Convert 8-Gate Pipeline sang GitLab CI syntax
- Thực hành: PR không pass CI → không merge được
PHẦN 1: LÝ THUYẾT (35 phút)
Multi-Platform CI Comparison
Không phải lúc nào code cũng deploy trên GitHub. Công ty có thể xài GitLab, Bitbucket, hay Jenkins. Đây là bảng so sánh syntax:
Bảng So Sánh GitHub Actions vs GitLab CI vs Bitbucket
| Feature | GitHub Actions | GitLab CI | Bitbucket Pipelines |
|---|---|---|---|
| Config File | .github/workflows/*.yml | .gitlab-ci.yml | bitbucket-pipelines.yml |
| Job Definition | jobs: { job-name: { } } | job-name: { } | pipelines: { default: [ - step: ] } |
| Job Dependencies | needs: [prev-job] | dependencies: [prev-job] | Sequential steps by default |
| Environment Variables | env: in job | variables: global or job-level | variables: |
| Artifacts Upload | actions/upload-artifact | artifacts: { paths: [...] } | artifacts: { paths: [...] } |
| Caching | actions/cache | cache: { paths: [...] } | caches: [custom] |
| Conditional Execution | if: condition | only: / except: or rules: | condition: |
| Secret Management | Settings → Secrets → Actions | Settings → CI/CD → Variables | Repository Settings → Pipelines → Secrets |
| Parallelization | Multiple jobs, no needs: | parallel: n | Multiple steps |
| Cost Model | Free (2000 min/month for private) | Free (400 min/month) | Free (50 min/month) |
| Best For | GitHub-hosted code | Enterprise (GitLab-hosted) | Bitbucket-hosted code |
GitHub Actions Workflow Anatomy
name: "Pipeline Name" # Workflow tên
on: # Trigger điều kiện
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs: # Tất cả jobs
job-1: # Job tên
runs-on: ubuntu-latest # Runner (máy chạy)
needs: [] # Dependencies
steps: # Steps trong job
- uses: actions/checkout@v4
- run: npm ciGitLab CI Workflow Anatomy
stages: # Pipeline stages (sequential)
- stage-1
- stage-2
variables: # Global env vars
NODE_VERSION: "20"
job-1: # Job tên
stage: stage-1
image: node:20 # Docker image (alternative to runner)
cache:
paths:
- node_modules
artifacts:
paths:
- dist/
dependencies: [previous-job] # Phụ thuộc vào job nào
only:
- main # Chỉ chạy trên main branch
script:
- npm ci
- npm run testKey Differences Summary
GitHub Actions:
- Job-based, có
needs:để serial execution - Dùng Actions Marketplace cho setup (checkout, node, etc)
- Runner là GH-hosted hoặc self-hosted
- Best fit: GitHub repos
GitLab CI:
- Stage-based (stage 1 → stage 2 → stage 3)
- Mỗi stage gồm nhiều jobs chạy parallel
- Dùng Docker image
- Best fit: GitLab instances, enterprise
Bitbucket:
- Step-based, sequential by default
- Tích hợp JIRA issues
- Limited free tier (50 min/month)
- Best fit: Atlassian suite users
Branch Protection Rules — Tại Sao?
Bài toán: Bạn có branch main. Developer X tạo PR, code chưa test, nhưng anh ta force merge. Disaster.
Giải pháp: Branch Protection Rules.
Push to main
↓
Blocked (branch protected)
↓
Create PR (forces code review)
↓
CI must pass (8-gate pipeline)
↓
Require 1 approved review
↓
Approve + CI pass → Can mergeRules:
- Require status checks: PR không merge nếu CI fail
- Require code review: 1 người khác phải approve
- Dismiss stale reviews: Nếu code change → previous approvals invalid
- Linear history: Không allow merge commits, chỉ squash/rebase (clean history)
- Require branches to be up to date: PR phải rebase main trước merge
cm-identity-guard Concept
Từ cm-identity-guard, trước khi push code, verify rằng:
- Git user email == công ty email
- Commits có GPG signature
- SSH key được authorized
Ví dụ:
git config --global user.email "your@company.vn"
git config --global commit.gpgsign true
git config --global user.signingkey YOUR_GPG_KEY_ID
# Bây giờ mỗi commit tự động signed
git commit -m "message" # Auto-sign
# Push → GitHub verify signature → ✓ Verified badgePHẦN 2: THỰC HÀNH (50 phút)
BÀI TẬP 1: Convert 8-Gate Pipeline to GitLab CI (20 phút)
Yêu cầu:
- Tạo file
.gitlab-ci.yml - Convert tất cả 8 gates từ GitHub Actions
- Use stages để enforce sequential
- Implement artifacts passing giữa stages
File: .gitlab-ci.yml
image: node:20
variables:
NODE_VERSION: "20"
NPM_CACHE_FOLDER: .npm
NODE_ENV: "production"
stages:
- gate-0
- gate-0-5
- gate-1
- gate-2
- gate-3
- gate-4
- gate-5
- gate-6
- summary
cache:
key:
files:
- package-lock.json
paths:
- node_modules/
- .npm/
# ============================================
# GATE 0: Secret Hygiene
# ============================================
secret-hygiene:
stage: gate-0
script:
- echo "Checking for tracked .env files..."
- if git ls-files | grep -E '\.env|\.env\.local'; then
echo "ERROR: .env files are tracked in Git!";
exit 1;
fi
- echo "✓ No .env files tracked"
- 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"
only:
- merge_requests
- main
- develop
# ============================================
# GATE 0.5: Security Scan
# ============================================
security-scan:
stage: gate-0-5
before_script:
- npm ci --prefer-offline --no-audit
script:
- echo "Running npm audit..."
- 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"
artifacts:
reports:
dependency_scanning: audit-report.json
expire_in: 1 hour
only:
- merge_requests
- main
- develop
# ============================================
# GATE 1: Syntax Check
# ============================================
syntax-check:
stage: gate-1
script:
- echo "Running syntax check on app.js..."
- node -c public/static/app.js
- echo "✓ app.js syntax OK"
- echo "Validating package.json..."
- node -e "JSON.parse(require('fs').readFileSync('package.json', 'utf8'))"
- echo "✓ package.json valid"
- |
if [ -f ".eslintrc.json" ]; then
npx eslint public/static/app.js --max-warnings 0 || true
echo "✓ ESLint check passed"
fi
only:
- merge_requests
- main
- develop
# ============================================
# GATE 2: Test Suite (5 Test Layers)
# ============================================
test-suite:
stage: gate-2
before_script:
- npm ci --prefer-offline --no-audit
script:
- echo "Running test:gate (5 test layers)..."
- npm run test:gate
artifacts:
reports:
junit: coverage/junit.xml
paths:
- coverage/
- coverage/summary.json
expire_in: 30 days
coverage: '/Lines\s*:\s*(\d+\.\d+)%/'
only:
- merge_requests
- main
- develop
# ============================================
# GATE 3: i18n Parity
# ============================================
i18n-check:
stage: gate-3
script:
- |
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
allow_failure: true
only:
- merge_requests
- main
- develop
# ============================================
# GATE 4: Build
# ============================================
build:
stage: gate-4
before_script:
- npm ci --prefer-offline --no-audit
script:
- echo "Building application..."
- npm run build
- echo "Checking build output..."
- |
if [ -d "dist" ]; then
du -sh dist/
echo "✓ Build completed"
else
echo "✗ dist/ directory not created"
exit 1
fi
artifacts:
paths:
- dist/
expire_in: 1 hour
only:
- merge_requests
- main
- develop
# ============================================
# GATE 5: Dist Verify
# ============================================
dist-verify:
stage: gate-5
script:
- 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 -c%s "$file" 2>/dev/null || stat -f%z "$file" 2>/dev/null)
echo " ✓ $file ($SIZE bytes)"
done
- echo "✓ All required dist files present"
- 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/"
dependencies:
- build
only:
- merge_requests
- main
- develop
# ============================================
# GATE 6: Deploy + Smoke Test
# ============================================
smoke-test:
stage: gate-6
before_script:
- npm ci --prefer-offline --no-audit
script:
- 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"
dependencies:
- dist-verify
only:
- main
# ============================================
# FINAL: Summary Job
# ============================================
all-gates-passed:
stage: summary
script:
- |
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 "================================================"
when: on_success
only:
- main
- developCác điểm khác so với GitHub Actions:
- Stages: Tuần tự (gate-0 → gate-0.5 → ... → gate-6)
- Artifacts: Passing giữa stages qua
artifacts:+dependencies: - Docker image:
image: node:20(default cho tất cả jobs) - Variables: Khai báo global top-level
- Cache: GitLab quản lý
.npmfolder - Conditions:
only:cho branches,allow_failure:cho optional gates
BÀI TẬP 2: Enable Branch Protection Rules (20 phút)
Bước 1: Vào GitHub Repo Settings
Settings → Branches → Add ruleBước 2: Configure Protection Rule for main Branch
Branch name pattern: mainChecks to Enable:
✓ Require pull request reviews before merging
- Required number of reviews: 1
- Dismiss stale pull request approvals: ON
- Require code owner reviews: OFF (unless you have CODEOWNERS)
✓ Require status checks to pass before merging
- Require branches to be up to date: ON
- Status checks required:
- ci/github/secret-hygiene
- ci/github/security-scan
- ci/github/syntax-check
- ci/github/test-suite
- ci/github/i18n-check
- ci/github/build
- ci/github/dist-verify
- ci/github/smoke-test
✓ Require conversation resolution before merging
- ON (resolve all comments trước merge)
✓ Require signed commits
- ON (enforce GPG signatures)
✓ Require linear history
- ON (no merge commits, chỉ squash/rebase)
✓ Require deployments to succeed before merging
- Required deployment environments: (nếu có)
✓ Restrict who can push to matching branches
- (Optional) giới hạn push access
Kết quả:
Branch Protection Rules:
├─ Require PR reviews: 1 approval required
├─ Status checks: 8 gates must pass
├─ Linear history: Only squash/rebase merges
├─ Signed commits: GPG signature required
└─ Conversations: All comments must be resolvedBÀI TẬP 3: Test Branch Protection (10 phút)
Scenario 1: Tạo PR, CI Fail
# Tạo branch
git checkout -b feature/test-protection
# Thêm failing test
echo 'it("fail", () => expect(1).toBe(2));' >> tests/unit/main.test.js
git add tests/unit/main.test.js
git commit -m "Test: intentional failure"
git push origin feature/test-protection- Vào GitHub, tạo PR
- CI chạy → test fail
- PR status: "Checks failed"
- Merge button: DISABLED
- Message: "All status checks must pass"
Scenario 2: Fix code, CI Pass, Try to Merge Without Review
# Fix test
echo 'it("pass", () => expect(1).toBe(1));' >> tests/unit/main.test.js
git add tests/unit/main.test.js
git commit -m "Fix: correct test"
git push origin feature/test-protection- CI rerun → all gates pass
- PR status: "Checks passed"
- Merge button: Still DISABLED
- Message: "1 approval required"
- Require: Assign reviewer → approve PR → then merge
Scenario 3: Merge Successfully
Step 1: Request review (Assign someone)
Step 2: Reviewer approves
Step 3: CI passes
Step 4: Merge button ENABLED
Step 5: Click "Squash and merge"
Step 6: PR closed, branch deletedPHẦN 3: ADVANCED — GitLab vs GitHub Syntax Cheat Sheet
Workflow Trigger Comparison
GitHub Actions:
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
schedule:
- cron: '0 2 * * *'GitLab CI:
only:
- main
- develop
- merge_requests
# OR (newer syntax)
rules:
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "main"'
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'Job Dependencies
GitHub Actions:
jobs:
job-1:
runs-on: ubuntu-latest
job-2:
needs: [job-1]GitLab CI:
stages:
- stage-1
- stage-2
job-1:
stage: stage-1
job-2:
stage: stage-2
dependencies: [job-1]Conditional Execution
GitHub Actions:
steps:
- if: github.event_name == 'pull_request'
run: echo "This is a PR"GitLab CI:
job:
script:
- echo "Running"
only:
- merge_requests
except:
- schedulesEnvironment Variables
GitHub Actions:
jobs:
job:
env:
MY_VAR: value
steps:
- run: echo $MY_VARGitLab CI:
variables:
MY_VAR: value
job:
variables:
JOB_VAR: job-level
script:
- echo $MY_VAR $JOB_VARPHẦN 4: QUIZ & HOMEWORK (5 phút)
5 Câu Quiz
Q1: GitLab CI uses _______ để organize jobs sequentially. A. needs:
B. stages: ✓
C. dependencies: (cái này là alternative) D. requires:
Q2: Nếu muốn prevent merge khi CI fail, sử dụng: A. .github/workflows/ci.yml config
B. GitHub Branch Protection Rules ✓
C. Cài đặt repo-level
D. Git hooks local
Q3: Branch Protection Rule "Require linear history" là gì? A. Merge commits không allowed, chỉ squash/rebase ✓
B. Commits phải có GPG signature
C. Mỗi commit phải có code review
D. Commits phải theo alphabetical order
Q4: Tại sao cần "Dismiss stale pull request approvals"? A. Vì review quá lâu, không còn valid
B. Vì code changed sau approval, cần re-review ✓
C. Vì reviewer quên git pull
D. Vì lỗi GitHub
Q5: GitLab CI allow_failure: true có ý nghĩa gì? A. Job chạy, nhưng không fail pipeline nếu job fail ✓
B. Job bị skip
C. Job chạy song song
D. Job chạy ngoài container
Homework
Convert GitHub Actions → Bitbucket Pipelines:
- Research Bitbucket
bitbucket-pipelines.ymlsyntax - Convert 8-gate pipeline sang Bitbucket
- Document khác biệt so với GitHub/GitLab
- Research Bitbucket
Setup GitLab CI locally (nếu có GitLab instance):
- Push
.gitlab-ci.ymllên GitLab repo - Watch runner execute pipeline
- Verify stages execute sequentially
- Push
Add PR template:
- Tạo
.github/pull_request_template.md - Require checklist: "[ ] Tests passing", "[ ] No hardcoded secrets"
- Test: tạo PR without checklist → encourage completion
- Tạo
Document your CI/CD:
- Viết
docs/CI_CD.mdexplaining 8 gates - Include screenshot của GitHub Actions run
- Include screenshot của branch protection rules
- Viết
TaskFlow Checkpoint
Objectives:
- [ ]
.gitlab-ci.ymlcreate + working - [ ] Branch protection rules enabled on
main - [ ] PR fail CI → cannot merge
- [ ] PR need review + CI pass → can merge
- [ ] Signed commits enforced (gpg)
Evidence:
- Screenshot: GitLab pipeline running 8 stages
- Screenshot: GitHub branch protection settings
- Screenshot: PR blocked by CI (can't merge)
- Screenshot: PR approved + CI pass (can merge)
By end of session: Multi-platform CI setup + branch protection enforced.
Tài Liệu Tham Khảo
- GitHub Branch Protection: https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/about-protected-branches
- GitLab CI/CD: https://docs.gitlab.com/ee/ci/
- Bitbucket Pipelines: https://bitbucket.org/product/features/pipelines
- cm-safe-deploy (trong course materials)
- cm-identity-guard (trong course materials)