From 157d2dcf507c632b39cdc0a4df5b8b5a7945a89b Mon Sep 17 00:00:00 2001 From: laurenhitchon Date: Thu, 30 Jul 2026 12:27:33 +1000 Subject: [PATCH] feat(ci): cache npm tarballs and supersede in-flight PR runs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - cache: npm + cache-dependency-path on all 8 setup-node steps in reusable-ci — restores ~/.npm keyed on the lockfile hash; npm ci still wipes node_modules and verifies lockfile integrity hashes, so the merge-gate guarantee is unchanged. Release path stays uncached. - concurrency group in the ci.yml stub: rapid successive pushes cancel superseded runs instead of burning 4 duplicate jobs to completion. --- .github/workflows/reusable-ci.yml | 24 ++++++++++++++++++++++++ workflow-stubs/ci.yml | 9 +++++++++ 2 files changed, 33 insertions(+) diff --git a/.github/workflows/reusable-ci.yml b/.github/workflows/reusable-ci.yml index 6c86906..c09d23d 100644 --- a/.github/workflows/reusable-ci.yml +++ b/.github/workflows/reusable-ci.yml @@ -1,6 +1,14 @@ # Reusable implementation — repos call this via the synced ci.yml stub. # Edit here; never in a consumer repo. # +# Caching policy: every setup-node step caches ~/.npm keyed on the lockfile +# hash. This only changes where npm ci fetches tarballs from — node_modules +# is still wiped and rebuilt, and npm ci still verifies every package +# against the lockfile's integrity hashes, so the lockfile-integrity +# guarantee this workflow exists for is untouched. Lockfile-bump PRs are +# cold by definition. reusable-release deliberately stays UNCACHED +# (package-manager-cache: false) — keep the publish path pristine. +# # This is the merge gate rulesets require (context: "install / install"). # It ports nswds-design's local CI workflow: the pull_request event checks # out the test merge of PR + base, so a bad conflict resolution or a @@ -61,11 +69,15 @@ jobs: uses: actions/setup-node@v7 with: node-version-file: '.nvmrc' + cache: 'npm' + cache-dependency-path: package-lock.json - name: Setup Node.js (fallback) if: ${{ hashFiles('.nvmrc') == '' }} uses: actions/setup-node@v7 with: node-version: ${{ inputs.node-version }} + cache: 'npm' + cache-dependency-path: package-lock.json # Fails on any manifest/lockfile mismatch — the check Snyk-corrupted # lockfiles used to sail past when nothing enforced it. @@ -99,11 +111,15 @@ jobs: uses: actions/setup-node@v7 with: node-version-file: '.nvmrc' + cache: 'npm' + cache-dependency-path: package-lock.json - name: Setup Node.js (fallback) if: ${{ hashFiles('.nvmrc') == '' }} uses: actions/setup-node@v7 with: node-version: ${{ inputs.node-version }} + cache: 'npm' + cache-dependency-path: package-lock.json - name: Install dependencies run: npm clean-install @@ -145,11 +161,15 @@ jobs: uses: actions/setup-node@v7 with: node-version-file: '.nvmrc' + cache: 'npm' + cache-dependency-path: package-lock.json - name: Setup Node.js (fallback) if: ${{ steps.prettier.outputs.present == 'true' && hashFiles('.nvmrc') == '' }} uses: actions/setup-node@v7 with: node-version: ${{ inputs.node-version }} + cache: 'npm' + cache-dependency-path: package-lock.json - name: Install dependencies if: ${{ steps.prettier.outputs.present == 'true' }} @@ -190,11 +210,15 @@ jobs: uses: actions/setup-node@v7 with: node-version-file: '.nvmrc' + cache: 'npm' + cache-dependency-path: package-lock.json - name: Setup Node.js (fallback) if: ${{ hashFiles('.nvmrc') == '' }} uses: actions/setup-node@v7 with: node-version: ${{ inputs.node-version }} + cache: 'npm' + cache-dependency-path: package-lock.json - name: Install dependencies if: ${{ hashFiles('vitest.config.*') != '' }} diff --git a/workflow-stubs/ci.yml b/workflow-stubs/ci.yml index ec26634..4e0bf71 100644 --- a/workflow-stubs/ci.yml +++ b/workflow-stubs/ci.yml @@ -7,6 +7,15 @@ name: CI on: pull_request: +# Rapid successive pushes to a PR previously ran full duplicate 4-job CI to +# completion; only the latest push's run matters for the merge gate, so +# supersede in-flight runs. Lives HERE (not in the reusable) because +# workflow-level concurrency in a called workflow is not applied — the run +# belongs to this caller. +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + permissions: contents: read