Pull Requests workflow (lint-staged/lint-staged)
The Pull Requests workflow from lint-staged/lint-staged, 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 Pull Requests workflow from the lint-staged/lint-staged 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: Pull Requests
on:
pull_request:
branches:
- main
permissions:
contents: read
jobs:
npm:
uses: ./.github/workflows/npm.yml
Changesets:
needs: npm
# Changesets no longer exist when releasing
if: github.event_name == 'pull_request' && !startsWith(github.event.pull_request.title, '🦋 Changeset release')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # https://github.com/actions/checkout/releases/tag/v6.0.2
with:
fetch-depth: 0
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # https://github.com/actions/setup-node/releases/tag/v6.4.0
with:
node-version-file: .node-version
package-manager-cache: false
- uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # https://github.com/actions/cache/releases/tag/v5.0.5
id: cache-node_modules
with:
path: node_modules
key: node-modules-ubuntu-latest-${{ hashFiles('package-lock.json') }}
- run: npx changeset status --since=${{ github.event.pull_request.base.sha }}
Commitlint:
needs: npm
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # https://github.com/actions/checkout/releases/tag/v6.0.2
with:
fetch-depth: 0
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # https://github.com/actions/setup-node/releases/tag/v6.4.0
with:
node-version-file: .node-version
package-manager-cache: false
- uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # https://github.com/actions/cache/releases/tag/v5.0.5
id: cache-node_modules
with:
path: node_modules
key: node-modules-ubuntu-latest-${{ hashFiles('package-lock.json') }}
- run: npx commitlint --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }} --verbose
TypeScript:
needs: npm
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # https://github.com/actions/checkout/releases/tag/v6.0.2
with:
fetch-depth: 0
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # https://github.com/actions/setup-node/releases/tag/v6.4.0
with:
node-version-file: .node-version
package-manager-cache: false
- uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # https://github.com/actions/cache/releases/tag/v5.0.5
id: cache-node_modules
with:
path: node_modules
key: node-modules-ubuntu-latest-${{ hashFiles('package-lock.json') }}
- run: npm run typecheck
ESLint:
needs: npm
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # https://github.com/actions/checkout/releases/tag/v6.0.2
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # https://github.com/actions/setup-node/releases/tag/v6.4.0
with:
node-version-file: .node-version
package-manager-cache: false
- uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # https://github.com/actions/cache/releases/tag/v5.0.5
id: cache-node_modules
with:
path: node_modules
key: node-modules-ubuntu-latest-${{ hashFiles('package-lock.json') }}
- run: npm run lint
test-node-range:
name: Test Node.js version range
needs: npm
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # https://github.com/actions/checkout/releases/tag/v6.0.2
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # https://github.com/actions/setup-node/releases/tag/v6.4.0
with:
node-version-file: .node-version
package-manager-cache: false
- uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # https://github.com/actions/cache/releases/tag/v5.0.5
id: cache-node_modules
with:
path: node_modules
key: node-modules-ubuntu-latest-${{ hashFiles('package-lock.json') }}
- run: node ./scripts/test-node-range.js
test-matrix:
strategy:
fail-fast: false
matrix:
# Test with Node.js active LTS versions, and the latest version
node:
- '22.22.1' # lowest supported version
- '22.x'
- '24.x'
- '26.x'
# Test with Ubuntu, macOS, and Windows
os:
- ubuntu-latest
- macos-latest
- windows-latest
name: Node.js v${{ matrix.node }} on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
steps:
- if: matrix.os == 'windows-latest'
run: git config --global core.autocrlf true
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # https://github.com/actions/checkout/releases/tag/v6.0.2
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # https://github.com/actions/setup-node/releases/tag/v6.4.0
with:
node-version: ${{ matrix.node }}
package-manager-cache: false
- uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # https://github.com/actions/cache/releases/tag/v5.0.5
id: cache-node_modules
with:
path: node_modules
key: node-modules-${{ matrix.os }}-${{ hashFiles('package-lock.json') }}
- if: steps.cache-node_modules.outputs.cache-hit != 'true'
run: npm ci --ignore-scripts
- run: node --version
- run: npm --version
- run: git --version
- run: node bin/lint-staged.js --version
- run: node bin/lint-staged.js --help
# Run tests
- run: npm test
test-deno:
strategy:
fail-fast: false
matrix:
os:
- ubuntu-latest
- macos-latest
- windows-latest
name: Deno (latest) on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # https://github.com/actions/checkout/releases/tag/v6.0.2
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # https://github.com/actions/setup-node/releases/tag/v6.4.0
with:
node-version-file: .node-version
package-manager-cache: false
- uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # https://github.com/actions/cache/releases/tag/v5.0.5
id: cache-node_modules
with:
path: node_modules
key: node-modules-${{ matrix.os }}-${{ hashFiles('package-lock.json') }}
- if: steps.cache-node_modules.outputs.cache-hit != 'true'
run: npm ci --ignore-scripts
- uses: denoland/setup-deno@667a34cdef165d8d2b2e98dde39547c9daac7282 # https://github.com/denoland/setup-deno/releases/tag/v2.0.4
with:
deno-version: latest
- run: node --version
- run: npm --version
- run: deno --version
- run: git --version
- run: deno -A bin/lint-staged.js --version
- run: deno -A bin/lint-staged.js --help
- run: deno run test
test-bun:
strategy:
fail-fast: false
matrix:
os:
- ubuntu-latest
- macos-latest
- windows-latest
name: Bun (latest) on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # https://github.com/actions/checkout/releases/tag/v6.0.2
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # https://github.com/oven-sh/setup-bun/releases/tag/v2.2.0
with:
bun-version: latest
- run: bun install --frozen-lockfile
- run: bun --version
- run: git --version
- run: bun bin/lint-staged.js --version
- run: bun bin/lint-staged.js --help
- run: bun --bun run test
test-msys2:
strategy:
fail-fast: false
matrix:
# Test with Node.js active LTS versions, and the latest version
node:
- '22.22.1' # lowest supported version
- '22.x'
- '24.x'
- '25.x'
name: Node.js ${{ matrix.node }} on windows-latest (MSYS2)
runs-on: windows-latest
defaults:
run:
shell: msys2 {0}
steps:
- uses: msys2/setup-msys2@e9898307ac31d1a803454791be09ab9973336e1c # https://github.com/msys2/setup-msys2/releases/tag/v2.31.1
with:
msystem: UCRT64
path-type: inherit
install: >-
git
- run: git config --global core.autocrlf true
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # https://github.com/actions/checkout/releases/tag/v6.0.2
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # https://github.com/actions/setup-node/releases/tag/v6.4.0
with:
node-version: ${{ matrix.node }}
package-manager-cache: false
- uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # https://github.com/actions/cache/releases/tag/v5.0.5
id: cache-node_modules
with:
path: node_modules
key: node-modules-windows-msys2-latest-${{ hashFiles('package-lock.json') }}
- if: steps.cache-node_modules.outputs.cache-hit != 'true'
run: npm ci --ignore-scripts
- run: node --version
- run: npm --version
- run: git --version
- run: node bin/lint-staged.js --version
- run: node bin/lint-staged.js --help
- run: npm test
test-cygwin:
strategy:
fail-fast: false
matrix:
# Test with Node.js active LTS versions, and the latest version
node:
- '22.22.1' # lowest supported version
- '22.x'
- '24.x'
- '25.x'
name: Node.js ${{ matrix.node }} on windows-latest (Cygwin)
runs-on: windows-latest
steps:
- uses: cygwin/cygwin-install-action@711d29f3da23c9f4a1798e369a6f01198c13b11a # https://github.com/cygwin/cygwin-install-action/tree/v6.1
with:
site: |
https://mirrors.kernel.org/sourceware/cygwin/
- run: git config --global core.autocrlf true
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # https://github.com/actions/checkout/releases/tag/v6.0.2
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # https://github.com/actions/setup-node/releases/tag/v6.4.0
with:
node-version: ${{ matrix.node }}
package-manager-cache: false
- uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # https://github.com/actions/cache/releases/tag/v5.0.5
id: cache-node_modules
with:
path: node_modules
key: node-modules-windows-cygwin-latest-${{ hashFiles('package-lock.json') }}
- if: steps.cache-node_modules.outputs.cache-hit != 'true'
run: npm ci --ignore-scripts
- run: node --version
- run: npm --version
- run: git --version
- run: node bin/lint-staged.js --version
- run: node bin/lint-staged.js --help
- run: npm test
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: Pull Requests on: pull_request: branches: - main permissions: contents: read concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: npm: timeout-minutes: 30 uses: ./.github/workflows/npm.yml Changesets: timeout-minutes: 30 needs: npm # Changesets no longer exist when releasing if: github.event_name == 'pull_request' && !startsWith(github.event.pull_request.title, '🦋 Changeset release') runs-on: latchkey-small steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # https://github.com/actions/checkout/releases/tag/v6.0.2 with: fetch-depth: 0 - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # https://github.com/actions/setup-node/releases/tag/v6.4.0 with: cache: 'npm' node-version-file: .node-version package-manager-cache: false - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # https://github.com/actions/cache/releases/tag/v5.0.5 id: cache-node_modules with: path: node_modules key: node-modules-ubuntu-latest-${{ hashFiles('package-lock.json') }} - run: npx changeset status --since=${{ github.event.pull_request.base.sha }} Commitlint: timeout-minutes: 30 needs: npm runs-on: latchkey-small steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # https://github.com/actions/checkout/releases/tag/v6.0.2 with: fetch-depth: 0 - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # https://github.com/actions/setup-node/releases/tag/v6.4.0 with: cache: 'npm' node-version-file: .node-version package-manager-cache: false - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # https://github.com/actions/cache/releases/tag/v5.0.5 id: cache-node_modules with: path: node_modules key: node-modules-ubuntu-latest-${{ hashFiles('package-lock.json') }} - run: npx commitlint --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }} --verbose TypeScript: timeout-minutes: 30 needs: npm runs-on: latchkey-small steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # https://github.com/actions/checkout/releases/tag/v6.0.2 with: fetch-depth: 0 - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # https://github.com/actions/setup-node/releases/tag/v6.4.0 with: cache: 'npm' node-version-file: .node-version package-manager-cache: false - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # https://github.com/actions/cache/releases/tag/v5.0.5 id: cache-node_modules with: path: node_modules key: node-modules-ubuntu-latest-${{ hashFiles('package-lock.json') }} - run: npm run typecheck ESLint: timeout-minutes: 30 needs: npm runs-on: latchkey-small steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # https://github.com/actions/checkout/releases/tag/v6.0.2 - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # https://github.com/actions/setup-node/releases/tag/v6.4.0 with: cache: 'npm' node-version-file: .node-version package-manager-cache: false - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # https://github.com/actions/cache/releases/tag/v5.0.5 id: cache-node_modules with: path: node_modules key: node-modules-ubuntu-latest-${{ hashFiles('package-lock.json') }} - run: npm run lint test-node-range: timeout-minutes: 30 name: Test Node.js version range needs: npm runs-on: latchkey-small steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # https://github.com/actions/checkout/releases/tag/v6.0.2 - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # https://github.com/actions/setup-node/releases/tag/v6.4.0 with: cache: 'npm' node-version-file: .node-version package-manager-cache: false - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # https://github.com/actions/cache/releases/tag/v5.0.5 id: cache-node_modules with: path: node_modules key: node-modules-ubuntu-latest-${{ hashFiles('package-lock.json') }} - run: node ./scripts/test-node-range.js test-matrix: timeout-minutes: 30 strategy: fail-fast: false matrix: # Test with Node.js active LTS versions, and the latest version node: - '22.22.1' # lowest supported version - '22.x' - '24.x' - '26.x' # Test with Ubuntu, macOS, and Windows os: - ubuntu-latest - macos-latest - windows-latest name: Node.js v${{ matrix.node }} on ${{ matrix.os }} runs-on: ${{ matrix.os }} steps: - if: matrix.os == 'windows-latest' run: git config --global core.autocrlf true - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # https://github.com/actions/checkout/releases/tag/v6.0.2 - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # https://github.com/actions/setup-node/releases/tag/v6.4.0 with: cache: 'npm' node-version: ${{ matrix.node }} package-manager-cache: false - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # https://github.com/actions/cache/releases/tag/v5.0.5 id: cache-node_modules with: path: node_modules key: node-modules-${{ matrix.os }}-${{ hashFiles('package-lock.json') }} - if: steps.cache-node_modules.outputs.cache-hit != 'true' run: npm ci --ignore-scripts - run: node --version - run: npm --version - run: git --version - run: node bin/lint-staged.js --version - run: node bin/lint-staged.js --help # Run tests - run: npm test test-deno: timeout-minutes: 30 strategy: fail-fast: false matrix: os: - ubuntu-latest - macos-latest - windows-latest name: Deno (latest) on ${{ matrix.os }} runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # https://github.com/actions/checkout/releases/tag/v6.0.2 - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # https://github.com/actions/setup-node/releases/tag/v6.4.0 with: cache: 'npm' node-version-file: .node-version package-manager-cache: false - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # https://github.com/actions/cache/releases/tag/v5.0.5 id: cache-node_modules with: path: node_modules key: node-modules-${{ matrix.os }}-${{ hashFiles('package-lock.json') }} - if: steps.cache-node_modules.outputs.cache-hit != 'true' run: npm ci --ignore-scripts - uses: denoland/setup-deno@667a34cdef165d8d2b2e98dde39547c9daac7282 # https://github.com/denoland/setup-deno/releases/tag/v2.0.4 with: deno-version: latest - run: node --version - run: npm --version - run: deno --version - run: git --version - run: deno -A bin/lint-staged.js --version - run: deno -A bin/lint-staged.js --help - run: deno run test test-bun: timeout-minutes: 30 strategy: fail-fast: false matrix: os: - ubuntu-latest - macos-latest - windows-latest name: Bun (latest) on ${{ matrix.os }} runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # https://github.com/actions/checkout/releases/tag/v6.0.2 - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # https://github.com/oven-sh/setup-bun/releases/tag/v2.2.0 with: bun-version: latest - run: bun install --frozen-lockfile - run: bun --version - run: git --version - run: bun bin/lint-staged.js --version - run: bun bin/lint-staged.js --help - run: bun --bun run test test-msys2: timeout-minutes: 30 strategy: fail-fast: false matrix: # Test with Node.js active LTS versions, and the latest version node: - '22.22.1' # lowest supported version - '22.x' - '24.x' - '25.x' name: Node.js ${{ matrix.node }} on windows-latest (MSYS2) runs-on: windows-latest defaults: run: shell: msys2 {0} steps: - uses: msys2/setup-msys2@e9898307ac31d1a803454791be09ab9973336e1c # https://github.com/msys2/setup-msys2/releases/tag/v2.31.1 with: msystem: UCRT64 path-type: inherit install: >- git - run: git config --global core.autocrlf true - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # https://github.com/actions/checkout/releases/tag/v6.0.2 - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # https://github.com/actions/setup-node/releases/tag/v6.4.0 with: cache: 'npm' node-version: ${{ matrix.node }} package-manager-cache: false - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # https://github.com/actions/cache/releases/tag/v5.0.5 id: cache-node_modules with: path: node_modules key: node-modules-windows-msys2-latest-${{ hashFiles('package-lock.json') }} - if: steps.cache-node_modules.outputs.cache-hit != 'true' run: npm ci --ignore-scripts - run: node --version - run: npm --version - run: git --version - run: node bin/lint-staged.js --version - run: node bin/lint-staged.js --help - run: npm test test-cygwin: timeout-minutes: 30 strategy: fail-fast: false matrix: # Test with Node.js active LTS versions, and the latest version node: - '22.22.1' # lowest supported version - '22.x' - '24.x' - '25.x' name: Node.js ${{ matrix.node }} on windows-latest (Cygwin) runs-on: windows-latest steps: - uses: cygwin/cygwin-install-action@711d29f3da23c9f4a1798e369a6f01198c13b11a # https://github.com/cygwin/cygwin-install-action/tree/v6.1 with: site: | https://mirrors.kernel.org/sourceware/cygwin/ - run: git config --global core.autocrlf true - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # https://github.com/actions/checkout/releases/tag/v6.0.2 - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # https://github.com/actions/setup-node/releases/tag/v6.4.0 with: cache: 'npm' node-version: ${{ matrix.node }} package-manager-cache: false - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # https://github.com/actions/cache/releases/tag/v5.0.5 id: cache-node_modules with: path: node_modules key: node-modules-windows-cygwin-latest-${{ hashFiles('package-lock.json') }} - if: steps.cache-node_modules.outputs.cache-hit != 'true' run: npm ci --ignore-scripts - run: node --version - run: npm --version - run: git --version - run: node bin/lint-staged.js --version - run: node bin/lint-staged.js --help - run: npm test
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
This workflow runs 11 jobs (32 with the matrix expanded) per trigger. On Latchkey the same minutes cost up to 58% less than GitHub-hosted, with zero queue time.