Git

GitHub Actions cache dependency cho Composer, npm và pnpm

Hướng dẫn GitHub Actions cache dependency cho Composer, npm và pnpm: chọn cache path, hash lockfile, restore key, bảo mật và xử lý cache miss trong CI tái lập.

NHT
· 8 phút đọc
GitHub Actions cache dependency cho Composer, npm và pnpm

Dependency cache trong GitHub Actions có thể rút ngắn đáng kể thời gian chạy CI, nhưng chỉ hiệu quả khi cache đúng thư mục, dùng đúng lockfile và không biến cache thành nơi chứa credential. Với Composer, npm và pnpm, cách triển khai an toàn là xác định cache path theo package manager, tạo key có hash lockfile, đặt restore key theo mức độ ưu tiên và vẫn chạy lệnh cài dependency có lockfile trong mỗi job.

Bài viết này đi từ cơ chế cache hit/cache miss đến các workflow có thể áp dụng cho ứng dụng PHP, Node.js và monorepo. Mục tiêu không phải là làm cho mọi pipeline có thật nhiều cache, mà là tạo cache có khả năng vô hiệu hóa đúng lúc, không che giấu lỗi dependency và không làm lộ dữ liệu nhạy cảm.

1. GitHub Actions dependency cache hoạt động như thế nào?

Một cache entry gồm key, path và metadata phiên bản. Khi workflow chạy, action cache tìm exact match trước; nếu không có, nó thử các prefix match và restore key theo thứ tự. Exact match được xem là cache hit. Nếu không có exact match nhưng tìm được cache gần nhất, dependency có thể được khôi phục một phần, còn job vẫn phải chạy bước cài đặt để bảo đảm trạng thái thư mục phù hợp với lockfile hiện tại.

Cache là lớp tối ưu tốc độ, không phải nguồn sự thật của dependency. Lockfile và lệnh cài đặt có kiểm soát mới quyết định môi trường build.Diễn giải thực hành dựa trên GitHub Docs — Dependency caching reference.
- name: Cache npm
  uses: actions/cache@v4
  with:
    path: ~/.npm
    key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
    restore-keys: |
      ${{ runner.os }}-npm-

- name: Install dependencies
  run: npm ci

Key nên chứa hệ điều hành runner, tên package manager và hash của lockfile. Khi package-lock.json, pnpm-lock.yaml hoặc composer.lock thay đổi, key thay đổi theo và cache cũ không được dùng như exact match. GitHub cũng lưu ý cache hiện có không thể sửa tại chỗ; muốn nội dung mới cần tạo key mới.

2. Chọn cache path cho Composer, npm và pnpm

Điểm dễ nhầm nhất là cache package download với cache thư mục dependency đã cài. Cache package download thường ổn định và ít phụ thuộc hệ điều hành hơn. Cache node_modules có thể nhanh hơn trong một số dự án, nhưng dễ mang theo binary sai nền tảng, dependency dev không mong muốn hoặc trạng thái không đồng nhất. Với pipeline ưu tiên tính tái lập, nên cache thư mục download và vẫn chạy npm ci, pnpm install --frozen-lockfile hoặc composer install theo lockfile.

Package managerPath thường cacheLệnh cài có lockfileRủi ro cần tránh
Composer~/.composer/cache hoặc COMPOSER_CACHE_DIRcomposer install --no-interaction --prefer-distCache vendor chứa secret hoặc dependency dev ngoài ý muốn
npm~/.npmnpm ciCache node_modules khác OS/Node ABI
pnpmStore path do pnpm config get store-dir trả vềpnpm install --frozen-lockfileCopy store sai phiên bản pnpm hoặc lockfile

3. Cách cache Composer cho ứng dụng PHP

Composer có thể tải nhiều package từ Packagist hoặc repository riêng. Cache download giúp giảm số lần tải lại nhưng không thay thế composer.lock. Workflow nên cài đúng phiên bản PHP, bật Composer phù hợp, cache thư mục do Composer xác định và chạy composer install với --no-dev ở job build production. Nếu job test cần package dev, hãy để cùng lockfile quyết định phiên bản và phân biệt mục tiêu test với production.

name: PHP CI
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: shivammathur/setup-php@v2
        with:
          php-version: '8.3'
          tools: composer
      - name: Get Composer cache directory
        id: composer-cache
        run: echo "dir=$(composer config cache-files-dir)" >> "$GITHUB_OUTPUT"
      - name: Cache Composer downloads
        uses: actions/cache@v4
        with:
          path: ${{ steps.composer-cache.outputs.dir }}
          key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }}
          restore-keys: |
            ${{ runner.os }}-php-
      - name: Install locked dependencies
        run: composer install --no-interaction --prefer-dist
      - name: Test
        run: vendor/bin/phpunit

Nếu repository có nhiều ứng dụng PHP, hashFiles có thể nhận nhiều composer.lock và tạo key chung cho toàn repository. Khi muốn cache độc lập theo thư mục, hãy đặt tên project hoặc đường dẫn tương đối vào key. Không đưa .env, private repository token hoặc file credential vào path cache.

4. Cách cache npm và pnpm cho Node.js

Node.js có nhiều lựa chọn package manager, vì vậy pipeline phải pin package manager và dùng đúng lockfile. npm ci yêu cầu package-lock.json đồng bộ với package.json. pnpm install --frozen-lockfile không cho phép tự ý sửa lockfile trong CI. Nếu workflow dùng setup-node với cache: npm hoặc cache: pnpm, hãy kiểm tra input cache-dependency-path khi lockfile không nằm ở thư mục gốc.

- uses: actions/setup-node@v6
  with:
    node-version: 22
    cache: 'pnpm'
    cache-dependency-path: 'pnpm-lock.yaml'

- uses: pnpm/action-setup@v4
  with:
    version: 10

- run: pnpm install --frozen-lockfile
- run: pnpm test
- run: pnpm build

Trong monorepo, có thể dùng đường dẫn glob hoặc danh sách lockfile cho cache dependency path tùy khả năng của action. Hãy giữ một nguyên tắc: thay đổi lockfile phải tạo cache key mới. Nếu cache miss, pipeline vẫn phải cài dependency bình thường và báo rõ thời gian cài, thay vì coi miss là lỗi logic.

5. Thiết kế key và restore key không làm bẩn môi trường

Key quá rộng như linux-node-cache có thể khôi phục dữ liệu không còn tương thích. Key quá hẹp lại làm cache miss liên tục. Một cấu trúc thực dụng là runner OS, runtime major, package manager, nhánh hoặc mục tiêu build và hash lockfile. Restore key nên bỏ dần phần cụ thể từ phải sang trái để có fallback có kiểm soát.

key: ${{ runner.os }}-node-${{ matrix.node }}-build-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
  ${{ runner.os }}-node-${{ matrix.node }}-build-
  ${{ runner.os }}-node-${{ matrix.node }}-
  ${{ runner.os }}-node-
  • Không dùng secret, token, URL nội bộ hoặc nội dung nhạy cảm làm một phần key.
  • Không xem restore key cũ là dependency chính xác; vẫn chạy install theo lockfile.
  • Đưa runtime major vào key khi native module phụ thuộc phiên bản Node.js.
  • Tách cache test và production nếu hai mục tiêu cài dependency khác nhau.
  • Ghi cache-hit vào log để biết pipeline đang tối ưu hay đang chạy cold build.

6. Cache trong matrix và monorepo

Matrix chạy nhiều phiên bản Node.js hoặc PHP có thể tạo cache riêng theo runtime. Nếu bỏ runtime khỏi key, một cache được tạo bởi Node 20 có thể bị dùng cho Node 22 và gây lỗi native module. Với monorepo, mỗi package nên có lockfile rõ hoặc workspace lockfile duy nhất; key phải phản ánh cấu trúc đó để tránh cache của package A che lấp thay đổi của package B.

Cache branch cũng có phạm vi truy cập. Pull request có thể khôi phục cache từ base branch theo quy tắc của GitHub, nhưng không nên thiết kế workflow giả định rằng mọi branch đều nhìn thấy mọi cache. Hãy coi cache miss là trạng thái bình thường và bảo đảm job vẫn chạy đúng từ đầu.

7. Bảo mật: tuyệt đối không cache credential

GitHub Docs cảnh báo không nên lưu access token, login credential hoặc dữ liệu nhạy cảm trong cache path. Cache có thể được đọc trong một số bối cảnh workflow, đặc biệt khi repository có pull request và fork. Những file như .npmrc chứa token, auth.json của Composer, SSH key, cloud credential hoặc .env phải nằm ngoài cache path và được cấp qua secrets/environment phù hợp.

# .gitignore và cache guard
.env
.env.*
.auth/
composer-auth.json
.npmrc

# Không cache toàn bộ HOME nếu HOME chứa credential
# Chỉ cache thư mục package manager cần thiết

Ngoài việc không cache secret, cũng cần kiểm tra action bên thứ ba có quyền đọc workspace và môi trường hay không. Pin action theo phiên bản tin cậy, giới hạn permissions của GITHUB_TOKEN và tránh đưa dữ liệu từ pull request không tin cậy vào shell script có quyền truy cập secret.

8. Đo hiệu quả cache và xử lý cache miss

Cache chỉ đáng giữ nếu tổng thời gian restore và giải nén nhỏ hơn thời gian tải dependency từ đầu. Package quá nhỏ hoặc cache có nhiều file nhỏ có thể khiến restore chậm. Hãy đo cold run, warm run và run sau khi lockfile đổi. Đừng tối ưu bằng cách cache mọi thư mục mà không biết cache đó có làm pipeline khó debug hay không.

- name: Show cache result
  if: always()
  run: |
    echo "cache_hit=${{ steps.cache-npm.outputs.cache-hit }}"
    node --version
    npm --version
    npm ci --ignore-scripts
    npm test

Khi cache miss liên tục, kiểm tra path, quyền ghi, hashFiles có thực sự tìm thấy lockfile hay không và action version. Khi cache hit nhưng build lỗi ngẫu nhiên, hãy xóa giả định rằng cache luôn đúng: thử chạy một cold build, kiểm tra native module và xem package manager có thực sự chạy install hay chỉ khôi phục thư mục cũ.

9. Checklist workflow cache cho production

  1. Pin phiên bản runtime và package manager trước khi thiết kế cache.
  2. Chọn cache download path phù hợp thay vì mặc định cache toàn bộ node_modules hoặc vendor.
  3. Đưa lockfile hash vào key và đặt restore key từ cụ thể đến tổng quát.
  4. Chạy lệnh cài dependency theo lockfile ở mỗi job.
  5. Không cache token, .env, private key, auth file hoặc log chứa secret.
  6. Tách cache theo OS, runtime major, package manager và mục tiêu build khi cần.
  7. Ghi nhận cache-hit/cache-miss và đo thời gian trước/sau.
  8. Test cold build để bảo đảm pipeline không phụ thuộc cache.
  9. Giới hạn permissions và kiểm tra action bên thứ ba trước khi dùng.

Bạn có thể đặt cache vào pipeline CI/CD hiện có sau khi xem lại bài CI/CD và best practices. Nếu build chạy trong Docker, bài multi-stage Dockerfile cho PHP và Node.js giúp phân biệt dependency build với runtime.

10. Kết luận

GitHub Actions cache dependency nên được xem là một lớp tăng tốc có điều kiện. Composer, npm và pnpm đều có thể hưởng lợi nếu cache đúng thư mục, key gắn với lockfile và pipeline vẫn cài dependency theo trạng thái mong muốn. Khi ưu tiên tính tái lập, bảo mật và khả năng debug cao hơn vài phút build, một cache đơn giản nhưng minh bạch thường tốt hơn một cache phức tạp khó kiểm soát.

Tài liệu tham khảo

  1. GitHub Docs — Dependency caching reference.
  2. GitHub Docs — Secure use reference.
  3. actions/cache documentation.