PR sửa một dòng CSS — đổi margin-top từ 8px sang 12px. CI chạy 23 phút mới xong. Trong 23 phút đó: install toàn bộ node_modules từ đầu (4 phút), build Docker image full (6 phút), chạy toàn bộ 1,200 test kể cả integration test gọi database (11 phút), và 2 phút scan security. Kết quả: pass. Một dòng CSS, 23 phút chờ, context switch sang việc khác, quay lại quên đang làm gì.

Sau khi tối ưu pipeline: cùng PR đó chạy 3 phút 40 giây — cache dependency, skip integration test vì không có file backend thay đổi, Docker layer cache hit 90%. Không phải phép thuật, chỉ là hiểu CI pipeline đang lãng phí ở đâu và sửa đúng chỗ.

Bài này đi qua cache strategy, parallel test, flaky test quarantine, security scan và fail-fast ordering — nguyên tắc và pattern để CI pipeline vừa nhanh vừa đáng tin, áp dụng được bất kể GitHub Actions hay GitLab CI.


Tại sao CI chậm là vấn đề thật

CI pipeline chậm không chỉ là “chờ thêm vài phút”. Nó phá vỡ feedback loop của developer — và feedback loop là thứ quyết định tốc độ ship feature của cả team.

Khi CI mất 5 phút, developer push rồi đợi, thấy kết quả, sửa nếu cần — flow liên tục. Khi CI mất 25 phút, developer push rồi chuyển sang việc khác — task switching. Khi CI xong, phải load lại context của PR cũ, nhớ mình đang làm gì, hiểu lại lỗi. Research về cognitive load cho thấy mỗi lần context switch tốn 10-15 phút recovery. Tức là CI 25 phút thực tế tốn 40 phút của developer — gần 1 giờ cho mỗi iteration.

Nhân lên cho cả team: 8 developer, mỗi người trung bình 4 PR/ngày, mỗi PR trung bình 2 CI run (lần đầu fail, sửa, chạy lại). Đó là 64 CI run/ngày. Nếu mỗi run chậm hơn 15 phút so với mức tối ưu, team mất 16 giờ/ngày — tương đương 2 developer chỉ ngồi chờ CI.

CI chậm còn gây ra hành vi xấu: developer gom nhiều thay đổi vào một PR lớn để “chạy CI một lần cho đỡ chờ”. PR lớn khó review, dễ có bug, merge conflict nhiều hơn. Hoặc developer bỏ qua CI, merge mà không chờ kết quả — rồi main branch đỏ.

Mục tiêu mình đặt cho pipeline: dưới 10 phút cho PR thông thường, dưới 5 phút cho thay đổi nhỏ. Đạt được hay không phụ thuộc vào cách bạn tổ chức pipeline, không phải hardware.


Cache — tiết kiệm lớn nhất với ít effort nhất

Dependency install và Docker build thường chiếm 30-50% thời gian CI. Và phần lớn thời gian đó là lãng phí — dependency ít thay đổi giữa các run, Docker layer dưới cùng gần như không đổi. Cache là cách biến “lặp lại” thành “bỏ qua”.

Dependency cache

Mỗi CI run install lại node_modules, vendor/, hay .venv từ đầu là lãng phí lớn nhất. package-lock.json không đổi giữa 95% các commit — tại sao phải download lại 800 MB packages mỗi lần?

Pattern chuẩn: cache dependency directory theo hash của lock file. Khi lock file không đổi, CI restore cache thay vì install từ đầu — từ 4 phút xuống 10 giây.

# GitHub Actions example
- uses: actions/cache@v4
  with:
    path: ~/.npm
    key: npm-${{ hashFiles('**/package-lock.json') }}
    restore-keys: |
      npm-      

- run: npm ci

npm ci thông minh hơn npm install trong CI — nó xoá node_modules rồi install chính xác theo lock file, đảm bảo deterministic. Kết hợp với cache ~/.npm (download cache, không phải node_modules), lần chạy đầu download và cache, lần sau chỉ link từ cache.

Với Python, cache ~/.cache/pip theo hash của requirements.txt. Với Go, cache ~/go/pkg/mod theo hash của go.sum. Pattern tương tự cho mọi ngôn ngữ — chỉ khác đường dẫn cache và lock file.

Một lưu ý mình học được muộn: đừng cache node_modules trực tiếp — cache npm/yarn download cache rồi để npm ci link từ đó. Cache node_modules có thể gây phantom dependency (package đã xoá khỏi package.json nhưng vẫn nằm trong node_modules cache, code chạy trên CI nhưng fail khi install fresh).

Build cache

Ngoài dependency, build output cũng có thể cache. TypeScript compiler có incremental mode — chỉ compile file thay đổi. Webpack, esbuild, turbopack đều hỗ trợ persistent cache. Trong CI, cache thư mục .next/cache, node_modules/.cache, hoặc tsconfig.tsbuildinfo giữa các run.

- uses: actions/cache@v4
  with:
    path: |
      .next/cache
      node_modules/.cache      
    key: build-${{ hashFiles('**/*.ts', '**/*.tsx') }}
    restore-keys: |
      build-      

restore-keys quan trọng: khi key chính miss (file thay đổi), CI tìm key gần nhất để restore partial cache. Build từ partial cache vẫn nhanh hơn build từ đầu — compiler chỉ rebuild file thay đổi.

Docker layer cache

Docker build trong CI thường chậm vì mỗi run bắt đầu từ empty cache. Nhưng Dockerfile tốt thì layer dưới cùng (base image, dependency install) ít thay đổi — chỉ layer trên cùng (copy source, build) cần rebuild.

Hai cách tận dụng Docker layer cache trong CI. Cách thứ nhất là dùng registry làm cache source:

- run: |
    docker build \
      --cache-from=registry.example.com/myapp:cache \
      --build-arg BUILDKIT_INLINE_CACHE=1 \
      -t myapp:${{ github.sha }} .
    docker tag myapp:${{ github.sha }} registry.example.com/myapp:cache
    docker push registry.example.com/myapp:cache    

Cách thứ hai, hiệu quả hơn, là dùng BuildKit cache mount với GitHub Actions cache backend:

- uses: docker/build-push-action@v5
  with:
    context: .
    cache-from: type=gha
    cache-to: type=gha,mode=max

mode=max cache tất cả layer, kể cả intermediate — tốn storage nhưng cache hit rate cao hơn đáng kể. Mình từng giảm Docker build từ 6 phút xuống 45 giây chỉ bằng GHA cache + multi-stage Dockerfile sắp xếp lại thứ tự layer (COPY package*.json trước, RUN npm ci, rồi mới COPY source).

Nguyên tắc Dockerfile cho cache hiệu quả: layer ít thay đổi ở trên, layer hay thay đổi ở dưới. COPY package*.json + RUN npm ci trước COPY . . — vì dependency ít thay đổi hơn source code.


Parallel test execution

Chạy test tuần tự trên single core là cách chậm nhất. Hầu hết CI runner hiện đại có 2-4 core — không tận dụng tất cả là lãng phí.

Chia test theo thời gian, không theo file

Cách chia naive — mỗi worker nhận số file bằng nhau — thường không hiệu quả vì file test có thời gian chạy rất khác nhau. File A chạy 0.5 giây, file B chạy 30 giây — chia đều theo số file thì worker chạy file B chờ lâu hơn nhiều.

Cách tốt hơn: chia theo thời gian chạy dự kiến. Ghi lại thời gian mỗi test file ở lần chạy trước, lần chạy sau chia sao cho tổng thời gian mỗi worker gần bằng nhau. Jest có --shard flag, pytest-xdist có load balancing, và nhiều CI platform (CircleCI, GitLab) có built-in test splitting theo timing data.

# GitHub Actions matrix strategy
strategy:
  matrix:
    shard: [1/4, 2/4, 3/4, 4/4]
steps:
  - run: npx jest --shard=${{ matrix.shard }}

Bốn worker chạy song song, mỗi worker chạy 1/4 số test. Nếu test suite 12 phút chạy tuần tự, 4 shard giảm xuống khoảng 3-4 phút (không hoàn hảo 3 phút vì overhead startup và shard không đều hoàn toàn).

Tách unit test và integration test

Unit test nhanh, không phụ thuộc bên ngoài — chạy trước, song song, fail nhanh. Integration test chậm hơn, cần database hoặc external service — chạy sau khi unit test pass, có thể trên runner riêng.

jobs:
  unit-tests:
    runs-on: ubuntu-latest
    steps:
      - run: npm test -- --testPathPattern='unit'

  integration-tests:
    needs: unit-tests
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:16
    steps:
      - run: npm test -- --testPathPattern='integration'

Unit test fail trong 2 phút → developer biết ngay, không cần chờ integration test 10 phút. Integration test chỉ chạy khi unit test pass — tiết kiệm compute và thời gian feedback.


Flaky test — kẻ thù thầm lặng

Flaky test là test đôi khi pass, đôi khi fail, với cùng code. Nó là vấn đề nghiêm trọng nhất mà hầu hết team CI đều gặp nhưng ít team giải quyết triệt để.

Tại sao flaky test nguy hiểm

Flaky test phá huỷ niềm tin vào CI. Khi developer thấy test fail, phản ứng đầu tiên nên là “code mình có bug” — nhưng nếu 30% failure là flaky test, phản ứng thành “chắc flaky thôi, re-run đi”. Rồi một ngày test fail thật sự, developer vẫn re-run, bug lọt vào main. CI pipeline mà developer không tin tưởng thì cũng bằng không có.

Flaky test cũng tốn thời gian trực tiếp: mỗi lần re-run là thêm 10-20 phút chờ. Nếu team có 5 flaky re-run mỗi ngày, đó là 1-2 giờ/ngày chỉ để chờ CI chạy lại cái đã pass.

Phát hiện flaky test

Cách đơn giản nhất: retry test fail tự động và ghi nhận. Nếu test fail lần 1 nhưng pass lần 2 với cùng code — đó là flaky.

# Jest retry
# jest.config.js
module.exports = {
  retryTimes: 2,
  reporters: [
    'default',
    ['jest-junit', { outputDirectory: 'reports' }]
  ]
};

Ghi kết quả retry vào report. Aggregate report qua nhiều CI run — test nào có tỷ lệ retry > 0 là flaky candidate. Mình dùng script đơn giản đọc JUnit XML report, đếm retry, push metric vào dashboard.

Cách kỹ hơn: chạy test suite nhiều lần trên cùng commit (ví dụ 5 lần) trong nightly job. Test nào fail ít nhất một lần trong 5 lần chạy trên cùng code — chắc chắn flaky.

Quarantine flaky test

Khi phát hiện flaky test, có hai lựa chọn: fix ngay hoặc quarantine. Fix ngay là lý tưởng nhưng không phải lúc nào cũng khả thi — flaky test do race condition hay timing issue có thể mất vài ngày để fix đúng.

Quarantine nghĩa là tách flaky test ra khỏi pipeline chính — chạy riêng, không block merge. Developer vẫn thấy kết quả nhưng flaky test không chặn PR. Quan trọng: quarantine phải đi kèm ticket fix có deadline — không phải “để đó quên luôn”. Mình đặt rule: flaky test quarantine quá 2 sprint mà chưa fix → xoá test đó. Test không đáng tin thì giữ cũng không có giá trị.

# Tag flaky test
describe('Payment flow', () => {
  // @quarantine JIRA-456 - flaky due to Stripe sandbox timeout
  it.skip('should process payment', async () => {
    // ...
  });
});

Hoặc dùng annotation/tag riêng để CI biết exclude:

npx jest --testPathIgnorePatterns='quarantine'

Nguyên nhân flaky phổ biến

Timing dependency — test assume operation hoàn thành trong X ms. Fix bằng polling/retry thay vì sleep. Shared state — test A modify database, test B đọc state bẩn từ A. Fix bằng test isolation (transaction rollback, fresh database mỗi test). External dependency — test gọi API bên ngoài, API chậm hoặc rate limit. Fix bằng mock hoặc stub. Timestamp — test check createdAt bằng new Date(), chạy qua nửa đêm thì fail. Fix bằng clock mock.


Security scan trong CI

CI không chỉ verify tính đúng — nó cũng là nơi phù hợp nhất để scan security sớm. Phát hiện vulnerability ở CI rẻ hơn rất nhiều so với phát hiện trên production.

Dependency audit

Dependency supply chain là attack vector phổ biến nhất. npm audit, pip audit, hoặc tool chuyên biệt như Snyk, Dependabot scan dependency tree tìm known vulnerability.

- name: Audit dependencies
  run: npm audit --audit-level=high

--audit-level=high chỉ fail pipeline khi có vulnerability mức high hoặc critical — moderate và low thì log warning nhưng không block. Đây là trade-off thực tế: block mọi vulnerability level thì pipeline sẽ fail liên tục vì dependency cũ, developer mệt mỏi và bắt đầu ignore.

Snyk hoặc Trivy scan sâu hơn npm audit — phát hiện vulnerability trong transitive dependency, check license compliance, và có database cập nhật nhanh hơn. Mình dùng Trivy cho container image scan:

- name: Scan container image
  uses: aquasecurity/trivy-action@master
  with:
    image-ref: myapp:${{ github.sha }}
    severity: 'CRITICAL,HIGH'
    exit-code: '1'

Trivy scan cả OS package (Alpine, Debian) lẫn application dependency trong container image — phát hiện vulnerability mà npm audit không thấy vì nó chỉ check npm packages.

SAST — static application security testing

SAST tool phân tích source code tìm security issue: SQL injection, XSS, hardcoded secret, insecure crypto. Semgrep, CodeQL, SonarQube đều chạy được trong CI.

- name: Semgrep scan
  uses: returntocorp/semgrep-action@v1
  with:
    config: >-
      p/javascript
      p/typescript
      p/owasp-top-ten      

Semgrep nhanh (scan repo trung bình trong 1-2 phút), rule community tốt, và có thể viết custom rule cho pattern đặc thù của project. Mình dùng ruleset owasp-top-ten + javascript cho mọi project Node.js — phát hiện được vài issue mà code review bỏ sót.

Secret detection

Hardcoded secret trong code — API key, password, token — là lỗi mà mình thấy xuất hiện ở mọi quy mô team. Tool như gitleaks hoặc trufflehog scan commit history tìm pattern secret.

- name: Detect secrets
  uses: gitleaks/gitleaks-action@v2
  env:
    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Chạy trên mỗi PR — nếu developer vô tình commit AWS key, CI block ngay thay vì key lên main branch rồi bị scrape bởi bot trong vài phút.


Artifact versioning và immutable build

CI không chỉ verify — nó produce artifact: Docker image, compiled binary, static bundle. Artifact cần versioning rõ ràng và immutable — build một lần, dùng mọi nơi.

Tại sao immutable artifact quan trọng

Nếu bạn build lại trên staging rồi build lại trên production — hai artifact khác nhau dù cùng code. Dependency resolution có thể khác (version range), build tool có thể khác version, environment variable khác. “Works on staging” không đảm bảo “works on production” nếu artifact khác nhau.

Pattern đúng: CI build artifact một lần, tag bằng commit SHA hoặc version, push lên registry. Staging deploy artifact đó. Production deploy cùng artifact đó — byte-for-byte giống nhau.

- name: Build and push
  run: |
    IMAGE=registry.example.com/myapp:${{ github.sha }}
    docker build -t $IMAGE .
    docker push $IMAGE    

github.sha là unique identifier cho mỗi commit — mỗi artifact map 1:1 với code state. Không dùng tag latest cho production deployment — latest là moving target, bạn không biết chính xác version nào đang chạy.

Semantic versioning cho release

Khi cần release version cho user-facing artifact (library, CLI tool), dùng semantic versioning. CI có thể tự generate version từ commit message (conventional commits) hoặc từ tag.

# Từ git tag
VERSION=$(git describe --tags --always)
docker build -t myapp:$VERSION .

Mình dùng commit SHA cho internal service (deploy liên tục, không cần version number đẹp) và semantic version cho library/package publish lên registry công khai.


Fail-fast strategy — ordering matters

Thứ tự các step trong pipeline ảnh hưởng lớn đến thời gian feedback. Nguyên tắc: chạy thứ nhanh và hay fail trước, thứ chậm và hiếm fail sau.

Sắp xếp pipeline tối ưu

Một pipeline tối ưu thường có thứ tự như thế này. Lint và format check chạy đầu tiên — mất 10-30 giây, bắt lỗi syntax, style, import sai ngay lập tức. Type check (TypeScript, mypy) chạy tiếp — mất 30-60 giây, bắt lỗi type trước khi chạy test. Unit test chạy sau type check — mất 1-3 phút, bắt logic error. Build artifact sau khi unit test pass. Integration test chạy cuối — mất 5-10 phút, cần service phụ thuộc.

jobs:
  lint:
    steps:
      - run: npm run lint
      - run: npm run format:check

  typecheck:
    steps:
      - run: npx tsc --noEmit

  unit-test:
    needs: [lint, typecheck]
    steps:
      - run: npx jest --testPathPattern='unit'

  build:
    needs: unit-test
    steps:
      - run: docker build -t myapp:$SHA .

  integration-test:
    needs: build
    steps:
      - run: npx jest --testPathPattern='integration'

  security-scan:
    needs: build
    steps:
      - run: trivy image myapp:$SHA

Nếu lint fail ở giây thứ 15, developer biết ngay không cần chờ 12 phút integration test. Nếu lint pass nhưng unit test fail phút thứ 2, vẫn tiết kiệm 10 phút so với chờ integration test.

linttypecheck có thể chạy song song vì không phụ thuộc nhau — giảm thêm thời gian. security-scan cũng chạy song song với integration-test vì cả hai cần build nhưng không phụ thuộc lẫn nhau.

Cancel pipeline khi có commit mới

Developer push commit, CI chạy. Developer nhận ra thiếu gì đó, push commit mới. CI run cũ vẫn đang chạy — lãng phí resource vì kết quả không còn cần.

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

Auto-cancel CI run cũ khi có commit mới trên cùng branch. Đơn giản nhưng tiết kiệm đáng kể compute cost và giảm queue time khi CI runner có giới hạn.


CI cho monorepo

Monorepo — nhiều package hoặc service trong một repository — tạo thách thức riêng cho CI. Nếu mỗi PR chạy test cho tất cả package, pipeline sẽ cực chậm dù chỉ sửa một package.

Affected/changed detection

Pattern cốt lõi: phát hiện file nào thay đổi trong PR, suy ra package nào bị ảnh hưởng, chỉ chạy test cho package đó.

# Tìm file thay đổi so với main branch
CHANGED=$(git diff --name-only origin/main...HEAD)

# Kiểm tra package nào bị ảnh hưởng
if echo "$CHANGED" | grep -q '^packages/api/'; then
  echo "api_changed=true" >> $GITHUB_OUTPUT
fi
if echo "$CHANGED" | grep -q '^packages/web/'; then
  echo "web_changed=true" >> $GITHUB_OUTPUT
fi

Turborepo, Nx, và Bazel đều có built-in affected detection thông minh hơn — phân tích dependency graph giữa packages, nếu package A phụ thuộc package B và B thay đổi thì A cũng bị ảnh hưởng cần test lại.

# Turborepo: chỉ build/test package bị ảnh hưởng
npx turbo run test --filter='...[origin/main]'

Mình dùng Turborepo cho monorepo TypeScript — PR sửa packages/utils sẽ test packages/utils và tất cả package import nó, nhưng skip package không liên quan. Pipeline từ 15 phút xuống 4 phút cho phần lớn PR.

Shared dependency và cache trong monorepo

Monorepo có lợi thế cache: nhiều package share dependency, cache dependency một lần dùng cho tất cả. Turborepo có remote cache — build output của package A trên máy developer X có thể reuse bởi CI hoặc developer Y nếu input hash giống nhau.

- name: Turbo cache
  uses: actions/cache@v4
  with:
    path: node_modules/.cache/turbo
    key: turbo-${{ hashFiles('**/turbo.json', '**/package-lock.json') }}

Nhưng cẩn thận: shared dependency cũng có nghĩa update một dependency có thể ảnh hưởng nhiều package. CI cần chạy test cho tất cả package phụ thuộc dependency đó, không chỉ package có package.json thay đổi.


Anti-pattern CI hay gặp

Mình liệt kê mấy pattern tôi đã gặp hoặc tự mắc, gây chậm pipeline hoặc giảm độ tin cậy.

Test everything always. Mỗi PR chạy toàn bộ test suite bất kể thay đổi gì — sửa README cũng chạy integration test 15 phút. Fix bằng path filter: skip test nếu chỉ thay đổi docs, config không ảnh hưởng runtime.

on:
  pull_request:
    paths-ignore:
      - '*.md'
      - 'docs/**'
      - '.github/ISSUE_TEMPLATE/**'

Không cache gì cả. Mỗi run install dependency từ đầu, build Docker image từ scratch. CI mất 20 phút, 15 phút là lặp lại công việc đã làm run trước. Cache dependency và build output là thay đổi ảnh hưởng lớn nhất với effort nhỏ nhất.

Flaky test bị ignore. “Test đó hay fail, re-run là pass” — rồi mọi người quen re-run, mất 20 phút mỗi lần. Rồi một ngày test fail thật, developer vẫn re-run, bug lên production. Quarantine flaky test ngay, fix trong sprint, đừng để tích luỹ.

Pipeline monolith. Mọi thứ chạy tuần tự trong một job duy nhất: lint → build → unit test → integration test → security scan → deploy. Một step fail thì phải chạy lại từ đầu. Tách thành nhiều job song song, dùng needs để define dependency — lint fail thì unit test không cần chạy, nhưng lint và typecheck chạy song song được.

Build lại artifact cho mỗi environment. Build trên staging, build lại trên production — hai artifact khác nhau dù cùng code. Bug chỉ xuất hiện trên production vì build environment khác. Build một lần, tag bằng SHA, deploy cùng artifact mọi nơi.

Secret trong CI log. Pipeline in environment variable ra log để debug — vô tình lộ database password, API key. CI platform có mask secrets nhưng chỉ mask giá trị mà platform biết (secrets trong settings). Nếu bạn decode secret rồi echo ra, platform không biết phải mask gì. Quy tắc: không bao giờ echo secret, dùng *** placeholder khi cần debug.


Monitoring CI health

CI pipeline cũng cần monitoring — không chỉ “pass hay fail” mà cả trend về tốc độ, reliability, và chi phí.

Metric cần theo dõi

Pipeline duration P50 và P95 — median cho biết trải nghiệm thường ngày, P95 cho biết worst case. Nếu P50 là 5 phút nhưng P95 là 25 phút, nghĩa là 5% CI run chậm khủng khiếp — có thể do cache miss, flaky test retry, hoặc runner queue.

Success rate — phần trăm CI run pass trên main branch. Mục tiêu: > 95%. Dưới 90% nghĩa là main branch đỏ thường xuyên — developer không tin CI, bắt đầu ignore failure.

Flaky rate — phần trăm CI run phải retry mới pass. Nếu > 5%, team đang mất rất nhiều thời gian cho retry. Đo bằng cách track test nào pass sau retry — aggregate weekly.

Queue time — thời gian PR chờ trước khi runner available. Nếu queue time > 2 phút thường xuyên, cần thêm runner hoặc tối ưu để run xong nhanh hơn, giải phóng runner cho run tiếp theo.

Cost per run — đặc biệt quan trọng khi dùng hosted runner (GitHub Actions, CircleCI). CI cost tăng tỷ lệ thuận với số run × duration. Tối ưu pipeline không chỉ tiết kiệm thời gian mà tiết kiệm tiền thật.

Dashboard đơn giản

Không cần tool phức tạp. GitHub Actions có tab Insights hiển thị pipeline duration trend. GitLab có CI/CD analytics built-in. Nếu cần chi tiết hơn, export CI metadata (duration, status, trigger) vào Prometheus hoặc Datadog, build dashboard Grafana.

Mình có dashboard đơn giản hiển thị 4 metric trên: duration trend (đường line chart tuần), success rate (gauge), flaky rate (bar chart theo test), và queue time (heatmap theo giờ trong ngày). Review weekly trong team standup — nếu duration tăng trend, investigate ngay trước khi nó trở thành “bình thường mới”.


Incremental CI — chỉ chạy những gì cần

Ngoài cache và parallel, incremental CI là level tiếp theo: phân tích chính xác thay đổi nào cần verify gì, skip hoàn toàn phần không liên quan.

Path-based filtering

Đơn giản nhất: chạy job dựa trên file path thay đổi.

jobs:
  backend-test:
    if: contains(github.event.pull_request.changed_files_paths, 'src/api/')
    steps:
      - run: npm test -- --testPathPattern='api'

  frontend-test:
    if: contains(github.event.pull_request.changed_files_paths, 'src/web/')
    steps:
      - run: npm test -- --testPathPattern='web'

PR sửa src/api/orders.ts chỉ chạy backend test, skip frontend test. PR sửa src/web/Button.tsx chỉ chạy frontend test. PR sửa cả hai thì chạy cả hai.

Test impact analysis

Level cao hơn path filtering: phân tích code dependency để biết test nào cần chạy khi file X thay đổi. Nếu src/utils/format.ts thay đổi, chỉ chạy test file nào import format.ts — không cần chạy test không liên quan.

Jest có --findRelatedTests flag:

CHANGED_FILES=$(git diff --name-only origin/main...HEAD -- '*.ts' '*.tsx')
npx jest --findRelatedTests $CHANGED_FILES

Đây là cách tiếp cận chính xác nhất — chỉ chạy test thực sự bị ảnh hưởng bởi thay đổi code. Với test suite lớn (hàng nghìn test), reduction có thể từ 10 phút xuống 30 giây cho PR nhỏ.


Required checks và branch protection

CI pipeline nhanh và đáng tin chỉ có giá trị khi nó bắt buộc. Nếu developer có thể merge mà không chờ CI, mọi effort tối ưu đều vô nghĩa.

Branch protection rule nên yêu cầu: tất cả required check pass trước khi merge, branch phải up-to-date với main (tránh merge code đã outdated), và ít nhất 1 code review approval. Required check nên bao gồm lint, typecheck, unit test, và security scan — integration test có thể optional nếu nó quá chậm hoặc hay flaky.

Nhưng đừng biến CI thành barrier quá nặng — nếu CI mất 30 phút và bắt buộc, developer sẽ gom PR lớn hoặc tìm cách bypass. CI nhanh + bắt buộc mới tạo workflow lành mạnh: push → CI 5 phút → review → merge.


Tóm tắt

CI pipeline chậm phá vỡ feedback loop — mỗi phút chờ thêm là context switch, là thời gian mất. Mục tiêu dưới 10 phút cho PR thông thường, dưới 5 phút cho thay đổi nhỏ. Cache dependency và build output là thay đổi ảnh hưởng lớn nhất với effort nhỏ nhất — cache theo hash lock file, dùng restore-keys cho partial hit, Docker layer cache với BuildKit.

Parallel test execution chia theo thời gian chạy, không theo số file. Tách unit test (nhanh, chạy trước) và integration test (chậm, chạy sau). Fail-fast ordering: lint → typecheck → unit test → build → integration test → security scan — thứ nhanh và hay fail chạy trước.

Flaky test phá huỷ niềm tin vào CI — phát hiện bằng retry tracking, quarantine ngay, fix trong sprint, xoá nếu không fix được. Security scan (dependency audit, SAST, secret detection, container scan) chạy trong CI bắt vulnerability sớm hơn production. Artifact build một lần, tag bằng commit SHA, deploy cùng artifact mọi environment.

Monorepo cần affected detection — chỉ test package bị ảnh hưởng bởi thay đổi, không test toàn bộ. Monitor CI health: duration trend, success rate, flaky rate, queue time. CI pipeline không phải “setup xong quên” — nó cần attention thường xuyên vì nó ảnh hưởng đến tốc độ ship feature của cả team.


Tham khảo