CI workflow (nodejs/undici)
The CI workflow from nodejs/undici, explained and optimized by Latchkey.
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.
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
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
- Run on Latchkey managed runners with one line (
runs-on), which apply the fixes below automatically and self-heal transient failures. This example useslatchkey-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.