Skip to content
Latchkey

CI workflow (nodejs/undici)

The CI workflow from nodejs/undici, explained and optimized by Latchkey.

D

CI health: D - needs work

The optimized version below adds caching, run de-duplication, job timeouts.

Source: nodejs/undici.github/workflows/ci.ymlLicense MITView source

What it does

This is the CI workflow from the nodejs/undici repository, a real project running GitHub Actions. It is shown here with attribution under its MIT license.

Below, Latchkey shows a faster, safer version produced by its optimization engine.

The workflow

workflow (.yml)
name: CI

on:
  push:
    branches:
     - main
     - current
     - next
     - 'v*'
  pull_request:

permissions:
  contents: read

jobs:
  dependency-review:
    if: ${{ github.event_name == 'pull_request' }}
    runs-on: ubuntu-latest
    steps:
      - name: Harden Runner
        uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
        with:
          egress-policy: audit

      - name: Checkout Repository
        uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
        with:
          persist-credentials: false

      - name: Dependency Review
        uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0

  lint:
    name: Lint
    runs-on: ubuntu-latest
    steps:
      - name: Checkout Repository
        uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
        with:
          persist-credentials: false

      - name: Setup Node.js
        uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
        with:
          # Using `latest` as `lts` could point to previous major version.
          # Different versions of Node.js can cause different linting results
          # (e.g. dependening on process.getBuiltinModule())
          node-version: 'latest'

      - name: Install dependencies
        run: npm ci

      - name: Lint
        run: npm run lint

  test:
    name: Test ${{ matrix.node-version == '24' && matrix.runs-on == 'ubuntu-latest' && 'and Coverage ' || ''}}with Node.js ${{ matrix.node-version }} on ${{ matrix.runs-on }}
    strategy:
      fail-fast: false
      max-parallel: 0
      matrix:
        node-version: ['22', '24', '25', '26']
        runs-on: ['ubuntu-latest', 'windows-latest', 'macos-latest']
    uses: ./.github/workflows/nodejs.yml
    with:
      codecov: ${{ (matrix.node-version == '24' || matrix.node-version == '25') && matrix.runs-on == 'ubuntu-latest' }}
      node-version: ${{ matrix.node-version }}
      runs-on: ${{ matrix.runs-on }}
    secrets: inherit

  test-with-no-wasm-simd:
    name: Test with Node.js ${{ matrix.node-version }} on ${{ matrix.runs-on }} with WASM SIMD disabled
    strategy:
      fail-fast: false
      max-parallel: 0
      matrix:
        node-version: ['22', '24', '25', '26']
        runs-on: ['ubuntu-latest']
    uses: ./.github/workflows/nodejs.yml
    with:
      node-version: ${{ matrix.node-version }}
      runs-on: ${{ matrix.runs-on }}
      no-wasm-simd: '1'
    secrets: inherit

  test-without-intl:
    name: Test with Node.js ${{ matrix.node-version }} compiled --without-intl
    strategy:
      fail-fast: false
      max-parallel: 0
      matrix:
        node-version: ['22', '24', '25']
    runs-on: ubuntu-latest
    timeout-minutes: 120
    steps:
      - name: Checkout Repository
        uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
        with:
          persist-credentials: false
          submodules: recursive

      # Setup node, install deps, and build undici prior to building icu-less node and testing
      - name: Setup Node.js@${{ matrix.node-version }}
        uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
        with:
          node-version: ${{ matrix.node-version }}

      - name: Install dependencies
        run: npm ci

      - name: Build undici
        run: npm run build:node

      - name: Determine latest release
        id: release
        uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
        with:
          result-encoding: string
          script: |
            const req = await fetch('https://nodejs.org/download/release/index.json')
            const releases = await req.json()

            const latest = releases.find((r) => r.version.startsWith('v${{ matrix.node-version }}'))
            return latest.version

      - name: Download and extract source
        run: curl https://nodejs.org/download/release/${{ steps.release.outputs.result }}/node-${{ steps.release.outputs.result }}.tar.xz | tar xfJ -

      - name: Install ninja
        run: sudo apt-get install ninja-build

      - name: ccache
        uses: hendrikmuhs/ccache-action@d62db5f07c26379fc4b4e0916f098a92573c3b03 #v1.2.23
        with:
          key: node${{ matrix.node-version }}

      - name: Build node
        working-directory: ./node-${{ steps.release.outputs.result }}
        run: |
          export CC="ccache gcc"
          export CXX="ccache g++"
          ./configure --without-intl --ninja --prefix=./final
          make
          make install
          echo "$(pwd)/final/bin" >> $GITHUB_PATH

      - name: Print version information
        run: |
          echo OS: $(node -p "os.version()")
          echo Node.js: $(node --version)
          echo "Node.js built-in dependencies: $(node -p "'\r\n' + (Object.entries(process.versions).map(([k, v], i, arr) => (i !== arr.length - 1 ? '├──' : '└──') + k + '@' + v)).join('\r\n')")"
          echo npm: $(npm --version)
          echo git: $(git --version)
          echo icu config: $(node -e "console.log(process.config)" | grep icu)

      - name: Configure hosts file for WPT (Windows)
        if: runner.os == 'Windows'
        run: |
          cd ${{ github.workspace }}\test\web-platform-tests\wpt
          python wpt make-hosts-file | Out-File $env:SystemRoot\System32\drivers\etc\hosts -Encoding ascii -Append
        shell: powershell

      - name: Configure hosts file for WPT (Unix)
        if: runner.os != 'Windows'
        run: |
          cd ${{ github.workspace }}/test/web-platform-tests/wpt
          python3 wpt make-hosts-file | sudo tee -a /etc/hosts

      - name: Run tests
        run: npm run test:javascript:without-intl

  test-without-ssl:
    name: Test with Node.js ${{ matrix.node-version }} compiled --without-ssl
    strategy:
      fail-fast: false
      max-parallel: 0
      matrix:
        node-version: ['22', '24', '25']
    runs-on: ubuntu-latest
    timeout-minutes: 120
    steps:
      - name: Checkout Repository
        uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
        with:
          persist-credentials: false
          submodules: recursive

      # Setup node, install deps, and build undici prior to building icu-less node and testing
      - name: Setup Node.js@${{ matrix.node-version }}
        uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
        with:
          node-version: ${{ matrix.node-version }}

      - name: Install dependencies
        run: npm ci

      - name: Build undici
        run: npm run build:node

      - name: Determine latest release
        id: release
        uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
        with:
          result-encoding: string
          script: |
            const req = await fetch('https://nodejs.org/download/release/index.json')
            const releases = await req.json()

            const latest = releases.find((r) => r.version.startsWith('v${{ matrix.node-version }}'))
            return latest.version

      - name: Download and extract source
        run: curl https://nodejs.org/download/release/${{ steps.release.outputs.result }}/node-${{ steps.release.outputs.result }}.tar.xz | tar xfJ -

      - name: Install ninja
        run: sudo apt-get install ninja-build

      - name: ccache
        uses: hendrikmuhs/ccache-action@d62db5f07c26379fc4b4e0916f098a92573c3b03 #v1.2.23
        with:
          key: node${{ matrix.node-version }}

      - name: Build node
        working-directory: ./node-${{ steps.release.outputs.result }}
        run: |
          export CC="ccache gcc"
          export CXX="ccache g++"
          ./configure --without-ssl --ninja --prefix=./final
          make
          make install
          echo "$(pwd)/final/bin" >> $GITHUB_PATH

      - name: Print version information
        run: |
          echo OS: $(node -p "os.version()")
          echo Node.js: $(node --version)
          echo "Node.js built-in dependencies: $(node -p "'\r\n' + (Object.entries(process.versions).map(([k, v], i, arr) => (i !== arr.length - 1 ? '├──' : '└──') + k + '@' + v)).join('\r\n')")"
          echo npm: $(npm --version)
          echo git: $(git --version)
          echo icu config: $(node -e "console.log(process.config)" | grep icu)

      - name: Try loading Node.js without Crypto
        run: node index.js

  test-fuzzing:
    name: Fuzzing
    runs-on: ubuntu-latest
    steps:
      - name: Checkout Repository
        uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
        with:
          persist-credentials: false

      - name: Setup Node.js
        uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
        with:
          node-version: lts/*

      - name: Install dependencies
        run: npm ci

      - name: Run fuzzing tests
        run: npm run test:fuzzing

  # Disabled while the absolute-form proxy forwarding fix from
  # https://github.com/nodejs/undici/pull/5116 lives only in undici main:
  # nodejs/node still embeds the previous behavior, so the shared-builtin
  # build picks up undici from this checkout but runs the bundled Node.js
  # tests, which expect the old semantics. Re-add Node.js 26 to the matrix
  # once a Node.js 26 release embeds an undici that includes #5116.
  # Node.js 24 stays off this job because the change is not being backported.
  # test-shared-builtin:
  #   name: Test with Node.js ${{ matrix.node-version }} compiled --shared-builtin-undici/undici-path
  #   uses: ./.github/workflows/nodejs-shared.yml
  #   strategy:
  #     fail-fast: false
  #     max-parallel: 0
  #     matrix:
  #       node-version: ['26']
  #       runs-on: ['ubuntu-latest']
  #   with:
  #     node-version: ${{ matrix.node-version }}

  test-types:
    name: Test TypeScript types
    timeout-minutes: 15
    runs-on: ubuntu-latest
    steps:
      - name: Checkout Repository
        uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
        with:
          persist-credentials: false

      - name: Setup Node.js
        uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
        with:
          node-version: lts/*

      - name: Install dependencies
        run: npm ci

      - name: Run typings tests
        run: npm run test:typescript

  automerge:
    if: >
      github.event_name == 'pull_request' && github.event.pull_request.user.login == 'dependabot[bot]'
    needs:
      - dependency-review
      - test
      - test-types
      - test-with-no-wasm-simd
      - test-without-intl
      - test-fuzzing
      - lint
    runs-on: ubuntu-latest
    permissions:
      contents: write
      pull-requests: write
    steps:
      - name: Merge Dependabot PR
        uses: fastify/github-action-merge-dependabot@73ec4cbb5e56df5591eae286972d5b2201ffe90f # v3.15.0
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}

The same workflow, on Latchkey

Estimated ~20% faster on cache hits, plus fewer wasted runs and a safer supply chain. Added and changed lines are highlighted.

name: CI on:  push:    branches:     - main     - current     - next     - 'v*'  pull_request: permissions:  contents: read concurrency:  group: ${{ github.workflow }}-${{ github.ref }}  cancel-in-progress: true jobs:  dependency-review:    timeout-minutes: 30    if: ${{ github.event_name == 'pull_request' }}    runs-on: latchkey-small    steps:      - name: Harden Runner        uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4        with:          egress-policy: audit       - name: Checkout Repository        uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0        with:          persist-credentials: false       - name: Dependency Review        uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0   lint:    timeout-minutes: 30    name: Lint    runs-on: latchkey-small    steps:      - name: Checkout Repository        uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0        with:          persist-credentials: false       - name: Setup Node.js        uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0        with:          cache: 'npm'          # Using `latest` as `lts` could point to previous major version.          # Different versions of Node.js can cause different linting results          # (e.g. dependening on process.getBuiltinModule())          node-version: 'latest'       - name: Install dependencies        run: npm ci       - name: Lint        run: npm run lint   test:    timeout-minutes: 30    name: Test ${{ matrix.node-version == '24' && matrix.runs-on == 'ubuntu-latest' && 'and Coverage ' || ''}}with Node.js ${{ matrix.node-version }} on ${{ matrix.runs-on }}    strategy:      fail-fast: false      max-parallel: 0      matrix:        node-version: ['22', '24', '25', '26']        runs-on: latchkey-small    uses: ./.github/workflows/nodejs.yml    with:      codecov: ${{ (matrix.node-version == '24' || matrix.node-version == '25') && matrix.runs-on == 'ubuntu-latest' }}      node-version: ${{ matrix.node-version }}      runs-on: latchkey-small    secrets: inherit   test-with-no-wasm-simd:    timeout-minutes: 30    name: Test with Node.js ${{ matrix.node-version }} on ${{ matrix.runs-on }} with WASM SIMD disabled    strategy:      fail-fast: false      max-parallel: 0      matrix:        node-version: ['22', '24', '25', '26']        runs-on: latchkey-small    uses: ./.github/workflows/nodejs.yml    with:      node-version: ${{ matrix.node-version }}      runs-on: latchkey-small      no-wasm-simd: '1'    secrets: inherit   test-without-intl:    name: Test with Node.js ${{ matrix.node-version }} compiled --without-intl    strategy:      fail-fast: false      max-parallel: 0      matrix:        node-version: ['22', '24', '25']    runs-on: latchkey-small    timeout-minutes: 120    steps:      - name: Checkout Repository        uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0        with:          persist-credentials: false          submodules: recursive       # Setup node, install deps, and build undici prior to building icu-less node and testing      - name: Setup Node.js@${{ matrix.node-version }}        uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0        with:          cache: 'npm'          node-version: ${{ matrix.node-version }}       - name: Install dependencies        run: npm ci       - name: Build undici        run: npm run build:node       - name: Determine latest release        id: release        uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0        with:          result-encoding: string          script: |            const req = await fetch('https://nodejs.org/download/release/index.json')            const releases = await req.json()             const latest = releases.find((r) => r.version.startsWith('v${{ matrix.node-version }}'))            return latest.version       - name: Download and extract source        run: curl https://nodejs.org/download/release/${{ steps.release.outputs.result }}/node-${{ steps.release.outputs.result }}.tar.xz | tar xfJ -       - name: Install ninja        run: sudo apt-get install ninja-build       - name: ccache        uses: hendrikmuhs/ccache-action@d62db5f07c26379fc4b4e0916f098a92573c3b03 #v1.2.23        with:          key: node${{ matrix.node-version }}       - name: Build node        working-directory: ./node-${{ steps.release.outputs.result }}        run: |          export CC="ccache gcc"          export CXX="ccache g++"          ./configure --without-intl --ninja --prefix=./final          make          make install          echo "$(pwd)/final/bin" >> $GITHUB_PATH       - name: Print version information        run: |          echo OS: $(node -p "os.version()")          echo Node.js: $(node --version)          echo "Node.js built-in dependencies: $(node -p "'\r\n' + (Object.entries(process.versions).map(([k, v], i, arr) => (i !== arr.length - 1 ? '├──' : '└──') + k + '@' + v)).join('\r\n')")"          echo npm: $(npm --version)          echo git: $(git --version)          echo icu config: $(node -e "console.log(process.config)" | grep icu)       - name: Configure hosts file for WPT (Windows)        if: runner.os == 'Windows'        run: |          cd ${{ github.workspace }}\test\web-platform-tests\wpt          python wpt make-hosts-file | Out-File $env:SystemRoot\System32\drivers\etc\hosts -Encoding ascii -Append        shell: powershell       - name: Configure hosts file for WPT (Unix)        if: runner.os != 'Windows'        run: |          cd ${{ github.workspace }}/test/web-platform-tests/wpt          python3 wpt make-hosts-file | sudo tee -a /etc/hosts       - name: Run tests        run: npm run test:javascript:without-intl   test-without-ssl:    name: Test with Node.js ${{ matrix.node-version }} compiled --without-ssl    strategy:      fail-fast: false      max-parallel: 0      matrix:        node-version: ['22', '24', '25']    runs-on: latchkey-small    timeout-minutes: 120    steps:      - name: Checkout Repository        uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0        with:          persist-credentials: false          submodules: recursive       # Setup node, install deps, and build undici prior to building icu-less node and testing      - name: Setup Node.js@${{ matrix.node-version }}        uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0        with:          cache: 'npm'          node-version: ${{ matrix.node-version }}       - name: Install dependencies        run: npm ci       - name: Build undici        run: npm run build:node       - name: Determine latest release        id: release        uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0        with:          result-encoding: string          script: |            const req = await fetch('https://nodejs.org/download/release/index.json')            const releases = await req.json()             const latest = releases.find((r) => r.version.startsWith('v${{ matrix.node-version }}'))            return latest.version       - name: Download and extract source        run: curl https://nodejs.org/download/release/${{ steps.release.outputs.result }}/node-${{ steps.release.outputs.result }}.tar.xz | tar xfJ -       - name: Install ninja        run: sudo apt-get install ninja-build       - name: ccache        uses: hendrikmuhs/ccache-action@d62db5f07c26379fc4b4e0916f098a92573c3b03 #v1.2.23        with:          key: node${{ matrix.node-version }}       - name: Build node        working-directory: ./node-${{ steps.release.outputs.result }}        run: |          export CC="ccache gcc"          export CXX="ccache g++"          ./configure --without-ssl --ninja --prefix=./final          make          make install          echo "$(pwd)/final/bin" >> $GITHUB_PATH       - name: Print version information        run: |          echo OS: $(node -p "os.version()")          echo Node.js: $(node --version)          echo "Node.js built-in dependencies: $(node -p "'\r\n' + (Object.entries(process.versions).map(([k, v], i, arr) => (i !== arr.length - 1 ? '├──' : '└──') + k + '@' + v)).join('\r\n')")"          echo npm: $(npm --version)          echo git: $(git --version)          echo icu config: $(node -e "console.log(process.config)" | grep icu)       - name: Try loading Node.js without Crypto        run: node index.js   test-fuzzing:    timeout-minutes: 30    name: Fuzzing    runs-on: latchkey-small    steps:      - name: Checkout Repository        uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0        with:          persist-credentials: false       - name: Setup Node.js        uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0        with:          cache: 'npm'          node-version: lts/*       - name: Install dependencies        run: npm ci       - name: Run fuzzing tests        run: npm run test:fuzzing   # Disabled while the absolute-form proxy forwarding fix from  # https://github.com/nodejs/undici/pull/5116 lives only in undici main:  # nodejs/node still embeds the previous behavior, so the shared-builtin  # build picks up undici from this checkout but runs the bundled Node.js  # tests, which expect the old semantics. Re-add Node.js 26 to the matrix  # once a Node.js 26 release embeds an undici that includes #5116.  # Node.js 24 stays off this job because the change is not being backported.  # test-shared-builtin:  #   name: Test with Node.js ${{ matrix.node-version }} compiled --shared-builtin-undici/undici-path  #   uses: ./.github/workflows/nodejs-shared.yml  #   strategy:  #     fail-fast: false  #     max-parallel: 0  #     matrix:  #       node-version: ['26']  #       runs-on: ['ubuntu-latest']  #   with:  #     node-version: ${{ matrix.node-version }}   test-types:    name: Test TypeScript types    timeout-minutes: 15    runs-on: latchkey-small    steps:      - name: Checkout Repository        uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0        with:          persist-credentials: false       - name: Setup Node.js        uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0        with:          cache: 'npm'          node-version: lts/*       - name: Install dependencies        run: npm ci       - name: Run typings tests        run: npm run test:typescript   automerge:    timeout-minutes: 30    if: >      github.event_name == 'pull_request' && github.event.pull_request.user.login == 'dependabot[bot]'    needs:      - dependency-review      - test      - test-types      - test-with-no-wasm-simd      - test-without-intl      - test-fuzzing      - lint    runs-on: latchkey-small    permissions:      contents: write      pull-requests: write    steps:      - name: Merge Dependabot PR        uses: fastify/github-action-merge-dependabot@73ec4cbb5e56df5591eae286972d5b2201ffe90f # v3.15.0        with:          github-token: ${{ secrets.GITHUB_TOKEN }} 

What changed

  • Run on Latchkey managed runners with one line (runs-on), which apply the fixes below automatically and self-heal transient failures. This example uses latchkey-small; pick the runner size that fits the job.
  • Cancel superseded runs when a branch or PR gets a newer push.
  • Cache dependency installs on the setup step so they are served from cache.
  • Add a job timeout so a hung step cannot burn hours of runner time.

What Latchkey heals here

This workflow has steps that commonly fail on transient issues (network, registries, flaky browsers). On Latchkey managed runners they are detected, retried, and self-healed instead of failing your build:

  • Dependency installs
  • Network fetches

This workflow runs 9 jobs (27 with the matrix expanded) per trigger. On Latchkey the same minutes cost up to 58% less than GitHub-hosted, with zero queue time.

Actions used in this workflow

Frequently asked questions

What does the CI workflow (nodejs/undici) workflow do?
This is the CI workflow from the nodejs/undici repository, a real project running GitHub Actions. It is shown here with attribution under its MIT license.
What CI health grade does this workflow get?
This Node.js workflow grades D. Paste your own workflow into the Latchkey grader to see its grade and the exact fixes.
How can I improve this Node.js workflow?
Apply caching, run de-duplication, job timeouts. Latchkey applies these automatically on managed runners when you point runs-on at Latchkey.

References