parche-cli 0.7.3 → 0.7.6

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
@@ -1,6 +1,35 @@
1
1
  # Cambios
2
2
 
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).
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, la 0.7.2, la 0.7.3 y de la 0.7.5 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 y la 0.7.4 tampoco: salieron junto con la 0.7.2 y la 0.7.5. 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
+
5
+ ## 0.7.6 — 2026-10-25
6
+
7
+ Tres cosas que frenaban las corridas semanales y las primeras instalaciones. Las dos primeras las encontró el chat de página armando el diseño del resumen semanal, y se confirmaron leyendo el código.
8
+
9
+ ### Arreglos
10
+ - **Un PR de Parche que ya está abierto.** Hasta la 0.7.5, la corrida siguiente pusheaba la rama `parche/<pack>` con `--force` (y pisaba los commits que alguien le hubiera sumado al PR) y pedía otro PR, que GitHub rechaza: la corrida fallaba y los packs que venían después no corrían. Ahora `pr` busca el PR abierto de esa rama. Si el arreglo es el mismo, no lo toca. Si cambió y el PR tiene solo el commit de Parche, actualiza la rama y el PR, sin abrir otro. Si la rama tiene commits de otra persona (con PR abierto o sin él), no la pisa y lo dice. La regla: Parche nunca hace force push sobre un commit que no escribió.
11
+ - **Un pack que falla no corta la corrida.** Si la verificación de un pack falla, o GitHub responde con un error, los packs que siguen corren igual. El resumen del job suma una sección "No se pudo", el registro anota un evento `error` y `pr` termina con error al final.
12
+ - **El `fingerprint` ya no se hereda entre líneas con el mismo texto.** El orden se contaba entre los avisos: cuando se arreglaba uno, el siguiente con el mismo texto de línea se quedaba con su `fingerprint`. En `tienda-demo`, después del arreglo de ARCA, la factura A, que sigue con "falta un dato", salía con el `fingerprint` de otra y, comparando corridas, como resuelta. Ahora el orden se cuenta entre todas las líneas del archivo con ese texto, tengan aviso o no, y dos avisos del mismo pack en una misma línea se distinguen por su orden en esa línea. Con un solo aviso por línea, el `fingerprint` es el mismo que en la 0.7.3, así que los de siempre no cambian.
13
+ - **GitHub no deja que Actions abra PRs.** En los repos personales y las organizaciones nuevas viene apagado "Allow GitHub Actions to create and approve pull requests", y GitHub responde 403 con un mensaje en inglés. Ahora Parche lo explica en castellano: dónde se prende el ajuste (en el repo, o en la organización) o que se use un token de una GitHub App o uno personal. El README y la plantilla del workflow lo dicen.
14
+
15
+ ## 0.7.5 — 2026-10-24
16
+
17
+ Mercado Pago: dos cosas del SDK oficial que el pack no tenía en cuenta, verificadas en su código (github.com/mercadopago/sdk-nodejs, 24/10/2026).
18
+
19
+ ### Cambios
20
+ - **Ya no se agrega `idempotencyKey` al migrar a `Order.create`.** El SDK manda una `X-Idempotency-Key` nueva en cada escritura cuando no recibe una, así que agregarla no aportaba nada. Y por un bug abierto del SDK (mercadopago/sdk-nodejs#481), las `requestOptions` de `create` quedan guardadas en la configuración del `client`: la clave que agregaba Parche salía también en los reembolsos y las capturas siguientes de ese `client`. Si el código ya traía su propia clave, se deja como está y el PR avisa del #481. Tampoco se agrega más el import de `randomUUID`.
21
+ - **Versión del SDK:** `Order` existe en `mercadopago` desde la 2.4.0 (1/4/2025). Con una versión anterior instalada (según el lockfile o `node_modules`), la migración queda **bloqueada** y pide actualizar el SDK, porque migrada dejaría `Order` en undefined. Si solo se conoce el rango de `package.json` y permite una versión anterior, se migra para revisar y pide fijar `^2.4.0`. Si no se ve ninguna versión, pide confirmarla.
22
+ - **Pack `mercadopago.payments-to-orders` 2026.10.24:** el checklist ya no pide mandar `X-Idempotency-Key` a mano con el SDK (sí por HTTP directo) y pide `mercadopago` 2.4.0 o posterior. Suma como fuentes el release 2.4.0 del SDK y el issue #481.
23
+
24
+ ## 0.7.4 — 2026-10-24
25
+
26
+ Un repo con `.parche/` commiteado, y los códigos de error de ARCA según su manual oficial.
27
+
28
+ ### Cambios
29
+ - **`.parche/` commiteado:** si alguien commiteó `.parche/` (pasó copiando una carpeta ya escaneada a un repo nuevo), `scan` lo modificaba y después `pr` frenaba con "Hay cambios sin commitear"; y si hubiera seguido, `git add -A` metía `.parche/` en el PR. Ahora `pr` no cuenta `.parche/` al ver si el repo está limpio, nunca lo incluye en el commit, y avisa con una línea cómo dejar de versionarlo. Si la verificación falla, se descarta el arreglo pero no el registro de esa corrida.
30
+ - **Pack `arca.wsfe.condicion-iva-receptor`:** cita como fuentes los dos manuales para el desarrollador de WSFEv1 de ARCA: el de producción (revisión del 1/9/2026), cuyo anexo confirma la tabla de códigos válidos por clase de comprobante que ya usaba el pack, y el de homologación (v4.8, revisión del 1/12/2026), que fija el 01/12/2026 como la fecha en que el campo pasa a ser obligatorio. Hasta ahora esa fecha salía solo de fuentes de terceros.
31
+ - **Guía de `CondicionIVAReceptorId`:** los códigos son los de los manuales. Cuando falta el campo, el código es la observación 10245 (que queda en desuso el 1/12) y, cuando pasa a ser obligatorio, el error 10246; el 10242 es un código que no existe y el 10243, uno que no corresponde a la clase del comprobante, y los dos ya rechazan hoy. Antes decía que el campo faltante era el 10242. Con afip.js 0.x y `ts-afip-ws` dice "tratalo como si no viajara", porque no está confirmado que no viaje (PR #26). El README dice lo mismo.
32
+ - README e inventario: `.parche/` es salida local y no va en git.
4
33
 
5
34
  ## 0.7.3 — 2026-10-24
6
35
 
package/README.md CHANGED
@@ -82,6 +82,7 @@ npx parche-cli feedback . --pack <id> --outcome merged|edited|rejected
82
82
 
83
83
  - **Desde un fork:** `parche pr . --push-remote fork` pushea la rama al remote `fork` (tu fork, con tus credenciales de git) y abre el PR contra `origin`.
84
84
  - **Nunca deja el repo sucio:** si la verificación falla, descarta lo que escribió y borra su rama. Si no hay cambios de código, o solo cambiarían tests o mocks, no abre PR: deja el texto como issue en `.parche/`.
85
+ - **`.parche/` no va en git:** es lo que Parche escribe en cada corrida (el reporte, el inventario, el registro y los cuerpos de los PRs), y lo agrega a `.git/info/exclude`. Si ya lo commiteaste (por ejemplo, copiando una carpeta escaneada a un repo nuevo), Parche funciona igual: no lo cuenta como un cambio sin commitear, nunca lo incluye en sus PRs y te avisa. Para dejar de versionarlo: `git rm -r --cached .parche` y sumá `.parche/` al `.gitignore`.
85
86
 
86
87
  ### Qué cambia en las versiones nuevas de tus SDKs
87
88
 
@@ -113,7 +114,9 @@ Para que corra todas las semanas, copiá [`templates/github-workflow.yml`](templ
113
114
  # dry-run: 'true' # opcional: armar las ramas sin abrir PRs
114
115
  ```
115
116
 
116
- El workflow necesita `permissions: contents: write` y `pull-requests: write`. Antes de Parche, la plantilla instala las dependencias de tu proyecto para que corran sus tests: `npm ci` si hay `package-lock.json` y `composer install` si hay `composer.json` (Composer viene en los runners de Ubuntu de GitHub). Así anda igual en un repo de Node y en uno de PHP.
117
+ El workflow necesita `permissions: contents: write` y `pull-requests: write`. Además, en el repo (o en la organización, si el repo es de una) tiene que estar prendido **Settings → Actions → General → Workflow permissions → "Allow GitHub Actions to create and approve pull requests"**: viene apagado en los repos personales y las organizaciones nuevas, y sin eso GitHub no deja que la Action abra el PR (Parche te lo dice en la corrida). Con un token de una GitHub App o uno personal (abajo), no hace falta. Antes de Parche, la plantilla instala las dependencias de tu proyecto para que corran sus tests: `npm ci` si hay `package-lock.json` y `composer install` si hay `composer.json` (Composer viene en los runners de Ubuntu de GitHub). Así anda igual en un repo de Node y en uno de PHP.
118
+
119
+ **Si ya hay un PR de Parche abierto**, la corrida siguiente no abre otro: si el arreglo es el mismo, no lo toca; si cambió (por ejemplo, salió una versión nueva del pack) y el PR tiene solo el commit de Parche, lo actualiza. Si alguien le sumó un commit, Parche no lo pisa: lo deja como está y lo dice en el resumen del job. Si un pack falla (por ejemplo, porque no pasan tus tests), los que siguen corren igual y la corrida termina con error al final.
117
120
 
118
121
  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).
119
122
 
@@ -155,7 +158,7 @@ Cómo contribuir, las reglas para el código y cómo se saca una versión: [`CON
155
158
  | Pack | Proveedor | Tipo | Fecha límite | Arreglo |
156
159
  |---|---|---|---|---|
157
160
  | `arca.wsfe.condicion-iva-receptor` | ARCA | rompe | 2026-12-01 | sí, en JS (`afip.js` y `@arcasdk/core`) y PHP (`afipsdk/afip.php` y forks): consumidor final y DNI B/C; con `DocTipo` en una variable, consumidor final al ejecutarse (PR parcial) |
158
- | `mercadopago.payments-to-orders` | Mercado Pago | migración | — | sí (creación de pagos en SDK Node v2) |
161
+ | `mercadopago.payments-to-orders` | Mercado Pago | migración | — | sí (creación de pagos con el SDK de Node, 2.4.0 o posterior) |
159
162
  | `ai.model-retirements` | Anthropic, OpenAI | retiro | varias | solo detección (también modelos fine-tuned de OpenAI, `ft:…`). De OpenAI, los modelos que su página de deprecaciones apaga desde 2026: texto, código, imagen, video, voz, transcripción y realtime; y los productos que cierran: Assistants API, prompts reutilizables (`pmpt_…`, `v1/prompts`), la plataforma de Evals, ChatKit con un workflow de Agent Builder y la Videos API (cerrada el 24/9/2026). De Anthropic, los modelos retirados o con fecha, también por su alias (`claude-sonnet-4-5`, que apunta a `claude-sonnet-4-5-20250929` y se retira el 30/11/2026), `temperature`, `top_p` y `top_k` con un valor que no es el de por defecto en Claude 4.7 y posteriores, que dan error 400 (SDK de JS/TS y de Python, y HTTP directo), y los mismos tres parámetros con cualquier modelo en la API version `2026-10-21`, que usan el SDK de TypeScript 1.0.0 y el de Python 2.0.0 (del 21/10/2026): dice si el pedido ya falla, si falla en una instalación nueva o si va a fallar cuando suban el SDK |
160
163
 
161
164
  ## Verificación
@@ -172,7 +175,7 @@ Parche corre en tu máquina o en tu runner. Tu código no sale de ahí, salvo el
172
175
 
173
176
  | | |
174
177
  |---|---|
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. |
178
+ | **Red** | `scan`, `fix` y `packs` no usan la red. `pr` llama a `api.github.com` para buscar, abrir o actualizar el PR en tu repo y pushea su rama `parche/<pack>`. La pisa con `--force` solo si todos sus commits son de Parche: si alguien le sumó uno, no la toca y lo dice en la corrida. `diff` y `scan --upgrades` bajan metadatos y paquetes de `registry.npmjs.org`, que ve qué paquetes y versiones consultás. Nada más. |
176
179
  | **Telemetría** | Ninguna. `parche feedback` escribe en `.parche/ledger.jsonl` de tu repo y queda ahí. |
177
180
  | **Qué ejecuta** | `git` y, solo con `--verify`, los tests de tu repo (`npm test`, `composer test` o `vendor/bin/phpunit`). |
178
181
  | **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'`). |
@@ -197,7 +200,7 @@ Para `afipsdk/afip.php` y sus forks (`fertandil87/afip.php`, `ivanalemunioz/afip
197
200
  - Copia el estilo del array: comillas de las claves, indentación (espacios o tabs), alineación de los `=>`, coma final y fin de línea (LF o CRLF).
198
201
  - Saltea `vendor/`.
199
202
 
200
- **La versión de la librería importa.** Con `afip.js`/`afip.php` 0.x, `eesnaola/afip.php` y `ts-afip-ws` (un fork de afip.js 0.x; su última versión, la 1.3.2, trae el WSDL sin el campo) el pedido se arma con un WSDL de 2021 que no tiene `CondicionIVAReceptorId`: aunque el campo esté en el código, no llega a ARCA. Parche lee la versión instalada (`composer.lock`, `package-lock.json`, `node_modules`) y, en esos casos, el arreglo sale como **"revisar"** y lo que pide un dato lleva el mismo aviso: qué actualizar para que el campo viaje. También avisa si el repo tiene un `.wsdl` de WSFEv1 sin el campo.
203
+ **La versión de la librería importa.** Con `afip.php` 0.x y `eesnaola/afip.php` el pedido se arma con un WSDL de 2021 que no tiene `CondicionIVAReceptorId`: aunque el campo esté en el código, no llega a ARCA. Con `afip.js` 0.x y `ts-afip-ws` (un fork de afip.js 0.x; su última versión, la 1.3.2, trae el WSDL sin el campo), el WSDL que traen tampoco lo tiene; no está confirmado que el campo no viaje, así que conviene tratarlo como si no viajara. Parche lee la versión instalada (`composer.lock`, `package-lock.json`, `node_modules`) y, en esos casos, el arreglo sale como **"revisar"** y lo que pide un dato lleva el mismo aviso: qué actualizar para que el campo viaje. También avisa si el repo tiene un `.wsdl` de WSFEv1 sin el campo.
201
204
 
202
205
  **Clave casi bien escrita.** Si el detalle del comprobante lleva `CondicionIVAReceptor` (sin `Id`), `CondicionIvaReceptorId` o `condicionIVAReceptorId`, ese elemento no existe en el WSDL: el dato nunca llega y el repo parece migrado. Parche renombra la clave a `CondicionIVAReceptorId` sin tocar el valor, en JS y en PHP, y el PR sale **borrador** para revisar que el valor sea un código válido. Solo lo hace dentro de objetos o arrays con al menos dos claves de WSFEv1 (`DocTipo`, `CbteDesde`, `ImpTotal`…), para no tocar modelos propios con un nombre parecido, y nunca en tests. También renombra una asignación aparte (`det.CondicionIVAReceptor = x`, `det['CondicionIVAReceptor'] = x` o `$det['CondicionIVAReceptor'] = $x`) cuando `det` es, en la misma función, un objeto o array con esas claves; si no puede confirmarlo, lo avisa sin tocar. Si un `interface` o un `type` del repo declara la clave mal escrita, no renombra, porque TypeScript dejaría de compilar, y el aviso dice dónde está ese tipo.
203
206
 
@@ -235,6 +238,6 @@ Cuando `DocTipo` no es un número fijo (`DocTipo: docTipo`), Parche no puede sab
235
238
  - IA, parámetros de Claude 4.7 y posteriores: avisa solo si el modelo del pedido se lee en el mismo archivo (un literal, una constante asignada una vez o el valor por defecto de una variable de entorno) y el parámetro es un número escrito en el código. Si el modelo llega de otro archivo o de la configuración, no avisa: no se puede saber si el pedido va a fallar.
236
239
  - IA, API version `2026-10-21` de Anthropic: la versión del SDK sale de `package-lock.json`, `node_modules` o el rango de `package.json`, y en Python solo de `requirements.txt` (todavía no de `pyproject.toml`, `poetry.lock` ni `uv.lock`). Un rango sin número (`latest`, `*`) cuenta como versión desconocida. Por HTTP directo avisa solo si el archivo manda `anthropic-version: 2026-10-21`. Los SDKs de Go, Java, .NET, Ruby y PHP no se leen.
237
240
  - IA, alias de Anthropic: avisa los de Claude Sonnet 4.5, Opus 4.5 y Haiku 4.5 (`claude-sonnet-4-5`, `claude-opus-4-5`, `claude-haiku-4-5`), escritos enteros entre comillas. Los alias de modelos ya retirados (`claude-3-5-sonnet-latest`, `claude-3-7-sonnet-latest` y otros) todavía no: la documentación actual de Anthropic ya no los nombra.
238
- - Mercado Pago: migra la creación de pagos con el SDK v2. Las consultas, capturas y reembolsos de pagos existentes se dejan a propósito en Payments API, como indica la guía oficial.
241
+ - Mercado Pago: migra la creación de pagos con el SDK de Node 2.4.0 o posterior, la primera versión con `Order` (con una anterior instalada, no migra y pide actualizar). No agrega una `idempotencyKey`: el SDK genera una en cada escritura, y una fija quedaría en el client por un bug abierto del SDK (mercadopago/sdk-nodejs#481). Las consultas, capturas y reembolsos de pagos existentes se dejan a propósito en Payments API, como indica la guía oficial.
239
242
  - Se distribuye como CLI (`npx parche-cli`) + GitHub Action (`Pepo08/parche@v0`). La GitHub App (instalación en un click, PRs automáticos al publicarse un pack nuevo) es el próximo paso.
240
243
  - La capa de LLM (redactar packs desde changelogs y resolver casos "falta un dato" leyendo el modelo de datos del cliente) todavía no está incluida; cuando entre, va a pasar por la misma verificación.
package/bin/parche.js CHANGED
@@ -7,7 +7,8 @@ import { loadPacks } from '../src/packs.js';
7
7
  import { scanReport, prBody, counts, LABEL, daysUntil, partialCovers } from '../src/report.js';
8
8
  import { verify } from '../src/verify.js';
9
9
  import * as git from '../src/git.js';
10
- import { parseGithubRemote, openPullRequest } from '../src/github.js';
10
+ import { parseGithubRemote, githubClient } from '../src/github.js';
11
+ import { publishPr } from '../src/publish.js';
11
12
  import { record } from '../src/ledger.js';
12
13
  import { apiLabel, noUsesLabel } from '../src/inventory/index.js';
13
14
  import { isTestFile } from '../src/paths.js';
@@ -50,6 +51,15 @@ function summary(findings) {
50
51
  .filter((k) => c[k]).map((k) => `${LABEL[k]}: ${c[k]}`).join(' ');
51
52
  }
52
53
 
54
+ // Qué pasó con el PR de cada pack (src/publish.js), en la salida de `pr`.
55
+ const PUBLISHED = {
56
+ opened: ({ draft, url }) => `PR abierto${draft ? ' (borrador)' : ''}: ${url}`,
57
+ updated: ({ url }) => `Ya había un PR abierto con solo el commit de Parche; se actualizó con el arreglo de hoy: ${url}`,
58
+ unchanged: ({ url }) => `Ya hay un PR abierto con este mismo arreglo; no se tocó: ${url}`,
59
+ kept: ({ branch, url }) => `La rama ${branch} tiene commits de otra persona${url ? ` (${url})` : ''}: Parche no la pisa. `
60
+ + `Si querés que la rehaga, cerrá el PR y borrá la rama.`,
61
+ };
62
+
53
63
  function ensureLocalDir(root) {
54
64
  fs.mkdirSync(path.join(root, '.parche'), { recursive: true });
55
65
  const exclude = path.join(root, '.git', 'info', 'exclude');
@@ -215,6 +225,9 @@ async function main() {
215
225
  if (cmd === 'pr') {
216
226
  if (!git.isRepo(root)) throw new Error('`parche pr` necesita un repositorio git.');
217
227
  if (!git.isClean(root)) throw new Error('Hay cambios sin commitear. Commiteá o guardá (stash) antes de correr `parche pr`.');
228
+ if (git.tracksParcheDir(root)) {
229
+ console.warn('Ojo: `.parche/` está commiteado en el repo. Parche lo usa para sus reportes y no lo incluye en sus PRs. Si no lo querés versionar: `git rm -r --cached .parche` y sumalo al .gitignore.');
230
+ }
218
231
  // Quién firma el commit se resuelve antes de escribir nada: si falla al commitear, el repo quedaría sucio.
219
232
  const env = args.author !== undefined ? { ...process.env, ...git.parseAuthor(args.author) } : process.env;
220
233
  const identity = git.commitIdentity(root, env);
@@ -231,6 +244,10 @@ async function main() {
231
244
  // Para el resumen del job en GitHub Actions: los PRs de esta corrida y lo que quedó sin PR.
232
245
  const opened = [];
233
246
  const withoutPr = [];
247
+ // Un pack que falla (la verificación, la API de GitHub) no corta la corrida: los que siguen corren igual, el resumen
248
+ // del job lo muestra y `pr` termina con error al final (P9, 0.7.6).
249
+ const failed = [];
250
+ const client = process.env.GITHUB_TOKEN ? githubClient(process.env.GITHUB_TOKEN) : null;
234
251
  for (const pack of packs) {
235
252
  const preview = run(root, { write: false, packIds: [pack.id] });
236
253
  if (!preview.findings.length) continue;
@@ -275,18 +292,20 @@ async function main() {
275
292
  console.log(` Cuerpo del PR: ${bodyPath}`);
276
293
  const target = parseGithubRemote(git.originUrl(root));
277
294
  const pushTo = parseGithubRemote(git.remoteUrl(root, pushRemote));
278
- let url = null;
279
- if (!args['dry-run'] && process.env.GITHUB_TOKEN && target && pushTo) {
280
- git.push(root, branch, pushRemote);
281
- const head = pushTo.owner !== target.owner ? `${pushTo.owner}:${branch}` : branch;
282
- url = await openPullRequest({ token: process.env.GITHUB_TOKEN, ...target, head, base, title, body: fs.readFileSync(bodyPath, 'utf8'), draft });
283
- console.log(` PR abierto${draft ? ' (borrador)' : ''}: ${url}`);
295
+ let pr = { status: null, url: null };
296
+ if (!args['dry-run'] && client && target && pushTo) {
297
+ pr = await publishPr({ root, client, target, pushTo, pushRemote, branch, base, pack, title, body: fs.readFileSync(bodyPath, 'utf8'), draft });
298
+ console.log(` ${PUBLISHED[pr.status]({ draft, branch, url: pr.url })}`);
284
299
  } else {
285
300
  console.log(` Modo prueba: no se abrió el PR${draft ? ' (iría como borrador por tener ítems para revisar)' : ''}.`);
286
301
  }
287
- opened.push({ title, url, draft });
288
- record(root, { type: 'pr', pack: pack.id, packVersion: pack.version, branch, sha, url, draft, files: changedFiles, findings: c,
289
- partial: result.findings.filter((f) => f.partial).length });
302
+ opened.push({ title, url: pr.url, draft, status: pr.status });
303
+ record(root, { type: 'pr', pack: pack.id, packVersion: pack.version, branch, sha, url: pr.url, draft, files: changedFiles, findings: c,
304
+ partial: result.findings.filter((f) => f.partial).length, ...(pr.status && pr.status !== 'opened' ? { existing: pr.status } : {}) });
305
+ } catch (err) {
306
+ console.error(` No se pudo con ${pack.id}: ${err.message.split('\n')[0]}`);
307
+ failed.push({ pack, message: err.message });
308
+ record(root, { type: 'error', pack: pack.id, message: err.message.split('\n')[0] });
290
309
  } finally {
291
310
  // Pase lo que pase, se vuelve a la rama base; la rama de Parche solo queda si tiene un commit.
292
311
  git.checkout(root, base);
@@ -303,9 +322,10 @@ async function main() {
303
322
  printFindings(detected);
304
323
  }
305
324
  if (process.env.GITHUB_ACTIONS === 'true') {
306
- if (process.env.GITHUB_STEP_SUMMARY) fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, stepSummary({ opened, reportOnly: withoutPr, notices }));
325
+ if (process.env.GITHUB_STEP_SUMMARY) fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, stepSummary({ opened, reportOnly: withoutPr, notices, failed }));
307
326
  for (const line of annotations([...notices, ...withoutPr], workspacePrefix(root, process.env.GITHUB_WORKSPACE))) console.log(line);
308
327
  }
328
+ if (failed.length) throw new Error(failed.map((f) => (failed.length > 1 ? `${f.pack.id}: ${f.message}` : f.message)).join('\n'));
309
329
  return 0;
310
330
  }
311
331
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "parche-cli",
3
- "version": "0.7.3",
3
+ "version": "0.7.6",
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",
@@ -10,6 +10,8 @@
10
10
  "summary_es": "Desde el 1/12/2026 ARCA rechaza automáticamente las solicitudes de CAE que no informen CondicionIVAReceptorId.",
11
11
  "why_es": "La RG 5616/2024 hizo obligatorio informar la condición frente al IVA del receptor en cada comprobante electrónico. El manual del desarrollador de WSFEv1 fija el 1 de diciembre de 2026 como fecha desde la cual el campo es obligatorio: si viaja vacío, nulo o no viaja, el pedido de CAE se rechaza y no se puede facturar.",
12
12
  "sources": [
13
+ { "title": "ARCA: Manual para el desarrollador WSFEv1 (RG 4291, revisión del 1/9/2026): el campo, los códigos 10242, 10243, 10245 y 10246 y el anexo de condiciones por clase de comprobante", "url": "https://www.afip.gob.ar/ws/documentacion/manuales/manual-desarrollador-ARCA-COMPG.pdf" },
14
+ { "title": "ARCA: Manual para el desarrollador WSFEv1 de homologación (v4.8, revisión del 1/12/2026): el campo pasa a ser obligatorio el 01/12/2026 y la observación 10245 queda en desuso", "url": "https://www.afip.gob.ar/fe/ayuda/documentos/wsfev1-RG-4291.pdf" },
13
15
  { "title": "VATupdate: Argentina makes recipient VAT status mandatory in electronic invoices", "url": "https://www.vatupdate.com/2026/09/09/argentina-makes-recipient-vat-status-mandatory-in-electronic-invoices/" },
14
16
  { "title": "iProfesional: ARCA rechazará facturas sin la condición del cliente frente al IVA", "url": "https://www.iprofesional.com/impuestos/464104-nuevo-requisito-arca-puede-trabar-emision-facturas" },
15
17
  { "title": "Tabla de códigos de FEParamGetCondicionIvaReceptor", "url": "https://app.spomsolutions.com/blog/condicion-iva-receptor-rg-5616" },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "mercadopago.payments-to-orders",
3
- "version": "2026.10.15",
3
+ "version": "2026.10.24",
4
4
  "provider": "Mercado Pago",
5
5
  "title": "Migración de creación de pagos: Payments API → Orders API",
6
6
  "kind": "migration",
@@ -11,7 +11,9 @@
11
11
  "why_es": "Payments API sigue funcionando pero solo recibe correcciones de seguridad y estabilidad. Orders API unifica el procesamiento online, trae varias transacciones por orden, captura dedicada, 3DS 2.0 nativo y errores consolidados. Los pagos ya creados se siguen consultando, capturando, cancelando y reembolsando por la API con la que se crearon, así que Parche migra solo la creación y deja el resto marcado para revisión.",
12
12
  "sources": [
13
13
  { "title": "Mercado Pago: Cómo migrar de Payments API a Orders API", "url": "https://www.mercadopago.com.ar/developers/es/docs/checkout-api-orders/resources/migrate-payments-to-orders" },
14
- { "title": "SDK Node.js de Mercado Pago (clase Order)", "url": "https://www.npmjs.com/package/mercadopago" }
14
+ { "title": "SDK Node.js de Mercado Pago (clase Order)", "url": "https://www.npmjs.com/package/mercadopago" },
15
+ { "title": "SDK Node.js de Mercado Pago 2.4.0 (1/4/2025): \"Feature: Order\", la primera versión con Order", "url": "https://github.com/mercadopago/sdk-nodejs/releases/tag/2.4.0" },
16
+ { "title": "mercadopago/sdk-nodejs#481 (abierto, 18/9/2026): las requestOptions de create quedan en la configuración compartida y su idempotencyKey sale en las llamadas siguientes", "url": "https://github.com/mercadopago/sdk-nodejs/issues/481" }
15
17
  ],
16
18
  "detect": {
17
19
  "packages": ["mercadopago"],
@@ -47,7 +49,8 @@
47
49
  "codemod": "mercadopago-payments-to-orders",
48
50
  "review_checklist_es": [
49
51
  "Configurar el webhook con el tópico `orders` en Tus integraciones y mantener `payment` en paralelo durante la transición.",
50
- "Enviar `X-Idempotency-Key` en todas las escrituras (crear, capturar, cancelar, reembolsar).",
52
+ "Con el SDK, no le pases una `idempotencyKey` fija en `requestOptions`: el SDK manda una `X-Idempotency-Key` nueva en cada escritura, y una fija queda en el client (mercadopago/sdk-nodejs#481) y sale también en los reembolsos y capturas siguientes. Por HTTP directo, mandá `X-Idempotency-Key` en todas las escrituras (crear, capturar, cancelar, reembolsar).",
53
+ "Tener `mercadopago` 2.4.0 o posterior: `Order` existe desde esa versión.",
51
54
  "Manejar `status` y `status_detail` a nivel orden y a nivel transacción.",
52
55
  "Persistir también `transactions.payments[].id` (los reembolsos parciales lo necesitan).",
53
56
  "En sandbox, el email del pagador tiene que ser test@testuser.com.",
package/src/actions.js CHANGED
@@ -41,18 +41,32 @@ export function annotations(groups, prefix = '') {
41
41
  return out;
42
42
  }
43
43
 
44
- // El resumen del job: los PRs de esta corrida, lo que quedó sin PR y los avisos de solo detección.
45
- // opened: [{ title, url, draft }] (url null en modo prueba); reportOnly y notices: grupos { pack, findings }.
46
- export function stepSummary({ opened = [], reportOnly = [], notices = [] }) {
44
+ // Qué pasó con un PR que ya existía (src/publish.js, P9).
45
+ const EXISTING = {
46
+ updated: 'ya estaba abierto: se actualizó con el arreglo de hoy',
47
+ unchanged: 'ya estaba abierto con este mismo arreglo: no se tocó',
48
+ kept: 'la rama tiene commits de otra persona: Parche no la pisa (si querés que la rehaga, cerrá el PR y borrá la rama)',
49
+ };
50
+
51
+ // El resumen del job: los PRs de esta corrida, lo que quedó sin PR, los avisos de solo detección y los packs que fallaron.
52
+ // opened: [{ title, url, draft, status }] (url null en modo prueba; status, el de publishPr); reportOnly y notices:
53
+ // grupos { pack, findings }; failed: [{ pack, message }].
54
+ export function stepSummary({ opened = [], reportOnly = [], notices = [], failed = [] }) {
47
55
  const lines = ['## Parche', ''];
48
56
  if (opened.length) {
49
57
  lines.push('### PRs', '');
50
58
  for (const o of opened) {
51
- const draft = o.draft ? ' · borrador' : '';
52
- lines.push(o.url ? `- [${o.title}](${o.url})${draft}` : `- ${o.title}${draft} · modo prueba: no se abrió`);
59
+ const note = EXISTING[o.status] ? ` · ${EXISTING[o.status]}` : o.draft ? ' · borrador' : '';
60
+ if (o.url) lines.push(`- [${o.title}](${o.url})${note}`);
61
+ else lines.push(`- ${o.title}${note}${o.status ? '' : ' · modo prueba: no se abrió'}`);
53
62
  }
54
63
  lines.push('');
55
64
  }
65
+ if (failed.length) {
66
+ lines.push('### No se pudo', '');
67
+ for (const f of failed) lines.push(`- **${f.pack.title}** · ${f.message.split('\n')[0]}`);
68
+ lines.push('');
69
+ }
56
70
  const section = (title, intro, groups) => {
57
71
  if (!groups.length) return;
58
72
  lines.push(`### ${title}`, '', intro, '');
@@ -64,7 +78,7 @@ export function stepSummary({ opened = [], reportOnly = [], notices = [] }) {
64
78
  };
65
79
  section('Sin PR', 'Parche no abre un PR cuando el arreglo depende de un dato de tu negocio o cuando no hay código para cambiar. Esto es lo que encontró y lo que falta.', reportOnly);
66
80
  section('Avisos', 'Cambios con fecha que afectan a tu código. Parche no los cambia solo: cambiar de modelo, por ejemplo, cambia cómo responde tu producto.', notices);
67
- if (!opened.length && !reportOnly.length && !notices.length) {
81
+ if (!opened.length && !reportOnly.length && !notices.length && !failed.length) {
68
82
  lines.push('Ninguno de los packs verificados tiene cambios que te afecten hoy.', '');
69
83
  }
70
84
  lines.push('El inventario completo de las APIs del repo: `npx parche-cli scan .`');
@@ -6,8 +6,31 @@ import {
6
6
  jFor, detectQuote, eolOf, getProp, hasSpread, keyName, literalValue, isStringLit, resolveBinding,
7
7
  memberChain, buildMember, isReference, cloneNode, finding, tidy, snippet,
8
8
  } from '../ast.js';
9
+ import { emptyContext } from '../context.js';
9
10
 
10
11
  const PACK = 'mercadopago.payments-to-orders';
12
+ // `Order` existe en el SDK desde la 2.4.0 (release del 1/4/2025, "Feature: Order"; no está en el index de la 2.3.0):
13
+ // con una versión anterior, `import { Order }` queda undefined. Verificado en github.com/mercadopago/sdk-nodejs el 24/10.
14
+ const ORDER_SINCE = [2, 4, 0];
15
+ const isOlder = (version, min) => {
16
+ const v = String(version).split('.').map((n) => Number.parseInt(n, 10) || 0);
17
+ for (let i = 0; i < min.length; i++) if (v[i] !== min[i]) return v[i] < min[i];
18
+ return false;
19
+ };
20
+ // Lo que se sabe de la versión instalada (src/context.js): la del lockfile o node_modules es la que corre; la
21
+ // "declarada" sale del rango de package.json y puede instalar otra.
22
+ function sdkVersion(context) {
23
+ const sdk = context?.versions?.npm?.mercadopago;
24
+ if (!sdk) return { unknown: true };
25
+ if (!isOlder(sdk.version, ORDER_SINCE)) return { ok: true };
26
+ return sdk.source === 'declarada' ? { declaredOld: sdk } : { installedOld: sdk };
27
+ }
28
+ // mercadopago/sdk-nodejs#481 (abierto desde el 18/9/2026): `create` guarda sus requestOptions en la configuración del
29
+ // client (`this.config.options = { ...this.config.options, ...requestOptions }`, de la 2.4.0 a la 3.6.1, también en
30
+ // Payment), y el RestClient solo genera una X-Idempotency-Key nueva cuando no recibe una.
31
+ const IDEMPOTENCY_LEAK = 'Por un bug abierto del SDK (mercadopago/sdk-nodejs#481), las `requestOptions` de `create` quedan guardadas '
32
+ + 'en la configuración del `client`: esta `idempotencyKey` va a salir también en las escrituras siguientes de ese `client` que no traigan '
33
+ + 'la suya (reembolsos, capturas). Ya pasaba con `Payment.create`. Si no necesitás una clave fija, sacala: el SDK genera una nueva en cada escritura.';
11
34
  const DEBIT = new Set(['debvisa', 'debmaster', 'debcabal', 'maestro']);
12
35
  const CREDIT = new Set(['visa', 'master', 'amex', 'naranja', 'cabal', 'tarshop', 'cencosud', 'diners', 'argencard', 'cmr']);
13
36
  const TICKET = new Set(['rapipago', 'pagofacil']);
@@ -28,7 +51,7 @@ const RESPONSE_RENAMES = {
28
51
  };
29
52
  const READ_METHODS = new Set(['get', 'search', 'cancel', 'capture', 'update']);
30
53
 
31
- export default function transform({ source, file, pack }) {
54
+ export default function transform({ source, file, pack, context = emptyContext() }) {
32
55
  const findings = [];
33
56
  const usages = [];
34
57
  if (!/mercadopago/.test(source)) return { output: null, findings, usages };
@@ -50,7 +73,7 @@ export default function transform({ source, file, pack }) {
50
73
  return node;
51
74
  };
52
75
  let changed = false;
53
- let needUUID = false;
76
+ const sdk = sdkVersion(context);
54
77
 
55
78
  textDetections(source, file, findings, usages);
56
79
 
@@ -121,7 +144,14 @@ export default function transform({ source, file, pack }) {
121
144
  }
122
145
  const paymentRefs = root.find(j.Identifier, { name: paymentLocal }).filter(isReference).size();
123
146
  if (paymentRefs === 0) mp.removeSpecifier('Payment');
124
- if (needUUID) ensureRandomUUID(j, root, mp, detectQuote(source) === 'double' ? '"' : "'");
147
+ // Sin la versión instalada a la vista no se puede saber si `Order` existe: se migra, pero para revisar.
148
+ if (sdk.declaredOld) {
149
+ findings.push({ pack: PACK, file, line: mp.line, status: 'needs-review',
150
+ message: `\`package.json\` declara mercadopago \`${sdk.declaredOld.range}\`, que permite instalar una versión sin \`Order\` (existe desde la 2.4.0, del 1/4/2025): fijá \`"mercadopago": "^2.4.0"\` o posterior.` });
151
+ } else if (sdk.unknown) {
152
+ findings.push({ pack: PACK, file, line: mp.line, status: 'needs-review',
153
+ message: 'No pude leer qué versión de mercadopago tenés instalada: `Order` existe desde la 2.4.0 (1/4/2025). Confirmá que tenés esa o una posterior.' });
154
+ }
125
155
  }
126
156
 
127
157
  webhookDetections(j, root, file, findings);
@@ -162,6 +192,12 @@ export default function transform({ source, file, pack }) {
162
192
  'Usa `application_fee` (Marketplace / Split 1:1). Orders API todavía no tiene un equivalente documentado: NO migrar este flujo hasta que Mercado Pago lo soporte.'));
163
193
  return false;
164
194
  }
195
+ if (sdk.installedOld) {
196
+ const { version, source: from } = sdk.installedOld;
197
+ findings.push(finding(PACK, file, call, 'blocked',
198
+ `Tenés mercadopago ${version} (según \`${from}\`) y \`Order\` existe desde la 2.4.0 (1/4/2025): migrado ahora, \`Order\` quedaría undefined. Subí el SDK a 2.4.0 o posterior y volvé a correr Parche.`));
199
+ return false;
200
+ }
165
201
  const amount = getProp(body, 'transaction_amount');
166
202
  if (!amount) {
167
203
  findings.push(finding(PACK, file, call, 'needs-input', 'El pago no tiene `transaction_amount` literal en el body. Migrar a mano.'));
@@ -271,16 +307,13 @@ export default function transform({ source, file, pack }) {
271
307
  findings.push(finding(PACK, file, call, 'needs-review', 'No hay `external_reference`: Orders lo exige para algunos medios de pago y sirve para conciliar.'));
272
308
  }
273
309
 
274
- // Idempotencia obligatoria en todas las escrituras de Orders.
310
+ // Orders exige X-Idempotency-Key en cada escritura, y el SDK ya manda una nueva en cada una si no recibe ninguna.
311
+ // Hasta la 0.7.4 Parche agregaba `requestOptions: { idempotencyKey: randomUUID() }`: no aportaba nada y, por el
312
+ // #481, esa clave quedaba en el client y salía también en las escrituras siguientes. Si el código ya traía una, se
313
+ // deja como está y se avisa.
275
314
  const ro = getProp(arg, 'requestOptions');
276
- const idem = prop('idempotencyKey', j.callExpression(id('randomUUID'), []));
277
- if (!ro) {
278
- arg.properties.push(prop('requestOptions', snippet(j, 'x = { idempotencyKey: randomUUID() };', 'ObjectExpression')));
279
- needUUID = true;
280
- findings.push(finding(PACK, file, call, 'auto', 'Se agregó `idempotencyKey` (X-Idempotency-Key es obligatorio en Orders).'));
281
- } else if (ro.value.type === 'ObjectExpression' && !getProp(ro.value, 'idempotencyKey')) {
282
- ro.value.properties.push(idem);
283
- needUUID = true;
315
+ if (ro && ro.value.type === 'ObjectExpression' && getProp(ro.value, 'idempotencyKey')) {
316
+ findings.push(finding(PACK, file, call, 'needs-review', IDEMPOTENCY_LEAK));
284
317
  }
285
318
 
286
319
  rewriteResponse(p);
@@ -415,23 +448,6 @@ function findMpImport(j, root) {
415
448
  return null;
416
449
  }
417
450
 
418
- function ensureRandomUUID(j, root, mp, quoteChar) {
419
- const already = root.find(j.Identifier, { name: 'randomUUID' }).filter((p) => {
420
- const parent = p.parent.node;
421
- return parent.type === 'ImportSpecifier' || parent.type === 'ObjectProperty' || parent.type === 'Property';
422
- }).size();
423
- if (already) return;
424
- const q = quoteChar;
425
- if (mp.kind === 'esm') {
426
- const decl = snippet(j, `import { randomUUID } from ${q}node:crypto${q};`, 'ImportDeclaration');
427
- const imports = root.find(j.ImportDeclaration);
428
- j(imports.paths()[imports.size() - 1]).insertAfter(decl);
429
- } else {
430
- const decl = snippet(j, `const { randomUUID } = require(${q}node:crypto${q});`, 'VariableDeclaration');
431
- j(mp.statement).insertAfter(decl);
432
- }
433
- }
434
-
435
451
  function textDetections(source, file, findings, usages) {
436
452
  const lines = source.split('\n');
437
453
  lines.forEach((l, i) => {
package/src/engine.js CHANGED
@@ -37,13 +37,31 @@ export function listFiles(dir, skipped = []) {
37
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
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
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.
40
+ // (una reindentación o pasar a CRLF no es otro aviso).
41
+ // Si el mismo texto se repite en el archivo, cuenta el orden entre las líneas del archivo con ese texto, tengan aviso o
42
+ // no (P10, 0.7.6). Antes contaba entre los avisos: al arreglarse uno, el siguiente con el mismo texto heredaba su
43
+ // fingerprint, y en tienda-demo la factura A, que sigue con "falta un dato", salía como "resuelta". Dos avisos del mismo
44
+ // pack en la misma línea se distinguen por su orden en esa línea; un aviso sin línea, por su orden en el archivo.
45
+ const normalized = (line) => line.replace(/\s+/g, ' ').trim();
46
+
41
47
  export function fingerprint(finding, source, seen) {
42
- const text = finding.line ? (source.split('\n')[finding.line - 1] ?? '').replace(/\s+/g, ' ').trim() : '';
48
+ const lines = finding.line ? source.split('\n') : [];
49
+ const text = finding.line ? normalized(lines[finding.line - 1] ?? '') : '';
43
50
  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);
51
+ let n;
52
+ if (finding.line) {
53
+ n = 0;
54
+ for (let i = 0; i < finding.line && i < lines.length; i++) if (normalized(lines[i]) === text) n++;
55
+ n = Math.max(n, 1);
56
+ }
57
+ // Cuántos avisos de este pack ya salieron en esta línea (o sin línea, en este archivo).
58
+ const slot = `${finding.pack}\u0000${finding.file}\u0000${finding.line || ''}`;
59
+ const k = (seen.get(slot) || 0) + 1;
60
+ seen.set(slot, k);
61
+ if (!finding.line) n = k;
62
+ // Con un solo aviso en la línea, la clave es la misma que en la 0.7.3: no cambian los fingerprint de siempre.
63
+ const suffix = finding.line && k > 1 ? `\u0000${k}` : '';
64
+ return crypto.createHash('sha256').update(`${key}\u0000${n}${suffix}`).digest('hex').slice(0, 16);
47
65
  }
48
66
 
49
67
  export function run(dir, { write = false, packIds = null, today } = {}) {
@@ -60,7 +78,7 @@ export function run(dir, { write = false, packIds = null, today } = {}) {
60
78
  const usages = [];
61
79
  const fileScans = []; // inventario de cualquier API (nivel 1), por archivo
62
80
  const changed = new Map(); // archivo relativo → [packs]
63
- const seen = new Map(); // para fingerprint(): cuántas veces apareció cada pack + archivo + texto
81
+ const seen = new Map(); // para fingerprint(): cuántos avisos de cada pack salieron en cada línea
64
82
 
65
83
  files.forEach((abs, i) => {
66
84
  const rel = rels[i];
package/src/git.js CHANGED
@@ -17,8 +17,19 @@ export function currentBranch(cwd) {
17
17
  return git(cwd, 'rev-parse', '--abbrev-ref', 'HEAD');
18
18
  }
19
19
 
20
+ // `.parche/` es lo que Parche escribe en cada corrida (reportes, inventario, registro, cuerpos de PR). No cuenta para
21
+ // "repo limpio" y nunca entra en un PR, aunque alguien lo haya commiteado: pasó copiando una carpeta ya escaneada a un
22
+ // repo nuevo, y `scan` lo modificaba y `pr` frenaba con "Hay cambios sin commitear". Todo el repo (`:/`) menos
23
+ // `.parche/` de la carpeta que se revisa (las rutas de exclusión son relativas a `cwd`, que es donde vive `.parche/`).
24
+ const OUTSIDE_PARCHE = ['--', ':/', ':(exclude).parche'];
25
+
20
26
  export function isClean(cwd) {
21
- return git(cwd, 'status', '--porcelain') === '';
27
+ return git(cwd, 'status', '--porcelain', ...OUTSIDE_PARCHE) === '';
28
+ }
29
+
30
+ // ¿`.parche/` está commiteado? Solo para avisarlo: Parche funciona igual.
31
+ export function tracksParcheDir(cwd) {
32
+ return git(cwd, 'ls-files', '--', '.parche') !== '';
22
33
  }
23
34
 
24
35
  export function checkoutNewBranch(cwd, name, base) {
@@ -65,6 +76,10 @@ export const isNoreply = (email) => /@users\.noreply\.github\.com$/i.test(email
65
76
  export function commitAll(cwd, message, env = process.env) {
66
77
  const id = commitIdentity(cwd, env);
67
78
  git(cwd, 'add', '-A');
79
+ // `.parche/` nunca entra en el commit. Si no está commiteado, `add` ya no lo toma (está en .git/info/exclude); si lo
80
+ // está, se saca del índice lo que `add` tomó de ahí. No va como pathspec de exclusión en `add`: git sale con error
81
+ // cuando una pathspec nombra una carpeta ignorada, aunque sea para excluirla.
82
+ if (tracksParcheDir(cwd)) git(cwd, 'reset', '-q', 'HEAD', '--', '.parche');
68
83
  git(cwd, '-c', `user.name=${id.name}`, '-c', `user.email=${id.email}`, 'commit', '-q', '-m', message);
69
84
  return git(cwd, 'rev-parse', '--short', 'HEAD');
70
85
  }
@@ -83,13 +98,42 @@ export function remoteUrl(cwd, remote = 'origin') {
83
98
 
84
99
  export const originUrl = (cwd) => remoteUrl(cwd, 'origin');
85
100
 
101
+ // Con `--force`: reemplaza el commit anterior de Parche en su rama. Quien llama se fija antes que la rama remota no
102
+ // tenga commits de otra persona (src/publish.js, P9).
86
103
  export function push(cwd, branch, remote = 'origin') {
87
104
  git(cwd, 'push', '-u', '--force', remote, branch);
88
105
  }
89
106
 
90
- // Descarta lo que Parche escribió en su rama (el árbol estaba limpio antes de empezar; .parche/ está excluido).
107
+ // El commit de la rama `branch` en `remote`, o null si no existe. No toca las ramas locales.
108
+ export function fetchBranch(cwd, remote, branch) {
109
+ try {
110
+ git(cwd, 'fetch', '-q', '--no-tags', remote, `refs/heads/${branch}`);
111
+ return git(cwd, 'rev-parse', 'FETCH_HEAD');
112
+ } catch {
113
+ return null;
114
+ }
115
+ }
116
+
117
+ // Los commits de `to` que no están en `from`, con su mensaje completo.
118
+ export function commitsBetween(cwd, from, to) {
119
+ return git(cwd, 'log', '--format=%H%x00%B%x1e', `${from}..${to}`)
120
+ .split('\x1e').map((s) => s.trim()).filter(Boolean)
121
+ .map((s) => { const [sha, ...rest] = s.split('\0'); return { sha, message: rest.join('\0').trim() }; });
122
+ }
123
+
124
+ // Un identificador del cambio que hace `to` desde donde se separó de `from`, sin contar números de línea ni espacios
125
+ // (`git patch-id`). Dos ramas con el mismo arreglo sobre bases distintas dan el mismo; sin cambios, ''.
126
+ export function patchId(cwd, from, to) {
127
+ const diff = execFileSync('git', gitArgs(['diff', `${from}...${to}`]), { cwd, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024, stdio: ['ignore', 'pipe', 'pipe'] });
128
+ if (!diff.trim()) return '';
129
+ return execFileSync('git', gitArgs(['patch-id', '--stable']), { cwd, input: diff, encoding: 'utf8' }).trim().split(/\s+/)[0] || '';
130
+ }
131
+
132
+ // Descarta lo que Parche escribió en su rama (el árbol estaba limpio antes de empezar). Antes era `reset --hard`, que
133
+ // con un `.parche/` commiteado también borraba el registro de esta corrida; los codemods solo modifican archivos que
134
+ // ya existen, así que alcanza con volver todo, menos `.parche/`, a lo que hay en HEAD.
91
135
  export function discardChanges(cwd) {
92
- git(cwd, 'reset', '--hard', '-q');
136
+ git(cwd, 'checkout', '-q', 'HEAD', ...OUTSIDE_PARCHE);
93
137
  }
94
138
 
95
139
  export function deleteBranch(cwd, name) {
package/src/github.js CHANGED
@@ -1,22 +1,51 @@
1
- // Apertura del PR por la API REST de GitHub (sin dependencias).
1
+ // La API REST de GitHub para los PRs de Parche (sin dependencias).
2
2
  export function parseGithubRemote(url) {
3
3
  if (!url) return null;
4
4
  const m = url.match(/github\.com[:/]([^/]+)\/([^/.]+?)(\.git)?$/);
5
5
  return m ? { owner: m[1], repo: m[2] } : null;
6
6
  }
7
7
 
8
- export async function openPullRequest({ token, owner, repo, head, base, title, body, draft }) {
9
- const res = await fetch(`https://api.github.com/repos/${owner}/${repo}/pulls`, {
10
- method: 'POST',
11
- headers: {
12
- Authorization: `Bearer ${token}`,
13
- Accept: 'application/vnd.github+json',
14
- 'X-GitHub-Api-Version': '2022-11-28',
15
- 'Content-Type': 'application/json',
8
+ // GitHub responde 403 con este mensaje cuando el repo (o su organización) tiene apagado "Allow GitHub Actions to create
9
+ // and approve pull requests". Viene apagado en los repos personales y las organizaciones nuevas (docs de GitHub,
10
+ // verificado el 25/10/2026): la primera corrida fallaba con el error en inglés y sin decir dónde se arregla (P8, 0.7.6).
11
+ const ACTIONS_CANNOT_OPEN_PRS = /not permitted to create or approve pull requests/i;
12
+
13
+ export const ACTIONS_CANNOT_OPEN_PRS_MESSAGE = 'GitHub no deja que GitHub Actions abra PRs en este repo. Prendelo en '
14
+ + 'Settings → Actions → General → Workflow permissions → "Allow GitHub Actions to create and approve pull requests" '
15
+ + '(si el repo es de una organización, el ajuste está en la configuración de la organización). O pasale a la Action '
16
+ + 'un token de una GitHub App o uno personal, con el input `token`. La rama del arreglo ya quedó pusheada: con el '
17
+ + 'ajuste prendido, la próxima corrida abre el PR.';
18
+
19
+ export function apiError(status, data) {
20
+ const message = data?.message || JSON.stringify(data);
21
+ if (status === 403 && ACTIONS_CANNOT_OPEN_PRS.test(message)) return new Error(ACTIONS_CANNOT_OPEN_PRS_MESSAGE);
22
+ return new Error(`GitHub respondió ${status}: ${message}`);
23
+ }
24
+
25
+ // Un cliente con el token de quien corre Parche. `request` es `fetch` (los tests le pasan uno propio, sin red).
26
+ export function githubClient(token, request = fetch) {
27
+ const call = async (method, route, body) => {
28
+ const res = await request(`https://api.github.com${route}`, {
29
+ method,
30
+ headers: {
31
+ Authorization: `Bearer ${token}`,
32
+ Accept: 'application/vnd.github+json',
33
+ 'X-GitHub-Api-Version': '2022-11-28',
34
+ ...(body ? { 'Content-Type': 'application/json' } : {}),
35
+ },
36
+ ...(body ? { body: JSON.stringify(body) } : {}),
37
+ });
38
+ const data = await res.json().catch(() => ({}));
39
+ if (!res.ok) throw apiError(res.status, data);
40
+ return data;
41
+ };
42
+ return {
43
+ // El PR abierto cuya rama es `head` ("dueño:rama"), o null.
44
+ async findOpenPull({ owner, repo, head }) {
45
+ const list = await call('GET', `/repos/${owner}/${repo}/pulls?state=open&head=${encodeURIComponent(head)}&per_page=1`);
46
+ return Array.isArray(list) && list.length ? list[0] : null;
16
47
  },
17
- body: JSON.stringify({ title, head, base, body, draft }),
18
- });
19
- const data = await res.json();
20
- if (!res.ok) throw new Error(`GitHub respondió ${res.status}: ${data.message || JSON.stringify(data)}`);
21
- return data.html_url;
48
+ openPull: ({ owner, repo, head, base, title, body, draft }) => call('POST', `/repos/${owner}/${repo}/pulls`, { title, head, base, body, draft }),
49
+ updatePull: ({ owner, repo, number, title, body }) => call('PATCH', `/repos/${owner}/${repo}/pulls/${number}`, { title, body }),
50
+ };
22
51
  }
package/src/publish.js ADDED
@@ -0,0 +1,31 @@
1
+ // Publica el PR de un pack: lo abre o, si ya hay uno abierto para su rama, lo actualiza sin pisar el trabajo de otros
2
+ // (P9, 0.7.6). Hasta la 0.7.5, `parche pr` pusheaba la rama con --force y pedía otro PR: si alguien le había sumado un
3
+ // commit al PR, se perdía; y GitHub rechazaba el PR repetido, así que con un PR abierto cada corrida semanal fallaba y
4
+ // los packs que venían después no corrían (lo encontró el chat de página el 25/10, F2).
5
+ import * as git from './git.js';
6
+
7
+ // El commit que escribe Parche: título "Parche: …" y la línea "Pack <id> v<versión>." (bin/parche.js, `commitAll`).
8
+ export const isParcheCommit = (message, pack) => /^Parche: /.test(message) && message.includes(`\nPack ${pack.id} v`);
9
+
10
+ // status: 'opened' (PR nuevo), 'updated' (el PR abierto tenía solo el commit de Parche y el arreglo cambió),
11
+ // 'unchanged' (mismo arreglo: no se pushea ni se toca) o 'kept' (la rama remota tiene commits de otra persona, con PR
12
+ // abierto o sin él: no se toca). La regla: Parche nunca hace force push sobre un commit que no escribió.
13
+ export async function publishPr({ root, client, target, pushTo, pushRemote, branch, base, pack, title, body, draft }) {
14
+ const head = pushTo.owner !== target.owner ? `${pushTo.owner}:${branch}` : branch;
15
+ const existing = await client.findOpenPull({ ...target, head: `${pushTo.owner}:${branch}` });
16
+ const found = existing ? { url: existing.html_url, number: existing.number } : { url: null, number: null };
17
+ const remote = git.fetchBranch(root, pushRemote, branch);
18
+ if (remote) {
19
+ // Un commit que no es de Parche (de una persona, o el "Update branch" de GitHub) hace que la rama sea de alguien más.
20
+ const others = git.commitsBetween(root, base, remote).filter((c) => !isParcheCommit(c.message, pack));
21
+ if (others.length) return { status: 'kept', ...found, others: others.length };
22
+ if (existing && git.patchId(root, base, remote) === git.patchId(root, base, branch)) return { status: 'unchanged', ...found };
23
+ }
24
+ git.push(root, branch, pushRemote);
25
+ if (!existing) {
26
+ const pr = await client.openPull({ ...target, head, base, title, body, draft });
27
+ return { status: 'opened', url: pr.html_url, number: pr.number };
28
+ }
29
+ await client.updatePull({ ...target, number: existing.number, title, body });
30
+ return { status: 'updated', ...found };
31
+ }
@@ -9,6 +9,8 @@ on:
9
9
  workflow_dispatch:
10
10
 
11
11
  # Por defecto, solo lectura. La escritura la pide el único job que la necesita.
12
+ # En un repo o una organización nuevos, prendé además Settings → Actions → General → Workflow permissions →
13
+ # "Allow GitHub Actions to create and approve pull requests": viene apagado, y sin eso la Action no puede abrir el PR.
12
14
  permissions:
13
15
  contents: read
14
16