Danh sách bài viết

Bài 72: GitHub Actions Matrix Sharding — Full Workflow

Bài này trình bày pattern hoàn chỉnh để chạy Playwright suite trên GitHub Actions với matrix sharding: cấu trúc 2 jobs (test chạy song song theo matrix, merge-reports chờ tất cả shard xong), cơ chế fail-fast và cancelled(), browser cache, multi-browser matrix, tự động comment PR với link report, schedule trigger, và 4 pitfall hay gặp khi setup thực tế.

28/05/2026
0 lượt xem
1

Mục Tiêu Bài Học

Sau bài này bạn sẽ:

  • Hiểu cấu trúc 2-job pattern: test (matrix) + merge-reports (single).
  • Biết cách dùng fail-fast: falseif: !cancelled() để đảm bảo merge job luôn có đủ data.
  • Cache browser binaries để giảm thời gian setup mỗi shard.
  • Mở rộng matrix để test đồng thời trên nhiều browser.
  • Tự động comment link report vào PR.
  • Tránh 4 pitfall phổ biến trong GHA matrix sharding.

Phạm vi bài: Bài này tập trung vào cách tổ chức GitHub Actions workflow. Cơ chế phân phối test theo shard đã được đào sâu ở bài 69. Blob reporter và merge-reports CLI đã được đào sâu ở bài 70 và 71.

2

Tổng Quan Kiến Trúc 2 Jobs

Pattern chuẩn gồm 2 job:

  • Job test — chạy theo strategy.matrix. Mỗi combination trong matrix tương ứng một runner riêng, chạy song song. Mỗi runner thực thi một shard và upload blob artifact.
  • Job merge-reports — một runner duy nhất, chạy sau khi tất cả runners trong test hoàn thành (needs: [test]). Tải về tất cả blob artifact, merge lại, upload HTML report.
Push / PR trigger
       │
       ▼
┌─────────────────────────────────────┐
│  job: test  (strategy.matrix)       │
│  ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│  │ shard 1  │ │ shard 2  │ │ shard 3  │ │ shard 4  │ │
│  │ blob-1   │ │ blob-2   │ │ blob-3   │ │ blob-4   │ │
│  └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
└─────────────────────────────────────┘
       │  (tất cả shard xong — kể cả khi có fail)
       ▼
┌─────────────────────────────────────┐
│  job: merge-reports                 │
│  download blob-1..blob-4            │
│  merge → HTML report                │
│  upload playwright-report           │
└─────────────────────────────────────┘

Ưu điểm của kiến trúc này: thời gian chờ bằng thời gian của shard chậm nhất, không phải tổng thời gian của tất cả shard. Suite 2000 test mất 20 phút — với 4 shard còn khoảng 5–6 phút (cộng overhead artifact upload/download).

3

Full Workflow YAML

File .github/workflows/e2e.yml:

name: E2E Tests

on:
  push:
    branches: [main]
  pull_request:

jobs:
  test:
    timeout-minutes: 60
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        shardIndex: [1, 2, 3, 4]
        shardTotal: [4]
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 22

      - name: Install dependencies
        run: npm ci

      - name: Install Playwright browsers
        run: npx playwright install --with-deps chromium

      - name: Run Playwright tests
        run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --reporter=blob

      - name: Upload blob report
        if: ${{ !cancelled() }}
        uses: actions/upload-artifact@v4
        with:
          name: blob-${{ matrix.shardIndex }}
          path: blob-report/
          retention-days: 7

  merge-reports:
    if: ${{ !cancelled() }}
    needs: [test]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 22

      - run: npm ci

      - name: Download blob reports
        uses: actions/download-artifact@v4
        with:
          path: all-blobs
          pattern: blob-*
          merge-multiple: true

      - name: Merge reports
        run: npx playwright merge-reports --reporter=html ./all-blobs

      - name: Upload HTML report
        uses: actions/upload-artifact@v4
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 14

Một số điểm đáng chú ý trong YAML trên:

  • shardTotal: [4] là array một phần tử — GitHub Actions yêu cầu cú pháp này dù chỉ có một giá trị. Cách này cho phép thay đổi tổng số shard mà không cần sửa nhiều chỗ.
  • timeout-minutes: 60 — giới hạn tổng thời gian runner. Nếu shard treo (ví dụ test deadlock), runner bị cancel sau 60 phút thay vì chạy đến hết billing limit.
  • --with-deps chromium — cài thêm system dependencies (libglib, libnss, ...) cần thiết để chạy Chromium trên ubuntu-latest. Thiếu flag này thường gây lỗi launch browser.
  • merge-multiple: true — download artifact từ nhiều job (blob-1, blob-2, blob-3, blob-4) vào cùng một path thay vì tạo subfolder per artifact.
4

fail-fast: falseif: !cancelled()

Hai setting này phối hợp với nhau để đảm bảo merge job luôn nhận được đủ data, kể cả khi một hoặc nhiều shard có test fail.

fail-fast: false

Mặc định GitHub Actions matrix có fail-fast: true: khi một job trong matrix fail, GHA hủy tất cả job còn lại chưa chạy xong. Với sharding, hành vi này gây vấn đề:

  • Shard 1 fail (có test fail) → GHA cancel shard 2, 3, 4 đang chạy.
  • Blob artifact của shard 2, 3, 4 không bao giờ được upload.
  • Merge job nhận artifact thiếu → report không đầy đủ hoặc merge job cũng fail.

Set fail-fast: false để mỗi shard chạy độc lập đến hết, bất kể shard khác có fail hay không.

if: ${{ !cancelled() }}

Condition này khác với if: always():

Condition Chạy khi success Chạy khi fail Chạy khi cancelled
(không có) Không Không
always()
!cancelled() Không

!cancelled() là lựa chọn phù hợp nhất cho upload blob và merge job: chạy kể cả khi test fail (để vẫn có report về các test lỗi), nhưng không chạy khi workflow bị người dùng cancel thủ công (tránh lãng phí runner time tạo report cho run đã bị dừng).

always() thích hợp hơn ở bước upload artifact khi muốn giữ artifact ngay cả khi cancel — nhưng trong hầu hết trường hợp, !cancelled() là đủ và tiết kiệm hơn.

5

Browser Cache Với actions/cache

npx playwright install --with-deps chromium download browser binary (~150 MB) và cài system packages mỗi lần shard chạy. Với 4 shard, đây là bước tốn khoảng 1–2 phút per shard, tức 4–8 phút mỗi CI run bị "lãng phí" vào việc download.

Thêm bước cache vào trước Install Playwright browsers:

steps:
  - uses: actions/checkout@v4

  - uses: actions/setup-node@v4
    with:
      node-version: 22

  - name: Install dependencies
    run: npm ci

  - name: Cache Playwright browsers
    uses: actions/cache@v4
    id: playwright-cache
    with:
      path: ~/.cache/ms-playwright
      key: playwright-${{ hashFiles('package-lock.json') }}

  - name: Install Playwright browsers
    if: steps.playwright-cache.outputs.cache-hit != 'true'
    run: npx playwright install --with-deps chromium

  - name: Install system deps only (cache hit)
    if: steps.playwright-cache.outputs.cache-hit == 'true'
    run: npx playwright install-deps chromium

  - name: Run Playwright tests
    run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --reporter=blob

Một số điểm quan trọng:

  • Cache key dựa trên package-lock.json hash — khi update Playwright version, package-lock.json thay đổi, cache key thay đổi, browser được tải lại. Không cần quản lý cache invalidation thủ công.
  • Cache hit vẫn cần install-deps — browser binary được cache, nhưng system packages (libglib, libnss, ...) không được cache và phải install lại mỗi run vì runner là fresh VM. npx playwright install-deps chromium chỉ cài system deps, không download browser binary.
  • Cache scope — mặc định GHA cache chia sẻ giữa các job trong cùng workflow run. Nếu shard 1 là job đầu tiên populate cache, shard 2, 3, 4 sẽ hit cache. Tuy nhiên, cache cũng chia sẻ qua run — run sau hit cache ngay từ shard đầu tiên.

Nếu không cần phân biệt cache hit hay không, dùng cách đơn giản hơn:

- uses: actions/cache@v4
  with:
    path: ~/.cache/ms-playwright
    key: playwright-${{ hashFiles('package-lock.json') }}

- name: Install Playwright browsers
  run: npx playwright install --with-deps chromium

Cách này đơn giản hơn — npx playwright install tự detect browser đã có trong cache và skip download, chỉ cài system deps. Trade-off: vẫn chạy lệnh nhưng Playwright tự xử lý skip logic.

6

Multi-Browser Matrix — 3 × 2 = 6 Jobs

Matrix có thể kết hợp nhiều dimension. Ví dụ test 3 browser × 2 shard = 6 jobs:

jobs:
  test:
    timeout-minutes: 60
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        browser: [chromium, firefox, webkit]
        shardIndex: [1, 2]
        shardTotal: [2]
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 22

      - run: npm ci

      - name: Install Playwright browsers
        run: npx playwright install --with-deps ${{ matrix.browser }}

      - name: Run Playwright tests
        run: npx playwright test --project=${{ matrix.browser }} --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --reporter=blob

      - name: Upload blob report
        if: ${{ !cancelled() }}
        uses: actions/upload-artifact@v4
        with:
          name: blob-${{ matrix.browser }}-${{ matrix.shardIndex }}
          path: blob-report/
          retention-days: 7

6 jobs được tạo tự động:

  • chromium / shard 1
  • chromium / shard 2
  • firefox / shard 1
  • firefox / shard 2
  • webkit / shard 1
  • webkit / shard 2

Artifact name dùng cả browser lẫn shard index (blob-chromium-1, blob-firefox-2, ...) để tránh trùng tên — GHA không cho phép 2 artifact cùng tên trong cùng workflow run.

Merge job vẫn dùng pattern: blob-* để download tất cả 6 artifact, sau đó merge chung vào một HTML report:

  merge-reports:
    if: ${{ !cancelled() }}
    needs: [test]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
      - run: npm ci

      - name: Download blob reports
        uses: actions/download-artifact@v4
        with:
          path: all-blobs
          pattern: blob-*
          merge-multiple: true

      - name: Merge reports
        run: npx playwright merge-reports --reporter=html ./all-blobs

      - name: Upload HTML report
        uses: actions/upload-artifact@v4
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 14

HTML report tổng hợp sẽ hiển thị test grouped theo project (browser) — thấy ngay test nào pass trên Chromium nhưng fail trên WebKit.

Lưu ý về free tier: 6 jobs chạy song song tốn nhiều concurrent runner hơn. Tài khoản free GitHub Actions có giới hạn 20 concurrent jobs (public repo) hoặc 5 (private repo). Nếu queue dài, jobs sẽ bị xếp hàng chờ.

7

PR Comment Với Link Report

Sau khi merge job hoàn thành, có thể tự động post comment vào PR với link đến artifact report. Thêm step cuối vào merge-reports job:

      - name: Comment PR with report link
        if: github.event_name == 'pull_request'
        uses: actions/github-script@v7
        with:
          script: |
            const url = `https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}`;
            github.rest.issues.createComment({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: context.issue.number,
              body: `Playwright report: ${url}`,
            });

Link dẫn đến trang Actions run — từ đó reviewer click vào artifact playwright-report để download và xem HTML report.

Điều kiện if: github.event_name == 'pull_request' — chỉ comment khi trigger là PR. Không comment khi push to main (không có issue number để comment vào).

Permission cần thiết: Workflow cần quyền pull-requests: write. Thêm vào đầu file workflow:

permissions:
  pull-requests: write

Nếu repository dùng GITHUB_TOKEN mặc định (không setup custom token), permission này phải được khai báo tường minh kể từ khi GitHub thay đổi default permissions về read-only vào 2023.

Tránh comment nhiều lần: Nếu PR có nhiều commit (mỗi commit trigger một workflow run), mỗi run post một comment mới. Để chỉ update comment thay vì tạo mới, cần tìm comment cũ và edit — logic phức tạp hơn nhưng giảm noise trong PR thread.

8

Schedule Trigger

Bổ sung schedule trigger để chạy full suite định kỳ, thường trên nhánh main:

on:
  push:
    branches: [main]
  pull_request:
  schedule:
    - cron: '0 2 * * *'  # 2am UTC daily

Schedule trigger hữu ích cho:

  • Chạy test trên nhiều browser (không chỉ Chromium) mà không làm chậm mỗi PR.
  • Test với data thực tế hoặc external API — không muốn chạy mỗi commit.
  • Phát hiện flaky test qua nhiều lần chạy liên tiếp.

Cú pháp cron: phút giờ ngày tháng thứ. Một số ví dụ:

schedule:
  - cron: '0 2 * * *'       # 2:00 AM UTC mỗi ngày
  - cron: '0 2 * * 1-5'     # 2:00 AM UTC thứ 2-6 (bỏ cuối tuần)
  - cron: '0 0,12 * * *'    # 00:00 và 12:00 UTC mỗi ngày
  - cron: '0 9 * * 1'       # 9:00 AM UTC mỗi thứ 2

Lưu ý: GitHub Actions schedule có thể bị delay khi server load cao — không nên dùng schedule cho deadline-sensitive task. Minimum interval là 5 phút (*/5 * * * *).

Khi chạy qua schedule trên nhánh main, PR comment step sẽ bị skip tự động vì github.event_name == 'schedule', không phải 'pull_request'.

9

Performance Optimization

Các điểm tối ưu theo thứ tự ảnh hưởng từ lớn đến nhỏ:

1. Dùng npm ci thay vì npm install

npm ci install từ package-lock.json không resolve dependency — nhanh hơn và deterministic. Luôn dùng npm ci trên CI.

2. Cache node_modules

- uses: actions/setup-node@v4
  with:
    node-version: 22
    cache: 'npm'           # cache ~/.npm directory

actions/setup-node@v4 có built-in cache support — chỉ cần thêm cache: 'npm' là đủ, không cần cấu hình actions/cache riêng cho node_modules.

3. Cache browser binaries

Đã trình bày ở mục 5. Tiết kiệm ~1–2 phút per shard sau lần đầu populate cache.

4. Chỉ install browser cần thiết

Dùng npx playwright install --with-deps chromium thay vì npx playwright install --with-deps (cài tất cả browser). Chromium ~150 MB, tất cả browser ~600 MB.

5. Tối ưu số shard

Số shard tối ưu phụ thuộc vào suite size và thời gian mong muốn. Công thức thực tế:

Số shard = round_up(total_test_time / target_time_per_shard)

Ví dụ: suite 20 phút, target 5 phút → 4 shard
       suite 20 phút, target 3 phút → 7 shard (làm tròn lên)

Quá nhiều shard có overhead ngược: artifact upload/download, runner startup time — mỗi runner mất khoảng 30–60 giây khởi động. Với suite nhỏ (<200 test), 2 shard thường đủ.

10

Limitations

  • Concurrent job limits. Free tier GitHub Actions: 20 concurrent jobs cho public repo, 5 cho private repo. Với 4 shard + 3 browser = 12 jobs, private repo free tier sẽ phải xếp hàng — thực tế không chạy song song hoàn toàn.
  • Không có shared state giữa runners. Mỗi runner là VM riêng biệt, không có filesystem chung hay memory chung. Test cần state từ runner khác (ví dụ: test A seed DB, test B verify) sẽ bị phân tách shard — cần thiết kế test độc lập hoặc dùng external state (DB, API).
  • Artifact upload/download overhead. Blob artifact có thể lớn khi trace/video enabled. Download 4 blob artifact trong merge job thêm 1–3 phút tùy network và file size. Nếu artifact quá lớn (>500 MB), cần tăng timeout cho merge job.
  • Phân phối shard không đều. Playwright phân phối test theo thứ tự file — không theo execution time. Nếu test nặng tập trung trong một file, shard chứa file đó chạy lâu hơn. Tính năng phân phối theo execution time đang được phát triển nhưng chưa có trong stable release.
  • Cache không share giữa fork PR. Khi PR đến từ fork, GitHub Actions không cho phép fork workflows đọc cache của repository gốc vì lý do bảo mật. Browser sẽ phải tải lại mỗi run từ fork.
11

4 Pitfalls

Pitfall 1: Quên fail-fast: false

Đây là lỗi phổ biến nhất khi setup matrix sharding. Mặc định fail-fast: true — khi shard 1 có test fail, GHA cancel shard 2, 3, 4 đang chạy. Blob của các shard bị cancel không được upload. Merge job nhận dữ liệu thiếu, báo lỗi hoặc tạo report không đầy đủ.

# Sai — thiếu fail-fast
strategy:
  matrix:
    shardIndex: [1, 2, 3, 4]
    shardTotal: [4]

# Đúng
strategy:
  fail-fast: false
  matrix:
    shardIndex: [1, 2, 3, 4]
    shardTotal: [4]

Pitfall 2: Cache key không bao gồm OS

Browser binary khác nhau theo OS. Nếu CI matrix có nhiều OS (ubuntu, windows, macos) và dùng chung cache key, runner Windows có thể load cache binary của Linux:

# Có thể gây vấn đề khi cross-OS
key: playwright-${{ hashFiles('package-lock.json') }}

# Đúng khi có nhiều OS trong matrix
key: playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }}

Với workflow chỉ dùng ubuntu-latest, cache key không cần runner.os — nhưng thêm vào cũng không hại gì và safer khi workflow thay đổi sau.

Pitfall 3: Upload artifact path sai → merge không có data

Blob reporter mặc định ghi vào blob-report/. Nếu upload step trỏ sai path, artifact được upload nhưng rỗng hoặc không có file zip:

# Sai path
- uses: actions/upload-artifact@v4
  with:
    name: blob-${{ matrix.shardIndex }}
    path: playwright-report/   # sai — đây là path của HTML reporter

# Đúng
- uses: actions/upload-artifact@v4
  with:
    name: blob-${{ matrix.shardIndex }}
    path: blob-report/         # path mặc định của blob reporter

Nếu config playwright.config.ts khai báo custom outputFile hoặc outputDir cho blob reporter, path trong upload-artifact phải khớp với config đó.

Pitfall 4: merge-reports output path không khớp upload path

Mặc định npx playwright merge-reports output vào playwright-report/. Nếu upload step khai báo path khác mà không chỉ định --output:

# Merge command mặc định output vào playwright-report/
- run: npx playwright merge-reports --reporter=html ./all-blobs

# Upload step phải dùng playwright-report/
- uses: actions/upload-artifact@v4
  with:
    name: playwright-report
    path: playwright-report/   # phải khớp với output của merge command

# Nếu muốn output khác, phải chỉ định --output
- run: npx playwright merge-reports --reporter=html --output=merged-report/ ./all-blobs
- uses: actions/upload-artifact@v4
  with:
    name: playwright-report
    path: merged-report/       # phải khớp

Artifact upload thành công nhưng rỗng thường là dấu hiệu của pitfall này — check log bước upload để xem actual file count.

12

Quiz + Bài Tiếp

Quiz

1. Workflow có 4 shard, fail-fast: false. Shard 3 bị crash (runner lỗi hệ thống, không phải test fail) và không upload artifact. Merge job chạy với 3 blob. Điều gì xảy ra với HTML report?

Đáp án

Merge job vẫn chạy thành công với 3 blob có sẵn (nếu if: !cancelled()). HTML report được tạo nhưng chỉ chứa kết quả của shard 1, 2, và 4 — các test thuộc shard 3 không xuất hiện trong report. Report bị partial mà không có warning rõ ràng về shard bị thiếu. Để phát hiện trường hợp này, cần kiểm tra số lượng test trong report so với tổng số test trong suite.

2. Matrix khai báo shardTotal: [4] nhưng chỉ có shardIndex: [1, 2, 3] (thiếu 4). Điều gì xảy ra?

Đáp án

Chỉ có 3 job được tạo, mỗi job chạy: --shard=1/4, --shard=2/4, --shard=3/4. Test thuộc shard 4 (1/4 cuối suite) không bao giờ được chạy. Report cuối thiếu khoảng 25% test mà không có lỗi — đây là loại lỗi âm thầm rất khó phát hiện. Luôn đảm bảo số phần tử trong shardIndex bằng giá trị shardTotal.

3. Workflow trigger cả pushpull_request. PR được push commit mới, cả 2 trigger cùng fire. Có vấn đề gì không?

Đáp án

Có thể có duplicate run: một từ push event và một từ pull_request event — cùng chạy song song, tốn double runner minutes. Để tránh, thêm concurrency setting hoặc chỉ trigger một trong hai. Một cách phổ biến: chỉ dùng pull_request cho PR check, và push chỉ cho nhánh không phải feature branch (main, release...). Cũng có thể dùng concurrency: group: ${{ github.workflow }}-${{ github.ref }} để cancel run cũ khi có run mới.

4. Cache Playwright browsers với key playwright-${{ hashFiles('package-lock.json') }}. Team update Playwright từ 1.49 lên 1.50. Cache có bị invalidate không?

Đáp án

Có, nếu package-lock.json được update khi nâng version. npm install @playwright/[email protected] cập nhật package-lock.json → hash thay đổi → cache key mới → cache miss → browser 1.50 được tải về và populate cache mới. Đây là hành vi đúng. Tuy nhiên, nếu ai đó cập nhật package.json nhưng không commit package-lock.json, CI vẫn dùng cache cũ với browser cũ — có thể gây version mismatch.

5. Merge job cần quyền gì để post PR comment? Khai báo ở đâu?

Đáp án

Cần quyền pull-requests: write. Khai báo trong phần permissions ở cấp workflow (áp dụng cho tất cả job) hoặc ở cấp job cụ thể. Ví dụ cấp job: jobs: merge-reports: permissions: pull-requests: write. Nếu khai báo ở cấp workflow, tất cả job đều có quyền này — ít granular hơn nhưng đơn giản hơn. Khai báo ở cấp job theo nguyên tắc least privilege.

Bài Tiếp Theo

Bài 73: Shard Load Balance — Phân Phối Test Đều Giữa Các Shard