yaml dockerfile 99 lines · 3 tabs

GitHub Actions: cache + tests + build

Shared by codesnips Jan 2026
3 tabs
name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

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

jobs:
  install:
    uses: ./.github/workflows/_install.yml

  test:
    needs: install
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        node: [18, 20, 22]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node }}
          cache: npm
      - name: Restore node_modules
        uses: actions/cache@v4
        with:
          path: node_modules
          key: ${{ runner.os }}-node-${{ matrix.node }}-${{ hashFiles('package-lock.json') }}
          restore-keys: |
            ${{ runner.os }}-node-${{ matrix.node }}-
      - run: npm ci
      - run: npm test -- --ci --reporters=default

  build:
    needs: [test]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: docker/setup-buildx-action@v3
      - name: Build image
        uses: docker/build-push-action@v6
        with:
          context: .
          push: ${{ github.event_name == 'push' }}
          tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
          cache-from: type=gha
          cache-to: type=gha,mode=max
3 files · yaml, dockerfile Explain with highlit

This set of workflow files shows how a real CI pipeline for a Node service is wired so that dependency installation is cached, tests run in a parallel matrix, and the Docker image only gets built after tests pass. The three tabs collaborate: a reusable install/cache workflow, a test matrix that consumes it, and a build job gated on the test results.

In ci.yml, the top-level on block restricts runs to pushes and PRs against main, and the concurrency group cancels superseded runs for the same ref so a rapid series of pushes does not burn runners on stale commits. The install job calls the reusable _install.yml via uses, keeping the caching logic in one place instead of duplicating it across every job. The test job then needs install and runs a strategy.matrix over several Node versions with fail-fast: false, which means one failing version still lets the others report — useful for spotting a single-version regression rather than aborting the whole grid.

The _install.yml reusable workflow is where the caching actually happens. actions/setup-node is given cache: npm, and a separate actions/cache step keys the node_modules directory on the OS plus a hash of package-lock.json via hashFiles. The restore-keys fallback lets a partial cache hit (same OS, changed lockfile) still seed most dependencies, so npm ci mostly relinks rather than downloading everything. The if: steps.cache.outputs.cache-hit != 'true' guard skips the install entirely on an exact hit, which is the main time saving. Note the trade-off: caching node_modules is faster but riskier than caching only the npm download cache, so the lockfile hash in the key is what keeps it correct.

The build job in ci.yml declares needs: [test], so Docker never builds against code that failed tests. It uses docker/build-push-action with push: false on PRs and layer caching through cache-from/cache-to type gha, which reuses image layers across runs. A developer reaches for this layout when CI time matters: cache to cut install cost, matrix to widen coverage, and a gated build to avoid publishing broken artifacts.


Related snips

Share this code

Here's the card — post it anywhere.

GitHub Actions: cache + tests + build — share card
Link copied