mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-17 12:22:34 +03:00
Every job installs through this composite — 36 times per ci.yml run, 8 per quality.yml run — and each call paid ~80-90 s of npm ci even with setup-node's npm tarball cache warm (measured 2026-09-01: 3,327 runner-seconds per ci.yml run just installing). A node_modules cache keyed on runner.os + runner.arch + the resolved Node version + hashFiles(package-lock.json, .npmrc, postinstall.mjs and its five helpers) lets an exact hit skip the install entirely. - No restore-keys, same rule as the ESLint cache (#11600): exact key or a full npm ci, never a partial tree from another lockfile / Node / postinstall. - The retry loop is unchanged and remains the miss path; --no-audit --no-fund because audit:deps is its own gate. - cache input (default true) lets a caller opt out. - actions/cache pinned to the v6.1.0 hash already used in nightly-mutation.yml (zizmor unpinned-uses blanket policy). - tests/unit/build/npm-ci-retry-composite.test.ts pins the key contents, the no-restore-keys rule and the miss path. Refs #8084
This commit is contained in:
committed by
GitHub
parent
713440be0a
commit
e26a649d26
48
.github/actions/npm-ci-retry/action.yml
vendored
48
.github/actions/npm-ci-retry/action.yml
vendored
@@ -1,9 +1,45 @@
|
||||
name: npm ci with retry
|
||||
description: Run npm ci with retries for transient registry/network failures.
|
||||
description: >-
|
||||
Install dependencies. Restores node_modules from the Actions cache when the exact
|
||||
lockfile / runner / Node version / postinstall inputs match; otherwise runs npm ci
|
||||
with retries for transient registry/network failures and saves the tree for the
|
||||
next run.
|
||||
inputs:
|
||||
cache:
|
||||
description: Set to "false" to skip the node_modules cache and always run npm ci.
|
||||
required: false
|
||||
default: "true"
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- shell: bash
|
||||
- name: Resolve Node version for the cache key
|
||||
id: node
|
||||
shell: bash
|
||||
run: echo "version=$(node --version)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# #8084 D3 (plan 3.8.51 task 5): every job used to pay ~80-90 s of `npm ci` even
|
||||
# with setup-node's npm tarball cache warm — 36 jobs per ci.yml run, ~55 min of
|
||||
# runner time per run just installing. A node_modules cache keyed on EVERYTHING
|
||||
# that shapes the tree lets a hit skip the install entirely.
|
||||
#
|
||||
# No restore-keys on purpose (same rule as the ESLint cache, #11600): a partial
|
||||
# tree from another lockfile / Node / postinstall script is exactly the kind of
|
||||
# silent drift a lockfile-pinned CI must never inherit. Exact key or a full npm ci.
|
||||
#
|
||||
# postinstall (scripts/build/postinstall.mjs + helpers) only mutates node_modules
|
||||
# on a plain install — its dist/ branch is gated on dist/ existing, which never
|
||||
# holds at install time in CI — so the cached tree already carries its effects.
|
||||
- name: Restore node_modules
|
||||
id: node-modules
|
||||
if: inputs.cache == 'true'
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: node_modules
|
||||
key: node-modules-${{ runner.os }}-${{ runner.arch }}-${{ steps.node.outputs.version }}-${{ hashFiles('package-lock.json', '.npmrc', 'scripts/build/postinstall.mjs', 'scripts/build/postinstallSupport.mjs', 'scripts/build/colocateOptionals.mjs', 'scripts/build/fixTlsClientNodeBinary.mjs', 'scripts/build/fixPlaywrightAndroid.mjs', 'scripts/build/native-binary-compat.mjs') }}
|
||||
|
||||
- name: npm ci (with retry)
|
||||
if: steps.node-modules.outputs.cache-hit != 'true'
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
@@ -15,7 +51,8 @@ runs:
|
||||
echo "npm ci attempt $attempt/$max_attempts after transient failure"
|
||||
fi
|
||||
|
||||
if npm ci; then
|
||||
# --no-audit: `audit:deps` is its own gate; the inline audit only adds latency.
|
||||
if npm ci --no-audit --no-fund; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
@@ -27,3 +64,8 @@ runs:
|
||||
sleep "$delay_seconds"
|
||||
delay_seconds=$((delay_seconds * 2))
|
||||
done
|
||||
|
||||
- name: node_modules restored from cache
|
||||
if: steps.node-modules.outputs.cache-hit == 'true'
|
||||
shell: bash
|
||||
run: echo "node_modules restored from cache (key hit) — npm ci skipped"
|
||||
|
||||
90
tests/unit/build/npm-ci-retry-composite.test.ts
Normal file
90
tests/unit/build/npm-ci-retry-composite.test.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* .github/actions/npm-ci-retry — node_modules cache contract (#8084 D3, plan 3.8.51 task 5).
|
||||
*
|
||||
* Every CI job installs through this composite (36× per ci.yml run, ~80-90 s each with only
|
||||
* the npm tarball cache). The node_modules cache must (a) key on everything that shapes the
|
||||
* tree, (b) never fall back to a partial tree from another key (#11600 rule), and (c) keep
|
||||
* the retry loop as the miss path. Pin those so a later "simplification" cannot reopen it.
|
||||
*/
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { parse } from "yaml";
|
||||
|
||||
const ACTION = path.resolve(
|
||||
import.meta.dirname,
|
||||
"../../../.github/actions/npm-ci-retry/action.yml"
|
||||
);
|
||||
const raw = fs.readFileSync(ACTION, "utf8");
|
||||
const action = parse(raw) as {
|
||||
runs: { using: string; steps: Array<Record<string, unknown>> };
|
||||
inputs?: Record<string, { default?: string }>;
|
||||
};
|
||||
|
||||
const step = (id: string) =>
|
||||
action.runs.steps.find((s) => s.id === id) as Record<string, unknown> | undefined;
|
||||
|
||||
test("composite restores node_modules via actions/cache with an exact, fully-qualified key", () => {
|
||||
const cache = step("node-modules");
|
||||
assert.ok(cache, "missing restore step with id node-modules");
|
||||
assert.match(String(cache!.uses), /^actions\/cache@/);
|
||||
const w = cache!.with as Record<string, string>;
|
||||
assert.equal(w.path, "node_modules");
|
||||
for (const input of [
|
||||
"runner.os",
|
||||
"runner.arch",
|
||||
"steps.node.outputs.version",
|
||||
"package-lock.json",
|
||||
".npmrc",
|
||||
]) {
|
||||
assert.ok(w.key.includes(input), `cache key must include ${input}`);
|
||||
}
|
||||
// Every postinstall script that mutates node_modules must be part of the key.
|
||||
for (const script of [
|
||||
"scripts/build/postinstall.mjs",
|
||||
"scripts/build/postinstallSupport.mjs",
|
||||
"scripts/build/colocateOptionals.mjs",
|
||||
"scripts/build/fixTlsClientNodeBinary.mjs",
|
||||
"scripts/build/fixPlaywrightAndroid.mjs",
|
||||
"scripts/build/native-binary-compat.mjs",
|
||||
]) {
|
||||
assert.ok(w.key.includes(script), `cache key must include ${script}`);
|
||||
assert.ok(
|
||||
fs.existsSync(path.resolve(import.meta.dirname, "../../..", script)),
|
||||
`${script} vanished — update the key`
|
||||
);
|
||||
}
|
||||
assert.equal(
|
||||
w["restore-keys"],
|
||||
undefined,
|
||||
"no restore-keys: exact key or a full npm ci (#11600)"
|
||||
);
|
||||
});
|
||||
|
||||
test("npm ci is the cache-miss path and still retries", () => {
|
||||
const install = action.runs.steps.find((s) => String(s.name).startsWith("npm ci"));
|
||||
assert.ok(install);
|
||||
assert.equal(install!.if, "steps.node-modules.outputs.cache-hit != 'true'");
|
||||
assert.match(String(install!.run), /max_attempts=3/);
|
||||
assert.match(String(install!.run), /npm ci --no-audit --no-fund/);
|
||||
});
|
||||
|
||||
test("cache can be disabled per caller and defaults on", () => {
|
||||
assert.equal(action.inputs?.cache?.default, "true");
|
||||
assert.equal(step("node-modules")!.if, "inputs.cache == 'true'");
|
||||
});
|
||||
|
||||
test("every postinstall helper imported by postinstall.mjs is in the cache key", () => {
|
||||
const post = fs.readFileSync(
|
||||
path.resolve(import.meta.dirname, "../../../scripts/build/postinstall.mjs"),
|
||||
"utf8"
|
||||
);
|
||||
const imports = [...post.matchAll(/from "\.\/([a-zA-Z-]+\.mjs)"/g)].map(
|
||||
(m) => `scripts/build/${m[1]}`
|
||||
);
|
||||
assert.ok(imports.length >= 4, "expected postinstall.mjs to import its helpers");
|
||||
const key = (step("node-modules")!.with as Record<string, string>).key;
|
||||
for (const imp of imports)
|
||||
assert.ok(key.includes(imp), `postinstall imports ${imp} but the cache key omits it`);
|
||||
});
|
||||
Reference in New Issue
Block a user