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

Point runs-on at Latchkey and get caching, run de-duplication, job timeouts, self-healing for flaky steps, and up to 58% lower cost, applied automatically.

Grade your own workflow free or run it on Latchkey →
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: ['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:
    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: ['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: 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

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:

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