CI workflow (nodejs/undici)
The CI workflow from nodejs/undici, explained and optimized by Latchkey.
- D
- CI health grade
- 53/100
- Health score
- 9
- Jobs per trigger
- 4
- Fixes in the optimized version
In short
- CI health: D (needs work)
- The optimized version below points runs-on at Latchkey managed runners and adds run de-duplication, caching, and job timeouts.
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: ubuntu-latest
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: ubuntu-latest
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: ubuntu-latest
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: ubuntu-latest
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: ubuntu-latest
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: ubuntu-latest
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: ubuntu-latest
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. Each Linux x64 job moves to the Latchkey size matching its vCPU count,latchkey-smallfor a standard 2 vCPU runner; macOS, Windows and arm64 jobs stay as written. - 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. 11 of the 27 run on a standard GitHub-hosted Linux x64 runner, which in a private repository bills $0.006 a minute against $0.0025 on latchkey-small, 58% lower; in a public repository like this one they are free. Estimate the monthly difference with your own run counts.
Actions used in this workflow
- step-security/harden-runner
- actions/checkout
- actions/dependency-review-action
- actions/setup-node
- actions/github-script
- hendrikmuhs/ccache-action
- fastify/github-action-merge-dependabot