CI workflow (changesets/changesets)
The CI workflow from changesets/changesets, explained and optimized by Latchkey.
A
CI health: A - excellent
Point runs-on at Latchkey and get 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 changesets/changesets 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:
pull_request:
# merge queue is required so all commits on target branches trigger this workflow
# despite lack of the push event trigger here
merge_group:
branches:
- main
- next
# merge group rulesets don't allow wildcards so in settings each maintenance branch needs to be added separately
- "maintenance/v*" # branch rulesets don't support v[0-9]+
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.sha }}
cancel-in-progress: true
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Check out repo
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- uses: ./.github/actions/ci-setup
- name: Build artifacts
run: pnpm build
- name: Upload artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
# the size of the dist artifacts is not very large, so we can use better compression without a slowdown
compression-level: 8
# store it for as short as possible, since we only need it for the same workflow run
retention-days: 1
name: dist
path: |
packages/*/dist
scripts/*/dist
lint-workflows:
name: Lint workflows
runs-on: ubuntu-latest
permissions:
actions: read # only required in private repos
security-events: write # allow writing security events
steps:
- name: Checkout repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- name: Run zizmor
uses: zizmorcore/zizmor-action@5f14fd08f7cf1cb1609c1e344975f152c7ee938d # v0.5.6
with:
persona: pedantic
annotations: true
advanced-security: false
test:
name: "Test using Node ${{ matrix.node_version }}"
needs: [build]
runs-on: ubuntu-latest
timeout-minutes: 20
strategy:
matrix:
node_version: [22, 24, 26]
fail-fast: false
steps:
- name: Check out repo
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- uses: ./.github/actions/ci-setup
with:
node-version: ${{ matrix.node_version }}
download-dist: true
- name: Check Git version
run: git --version
- name: Setup mock Git user
run: git config --global user.email "you@example.com" && git config --global user.name "Your Name"
- name: Vitest tests
if: matrix.node_version != 24
run: pnpm test
# Run coverage only once on node 24 to avoid duplicate codecov uploads
- name: Vitest tests with coverage
if: matrix.node_version == 24
run: pnpm test --coverage
- name: Upload coverage
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
if: matrix.node_version == 24
with:
token: ${{ secrets.CODECOV_TOKEN }}
typecheck:
name: Typecheck
needs: [build]
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Check out repo
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- uses: ./.github/actions/ci-setup
with:
download-dist: true
- name: Typecheck
run: pnpm types:check
lint:
name: Lint
needs: [build]
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Check out repo
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- uses: ./.github/actions/ci-setup
with:
download-dist: true
- name: Lint
run: pnpm lint
- name: Format
run: pnpm format
ci-ok:
name: CI OK
runs-on: ubuntu-latest
if: always()
needs: [lint-workflows, build, test, typecheck, lint]
steps:
- name: Exit with error if some jobs are not successful
run: exit 1
if: ${{ always() && (contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')) }}
The same workflow, on Latchkey
Removes redundant runs and caps runaway jobs. Added and changed lines are highlighted.
name: CI on: pull_request: # merge queue is required so all commits on target branches trigger this workflow # despite lack of the push event trigger here merge_group: branches: - main - next # merge group rulesets don't allow wildcards so in settings each maintenance branch needs to be added separately - "maintenance/v*" # branch rulesets don't support v[0-9]+ permissions: contents: read concurrency: group: ${{ github.workflow }}-${{ github.sha }} cancel-in-progress: true jobs: build: timeout-minutes: 30 name: Build runs-on: latchkey-small steps: - name: Check out repo uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false - uses: ./.github/actions/ci-setup - name: Build artifacts run: pnpm build - name: Upload artifacts uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: # the size of the dist artifacts is not very large, so we can use better compression without a slowdown compression-level: 8 # store it for as short as possible, since we only need it for the same workflow run retention-days: 1 name: dist path: | packages/*/dist scripts/*/dist lint-workflows: timeout-minutes: 30 name: Lint workflows runs-on: latchkey-small permissions: actions: read # only required in private repos security-events: write # allow writing security events steps: - name: Checkout repository uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false - name: Run zizmor uses: zizmorcore/zizmor-action@5f14fd08f7cf1cb1609c1e344975f152c7ee938d # v0.5.6 with: persona: pedantic annotations: true advanced-security: false test: name: "Test using Node ${{ matrix.node_version }}" needs: [build] runs-on: latchkey-small timeout-minutes: 20 strategy: matrix: node_version: [22, 24, 26] fail-fast: false steps: - name: Check out repo uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false - uses: ./.github/actions/ci-setup with: node-version: ${{ matrix.node_version }} download-dist: true - name: Check Git version run: git --version - name: Setup mock Git user run: git config --global user.email "you@example.com" && git config --global user.name "Your Name" - name: Vitest tests if: matrix.node_version != 24 run: pnpm test # Run coverage only once on node 24 to avoid duplicate codecov uploads - name: Vitest tests with coverage if: matrix.node_version == 24 run: pnpm test --coverage - name: Upload coverage uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 if: matrix.node_version == 24 with: token: ${{ secrets.CODECOV_TOKEN }} typecheck: name: Typecheck needs: [build] runs-on: latchkey-small timeout-minutes: 20 steps: - name: Check out repo uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false - uses: ./.github/actions/ci-setup with: download-dist: true - name: Typecheck run: pnpm types:check lint: name: Lint needs: [build] runs-on: latchkey-small timeout-minutes: 20 steps: - name: Check out repo uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false - uses: ./.github/actions/ci-setup with: download-dist: true - name: Lint run: pnpm lint - name: Format run: pnpm format ci-ok: timeout-minutes: 30 name: CI OK runs-on: latchkey-small if: always() needs: [lint-workflows, build, test, typecheck, lint] steps: - name: Exit with error if some jobs are not successful run: exit 1 if: ${{ always() && (contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')) }}
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. - Add a job timeout so a hung step cannot burn hours of runner time.
This workflow runs 6 jobs (8 with the matrix expanded) per trigger. On Latchkey the same minutes cost up to 58% less than GitHub-hosted, with zero queue time.