parche-cli 0.7.2 → 0.7.3

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 CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  Los cambios de cada versión de `parche-cli` y de la Action `Pepo08/parche@v0`. En npm están la 0.1.1, la 0.5.1, de la 0.6.0 a la 0.7.0 y de la 0.7.2 en adelante. La 0.1.0 y de la 0.2.0 a la 0.5.0 no se publicaron en npm; lo que traían está incluido en la 0.5.1. La 0.7.1 tampoco: salió junto con la 0.7.2. La fecha de cada versión es la del cambio; la de publicación figura en npm y en los [Releases](https://github.com/Pepo08/parche/releases).
4
4
 
5
+ ## 0.7.3 — 2026-10-24
6
+
7
+ La foto de cada corrida de la Action. Hasta la 0.7.2, el inventario de APIs, los avisos y el registro se escribían dentro del runner y se perdían al terminar el job: de una corrida quedaban solo el resumen del job y los PRs, y el resumen no se puede leer por la API de GitHub.
8
+
9
+ ### Nuevo
10
+ - **La Action guarda la foto de la corrida** como artifact del workflow (`parche`): `scan.json`, que es la salida de `parche scan --json` (el inventario y los avisos), y `ledger.jsonl`, el registro de esa corrida (los PRs que abrió y lo que quedó sin PR). Lleva rutas, números de línea, paquetes, versiones, operaciones y los mensajes de Parche; no lleva código. Se baja con `gh run download <id> -n parche`. Inputs nuevos: `snapshot` (`'true'` por defecto; `'false'` la apaga) y `snapshot-name`, para usar la Action más de una vez en el mismo workflow. Corre aunque `parche pr` haya fallado, y si la foto falla, la corrida no falla: queda un aviso.
11
+ - **Cada aviso tiene un `fingerprint`**, un identificador que sale del pack, el archivo y el texto de la línea, sin el número de línea ni el mensaje. No cambia si se mueven las líneas, se reindenta el código o el archivo pasa a CRLF; sí cambia si cambia el código de esa línea. Sirve para comparar una corrida con la anterior: qué es nuevo, qué sigue y qué se resolvió.
12
+ - **`parche scan --json` dice qué versión de Parche lo generó** (`parcheVersion`).
13
+
14
+ ### Cambios
15
+ - `actions/upload-artifact` (v7.0.2) fijada por SHA, como las demás Actions; un test controla que `action.yml` también las fije.
16
+
5
17
  ## 0.7.2 — 2026-10-24
6
18
 
7
19
  ARCA: lo que pidió el re-escaneo de repos públicos con la 0.7.0. Un PR preparado quedaba parcial sin decirlo, y dos casos podían sacar un PR que no anda.
package/README.md CHANGED
@@ -25,6 +25,8 @@ Copy [`templates/github-workflow.yml`](templates/github-workflow.yml) to `.githu
25
25
  | `verify` | `'true'` | Run the repo's tests (`npm test`, `composer test` or `vendor/bin/phpunit`) before opening each PR. |
26
26
  | `dry-run` | `'false'` | Build the branches and PR bodies without opening any PR. |
27
27
  | `token` | `github.token` | Pushes the branches and opens the PRs. With the default `GITHUB_TOKEN`, GitHub holds the PR's workflows until someone approves them; with a GitHub App token or a personal token they run on their own. |
28
+ | `snapshot` | `'true'` | Save a snapshot of the run (API inventory, findings and the PRs it opened, as JSON; no source code) as a workflow artifact. |
29
+ | `snapshot-name` | `parche` | Artifact name. Use a different one for each step if you call the Action more than once in a workflow. |
28
30
 
29
31
  `@v0` follows every 0.x release; to pin one, use its release commit: `Pepo08/parche@<sha> # v0.7.2`. PR bodies and the job output are in Spanish for now.
30
32
 
@@ -115,6 +117,8 @@ El workflow necesita `permissions: contents: write` y `pull-requests: write`. An
115
117
 
116
118
  Lo que no abre un PR también se ve en la corrida: los avisos de solo detección (modelos de IA que se retiran, por ejemplo) y lo que quedó sin PR porque falta un dato de tu negocio aparecen en el **resumen del job** y como **anotaciones** con el archivo y la línea (GitHub muestra las primeras 10; el resumen las tiene todas).
117
119
 
120
+ **La foto de la corrida:** además, la Action guarda un artifact del workflow (`parche`, salvo que le pongas otro nombre con `snapshot-name`) con dos archivos. `scan.json` es la salida de `parche scan --json`: el inventario de APIs y los avisos, cada uno con su `fingerprint`, un identificador que no cambia aunque se muevan las líneas, para comparar una semana con la anterior. `ledger.jsonl` es el registro de esa corrida: los PRs que abrió y lo que quedó sin PR. Lleva rutas, números de línea, paquetes, versiones, operaciones y los mensajes de Parche; no lleva código. Lo baja quien puede ver las corridas de Actions del repo (`gh run download <id> -n parche`), y dura lo que diga la retención de artifacts del repo. Si no lo querés: `snapshot: 'false'`. Si la foto falla, la corrida no falla: queda un aviso.
121
+
118
122
  Con el `GITHUB_TOKEN` por defecto, GitHub deja los workflows del PR que abre Parche esperando aprobación: alguien con permiso de escritura tiene que tocar **Approve workflows to run** (el cuerpo del PR lo avisa). Para que tu CI corra sola sobre esos PRs, pasale a la Action un token de una GitHub App o un token personal con permiso de escritura en `contents` y `pull-requests`:
119
123
 
120
124
  ```yaml
@@ -171,7 +175,7 @@ Parche corre en tu máquina o en tu runner. Tu código no sale de ahí, salvo el
171
175
  | **Red** | `scan`, `fix` y `packs` no usan la red. `pr` llama a `api.github.com` para abrir el PR en tu repo y pushea su rama `parche/<pack>` (con `--force`, así solo reemplaza su propio PR anterior). `diff` y `scan --upgrades` bajan metadatos y paquetes de `registry.npmjs.org`, que ve qué paquetes y versiones consultás. Nada más. |
172
176
  | **Telemetría** | Ninguna. `parche feedback` escribe en `.parche/ledger.jsonl` de tu repo y queda ahí. |
173
177
  | **Qué ejecuta** | `git` y, solo con `--verify`, los tests de tu repo (`npm test`, `composer test` o `vendor/bin/phpunit`). |
174
- | **Qué escribe** | Tu working copy (solo `fix` y `pr`), `.parche/` (se agrega a `.git/info/exclude`) y, para `diff` y `--upgrades`, un caché en `~/.parche/cache`. |
178
+ | **Qué escribe** | Tu working copy (solo `fix` y `pr`), `.parche/` (se agrega a `.git/info/exclude`) y, para `diff` y `--upgrades`, un caché en `~/.parche/cache`. En GitHub Actions, además, la foto de la corrida como artifact de tu workflow (inventario y avisos en JSON, sin código; se apaga con `snapshot: 'false'`). |
175
179
 
176
180
  Para reportar una vulnerabilidad, y el detalle de todo esto: [`SECURITY.md`](SECURITY.md). Si te llegó un PR de Parche: [por qué lo recibiste](docs/por-que-recibi-este-pr.md).
177
181
 
package/bin/parche.js CHANGED
@@ -139,12 +139,13 @@ function printFindings(result) {
139
139
  }
140
140
  }
141
141
 
142
+ const VERSION = () => JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
143
+
142
144
  async function main() {
143
145
  const args = parseArgs(process.argv.slice(2));
144
146
  const [cmd, dirArg] = args._;
145
147
  if (args.version || cmd === '-v') {
146
- const pkg = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
147
- console.log(pkg.version);
148
+ console.log(VERSION());
148
149
  return 0;
149
150
  }
150
151
  if (!cmd || args.help) { console.log(HELP); return 0; }
@@ -187,7 +188,8 @@ async function main() {
187
188
  fs.writeFileSync(path.join(root, '.parche', 'inventory.json'), JSON.stringify(result.inventory, null, 2));
188
189
  fs.writeFileSync(path.join(root, '.parche', 'report.md'), scanReport({ ...result, changed: [], upgrades }));
189
190
  record(root, { type: 'scan', findings: counts(result.findings) });
190
- if (args.json) { console.log(JSON.stringify({ inventory: result.inventory, findings: result.findings, skipped: result.skipped }, null, 2)); return 0; }
191
+ // parcheVersion: qué versión (y por lo tanto qué reglas) generó este JSON; la foto de cada corrida de la Action lo guarda.
192
+ if (args.json) { console.log(JSON.stringify({ parcheVersion: VERSION(), inventory: result.inventory, findings: result.findings, skipped: result.skipped }, null, 2)); return 0; }
191
193
  console.log(`Parche · scan de ${path.basename(root)}\n`);
192
194
  printInventory(result.inventory);
193
195
  if (upgrades) printUpgrades(upgrades);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "parche-cli",
3
- "version": "0.7.2",
3
+ "version": "0.7.3",
4
4
  "description": "Dependabot para APIs: detecta cambios de cualquier API de terceros en tu código (ARCA, Mercado Pago, OpenAI, Anthropic) y abre el PR con el arreglo.",
5
5
  "keywords": [
6
6
  "api",
package/src/engine.js CHANGED
@@ -1,4 +1,5 @@
1
1
  // Motor: recorre el repo, arma el inventario de APIs y corre los packs.
2
+ import crypto from 'node:crypto';
2
3
  import fs from 'node:fs';
3
4
  import path from 'node:path';
4
5
  import { loadPacks, transformFor, languageOf } from './packs.js';
@@ -33,6 +34,18 @@ export function listFiles(dir, skipped = []) {
33
34
  return out.sort((a, b) => (toPosix(a) < toPosix(b) ? -1 : 1));
34
35
  }
35
36
 
37
+ // Identificador estable de un aviso (P5 de docs/app-vigilancia.md): el pack, el archivo y el texto de la línea, sin el
38
+ // número de línea ni el mensaje. Así una corrida reconoce el mismo aviso de la anterior aunque se muevan las líneas o
39
+ // cambie el texto del pack, y "nuevo", "sigue" y "resuelto" no dependen de la posición. Los espacios y el \r no cuentan
40
+ // (una reindentación o pasar a CRLF no es otro aviso). Si el mismo texto se repite en el archivo, cuenta el orden.
41
+ export function fingerprint(finding, source, seen) {
42
+ const text = finding.line ? (source.split('\n')[finding.line - 1] ?? '').replace(/\s+/g, ' ').trim() : '';
43
+ const key = [finding.pack, finding.file, text].join('\u0000');
44
+ const n = (seen.get(key) || 0) + 1;
45
+ seen.set(key, n);
46
+ return crypto.createHash('sha256').update(`${key}\u0000${n}`).digest('hex').slice(0, 16);
47
+ }
48
+
36
49
  export function run(dir, { write = false, packIds = null, today } = {}) {
37
50
  const root = path.resolve(dir);
38
51
  const packs = loadPacks().filter((p) => !packIds || packIds.includes(p.id));
@@ -47,6 +60,7 @@ export function run(dir, { write = false, packIds = null, today } = {}) {
47
60
  const usages = [];
48
61
  const fileScans = []; // inventario de cualquier API (nivel 1), por archivo
49
62
  const changed = new Map(); // archivo relativo → [packs]
63
+ const seen = new Map(); // para fingerprint(): cuántas veces apareció cada pack + archivo + texto
50
64
 
51
65
  files.forEach((abs, i) => {
52
66
  const rel = rels[i];
@@ -60,10 +74,12 @@ export function run(dir, { write = false, packIds = null, today } = {}) {
60
74
  try {
61
75
  res = t({ source, file: rel, pack, today, context });
62
76
  } catch (err) {
63
- findings.push({ pack: pack.id, file: rel, line: null, status: 'needs-input', message: `Parche no pudo procesar este archivo: ${err.message}` });
77
+ const f = { pack: pack.id, file: rel, line: null, status: 'needs-input', message: `Parche no pudo procesar este archivo: ${err.message}` };
78
+ findings.push({ ...f, fingerprint: fingerprint(f, source, seen) });
64
79
  continue;
65
80
  }
66
- findings.push(...res.findings);
81
+ // El texto de la línea, del archivo tal como lo leyó este pack (antes de su propio cambio).
82
+ findings.push(...res.findings.map((f) => ({ ...f, fingerprint: fingerprint(f, source, seen) })));
67
83
  usages.push(...res.usages.map((u) => ({ ...u, file: rel, pack: pack.id })));
68
84
  if (res.output !== null && res.output !== source) {
69
85
  source = res.output;