Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions .github/actions/npm/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ runs:
with:
node-version-file: 'package.json'

- uses: prosopo/github_actions/.github/actions/restore_npm_cache@main
- id: cache
uses: prosopo/github_actions/.github/actions/restore_npm_cache@main
if: ${{ inputs.restore_npm_cache }}

- name: install npm
Expand All @@ -34,8 +35,18 @@ runs:
# Commented until npm fixes itself https://github.com/npm/cli/issues/8757
# npm i -g "npm@$(jq -r '.engines.npm // "latest"' < package.json)"

# Skip `npm ci` when node_modules was restored from an exact-lockfile cache hit: the tree
# already matches the lockfile, and `npm ci` would only delete it and reinstall the identical
# tree (~2 min wasted). On a cache miss (or when caching is disabled) the output is not 'true'
# and `npm ci` runs as normal, so this is always safe.
- name: install project
shell: bash
if: ${{ inputs.npm_ci }}
if: ${{ inputs.npm_ci && steps.cache.outputs.node-modules-cache-hit != 'true' }}
run: |
npm ci ${{ inputs.npm_ci_args }}

- name: skipped install (node_modules cache hit)
shell: bash
if: ${{ inputs.npm_ci && steps.cache.outputs.node-modules-cache-hit == 'true' }}
run: |
echo "node_modules restored from exact-lockfile cache hit; skipping 'npm ci'."
21 changes: 21 additions & 0 deletions .github/actions/restore_npm_cache/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ inputs:
required: false
default: "false"

outputs:
node-modules-cache-hit:
description: "'true' when the node_modules cache was hit exactly (lockfile unchanged). When 'true', node_modules is already populated for the current lockfile and `npm ci` can be skipped (running it would only wipe and recreate the identical tree)."
value: ${{ steps.restore-node-modules.outputs.cache-hit }}

runs:
using: "composite"
steps:
Expand Down Expand Up @@ -37,3 +42,19 @@ runs:
key: some-unused-cache-key
restore-keys: |
npm-${{ runner.os }}-${{ runner.arch }}-

# node_modules is cached separately from the npm/turbo/nx caches above because it needs the
# OPPOSITE matching behaviour: an EXACT match on the lockfile hash only, never a fuzzy fallback.
# A node_modules tree from a different lockfile would be wrong, so there is deliberately no
# restore-keys here. On an exact hit the consumer skips `npm ci` entirely (it would only wipe
# and recreate the identical tree); on a miss the consumer falls back to `npm ci`, which is
# also why a miss is always safe. The cache is populated by save_npm_cache (run on main), so
# lockfile-unchanged PRs hit; lockfile-changing PRs miss until main re-primes after merge.
- name: Restore node_modules
id: restore-node-modules
if: ${{ runner.environment != 'self-hosted' || inputs.restore-on-self-hosted == 'true' }}
uses: actions/cache/restore@v4
with:
path: |
**/node_modules
key: node-modules-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/package-lock.json') }}
26 changes: 26 additions & 0 deletions .github/actions/save_npm_cache/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,17 @@ runs:
.nx/cache
key: npm-${{ runner.os }}-${{ runner.arch }}-${{ github.run_id }}-${{ github.run_attempt }}

# node_modules is saved under an EXACT lockfile-hash key (matching restore_npm_cache) so that
# lockfile-unchanged consumers get an exact hit and can skip `npm ci`. The key is stable across
# runs while the lockfile is unchanged, so actions/cache/save is a no-op (warns, doesn't fail)
# once the cache for a given lockfile already exists.
- name: Save node_modules cache
uses: actions/cache/save@v4
with:
path: |
**/node_modules
key: node-modules-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/package-lock.json') }}

- name: Cleanup npm caches
shell: bash
run: |
Expand All @@ -42,3 +53,18 @@ runs:
do
gh cache delete "$cacheKey" -R "${{ github.repository }}"
done

- name: Cleanup node_modules caches
shell: bash
run: |
# node_modules caches are keyed by lockfile hash, so stale hashes accumulate as the
# lockfile changes. Keep only the most recent (the one we just saved) and remove the rest.
echo "Fetching list of node_modules cache keys"
cacheKeys=$(gh cache list --sort created_at --order desc --limit 100 -R "${{ github.repository }}" --key "node-modules-${{ runner.os }}-${{ runner.arch }}-" | cut -f 1 | tail -n +2)
echo caches to be removed:
echo "${cacheKeys}"
set +e
for cacheKey in $cacheKeys
do
gh cache delete "$cacheKey" -R "${{ github.repository }}"
done