@theoptimalpartner/npm-security-kit 2.0.0-beta.4 → 2.0.0-beta.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/README.md +61 -11
- package/package.json +1 -1
- package/src/checks/dockerfile.js +13 -0
- package/src/checks/node-version.js +86 -4
- package/src/checks/npm-policy.js +13 -6
- package/src/init.js +86 -22
- package/src/lib/node-deferral.js +39 -0
- package/src/policy.js +4 -0
package/CHANGELOG.md
CHANGED
|
@@ -38,6 +38,17 @@ Sale primero como beta (`2.0.0-beta.N`), con el dist-tag `beta`.
|
|
|
38
38
|
da EBADENGINE con el `engines.node` de `init`). Los pasos siguientes de `init` incluyen el cambio de línea, el
|
|
39
39
|
lockfile y el `prepare` del proyecto; la sección del pipeline menciona `custom: ci`.
|
|
40
40
|
- `check` sin `package.json` termina con exit 2 ("no es un proyecto npm"), como `init`.
|
|
41
|
+
- **2.0.0-beta.5** (ajustes de una tercera prueba):
|
|
42
|
+
- `init` guía el cambio de línea también desde una línea sin soporte o no LTS (antes solo desde una sin npm 11), y
|
|
43
|
+
sube `engines.node` si admite una línea que hay que cambiar.
|
|
44
|
+
- `init` señala el `npm ci` del step que corre en los PR (inline o por su ancla) y aparte los de deploy y
|
|
45
|
+
publicación, que no necesitan el kit. No repite la imagen si el pipeline ya usa la que propone.
|
|
46
|
+
- `dockerfile`: en un monorepo, un Dockerfile cuyo contexto de build es una subcarpeta sin `.npmrc` instala sin la
|
|
47
|
+
política aunque la raíz lo tenga; ahora es error.
|
|
48
|
+
- Excepción con fecha para diferir el cambio de línea de Node cuando el salto es de más de 2 versiones mayores
|
|
49
|
+
(`npmSecurityKit.nodeUpgradeDeferredUntil` y `nodeUpgradeReason` en el `package.json`, hasta 183 días): mientras
|
|
50
|
+
está vigente, los errores de la línea de Node y de npm 10 son avisos e `init` no toca `engines`. El kit la sugiere
|
|
51
|
+
cuando el salto es grande.
|
|
41
52
|
|
|
42
53
|
### BREAKING
|
|
43
54
|
|
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Reglas de seguridad de dependencias npm en un solo comando que corre en el pipeline **antes de `npm ci`**. No tiene
|
|
4
4
|
dependencias: `npx` descarga un solo tarball y revisa el lockfile antes de que se ejecute un solo install script.
|
|
5
5
|
|
|
6
|
-
Los ejemplos usan la versión de este paquete (`2.0.0-beta.
|
|
6
|
+
Los ejemplos usan la versión de este paquete (`2.0.0-beta.5`). Las publicadas, con sus dist-tags:
|
|
7
7
|
`npm view @theoptimalpartner/npm-security-kit dist-tags`.
|
|
8
8
|
|
|
9
9
|
## Qué revisa
|
|
@@ -12,7 +12,7 @@ Los ejemplos usan la versión de este paquete (`2.0.0-beta.4`). Las publicadas,
|
|
|
12
12
|
|---|---|---|---|
|
|
13
13
|
| `malware` | `npm audit` sobre **todo** el lockfile, incluidas las devDependencies | Cualquier advisory de malware (CWE-506), con cualquier severidad | — |
|
|
14
14
|
| `vulnerabilities` | `npm audit --omit=dev` y, para las de desarrollo, el de todo el árbol | Vulnerabilidades `critical` en producción | Resumen de las `high` en producción; `critical` solo en devDependencies |
|
|
15
|
-
| `node-version` | `.nvmrc`, `.node-version`, `.tool-versions`, `engines.node`, `volta.node`, `FROM node:` y `image:` del pipeline | Línea de Node sin soporte, que no es LTS o que no trae npm >= 11.17 en ninguna release (Node 22 trae npm 10) | Hay una LTS más nueva (mientras no tenga su primera release LTS, solo se informa), pocos días de soporte, parche de seguridad pendiente, versiones mayores distintas, nada fijado |
|
|
15
|
+
| `node-version` | `.nvmrc`, `.node-version`, `.tool-versions`, `engines.node`, `volta.node`, `FROM node:` y `image:` del pipeline | Línea de Node sin soporte, que no es LTS o que no trae npm >= 11.17 en ninguna release (Node 22 trae npm 10); con un salto de más de 2 versiones, diferible con una excepción con fecha | Hay una LTS más nueva (mientras no tenga su primera release LTS, solo se informa), pocos días de soporte, parche de seguridad pendiente, versiones mayores distintas, nada fijado |
|
|
16
16
|
| `npm-policy` | `.npmrc`, `engines.npm`, npm en uso, lockfile | Faltan `min-release-age`, `engine-strict` o `strict-allow-scripts`; npm < 11.17; no hay lockfile | Faltan `min-release-age-exclude[]`, `save-exact` o `audit-level`; lockfile < v3 |
|
|
17
17
|
| `install-scripts` | Paquetes con install scripts del lockfile contra `allowScripts` | Alguno sin aprobar (con `strict-allow-scripts`) | Alguno sin aprobar con el override por variable de entorno |
|
|
18
18
|
| `dockerfile` | `COPY` del `.npmrc` antes de `npm ci`, `.dockerignore` | Instala sin el `.npmrc` | Usa `npm install` en vez de `npm ci`; el `.dockerignore` deja fuera el lockfile |
|
|
@@ -48,7 +48,8 @@ subir a una LTS que traiga npm 11. `check` da error en cada lugar que fija la 22
|
|
|
48
48
|
`image:`) e `init` propone la línea nueva. Cuando hay que cambiar de línea (sin soporte, no LTS o sin npm 11), el
|
|
49
49
|
destino es uno solo: la LTS con soporte más baja que trae npm 11 (hoy la 24), en la release más nueva que **ya tiene
|
|
50
50
|
imagen oficial en Docker Hub** (las imágenes salen días después que la release en nodejs.org). En local, cambiar de
|
|
51
|
-
línea con `nvm install 24 && nvm use 24`: npm 11 sobre la 22 no alcanza, porque `engines.node` pide la 24.
|
|
51
|
+
línea con `nvm install 24 && nvm use 24`: npm 11 sobre la 22 no alcanza, porque `engines.node` pide la 24. Si el salto
|
|
52
|
+
es grande (18 o 20 → 24), ver **Diferir el cambio de Node** en las recetas.
|
|
52
53
|
|
|
53
54
|
## Uso
|
|
54
55
|
|
|
@@ -68,7 +69,7 @@ definitions:
|
|
|
68
69
|
- npm
|
|
69
70
|
script:
|
|
70
71
|
# Antes de instalar: malware, vulnerabilidades, Node, .npmrc, install scripts, Dockerfiles
|
|
71
|
-
- npx --yes --min-release-age=0 @theoptimalpartner/npm-security-kit@2.0.0-beta.
|
|
72
|
+
- npx --yes --min-release-age=0 @theoptimalpartner/npm-security-kit@2.0.0-beta.5 check
|
|
72
73
|
- npm ci # con strict-allow-scripts, los scripts no aprobados cortan acá
|
|
73
74
|
- npm audit signatures
|
|
74
75
|
# … y lo que ya corría el step: build, lint, tests
|
|
@@ -88,13 +89,13 @@ pipelines:
|
|
|
88
89
|
|
|
89
90
|
- **Antes de `npm ci`:** `npm audit` funciona solo con el lockfile, así que el malware se detecta antes de que
|
|
90
91
|
corra su `postinstall`.
|
|
91
|
-
- **Versión fija** (`@2.0.0-beta.
|
|
92
|
+
- **Versión fija** (`@2.0.0-beta.5`): subir la política en un repo es un cambio visible en su PR.
|
|
92
93
|
- **`--min-release-age=0` solo para `npx`:** `npx` no aplica `min-release-age-exclude` (npm 11.19.1 lo aplica al
|
|
93
94
|
instalar, no al resolver el paquete de `npx`). Sin el flag, dentro de un repo con `min-release-age=7` una versión
|
|
94
95
|
nueva del kit no se puede usar hasta cumplir 7 días (`ETARGET … with a date before`). Afecta solo al kit: la
|
|
95
96
|
versión va fija y no tiene dependencias. El `npm ci` que sigue aplica los 7 días como siempre.
|
|
96
97
|
- **`npm audit signatures`** va después de `npm ci` porque necesita `node_modules`.
|
|
97
|
-
- Para guardar el resultado como artefacto: `npx --yes --min-release-age=0 @theoptimalpartner/npm-security-kit@2.0.0-beta.
|
|
98
|
+
- Para guardar el resultado como artefacto: `npx --yes --min-release-age=0 @theoptimalpartner/npm-security-kit@2.0.0-beta.5 check --json > npm-security-kit.json`.
|
|
98
99
|
|
|
99
100
|
### Dockerfile
|
|
100
101
|
|
|
@@ -109,24 +110,28 @@ RUN npm ci
|
|
|
109
110
|
|
|
110
111
|
Si el `.dockerignore` excluye el `.npmrc` (por ejemplo con `.*`), agregar `!.npmrc` al final.
|
|
111
112
|
|
|
113
|
+
En un monorepo, si el Dockerfile se construye con una subcarpeta como contexto (`cd backend && docker build .`), el
|
|
114
|
+
`.npmrc` de la raíz no llega a la imagen: copiarlo a esa carpeta o construir con el contexto en la raíz. `check` lo
|
|
115
|
+
marca como error.
|
|
116
|
+
|
|
112
117
|
### Adoptarlo en un repo: `init`
|
|
113
118
|
|
|
114
119
|
Una vez por repo, en una rama:
|
|
115
120
|
|
|
116
121
|
```bash
|
|
117
|
-
npx --yes --min-release-age=0 @theoptimalpartner/npm-security-kit@2.0.0-beta.
|
|
122
|
+
npx --yes --min-release-age=0 @theoptimalpartner/npm-security-kit@2.0.0-beta.5 init --dry-run
|
|
118
123
|
```
|
|
119
124
|
|
|
120
125
|
```bash
|
|
121
|
-
npx --yes --min-release-age=0 @theoptimalpartner/npm-security-kit@2.0.0-beta.
|
|
126
|
+
npx --yes --min-release-age=0 @theoptimalpartner/npm-security-kit@2.0.0-beta.5 init
|
|
122
127
|
```
|
|
123
128
|
|
|
124
129
|
`init`:
|
|
125
130
|
|
|
126
131
|
- agrega al `.npmrc` las claves de la política que faltan (con un comentario) y corrige las que tienen otro valor,
|
|
127
132
|
sin borrar ni reordenar nada;
|
|
128
|
-
- completa `engines.npm` (`>=11.17`) y
|
|
129
|
-
si
|
|
133
|
+
- completa `engines.npm` (`>=11.17`) y `engines.node`: lo agrega si falta, con la línea de Node del repo si trae npm
|
|
134
|
+
11, y lo sube si admite una línea que hay que cambiar (sin soporte, no LTS o sin npm 11), a la línea propuesta;
|
|
130
135
|
- **no aprueba install scripts**: lista los pendientes y los comandos para revisarlos;
|
|
131
136
|
- **no edita** `bitbucket-pipelines.yml` ni los Dockerfiles: dice qué líneas agregar al step que corre `npm ci`, la
|
|
132
137
|
condition con las ramas que despliegan y los `COPY` corregidos;
|
|
@@ -137,7 +142,7 @@ Después: revisar el diff, aprobar o negar los install scripts, aplicar el step
|
|
|
137
142
|
### En local
|
|
138
143
|
|
|
139
144
|
```bash
|
|
140
|
-
npx --yes --min-release-age=0 @theoptimalpartner/npm-security-kit@2.0.0-beta.
|
|
145
|
+
npx --yes --min-release-age=0 @theoptimalpartner/npm-security-kit@2.0.0-beta.5 check
|
|
141
146
|
```
|
|
142
147
|
|
|
143
148
|
No se instala como devDependency ni usa husky: el `.npmrc` protege el `npm install` local y el pipeline es el
|
|
@@ -221,6 +226,51 @@ Si un repo no puede adoptar todavía la aprobación de install scripts, se defin
|
|
|
221
226
|
Con el override, npm ejecuta los scripts sin aprobar y el kit los informa como **aviso**, más un aviso de que el
|
|
222
227
|
override está activo. Dentro del `docker build` la variable no llega sola: hay que pasarla como `ARG`.
|
|
223
228
|
|
|
229
|
+
### Diferir el cambio de Node (salto grande)
|
|
230
|
+
|
|
231
|
+
Pasar de Node 18 o 20 a la 24 suele arrastrar dependencias y refactors. Para no frenar el repo mientras se planifica,
|
|
232
|
+
el cambio de línea se puede diferir con una excepción con fecha.
|
|
233
|
+
|
|
234
|
+
**Cuándo se puede usar**
|
|
235
|
+
|
|
236
|
+
- Solo si el salto de la línea del repo al destino (hoy la 24) es de **más de 2 versiones mayores**: 18 → 24 o
|
|
237
|
+
20 → 24 sí; 22 → 24 no (ese cambio es chico y no se difiere). `check` lo sugiere ("Salto grande (Node 18 → 24)…")
|
|
238
|
+
cuando aplica.
|
|
239
|
+
- Cuando subir ahora no es viable (dependencias que no soportan la línea nueva, un refactor que hay que planificar), y
|
|
240
|
+
con el OK de quien aprueba los cambios de seguridad del repo. No es para "lo veo después".
|
|
241
|
+
|
|
242
|
+
**Cómo**
|
|
243
|
+
|
|
244
|
+
En la raíz del `package.json` (al lado de `dependencies`), en un PR:
|
|
245
|
+
|
|
246
|
+
```json
|
|
247
|
+
"npmSecurityKit": {
|
|
248
|
+
"nodeUpgradeDeferredUntil": "2027-03-31",
|
|
249
|
+
"nodeUpgradeReason": "Express 4 → 5 y los tipos de Node 24"
|
|
250
|
+
}
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
- `nodeUpgradeDeferredUntil`: fecha `AAAA-MM-DD`, como mucho 183 días desde hoy.
|
|
254
|
+
- `nodeUpgradeReason`: por qué no se puede subir ahora. Es obligatorio.
|
|
255
|
+
- En la descripción del PR: el motivo, quién lo aprobó y el plan para subir antes de la fecha (tarjeta o PR).
|
|
256
|
+
|
|
257
|
+
**Qué cambia mientras está vigente**
|
|
258
|
+
|
|
259
|
+
- Los errores de la línea de Node y los de npm 10 (`npm` en uso, `engines.npm`) pasan a aviso, con la fecha y el motivo;
|
|
260
|
+
`check` dice cuántos días quedan. `init` no toca `engines`: los de la línea nueva, con `engine-strict`, romperían el
|
|
261
|
+
`npm ci` de la actual.
|
|
262
|
+
- Lo demás sigue bloqueando: malware, vulnerabilidades críticas en producción, install scripts sin aprobar, `.npmrc`
|
|
263
|
+
y Dockerfiles.
|
|
264
|
+
- **Ojo:** con npm 10 (la 18 no corre npm 11), `min-release-age` y `allowScripts` no hacen nada aunque estén en el
|
|
265
|
+
`.npmrc`. El repo queda sin la espera de 7 días ni el control de install scripts: el kit sigue revisando malware y
|
|
266
|
+
vulnerabilidades antes de `npm ci`, pero no reemplaza eso. Por eso la excepción vence.
|
|
267
|
+
|
|
268
|
+
**Al vencer**
|
|
269
|
+
|
|
270
|
+
- Los errores vuelven a bloquear el pipeline y `check` pide quitarla o renovarla.
|
|
271
|
+
- Renovarla es otro PR con fecha nueva (como mucho 183 días desde ese día), motivo y OK. Si la fecha es inválida,
|
|
272
|
+
falta el motivo o pasa de 183 días, el kit la ignora y lo avisa.
|
|
273
|
+
|
|
224
274
|
### Vulnerabilidades `critical` en devDependencies
|
|
225
275
|
|
|
226
276
|
No bloquean (no llegan a producción), pero corren en el pipeline y en las máquinas de los devs: `check` las informa
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@theoptimalpartner/npm-security-kit",
|
|
3
|
-
"version": "2.0.0-beta.
|
|
3
|
+
"version": "2.0.0-beta.5",
|
|
4
4
|
"description": "Revisa el lockfile antes de npm ci: malware, vulnerabilidades, versión de Node, .npmrc, install scripts y Dockerfiles",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/checks/dockerfile.js
CHANGED
|
@@ -138,6 +138,10 @@ export async function run(ctx) {
|
|
|
138
138
|
const own = dirOf(file);
|
|
139
139
|
const context = own && (ctx.exists(join(own, '.dockerignore')) || ctx.exists(join(own, 'package.json'))) ? own : '';
|
|
140
140
|
const hasNpmrc = ctx.exists(join(context, '.npmrc'));
|
|
141
|
+
// Monorepo: el contexto es una subcarpeta sin .npmrc y la política está en el de la raíz, que
|
|
142
|
+
// `docker build` no ve desde ahí.
|
|
143
|
+
const npmrcOutside = Boolean(context) && !hasNpmrc && ctx.exists('.npmrc');
|
|
144
|
+
let reportedOutside = false;
|
|
141
145
|
const ignoreFile = ctx.exists(`${file}.dockerignore`) ? `${file}.dockerignore` : join(context, '.dockerignore');
|
|
142
146
|
const ignoreText = ctx.readFile(ignoreFile);
|
|
143
147
|
const npmrcIgnored = hasNpmrc ? dockerignoreExcludes(ignoreText, '.npmrc') : null;
|
|
@@ -171,6 +175,15 @@ export async function run(ctx) {
|
|
|
171
175
|
const local = npmInstalls(ins.args).filter((n) => !n.global);
|
|
172
176
|
if (!local.length) continue;
|
|
173
177
|
installs = true;
|
|
178
|
+
if (npmrcOutside && !mountsNpmrc(ins.args) && !reportedOutside) {
|
|
179
|
+
reportedOutside = true;
|
|
180
|
+
findings.push({
|
|
181
|
+
level: 'error',
|
|
182
|
+
file,
|
|
183
|
+
line: ins.line,
|
|
184
|
+
message: `Instala sin el .npmrc: el contexto de build es ${context}/ (tiene su propio package.json o .dockerignore) y ahí no hay .npmrc; el de la raíz no llega a la imagen. Copiar el .npmrc a ${context}/ y agregarlo al COPY, o construir con el contexto en la raíz.`,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
174
187
|
if (hasNpmrc && !brought && !mountsNpmrc(ins.args) && !reported) {
|
|
175
188
|
reported = true;
|
|
176
189
|
findings.push({
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { findDockerfiles, nodeImageTag, parseStages } from '../lib/dockerfile.js';
|
|
6
6
|
import { jsonKeyLine } from '../lib/json-lines.js';
|
|
7
7
|
import { loadCalendar } from '../lib/node-calendar.js';
|
|
8
|
+
import { readDeferral } from '../lib/node-deferral.js';
|
|
8
9
|
import { imageExists, newestWithImage, upgradeTarget } from '../lib/node-target.js';
|
|
9
10
|
import { compareVersions, isRange, minVersion, parseVersion } from '../lib/semver.js';
|
|
10
11
|
|
|
@@ -166,11 +167,11 @@ async function evaluate(ctx, pin, r, cal, { target, newestLtsWithoutImage }) {
|
|
|
166
167
|
|
|
167
168
|
const phase = cal.phaseOf(r.major);
|
|
168
169
|
if (phase === 'eol') {
|
|
169
|
-
return { ...base, level: 'error', message: `${prefix}: sin soporte desde ${sch.end}. Actualizar a ${rec}${engineHint}.`, data };
|
|
170
|
+
return { ...base, level: 'error', message: `${prefix}: sin soporte desde ${sch.end}. Actualizar a ${rec}${engineHint}.`, data: { ...data, lineChange: true } };
|
|
170
171
|
}
|
|
171
172
|
if (phase === 'not-lts') {
|
|
172
173
|
const why = sch.lts ? `todavía no es LTS (lo será el ${sch.lts})` : 'línea impar, nunca va a ser LTS';
|
|
173
|
-
return { ...base, level: 'error', message: `${prefix}: no es LTS, ${why}. Usar ${rec}${engineHint}.`, data };
|
|
174
|
+
return { ...base, level: 'error', message: `${prefix}: no es LTS, ${why}. Usar ${rec}${engineHint}.`, data: { ...data, lineChange: true } };
|
|
174
175
|
}
|
|
175
176
|
// La política pide npm >= minNpmVersion y la imagen de Node trae el suyo: una línea que no lo trae en
|
|
176
177
|
// ninguna release (la 22 trae npm 10) deja el pipeline y el docker build con npm 10.
|
|
@@ -181,7 +182,7 @@ async function evaluate(ctx, pin, r, cal, { target, newestLtsWithoutImage }) {
|
|
|
181
182
|
...base,
|
|
182
183
|
level: 'error',
|
|
183
184
|
message: `${prefix}: trae npm ${lineNpm}, y ninguna release de la ${r.major} trae npm >= ${policy.minNpmVersion}, que pide la política. Subir a una LTS con npm 11: ${to} o más nueva${engineHint}.`,
|
|
184
|
-
data: { ...data, npm: lineNpm },
|
|
185
|
+
data: { ...data, npm: lineNpm, lineChange: true },
|
|
185
186
|
};
|
|
186
187
|
}
|
|
187
188
|
|
|
@@ -223,6 +224,40 @@ async function evaluate(ctx, pin, r, cal, { target, newestLtsWithoutImage }) {
|
|
|
223
224
|
return { ...base, level, message: `${prefix}: ${detail}`, data: { ...data, group: `v${r.major}: ${detail}` } };
|
|
224
225
|
}
|
|
225
226
|
|
|
227
|
+
// ¿Hay una excepción vigente que aplique a este repo? Aplica si alguna línea fijada que hay que cambiar
|
|
228
|
+
// (sin soporte, no LTS o sin npm 11) está a más de policy.nodeDeferMinJump versiones mayores del destino.
|
|
229
|
+
// La usan npm-policy (npm 10 y engines.npm) e init. Una vez por corrida.
|
|
230
|
+
const deferrals = new WeakMap();
|
|
231
|
+
export function activeDeferral(ctx) {
|
|
232
|
+
if (!deferrals.has(ctx)) deferrals.set(ctx, computeDeferral(ctx).catch(() => null));
|
|
233
|
+
return deferrals.get(ctx);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async function computeDeferral(ctx) {
|
|
237
|
+
const deferral = readDeferral(ctx);
|
|
238
|
+
if (deferral.status !== 'active') return null;
|
|
239
|
+
const cal = await loadCalendar(ctx);
|
|
240
|
+
const target = await upgradeTarget(ctx, cal);
|
|
241
|
+
if (!target) return null;
|
|
242
|
+
const majors = bigJumpLines(ctx, cal, target);
|
|
243
|
+
return majors.length ? { ...deferral, majors, target } : null;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Las líneas fijadas que hay que cambiar y están a más de nodeDeferMinJump versiones del destino.
|
|
247
|
+
function bigJumpLines(ctx, cal, target) {
|
|
248
|
+
const out = new Set();
|
|
249
|
+
for (const pin of findPins(ctx)) {
|
|
250
|
+
const r = resolvePin(pin, cal);
|
|
251
|
+
if (r.major === undefined || r.major === null || r.otherImage) continue;
|
|
252
|
+
if (target.major - r.major <= ctx.policy.nodeDeferMinJump) continue;
|
|
253
|
+
const phase = cal.phaseOf(r.major);
|
|
254
|
+
const npm = cal.npmOfLine(r.major);
|
|
255
|
+
const noNpm = npm && parseVersion(npm) && compareVersions(npm, ctx.policy.minNpmVersion) < 0;
|
|
256
|
+
if (phase === 'eol' || phase === 'not-lts' || noNpm) out.add(r.major);
|
|
257
|
+
}
|
|
258
|
+
return [...out].sort((a, b) => a - b);
|
|
259
|
+
}
|
|
260
|
+
|
|
226
261
|
export async function run(ctx) {
|
|
227
262
|
let cal;
|
|
228
263
|
try {
|
|
@@ -239,12 +274,59 @@ export async function run(ctx) {
|
|
|
239
274
|
const pins = findPins(ctx);
|
|
240
275
|
const findings = [];
|
|
241
276
|
const majors = new Set();
|
|
277
|
+
const deferral = readDeferral(ctx);
|
|
278
|
+
const max = ctx.policy.nodeDeferMaxDays;
|
|
279
|
+
const big = target ? new Set(bigJumpLines(ctx, cal, target)) : new Set();
|
|
280
|
+
const deferring = deferral.status === 'active' && big.size > 0;
|
|
242
281
|
let nodePins = 0;
|
|
243
282
|
for (const pin of pins) {
|
|
244
283
|
const r = resolvePin(pin, cal);
|
|
245
284
|
if (!r.otherImage) nodePins++;
|
|
246
285
|
if (r.major !== undefined && r.major !== null) majors.add(r.major);
|
|
247
|
-
|
|
286
|
+
const finding = await evaluate(ctx, pin, r, cal, { target, newestLtsWithoutImage });
|
|
287
|
+
// Con la excepción vigente, el cambio de una línea con salto grande pasa a aviso.
|
|
288
|
+
if (deferring && finding.level === 'error' && finding.data?.lineChange && big.has(r.major)) {
|
|
289
|
+
findings.push({
|
|
290
|
+
...finding,
|
|
291
|
+
level: 'warn',
|
|
292
|
+
message: `${finding.message} Diferido hasta ${deferral.until} (${deferral.reason}).`,
|
|
293
|
+
data: { ...finding.data, deferredUntil: deferral.until },
|
|
294
|
+
});
|
|
295
|
+
} else {
|
|
296
|
+
findings.push(finding);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
// El estado de la excepción, o la sugerencia de declararla cuando el salto es grande.
|
|
300
|
+
const deferralAt = { file: 'package.json', line: deferral.line };
|
|
301
|
+
const lines = [...big].map((m) => `Node ${m}`).join(' y ');
|
|
302
|
+
if (deferral.status === 'invalid') {
|
|
303
|
+
findings.push({ ...deferralAt, level: 'warn', message: `La excepción para diferir el cambio de Node no vale: ${deferral.problem}. Se ignora.` });
|
|
304
|
+
} else if (deferral.status === 'expired') {
|
|
305
|
+
findings.push({
|
|
306
|
+
...deferralAt,
|
|
307
|
+
level: 'warn',
|
|
308
|
+
message: `La excepción para diferir el cambio de Node venció el ${deferral.until}: los errores vuelven a bloquear. Quitarla del package.json, o renovarla con otra fecha y motivo (máximo ${max} días).`,
|
|
309
|
+
});
|
|
310
|
+
} else if (deferral.status === 'active' && !deferring) {
|
|
311
|
+
findings.push({
|
|
312
|
+
...deferralAt,
|
|
313
|
+
level: 'warn',
|
|
314
|
+
message: `La excepción para diferir el cambio de Node no hace falta: ninguna línea fijada está a más de ${ctx.policy.nodeDeferMinJump} versiones mayores del destino. Quitarla del package.json.`,
|
|
315
|
+
});
|
|
316
|
+
} else if (deferring) {
|
|
317
|
+
findings.push({
|
|
318
|
+
...deferralAt,
|
|
319
|
+
level: 'warn',
|
|
320
|
+
message: `Cambio de línea de Node diferido hasta ${deferral.until} (${deferral.reason}): quedan ${deferral.daysLeft} días. Mientras tanto, los errores de ${lines} y de npm 10 son avisos; al vencer, vuelven a bloquear. Destino: ${target.version}.`,
|
|
321
|
+
data: { deferredUntil: deferral.until, majors: [...big] },
|
|
322
|
+
});
|
|
323
|
+
} else if (big.size && findings.some((f) => f.level === 'error' && f.data?.lineChange && big.has(f.data.major))) {
|
|
324
|
+
findings.push({
|
|
325
|
+
level: 'info',
|
|
326
|
+
message:
|
|
327
|
+
`Salto grande (${lines} → ${target.major}): si no se puede subir ahora, se puede diferir con una excepción con fecha en el package.json, ` +
|
|
328
|
+
`"npmSecurityKit": { "nodeUpgradeDeferredUntil": "AAAA-MM-DD", "nodeUpgradeReason": "…" } (máximo ${max} días). Mientras esté vigente, estos errores son avisos.`,
|
|
329
|
+
});
|
|
248
330
|
}
|
|
249
331
|
if (!nodePins) {
|
|
250
332
|
const version = target?.version ?? cal.recommended;
|
package/src/checks/npm-policy.js
CHANGED
|
@@ -5,6 +5,7 @@ import { parseNpmrc, requiredSettings, settingProblem } from '../lib/npmrc.js';
|
|
|
5
5
|
import { jsonKeyLine } from '../lib/json-lines.js';
|
|
6
6
|
import { linesWithNpm, loadCalendar } from '../lib/node-calendar.js';
|
|
7
7
|
import { upgradeTarget } from '../lib/node-target.js';
|
|
8
|
+
import { activeDeferral } from './node-version.js';
|
|
8
9
|
import { compareVersions, minVersion, parseVersion } from '../lib/semver.js';
|
|
9
10
|
|
|
10
11
|
export const id = 'npm-policy';
|
|
@@ -85,6 +86,12 @@ export async function run(ctx) {
|
|
|
85
86
|
|
|
86
87
|
// engines.npm
|
|
87
88
|
const pkgText = ctx.readFile('package.json');
|
|
89
|
+
// Con el cambio de Node diferido (salto grande), la línea del repo puede no correr npm 11: engines.npm
|
|
90
|
+
// y el npm en uso son avisos hasta que venza la excepción. engines.npm >=11.17 con engine-strict
|
|
91
|
+
// rompería el npm ci de esa línea.
|
|
92
|
+
const deferral = await activeDeferral(ctx);
|
|
93
|
+
const deferredNote = deferral ? ` Diferido hasta ${deferral.until} junto con el cambio de Node (${deferral.reason}).` : '';
|
|
94
|
+
const npmLevel = deferral ? 'warn' : 'error';
|
|
88
95
|
if (pkgText === null) {
|
|
89
96
|
findings.push({ level: 'error', message: 'No hay package.json en el directorio revisado.' });
|
|
90
97
|
} else {
|
|
@@ -93,16 +100,16 @@ export async function run(ctx) {
|
|
|
93
100
|
const min = typeof range === 'string' ? minVersion(range) : null;
|
|
94
101
|
if (typeof range !== 'string') {
|
|
95
102
|
findings.push({
|
|
96
|
-
level:
|
|
103
|
+
level: npmLevel,
|
|
97
104
|
file: 'package.json',
|
|
98
|
-
message: `Falta engines.npm: agregar "npm": ">=${minNpmShort}" en engines (${NPM10_WHY})
|
|
105
|
+
message: `Falta engines.npm: agregar "npm": ">=${minNpmShort}" en engines (${NPM10_WHY}).${deferral ? ` Agregarlo al subir de Node: con engine-strict, en la línea actual rompe npm ci.${deferredNote}` : ''}`,
|
|
99
106
|
});
|
|
100
107
|
} else if (!min || compareVersions(min, minNpm) < 0) {
|
|
101
108
|
findings.push({
|
|
102
|
-
level:
|
|
109
|
+
level: npmLevel,
|
|
103
110
|
file: 'package.json',
|
|
104
111
|
line: jsonKeyLine(pkgText, ['engines', 'npm']),
|
|
105
|
-
message: `engines.npm "${range}" admite npm anteriores a ${minNpm} (${NPM10_WHY}). Cambiar a ">=${minNpmShort}"
|
|
112
|
+
message: `engines.npm "${range}" admite npm anteriores a ${minNpm} (${NPM10_WHY}). Cambiar a ">=${minNpmShort}".${deferredNote}`,
|
|
106
113
|
data: { range, min },
|
|
107
114
|
});
|
|
108
115
|
}
|
|
@@ -115,8 +122,8 @@ export async function run(ctx) {
|
|
|
115
122
|
findings.push({ level: 'warn', message: `No se pudo saber la versión de npm en uso (${(npmVersion.stderr || npmVersion.stdout).trim() || 'sin salida'}).` });
|
|
116
123
|
} else if (compareVersions(used, minNpm) < 0) {
|
|
117
124
|
findings.push({
|
|
118
|
-
level:
|
|
119
|
-
message: (({ pipeline, local }) => `npm ${used} en uso: hace falta npm >= ${minNpm} (${NPM10_WHY}). En el pipeline y en el Dockerfile, ${pipeline}. En local, ${local}
|
|
125
|
+
level: npmLevel,
|
|
126
|
+
message: (({ pipeline, local }) => `npm ${used} en uso: hace falta npm >= ${minNpm} (${NPM10_WHY}). En el pipeline y en el Dockerfile, ${pipeline}. En local, ${local}.${deferredNote}`)(
|
|
120
127
|
await npmHint(ctx, minNpm),
|
|
121
128
|
),
|
|
122
129
|
data: { npm: used },
|
package/src/init.js
CHANGED
|
@@ -6,6 +6,7 @@ import { join } from 'node:path';
|
|
|
6
6
|
import * as dockerfileCheck from './checks/dockerfile.js';
|
|
7
7
|
import { OVERRIDE_ENV, packagesWithScripts } from './checks/install-scripts.js';
|
|
8
8
|
import * as nodeVersionCheck from './checks/node-version.js';
|
|
9
|
+
import { activeDeferral } from './checks/node-version.js';
|
|
9
10
|
import * as npmPolicyCheck from './checks/npm-policy.js';
|
|
10
11
|
import { loadAllowPolicy, verdict } from './lib/allow-scripts.js';
|
|
11
12
|
import { lockfileName } from './lib/audit.js';
|
|
@@ -55,9 +56,10 @@ function jsonFormat(text) {
|
|
|
55
56
|
};
|
|
56
57
|
}
|
|
57
58
|
|
|
58
|
-
// engines.npm (si falta o admite un npm viejo) y engines.node (
|
|
59
|
-
//
|
|
60
|
-
|
|
59
|
+
// engines.npm (si falta o admite un npm viejo) y engines.node (si falta, o con `raiseNode` si admite
|
|
60
|
+
// una línea que hay que cambiar: sin soporte, no LTS o sin npm 11), conservando la indentación y el
|
|
61
|
+
// salto de línea final del archivo.
|
|
62
|
+
export function mergePackageJson(text, policy, nodeMajor, { raiseNode = false } = {}) {
|
|
61
63
|
const pkg = JSON.parse(text);
|
|
62
64
|
const changes = [];
|
|
63
65
|
const minNpm = policy.minNpmVersion.replace(/\.0$/, '');
|
|
@@ -67,9 +69,10 @@ export function mergePackageJson(text, policy, nodeMajor) {
|
|
|
67
69
|
changes.push(`engines.npm: ">=${minNpm}"${engines.npm ? ` (antes: "${engines.npm}")` : ''}`);
|
|
68
70
|
engines.npm = `>=${minNpm}`;
|
|
69
71
|
}
|
|
70
|
-
if (engines.node === undefined
|
|
72
|
+
if (nodeMajor && (engines.node === undefined || raiseNode)) {
|
|
73
|
+
const before = engines.node;
|
|
71
74
|
engines.node = `>=${nodeMajor}`;
|
|
72
|
-
changes.push(`engines.node: ">=${nodeMajor}"`);
|
|
75
|
+
changes.push(`engines.node: ">=${nodeMajor}"${before ? ` (antes: "${before}")` : ''}`);
|
|
73
76
|
}
|
|
74
77
|
if (!changes.length) return { text, changes };
|
|
75
78
|
pkg.engines = engines;
|
|
@@ -88,18 +91,22 @@ export function mergePackageJson(text, policy, nodeMajor) {
|
|
|
88
91
|
// soporte), el destino único de lib/node-target.js.
|
|
89
92
|
export async function repoNode(ctx, cal) {
|
|
90
93
|
const target = await upgradeTarget(ctx, cal);
|
|
91
|
-
|
|
94
|
+
const { ok } = linesWithNpm(cal, ctx.policy.minNpmVersion);
|
|
95
|
+
let inUse = null; // la línea más baja en uso que cumple (LTS con soporte y npm 11)
|
|
96
|
+
let from = null; // la línea más baja en uso que hay que cambiar, y por qué
|
|
92
97
|
let pipelinePin = null;
|
|
93
98
|
for (const pin of nodeVersionCheck.findPins(ctx)) {
|
|
94
99
|
const r = nodeVersionCheck.resolvePin(pin, cal);
|
|
95
100
|
if (r.major === undefined || r.major === null || r.latest) continue;
|
|
96
|
-
if (
|
|
97
|
-
|
|
98
|
-
|
|
101
|
+
if (ok.includes(r.major)) {
|
|
102
|
+
if (inUse === null || r.major < inUse) inUse = r.major;
|
|
103
|
+
if (!pipelinePin && pin.file === 'bitbucket-pipelines.yml' && pin.kind === 'docker' && r.exact) pipelinePin = r;
|
|
104
|
+
} else if (from === null || r.major < from.major) {
|
|
105
|
+
from = { major: r.major, why: lineProblem(cal, r.major, ctx.policy) };
|
|
106
|
+
}
|
|
99
107
|
}
|
|
100
|
-
if (!target) return { major: null, image: null, npm: null, from
|
|
101
|
-
const major = inUse
|
|
102
|
-
const from = inUse !== null && major !== inUse ? inUse : null;
|
|
108
|
+
if (!target) return { major: null, image: null, npm: null, from };
|
|
109
|
+
const major = inUse ?? target.major;
|
|
103
110
|
if (pipelinePin?.major === major) {
|
|
104
111
|
let patched = false;
|
|
105
112
|
for (const s of cal.securityAfter(major, pipelinePin.exact)) {
|
|
@@ -112,6 +119,43 @@ export async function repoNode(ctx, cal) {
|
|
|
112
119
|
return { major, image: found?.image ?? String(major), npm: found?.npm ?? cal.npmOfLine(major), from };
|
|
113
120
|
}
|
|
114
121
|
|
|
122
|
+
// Por qué una línea de Node no sirve, para el mensaje de init.
|
|
123
|
+
function lineProblem(cal, major, policy) {
|
|
124
|
+
const phase = cal.phaseOf(major);
|
|
125
|
+
if (phase === 'eol') return `sin soporte desde ${cal.lines.get(major).end}`;
|
|
126
|
+
if (phase === 'not-lts') return 'que no es LTS';
|
|
127
|
+
if (phase === 'unknown') return 'que no aparece en el calendario de Node';
|
|
128
|
+
return `pero ninguna release de la ${major} trae npm >= ${policy.minNpmVersion}, que pide la política`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Las líneas `- npm ci` del pipeline: las del step que corre en los PR (inline o por ancla) y el resto
|
|
132
|
+
// (deploys, publicación). El kit va en el de los PR. Sin sección pull-requests, todas cuentan como PR.
|
|
133
|
+
export function npmCiLines(text) {
|
|
134
|
+
const lines = (text ?? '').split(/\r?\n/);
|
|
135
|
+
const indent = (l) => l.match(/^ */)[0].length;
|
|
136
|
+
const isCi = (l) => /^\s*-\s*npm ci(\s|$)/.test(l);
|
|
137
|
+
const all = lines.flatMap((l, i) => (isCi(l) ? [i + 1] : []));
|
|
138
|
+
const blockAfter = (start) => {
|
|
139
|
+
const out = [];
|
|
140
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
141
|
+
if (!lines[i].trim() || lines[i].trim().startsWith('#')) continue;
|
|
142
|
+
if (indent(lines[i]) <= indent(lines[start])) break;
|
|
143
|
+
out.push(i);
|
|
144
|
+
}
|
|
145
|
+
return out;
|
|
146
|
+
};
|
|
147
|
+
const prStart = lines.findIndex((l) => /^\s+pull-requests:\s*(#.*)?$/.test(l));
|
|
148
|
+
if (prStart < 0) return { pr: all, others: [] };
|
|
149
|
+
const prBlock = blockAfter(prStart);
|
|
150
|
+
const pr = new Set(prBlock.filter((i) => isCi(lines[i])).map((i) => i + 1));
|
|
151
|
+
for (const anchor of new Set(prBlock.flatMap((i) => [...lines[i].matchAll(/\*([\w-]+)/g)].map((m) => m[1])))) {
|
|
152
|
+
const def = lines.findIndex((l) => new RegExp(`&${anchor}(\\s|$)`).test(l));
|
|
153
|
+
if (def >= 0) for (const i of blockAfter(def)) if (isCi(lines[i])) pr.add(i + 1);
|
|
154
|
+
}
|
|
155
|
+
if (!pr.size) return { pr: all, others: [] };
|
|
156
|
+
return { pr: [...pr].sort((a, b) => a - b), others: all.filter((n) => !pr.has(n)) };
|
|
157
|
+
}
|
|
158
|
+
|
|
115
159
|
// Las ramas que despliegan: las claves de `pipelines.branches` (sin globs), en orden.
|
|
116
160
|
export function deployBranches(text) {
|
|
117
161
|
const out = [];
|
|
@@ -227,7 +271,13 @@ export async function runInit(ctx, { dryRun = false, stdout = process.stdout, co
|
|
|
227
271
|
// 1. .npmrc y package.json
|
|
228
272
|
const npmrcBefore = ctx.readFile('.npmrc');
|
|
229
273
|
const npmrc = mergeNpmrc(npmrcBefore, ctx.policy);
|
|
230
|
-
|
|
274
|
+
// Con el cambio de Node diferido (salto grande, excepción vigente), engines no se toca: los de la línea
|
|
275
|
+
// nueva, con engine-strict, romperían el npm ci de la actual.
|
|
276
|
+
const deferral = cal.error ? null : await activeDeferral(ctx);
|
|
277
|
+
const engNode = JSON.parse(pkgText).engines?.node;
|
|
278
|
+
const engMin = typeof engNode === 'string' ? minVersion(engNode) : null;
|
|
279
|
+
const raiseNode = Boolean(engMin && !cal.error && node.major && !linesWithNpm(cal, ctx.policy.minNpmVersion).ok.includes(parseVersion(engMin)[0]));
|
|
280
|
+
const pkg = deferral ? { text: pkgText, changes: [] } : mergePackageJson(pkgText, ctx.policy, node.major, { raiseNode });
|
|
231
281
|
if (npmrc.changes.length) files['.npmrc'] = npmrc.text;
|
|
232
282
|
if (pkg.changes.length) files['package.json'] = pkg.text;
|
|
233
283
|
|
|
@@ -277,23 +327,35 @@ export async function runInit(ctx, { dryRun = false, stdout = process.stdout, co
|
|
|
277
327
|
const pipeline = ctx.readFile('bitbucket-pipelines.yml');
|
|
278
328
|
const used = pipeline?.match(/npm-security-kit@([\w.-]+)/);
|
|
279
329
|
const image = node.image ?? '<versión LTS>';
|
|
280
|
-
|
|
330
|
+
const bigJump = node.from && node.major - node.from.major > ctx.policy.nodeDeferMinJump;
|
|
331
|
+
if (deferral) {
|
|
332
|
+
say(
|
|
333
|
+
` Cambio de Node diferido hasta ${deferral.until} (${deferral.reason}): el step sigue con la imagen actual y engines no se toca. ` +
|
|
334
|
+
`Al vencer, pasar a la ${node.major} (node:${image}).`,
|
|
335
|
+
);
|
|
336
|
+
} else if (node.from) {
|
|
281
337
|
say(
|
|
282
|
-
` El repo usa Node ${node.from},
|
|
283
|
-
`se propone la ${node.major} (node:${image}${node.npm ? `, npm ${node.npm}` : ''}). Pasar a la ${node.major} el .nvmrc, las imágenes del pipeline
|
|
338
|
+
` El repo usa Node ${node.from.major}, ${node.from.why}: ` +
|
|
339
|
+
`se propone la ${node.major} (node:${image}${node.npm ? `, npm ${node.npm}` : ''}). Pasar a la ${node.major} el .nvmrc, las imágenes del pipeline, los FROM de los Dockerfiles y engines.node.` +
|
|
340
|
+
(bigJump ? ' Si el salto no se puede hacer ahora, se puede diferir con una excepción con fecha (README: "Diferir el cambio de Node").' : ''),
|
|
284
341
|
);
|
|
285
342
|
}
|
|
286
343
|
const branches = deployBranches(pipeline);
|
|
287
|
-
const
|
|
344
|
+
const ci = npmCiLines(pipeline);
|
|
288
345
|
if (used) {
|
|
289
346
|
say(` Ya corre el kit (${used[0]}).${used[1] !== ctx.kitVersion ? ` Para esta versión: ${PACKAGE}@${ctx.kitVersion}.` : ''}`);
|
|
290
|
-
} else if (
|
|
291
|
-
say(` Agregar al step que
|
|
347
|
+
} else if (ci.pr.length) {
|
|
348
|
+
say(` Agregar al step que corre en los PR (bitbucket-pipelines.yml:${ci.pr.join(', ')}), antes de su npm ci, sin reemplazar el resto del step:`);
|
|
292
349
|
say();
|
|
293
350
|
say(` - npx --yes --min-release-age=0 ${PACKAGE}@${ctx.kitVersion} check # antes de npm ci`);
|
|
294
351
|
say(' - npm audit signatures # después de npm ci');
|
|
295
352
|
say();
|
|
296
|
-
|
|
353
|
+
if (ci.others.length) {
|
|
354
|
+
say(` Los otros npm ci (líneas ${ci.others.join(', ')}: deploys, publicación) no necesitan el kit: ya pasó en el PR.`);
|
|
355
|
+
}
|
|
356
|
+
if (!deferral && !new RegExp(`node:${image.replace(/\./g, '\\.')}(\\s|$|-)`).test(pipeline)) {
|
|
357
|
+
say(` Con una imagen que traiga npm >= ${ctx.policy.minNpmVersion} (image: node:${image}).`);
|
|
358
|
+
}
|
|
297
359
|
if (!/^\s+ci:\s*(#.*)?$/m.test(pipeline)) {
|
|
298
360
|
say(' Para correrlo a mano ("Run pipeline"), un `custom: ci` con el mismo step (ver el README).');
|
|
299
361
|
}
|
|
@@ -324,9 +386,11 @@ export async function runInit(ctx, { dryRun = false, stdout = process.stdout, co
|
|
|
324
386
|
section('Pasos siguientes');
|
|
325
387
|
const steps = [
|
|
326
388
|
dryRun ? 'Correr init sin --dry-run.' : 'Revisar el diff: git diff .npmrc package.json',
|
|
327
|
-
...(
|
|
389
|
+
...(deferral
|
|
390
|
+
? [`Antes del ${deferral.until}, cuando vence la excepción: pasar de la ${node.from?.major ?? deferral.majors[0]} a la ${node.major} (.nvmrc, imágenes, FROM, engines.node y engines.npm).`]
|
|
391
|
+
: node.from
|
|
328
392
|
? [
|
|
329
|
-
`Pasar el repo de la ${node.from} a la ${node.major}: .nvmrc, imágenes del pipeline y FROM de los Dockerfiles (node:${node.image}). ` +
|
|
393
|
+
`Pasar el repo de la ${node.from.major} a la ${node.major}: .nvmrc, imágenes del pipeline y FROM de los Dockerfiles (node:${node.image}). ` +
|
|
330
394
|
`En local, \`nvm install ${node.major} && nvm use ${node.major}\`; si npm ci da EUSAGE, \`npm install --package-lock-only --ignore-scripts\`.`,
|
|
331
395
|
]
|
|
332
396
|
: []),
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Excepción para diferir el cambio de línea de Node cuando el salto es grande (decisión del 28-10):
|
|
2
|
+
// subir varias versiones mayores suele arrastrar dependencias y refactors. Se declara en el
|
|
3
|
+
// package.json, así queda en el diff del PR, y vence:
|
|
4
|
+
//
|
|
5
|
+
// "npmSecurityKit": { "nodeUpgradeDeferredUntil": "AAAA-MM-DD", "nodeUpgradeReason": "por qué" }
|
|
6
|
+
//
|
|
7
|
+
// Vale por hasta policy.nodeDeferMaxDays desde hoy y solo para las líneas cuyo salto al destino es de
|
|
8
|
+
// más de policy.nodeDeferMinJump versiones mayores (eso lo decide quien la usa).
|
|
9
|
+
import { parseDay } from '../context.js';
|
|
10
|
+
import { jsonKeyLine } from './json-lines.js';
|
|
11
|
+
|
|
12
|
+
const DAY = 86400000;
|
|
13
|
+
const iso = (date) => date.toISOString().slice(0, 10);
|
|
14
|
+
|
|
15
|
+
// `{ status: 'none' | 'invalid' | 'expired' | 'active', until?, reason?, line?, daysLeft?, problem? }`.
|
|
16
|
+
export function readDeferral(ctx) {
|
|
17
|
+
const text = ctx.readFile('package.json');
|
|
18
|
+
let pkg;
|
|
19
|
+
try {
|
|
20
|
+
pkg = JSON.parse(text ?? 'null');
|
|
21
|
+
} catch {
|
|
22
|
+
return { status: 'none' };
|
|
23
|
+
}
|
|
24
|
+
const config = pkg?.npmSecurityKit;
|
|
25
|
+
if (!config || config.nodeUpgradeDeferredUntil === undefined) return { status: 'none' };
|
|
26
|
+
const until = config.nodeUpgradeDeferredUntil;
|
|
27
|
+
const line = jsonKeyLine(text, ['npmSecurityKit', 'nodeUpgradeDeferredUntil']);
|
|
28
|
+
const reason = typeof config.nodeUpgradeReason === 'string' ? config.nodeUpgradeReason.trim() : '';
|
|
29
|
+
const date = typeof until === 'string' ? parseDay(until) : null;
|
|
30
|
+
if (!date) return { status: 'invalid', until, line, problem: 'la fecha tiene que ser "AAAA-MM-DD"' };
|
|
31
|
+
if (!reason) return { status: 'invalid', until, line, problem: 'falta "nodeUpgradeReason" (por qué no se puede subir ahora)' };
|
|
32
|
+
const daysLeft = Math.round((date - ctx.today) / DAY);
|
|
33
|
+
const max = ctx.policy.nodeDeferMaxDays;
|
|
34
|
+
if (daysLeft > max) {
|
|
35
|
+
return { status: 'invalid', until, reason, line, problem: `pasa del máximo de ${max} días (como mucho hasta ${iso(new Date(ctx.today.getTime() + max * DAY))})` };
|
|
36
|
+
}
|
|
37
|
+
if (daysLeft < 0) return { status: 'expired', until, reason, line };
|
|
38
|
+
return { status: 'active', until, reason, line, daysLeft };
|
|
39
|
+
}
|
package/src/policy.js
CHANGED
|
@@ -27,6 +27,10 @@ export const policy = freeze({
|
|
|
27
27
|
allowedRegistries: ['https://registry.npmjs.org/'],
|
|
28
28
|
// Días de soporte restantes de una línea de Node por debajo de los cuales se avisa.
|
|
29
29
|
nodeEolWarnDays: 180,
|
|
30
|
+
// El cambio de línea de Node se puede diferir con una excepción con fecha en el package.json solo si
|
|
31
|
+
// el salto al destino es de más de estas versiones mayores, y por hasta estos días.
|
|
32
|
+
nodeDeferMinJump: 2,
|
|
33
|
+
nodeDeferMaxDays: 183,
|
|
30
34
|
// Claves del .npmrc del proyecto. `error`: sin ellas la política no se cumple.
|
|
31
35
|
// `warn`: recomendadas.
|
|
32
36
|
requiredNpmrc: {
|