parche-cli 0.7.5 → 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 +10 -0
- package/README.md +4 -2
- package/bin/parche.js +28 -11
- package/package.json +1 -1
- package/src/actions.js +20 -6
- package/src/engine.js +24 -6
- package/src/git.js +27 -0
- package/src/github.js +43 -14
- package/src/publish.js +31 -0
- package/templates/github-workflow.yml +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
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, 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
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
|
+
|
|
5
15
|
## 0.7.5 — 2026-10-24
|
|
6
16
|
|
|
7
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).
|
package/README.md
CHANGED
|
@@ -114,7 +114,9 @@ Para que corra todas las semanas, copiá [`templates/github-workflow.yml`](templ
|
|
|
114
114
|
# dry-run: 'true' # opcional: armar las ramas sin abrir PRs
|
|
115
115
|
```
|
|
116
116
|
|
|
117
|
-
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.
|
|
118
120
|
|
|
119
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).
|
|
120
122
|
|
|
@@ -173,7 +175,7 @@ Parche corre en tu máquina o en tu runner. Tu código no sale de ahí, salvo el
|
|
|
173
175
|
|
|
174
176
|
| | |
|
|
175
177
|
|---|---|
|
|
176
|
-
| **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
|
|
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. |
|
|
177
179
|
| **Telemetría** | Ninguna. `parche feedback` escribe en `.parche/ledger.jsonl` de tu repo y queda ahí. |
|
|
178
180
|
| **Qué ejecuta** | `git` y, solo con `--verify`, los tests de tu repo (`npm test`, `composer test` o `vendor/bin/phpunit`). |
|
|
179
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'`). |
|
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,
|
|
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');
|
|
@@ -234,6 +244,10 @@ async function main() {
|
|
|
234
244
|
// Para el resumen del job en GitHub Actions: los PRs de esta corrida y lo que quedó sin PR.
|
|
235
245
|
const opened = [];
|
|
236
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;
|
|
237
251
|
for (const pack of packs) {
|
|
238
252
|
const preview = run(root, { write: false, packIds: [pack.id] });
|
|
239
253
|
if (!preview.findings.length) continue;
|
|
@@ -278,18 +292,20 @@ async function main() {
|
|
|
278
292
|
console.log(` Cuerpo del PR: ${bodyPath}`);
|
|
279
293
|
const target = parseGithubRemote(git.originUrl(root));
|
|
280
294
|
const pushTo = parseGithubRemote(git.remoteUrl(root, pushRemote));
|
|
281
|
-
let
|
|
282
|
-
if (!args['dry-run'] &&
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
url = await openPullRequest({ token: process.env.GITHUB_TOKEN, ...target, head, base, title, body: fs.readFileSync(bodyPath, 'utf8'), draft });
|
|
286
|
-
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 })}`);
|
|
287
299
|
} else {
|
|
288
300
|
console.log(` Modo prueba: no se abrió el PR${draft ? ' (iría como borrador por tener ítems para revisar)' : ''}.`);
|
|
289
301
|
}
|
|
290
|
-
opened.push({ title, url, draft });
|
|
291
|
-
record(root, { type: 'pr', pack: pack.id, packVersion: pack.version, branch, sha, url, draft, files: changedFiles, findings: c,
|
|
292
|
-
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] });
|
|
293
309
|
} finally {
|
|
294
310
|
// Pase lo que pase, se vuelve a la rama base; la rama de Parche solo queda si tiene un commit.
|
|
295
311
|
git.checkout(root, base);
|
|
@@ -306,9 +322,10 @@ async function main() {
|
|
|
306
322
|
printFindings(detected);
|
|
307
323
|
}
|
|
308
324
|
if (process.env.GITHUB_ACTIONS === 'true') {
|
|
309
|
-
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 }));
|
|
310
326
|
for (const line of annotations([...notices, ...withoutPr], workspacePrefix(root, process.env.GITHUB_WORKSPACE))) console.log(line);
|
|
311
327
|
}
|
|
328
|
+
if (failed.length) throw new Error(failed.map((f) => (failed.length > 1 ? `${f.pack.id}: ${f.message}` : f.message)).join('\n'));
|
|
312
329
|
return 0;
|
|
313
330
|
}
|
|
314
331
|
|
package/package.json
CHANGED
package/src/actions.js
CHANGED
|
@@ -41,18 +41,32 @@ export function annotations(groups, prefix = '') {
|
|
|
41
41
|
return out;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
//
|
|
45
|
-
|
|
46
|
-
|
|
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
|
|
52
|
-
|
|
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 .`');
|
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).
|
|
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
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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():
|
|
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
|
@@ -98,10 +98,37 @@ export function remoteUrl(cwd, remote = 'origin') {
|
|
|
98
98
|
|
|
99
99
|
export const originUrl = (cwd) => remoteUrl(cwd, 'origin');
|
|
100
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).
|
|
101
103
|
export function push(cwd, branch, remote = 'origin') {
|
|
102
104
|
git(cwd, 'push', '-u', '--force', remote, branch);
|
|
103
105
|
}
|
|
104
106
|
|
|
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
|
+
|
|
105
132
|
// Descarta lo que Parche escribió en su rama (el árbol estaba limpio antes de empezar). Antes era `reset --hard`, que
|
|
106
133
|
// con un `.parche/` commiteado también borraba el registro de esta corrida; los codemods solo modifican archivos que
|
|
107
134
|
// ya existen, así que alcanza con volver todo, menos `.parche/`, a lo que hay en HEAD.
|
package/src/github.js
CHANGED
|
@@ -1,22 +1,51 @@
|
|
|
1
|
-
//
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
|