Skip to content

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

FeatureGitHub ActionsGitLab CIBitbucket Pipelines
Config File.github/workflows/*.yml.gitlab-ci.ymlbitbucket-pipelines.yml
Job Definitionjobs: { job-name: { } }job-name: { }pipelines: { default: [ - step: ] }
Job Dependenciesneeds: [prev-job]dependencies: [prev-job]Sequential steps by default
Environment Variablesenv: in jobvariables: global or job-levelvariables:
Artifacts Uploadactions/upload-artifactartifacts: { paths: [...] }artifacts: { paths: [...] }
Cachingactions/cachecache: { paths: [...] }caches: [custom]
Conditional Executionif: conditiononly: / except: or rules:condition:
Secret ManagementSettings → Secrets → ActionsSettings → CI/CD → VariablesRepository Settings → Pipelines → Secrets
ParallelizationMultiple jobs, no needs:parallel: nMultiple steps
Cost ModelFree (2000 min/month for private)Free (400 min/month)Free (50 min/month)
Best ForGitHub-hosted codeEnterprise (GitLab-hosted)Bitbucket-hosted code

GitHub Actions Workflow Anatomy

yaml
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 ci

GitLab CI Workflow Anatomy

yaml
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 test

Key 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 merge

Rules:

  1. Require status checks: PR không merge nếu CI fail
  2. Require code review: 1 người khác phải approve
  3. Dismiss stale reviews: Nếu code change → previous approvals invalid
  4. Linear history: Không allow merge commits, chỉ squash/rebase (clean history)
  5. 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ụ:

bash
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 badge

PHẦ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:

  1. Tạo file .gitlab-ci.yml
  2. Convert tất cả 8 gates từ GitHub Actions
  3. Use stages để enforce sequential
  4. Implement artifacts passing giữa stages

File: .gitlab-ci.yml

yaml
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
    - develop

Các điểm khác so với GitHub Actions:

  1. Stages: Tuần tự (gate-0 → gate-0.5 → ... → gate-6)
  2. Artifacts: Passing giữa stages qua artifacts: + dependencies:
  3. Docker image: image: node:20 (default cho tất cả jobs)
  4. Variables: Khai báo global top-level
  5. Cache: GitLab quản lý .npm folder
  6. 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 rule

Bước 2: Configure Protection Rule for main Branch

Branch name pattern: main

Checks 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 resolved

BÀI TẬP 3: Test Branch Protection (10 phút)

Scenario 1: Tạo PR, CI Fail

bash
# 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

bash
# 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 deleted

PHẦN 3: ADVANCED — GitLab vs GitHub Syntax Cheat Sheet

Workflow Trigger Comparison

GitHub Actions:

yaml
on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]
  schedule:
    - cron: '0 2 * * *'

GitLab CI:

yaml
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:

yaml
jobs:
  job-1:
    runs-on: ubuntu-latest
  job-2:
    needs: [job-1]

GitLab CI:

yaml
stages:
  - stage-1
  - stage-2

job-1:
  stage: stage-1

job-2:
  stage: stage-2
  dependencies: [job-1]

Conditional Execution

GitHub Actions:

yaml
steps:
  - if: github.event_name == 'pull_request'
    run: echo "This is a PR"

GitLab CI:

yaml
job:
  script:
    - echo "Running"
  only:
    - merge_requests
  except:
    - schedules

Environment Variables

GitHub Actions:

yaml
jobs:
  job:
    env:
      MY_VAR: value
    steps:
      - run: echo $MY_VAR

GitLab CI:

yaml
variables:
  MY_VAR: value

job:
  variables:
    JOB_VAR: job-level
  script:
    - echo $MY_VAR $JOB_VAR

PHẦ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

  1. Convert GitHub Actions → Bitbucket Pipelines:

    • Research Bitbucket bitbucket-pipelines.yml syntax
    • Convert 8-gate pipeline sang Bitbucket
    • Document khác biệt so với GitHub/GitLab
  2. Setup GitLab CI locally (nếu có GitLab instance):

    • Push .gitlab-ci.yml lên GitLab repo
    • Watch runner execute pipeline
    • Verify stages execute sequentially
  3. 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
  4. Document your CI/CD:

    • Viết docs/CI_CD.md explaining 8 gates
    • Include screenshot của GitHub Actions run
    • Include screenshot của branch protection rules

TaskFlow Checkpoint

Objectives:

  • [ ] .gitlab-ci.yml create + 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

Powered by CodyMaster × VitePress