kcreate-scaffolding-gcp-script 0.2.0 → 0.2.1
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/README.md +89 -30
- package/bin/cli.js +0 -0
- package/package.json +1 -1
- package/skill/SKILL.md +23 -7
- package/skill/references/lessons-learned.md +84 -1
- package/skill/scripts/10_build_components.sh +22 -10
- package/skill/scripts/22_validate_no_app_name_leftover.sh +62 -0
- package/skill/scripts/lib/common.sh +41 -0
- package/skill/scripts/run_tracking.sh +24 -3
- package/vendor/github-proyect-tracking/SKILL.md +14 -5
- package/vendor/github-proyect-tracking/references/api.md +27 -5
- package/vendor/github-proyect-tracking/scripts/tracking.py +19 -1
- package/vendor/github-proyect-tracking/scripts/tracking_core/commands.py +102 -8
- package/vendor/github-proyect-tracking/scripts/tracking_core/common.py +47 -2
- package/vendor/github-proyect-tracking/scripts/tracking_core/events.py +30 -1
- package/vendor/github-proyect-tracking/scripts/tracking_core/github.py +12 -3
package/README.md
CHANGED
|
@@ -24,7 +24,6 @@ El despliegue es un proceso aparte, posterior y manual.
|
|
|
24
24
|
| `README.md` (este) | Manual de usuario: qué hace, cómo se instala y cómo se usa. |
|
|
25
25
|
| [`AGENT.md`](AGENT.md) | Guía operativa para agentes de IA: ciclo completo, contrato de exit codes, fallas comunes, receta de validación. |
|
|
26
26
|
| [`CONTRIBUTING.md`](CONTRIBUTING.md) | Flujo de contribución, convenciones e invariantes del código. |
|
|
27
|
-
| [`DeployNPM.md`](DeployNPM.md) | Manual operativo para publicar el paquete npm (comandos exactos, autenticación con OTP, verificación y errores comunes). |
|
|
28
27
|
| [`CHANGELOG.md`](CHANGELOG.md) | Historia de versiones (Keep a Changelog). |
|
|
29
28
|
| [`IDEA.md`](IDEA.md) | Especificación de origen del skill. |
|
|
30
29
|
| [`kcreate-scaffolding-gcp-script/SKILL.md`](kcreate-scaffolding-gcp-script/SKILL.md) | Instrucciones del agente, etapa por etapa (lo que ejecuta el skill instalado). |
|
|
@@ -44,42 +43,89 @@ diseño evita está en
|
|
|
44
43
|
|
|
45
44
|
## Instalación
|
|
46
45
|
|
|
47
|
-
Hay dos instaladores **intercambiables
|
|
48
|
-
resultado en el repo destino (mismos archivos, mismos
|
|
49
|
-
codes). Elige el que te acomode.
|
|
46
|
+
Hay dos instaladores **intercambiables** (`npx` y `install.sh`): dejan
|
|
47
|
+
exactamente el mismo resultado en el repo destino (mismos archivos, mismos
|
|
48
|
+
permisos, mismos exit codes). Elige el que te acomode.
|
|
50
49
|
|
|
51
50
|
En ambos casos se instala el skill (+ su dependencia
|
|
52
51
|
[`github-proyect-tracking`](https://github.com/kronops/KGithubTracking),
|
|
53
52
|
vendorizada en `vendor/`) en el repo donde se va a usar, como skill de
|
|
54
53
|
proyecto de Claude Code.
|
|
55
54
|
|
|
56
|
-
|
|
55
|
+
| Vía | Cuándo usarla | Requisitos |
|
|
56
|
+
|---|---|---|
|
|
57
|
+
| **A — `npx`** (paquete publicado en npm) | La normal: no hay que clonar este repo. | Node ≥ 18 |
|
|
58
|
+
| **A-bis — `npx` sobre un tarball** | Sin acceso al registro, o para probar una versión local antes de publicarla. | Node ≥ 18 + copia de este repo |
|
|
59
|
+
| **B — `install.sh`** | Prefieres bash puro, o trabajas con el repo ya clonado. | bash + git |
|
|
57
60
|
|
|
58
|
-
|
|
61
|
+
### Opción A — `npx` (recomendada, sin clonar el repo)
|
|
62
|
+
|
|
63
|
+
El paquete está publicado en el registro público de npm como
|
|
64
|
+
[`kcreate-scaffolding-gcp-script`](https://www.npmjs.com/package/kcreate-scaffolding-gcp-script)
|
|
65
|
+
(versión actual: `0.2.1`). Único requisito: **Node ≥ 18** (`npx` viene con
|
|
66
|
+
npm). El instalador no tiene dependencias npm; el skill en sí no usa Node:
|
|
67
|
+
es bash + Python 3.8+ stdlib.
|
|
59
68
|
|
|
60
69
|
```bash
|
|
70
|
+
# 0) (opcional) diagnóstico: no modifica nada, solo reporta el estado del destino
|
|
71
|
+
npx --yes kcreate-scaffolding-gcp-script doctor --target-repo /ruta/a/tu/repo-de-infra
|
|
72
|
+
|
|
73
|
+
# 1) instalar el skill en el repo destino
|
|
61
74
|
npx --yes kcreate-scaffolding-gcp-script --target-repo /ruta/a/tu/repo-de-infra
|
|
62
|
-
npx --yes ./kcreate-scaffolding-gcp-script-0.2.0.tgz --target-repo /ruta/a/tu/repo-de-infra
|
|
63
75
|
```
|
|
64
76
|
|
|
65
|
-
|
|
77
|
+
Salida esperada del paso 1:
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
[OK] kcreate-scaffolding-gcp-script instalado en <repo>/.claude/skills/kcreate-scaffolding-gcp-script
|
|
81
|
+
[OK] github-proyect-tracking instalado en <repo>/.claude/skills/github-proyect-tracking
|
|
82
|
+
[OK] Instalación completa en <repo>
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Notas de uso:
|
|
86
|
+
|
|
87
|
+
- `--yes` evita el prompt de npx (`Need to install the following packages`)
|
|
88
|
+
la primera vez. `npx` cachea el paquete, así que las siguientes corridas son
|
|
89
|
+
instantáneas.
|
|
90
|
+
- Para instalaciones reproducibles fija la versión:
|
|
91
|
+
`npx --yes kcreate-scaffolding-gcp-script@0.2.1 --target-repo <repo>`. Sin
|
|
92
|
+
sufijo toma `latest`.
|
|
93
|
+
- **Actualizar** = volver a correr el mismo comando con `--force`. Sin
|
|
94
|
+
`--force`, una instalación existente nunca se toca: termina con
|
|
95
|
+
`[ERROR] No se instaló nada…` y **exit 1**.
|
|
96
|
+
- También sirve instalarlo global: `npm install -g kcreate-scaffolding-gcp-script`
|
|
97
|
+
y después `kcreate-scaffolding-gcp-script --target-repo <repo>`.
|
|
98
|
+
- Ayuda y versión del instalador: `npx --yes kcreate-scaffolding-gcp-script --help`
|
|
99
|
+
y `--version` (hoy responde `kcreate-scaffolding-gcp-script/0.2.1 (instalador npx)`).
|
|
100
|
+
|
|
101
|
+
> `npx ... github:kronops/KCreateScaffoldingGCPScripts` **no** sirve: npm
|
|
102
|
+
exige el `package.json` en la raíz del repo y aquí vive en `npm/` (npm no
|
|
103
|
+
soporta subdirectorios en dependencias git). Por eso la opción `npx` usa el
|
|
104
|
+
paquete del registro o un tarball local.
|
|
105
|
+
|
|
106
|
+
### Opción A-bis — `npx` sobre un tarball local
|
|
107
|
+
|
|
108
|
+
Para probar una versión que aún no está publicada (o sin red):
|
|
66
109
|
|
|
67
110
|
```bash
|
|
68
111
|
git clone git@github-work:kronops/KCreateScaffoldingGCPScripts.git
|
|
69
112
|
cd KCreateScaffoldingGCPScripts/npm
|
|
70
113
|
npm pack # prepack empaqueta skill/ + vendor/ + README + LICENSE
|
|
71
|
-
npx --yes ./kcreate-scaffolding-gcp-script-0.2.
|
|
114
|
+
npx --yes ./kcreate-scaffolding-gcp-script-0.2.1.tgz --target-repo /ruta/a/tu/repo-de-infra
|
|
72
115
|
```
|
|
73
116
|
|
|
74
|
-
|
|
75
|
-
|
|
117
|
+
Detalle que muerde: `npx` solo acepta el tarball con ruta **relativa con
|
|
118
|
+
`./`** o como URL `file:`. Una ruta absoluta "pelada"
|
|
119
|
+
(`npx /tmp/x.tgz`) falla con `sh: Permission denied` y **exit 126**:
|
|
76
120
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
121
|
+
```bash
|
|
122
|
+
# mal
|
|
123
|
+
npx --yes /tmp/kcreate-scaffolding-gcp-script-0.2.1.tgz --target-repo /ruta/al/repo
|
|
124
|
+
# bien
|
|
125
|
+
npx --yes "file:/tmp/kcreate-scaffolding-gcp-script-0.2.1.tgz" --target-repo /ruta/al/repo
|
|
126
|
+
```
|
|
81
127
|
|
|
82
|
-
### Opción B — clonando el repo
|
|
128
|
+
### Opción B — clonando el repo (`install.sh`)
|
|
83
129
|
|
|
84
130
|
```bash
|
|
85
131
|
git clone git@github-work:kronops/KCreateScaffoldingGCPScripts.git
|
|
@@ -87,7 +133,7 @@ cd KCreateScaffoldingGCPScripts
|
|
|
87
133
|
./install.sh --target-repo /ruta/a/tu/repo-de-infra
|
|
88
134
|
```
|
|
89
135
|
|
|
90
|
-
### Resultado (idéntico en
|
|
136
|
+
### Resultado (idéntico en las tres vías)
|
|
91
137
|
|
|
92
138
|
```
|
|
93
139
|
<tu-repo>/.claude/skills/
|
|
@@ -100,6 +146,14 @@ skill asume la convención `live/<entorno>/<componente>/terragrunt.hcl`) pero
|
|
|
100
146
|
igual instala: la herramienta sirve para cualquier repo, aunque falle al
|
|
101
147
|
usarse.
|
|
102
148
|
|
|
149
|
+
### Exit codes del instalador npx
|
|
150
|
+
|
|
151
|
+
| Exit | Significado |
|
|
152
|
+
|---|---|
|
|
153
|
+
| `0` | El skill quedó instalado/actualizado en el destino. |
|
|
154
|
+
| `1` | No se instaló nada: el skill ya existía sin `--force`, el destino no existe o no es la raíz de un repo git. |
|
|
155
|
+
| `2` | Argumentos inválidos (falta `--target-repo`, flag desconocido, comando desconocido). |
|
|
156
|
+
|
|
103
157
|
### Flags
|
|
104
158
|
|
|
105
159
|
Los mismos nombres y semántica en `install.sh` y en el instalador npx:
|
|
@@ -141,15 +195,15 @@ listando los requisitos faltantes.
|
|
|
141
195
|
|
|
142
196
|
### Publicar el paquete npm (mantenedores)
|
|
143
197
|
|
|
144
|
-
El
|
|
145
|
-
[`DeployNPM.md`](DeployNPM.md). Resumen:
|
|
198
|
+
El paquete está publicado y su mantenedor es `kronops`. Procedimiento:
|
|
146
199
|
|
|
147
200
|
```bash
|
|
148
201
|
cd npm
|
|
149
|
-
npm whoami # ¿hay sesión? (
|
|
202
|
+
npm whoami # ¿hay sesión? (si da E401, hay que loguearse)
|
|
150
203
|
npm login --auth-type=legacy # usuario, password, email y, si la cuenta tiene 2FA, OTP
|
|
151
204
|
npm publish --dry-run # ensayo obligatorio: construye el tarball sin subir nada
|
|
152
205
|
npm publish # publicar (con 2FA: npm publish --otp=123456)
|
|
206
|
+
npm view kcreate-scaffolding-gcp-script version # confirmar la versión en el registro
|
|
153
207
|
```
|
|
154
208
|
|
|
155
209
|
Ojo: si la cuenta tiene 2FA, el login por navegador **no** sirve sin GUI; hay
|
|
@@ -163,7 +217,7 @@ nunca se comitean. Prueba local sin publicar:
|
|
|
163
217
|
|
|
164
218
|
```bash
|
|
165
219
|
cd npm && npm pack
|
|
166
|
-
npx --yes ./kcreate-scaffolding-gcp-script-0.2.
|
|
220
|
+
npx --yes ./kcreate-scaffolding-gcp-script-0.2.1.tgz --target-repo /ruta/al/repo
|
|
167
221
|
```
|
|
168
222
|
|
|
169
223
|
Para subir una versión nueva: `node -p "require('./package.json').version"`,
|
|
@@ -273,8 +327,9 @@ $S/02_print_config.sh xoloshop-prod # revisar antes de construir
|
|
|
273
327
|
|
|
274
328
|
$S/10_build_components.sh xoloshop-prod # construcción
|
|
275
329
|
|
|
276
|
-
$S/20_validate_file_parity.sh
|
|
277
|
-
$S/21_validate_project_pinning.sh
|
|
330
|
+
$S/20_validate_file_parity.sh boletera-qa xoloshop-prod
|
|
331
|
+
$S/21_validate_project_pinning.sh xoloshop-prod
|
|
332
|
+
$S/22_validate_no_app_name_leftover.sh xoloshop-prod
|
|
278
333
|
```
|
|
279
334
|
|
|
280
335
|
Cada script termina con un mensaje de una sola línea y un código de salida
|
|
@@ -292,6 +347,7 @@ acorde — pensado para scripting, no solo para lectura humana:
|
|
|
292
347
|
| `10_build_components.sh` | `BUILD_OK: <ruta>` | — | `BUILD_FAILED: <paso> - <detalle>` | — |
|
|
293
348
|
| `20_validate_file_parity.sh` | `FILE_PARITY_OK: <n>` | — | `FILE_PARITY_MISMATCH` | — |
|
|
294
349
|
| `21_validate_project_pinning.sh` | `PROJECT_PINNING_OK: <n>` | — | `PROJECT_PINNING_FAILED` | — |
|
|
350
|
+
| `22_validate_no_app_name_leftover.sh` | `APP_NAME_LEFTOVER_OK` | — | `APP_NAME_LEFTOVER_FOUND: <n>` | — |
|
|
295
351
|
| `run_tracking.sh` | comando OK | — | error de tracking | tracking no instalado |
|
|
296
352
|
|
|
297
353
|
`2` no es un error: es la señal de "faltan datos, llénalos y vuelve a correr
|
|
@@ -302,7 +358,6 @@ el mismo comando".
|
|
|
302
358
|
```
|
|
303
359
|
KCreateScaffoldingGCPScripts/
|
|
304
360
|
├── README.md / AGENT.md / CONTRIBUTING.md / CHANGELOG.md
|
|
305
|
-
├── DeployNPM.md # manual para publicar el paquete en npm
|
|
306
361
|
├── IDEA.md # especificación de origen del skill
|
|
307
362
|
├── CreateScriptsNewGCPProjectInstructions.md # guía manual de referencia (base de experiencia)
|
|
308
363
|
├── LICENSE # MIT
|
|
@@ -324,8 +379,9 @@ KCreateScaffoldingGCPScripts/
|
|
|
324
379
|
├── 01_init_config.sh # Pre requisitos, paso 1
|
|
325
380
|
├── 02_print_config.sh # Pre requisitos, paso 2
|
|
326
381
|
├── 10_build_components.sh # Construcción
|
|
327
|
-
├── 20_validate_file_parity.sh
|
|
328
|
-
├── 21_validate_project_pinning.sh
|
|
382
|
+
├── 20_validate_file_parity.sh # Validación 1
|
|
383
|
+
├── 21_validate_project_pinning.sh # Validación 2
|
|
384
|
+
├── 22_validate_no_app_name_leftover.sh # Validación 3
|
|
329
385
|
└── run_tracking.sh # envoltura sobre github-proyect-tracking
|
|
330
386
|
```
|
|
331
387
|
|
|
@@ -341,10 +397,13 @@ KCreateScaffoldingGCPScripts/
|
|
|
341
397
|
- El instalador npx **no** puede consumirse como
|
|
342
398
|
`npx github:kronops/KCreateScaffoldingGCPScripts`: npm exige el
|
|
343
399
|
`package.json` en la raíz del repo y aquí vive en `npm/` (no soporta
|
|
344
|
-
subdirectorios en dependencias git).
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
400
|
+
subdirectorios en dependencias git). Por eso se publica en el registro y
|
|
401
|
+
se instala como `npx kcreate-scaffolding-gcp-script`, o vía tarball local
|
|
402
|
+
(`npm pack`).
|
|
403
|
+
- El paquete del registro se actualiza solo cuando alguien corre
|
|
404
|
+
`npm publish` a mano: un cambio en este repo no llega a los usuarios de
|
|
405
|
+
`npx` hasta que sube la versión (y una versión publicada no se reutiliza,
|
|
406
|
+
`E409`).
|
|
348
407
|
- El nombre del proyecto (`PROJECT_NAME`, ej. `Xoloshop-Prod`) se infiere de
|
|
349
408
|
`NEW_ENV` con la convención `<App>-<QA|Prod>`; si la app tiene siglas o una
|
|
350
409
|
capitalización no obvia, corrígelo a mano en el config antes de construir.
|
package/bin/cli.js
CHANGED
|
File without changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kcreate-scaffolding-gcp-script",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Instalador npx del skill kcreate-scaffolding-gcp-script: genera el scaffolding de Terragrunt para un entorno GCP nuevo a partir de uno existente.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"kcreate-scaffolding-gcp-script": "bin/cli.js"
|
package/skill/SKILL.md
CHANGED
|
@@ -182,12 +182,13 @@ el resultado final; no las descartes en silencio.
|
|
|
182
182
|
|
|
183
183
|
## Etapa 3 — Validación
|
|
184
184
|
|
|
185
|
-
|
|
186
|
-
el resultado:
|
|
185
|
+
Tres scripts, cualquier orden, los tres obligatorios antes de dar por
|
|
186
|
+
bueno el resultado:
|
|
187
187
|
|
|
188
188
|
```bash
|
|
189
189
|
.claude/skills/kcreate-scaffolding-gcp-script/scripts/20_validate_file_parity.sh <SOURCE_ENV> <NEW_ENV>
|
|
190
190
|
.claude/skills/kcreate-scaffolding-gcp-script/scripts/21_validate_project_pinning.sh <NEW_ENV>
|
|
191
|
+
.claude/skills/kcreate-scaffolding-gcp-script/scripts/22_validate_no_app_name_leftover.sh <NEW_ENV>
|
|
191
192
|
```
|
|
192
193
|
|
|
193
194
|
- `20_validate_file_parity.sh` → `FILE_PARITY_OK` (exit 0) o
|
|
@@ -199,17 +200,32 @@ el resultado:
|
|
|
199
200
|
problema). Confirma que, sin importar qué proyecto tenga `gcloud` como
|
|
200
201
|
default en ese momento, cada componente de `NEW_ENV` resuelve siempre al
|
|
201
202
|
proyecto correcto.
|
|
202
|
-
|
|
203
|
-
|
|
203
|
+
- `22_validate_no_app_name_leftover.sh` → `APP_NAME_LEFTOVER_OK` (exit 0) o
|
|
204
|
+
`APP_NAME_LEFTOVER_FOUND` (exit 1, con archivo y línea exactos). Busca el
|
|
205
|
+
nombre corto de la app de `SOURCE_ENV` (sin el sufijo `-qa`/`-prod`/etc.),
|
|
206
|
+
en cualquier capitalización, en cualquier archivo de `live/<NEW_ENV>/`.
|
|
207
|
+
Complementa a `21`: un nombre de marca estilizado (ej. "XoloShop", con
|
|
208
|
+
una mayúscula interna) no es substring de "xoloshop-qa" ni de
|
|
209
|
+
"Xoloshop-QA", así que `21` no lo detecta — pero sí es un leftover real
|
|
210
|
+
del entorno fuente que no debe quedar en el nuevo.
|
|
211
|
+
|
|
212
|
+
SI cualquiera de los tres falla → `DETENTE`. No es un error de Terragrunt
|
|
204
213
|
ni de GCP — es un defecto de generación real, y el mensaje ya dice
|
|
205
214
|
exactamente qué componente/archivo revisar.
|
|
206
215
|
|
|
207
|
-
SI
|
|
216
|
+
SI los tres pasan y el tracking SÍ se inicializó en la Etapa 2 (no viste el
|
|
208
217
|
`[WARN]` de "no se pudo inicializar github-proyect-tracking") → marca el
|
|
209
|
-
proceso como terminado
|
|
218
|
+
proceso como terminado. Antes de llamar a `finalize`, construye el mismo
|
|
219
|
+
resumen de ejecución que vas a mostrarle al humano (paso 1 de la lista de
|
|
220
|
+
abajo) y escríbelo en un archivo de texto plano — ese archivo se publica
|
|
221
|
+
como un comentario aparte en el Issue, después del de FINALIZADO y antes
|
|
222
|
+
de cerrarlo, para que quien lea el Issue no tenga que pedírtelo por fuera:
|
|
210
223
|
|
|
211
224
|
```bash
|
|
212
|
-
|
|
225
|
+
cat > /tmp/kcreate-resumen-<NEW_ENV>.txt <<'EOF'
|
|
226
|
+
<el mismo resumen de ejecución del paso 1 de abajo>
|
|
227
|
+
EOF
|
|
228
|
+
.claude/skills/kcreate-scaffolding-gcp-script/scripts/run_tracking.sh finalize "$(ls -d .kcreate-scaffolding/<NEW_ENV>.config)" /tmp/kcreate-resumen-<NEW_ENV>.txt
|
|
213
229
|
```
|
|
214
230
|
|
|
215
231
|
SI el tracking nunca se inicializó (modo best-effort sin éxito, ver
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Lecciones reales incorporadas en este skill
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Seis fuentes:
|
|
4
4
|
|
|
5
5
|
- **A — descubiertas desplegando `xoloshop-qa` a mano** (sesión previa,
|
|
6
6
|
documentadas también en `docs/CreateScriptsNewGCPProjectInstructions.md`
|
|
@@ -25,6 +25,10 @@ Cinco fuentes:
|
|
|
25
25
|
propio fix de `artifact_registry` (una función nueva de esa misma
|
|
26
26
|
ronda) se había quedado con la misma contaminación que D1/D2 ya habían
|
|
27
27
|
corregido en las demás. Ver `Review_V3.md`.
|
|
28
|
+
- **F — encontradas ejecutando el skill de punta a punta contra el repo
|
|
29
|
+
real `club-tijuana/infra-cloud`** (no una copia descartable), generando
|
|
30
|
+
`coreservices-qa` desde `xoloshop-qa`, con Issue de tracking real
|
|
31
|
+
creado y cerrado.
|
|
28
32
|
|
|
29
33
|
Cada lección dice **qué pasaba**, **por qué**, y **en qué script/función
|
|
30
34
|
de este skill queda resuelta**.
|
|
@@ -611,6 +615,85 @@ excluye" pase sin ninguna pista de la causa.
|
|
|
611
615
|
|
|
612
616
|
---
|
|
613
617
|
|
|
618
|
+
## F — encontradas ejecutando el skill de punta a punta contra `infra-cloud` real
|
|
619
|
+
|
|
620
|
+
A diferencia de B-E (copias descartables), esta corrida fue real: generó
|
|
621
|
+
`coreservices-qa` desde `xoloshop-qa` en `club-tijuana/infra-cloud`, con
|
|
622
|
+
Issue de tracking real (#332) creado y cerrado. El usuario revisó el
|
|
623
|
+
resultado campo por campo y reportó F1 directamente.
|
|
624
|
+
|
|
625
|
+
### F1 — "XoloShop" (marca estilizada) sobrevivía la sustitución, sin ser detectado
|
|
626
|
+
|
|
627
|
+
**Qué pasaba:** `service_accounts/terragrunt.hcl`, generado desde
|
|
628
|
+
`xoloshop-qa`, dejó `display_name`/`description` de dos service accounts
|
|
629
|
+
con "XoloShop" (S interna mayúscula) sin tocar — debían decir
|
|
630
|
+
"Coreservices QA ...". `ksc_copy_component_files` solo hacía dos pasadas
|
|
631
|
+
de sustitución EXACTAS: `ksc_substr_replace` para `"xoloshop"` (minúsculas)
|
|
632
|
+
y una para `ksc_capitalize("xoloshop")` = `"Xoloshop"` (solo la primera
|
|
633
|
+
letra). Ninguna de las dos es substring de `"XoloShop"` (mayúscula
|
|
634
|
+
*interna*), así que esa capitalización en particular pasaba intacta.
|
|
635
|
+
Tampoco `21_validate_project_pinning.sh` lo atrapaba: busca el token
|
|
636
|
+
completo `"<app>-<entorno>"` exacto, y `"XoloShop"` no es substring de
|
|
637
|
+
`"xoloshop-qa"` ni de `"Xoloshop-QA"`.
|
|
638
|
+
**Resuelto en:**
|
|
639
|
+
- `lib/common.sh:ksc_substr_replace_app` — sustitución case-*insensitive*
|
|
640
|
+
(vía `re.IGNORECASE` en Python) con reemplazo case-*aware*: si el match
|
|
641
|
+
original era todo mayúsculas usa `APP_NEW` en mayúsculas, si era todo
|
|
642
|
+
minúsculas usa minúsculas, cualquier otro caso (incluida la "XoloShop"
|
|
643
|
+
original) usa capitalización simple (`Coreservices`). Así cualquier
|
|
644
|
+
variante de capitalización del nombre de marca queda cubierta, no solo
|
|
645
|
+
las dos que `ksc_capitalize` sabía producir.
|
|
646
|
+
- `10_build_components.sh:ksc_copy_component_files` — reemplaza las dos
|
|
647
|
+
pasadas exactas por una llamada a `ksc_substr_replace_app` (solo si
|
|
648
|
+
`APP_SOURCE != APP_NEW`, evitando trabajo innecesario cuando el nombre
|
|
649
|
+
de app no cambia entre entornos).
|
|
650
|
+
- `build_component_artifact_registry`'s detección de "ya es genérico" —
|
|
651
|
+
cambiada a `grep -qi` por la misma razón (consistencia: no tiene sentido
|
|
652
|
+
que la detección sea case-sensitive si la sustitución ya no lo es).
|
|
653
|
+
**Decisión explícita de NO tocar:** el check de `repository_id` en
|
|
654
|
+
`ksc_learn_artifact_registry_template` sigue siendo exacto/minúsculas —
|
|
655
|
+
GCP exige minúsculas en ese campo, así que ahí la sensibilidad a mayúsculas
|
|
656
|
+
es correcta, no un bug.
|
|
657
|
+
- **Nueva validación:** `22_validate_no_app_name_leftover.sh` — un script
|
|
658
|
+
de Etapa 3 dedicado, que busca `APP_SOURCE` (cualquier capitalización)
|
|
659
|
+
en TODO archivo de `live/<NEW_ENV>/`, fuera de comentarios. Complementa
|
|
660
|
+
(no reemplaza) a `21`: ese sigue validando el token completo exacto,
|
|
661
|
+
este cubre justo el caso que `21` no puede ver por diseño. Probado
|
|
662
|
+
contra una copia descartable: pasa limpio en un build correcto, y
|
|
663
|
+
detecta correctamente un leftover reintroducido a mano, reportando
|
|
664
|
+
archivo y línea exactos.
|
|
665
|
+
|
|
666
|
+
### F2 — El tracking no decía qué skill lo estaba usando ni con qué parámetros
|
|
667
|
+
|
|
668
|
+
**Qué pasaba:** el Issue de GitHub que abre `github-proyect-tracking` no
|
|
669
|
+
tenía forma de decir, por sí solo, que lo estaba usando
|
|
670
|
+
`/kcreate-scaffolding-gcp-script`, ni con qué `SOURCE_ENV`/`NEW_ENV` —
|
|
671
|
+
alguien leyendo el Issue sin contexto previo no podía saber qué se estaba
|
|
672
|
+
construyendo sin preguntar aparte.
|
|
673
|
+
**Resuelto:** `github-proyect-tracking` 0.7.0+ agrega `--process-name`/
|
|
674
|
+
`--process-inputs` a `init` (ver `vendor/VENDORED_FROM.md`); este skill
|
|
675
|
+
los manda siempre, automáticamente, desde `run_tracking.sh init-plan`
|
|
676
|
+
(`--process-name "/kcreate-scaffolding-gcp-script"` y
|
|
677
|
+
`--process-inputs '{"SOURCE_ENV":...,"NEW_ENV":...}'`), sin que el agente
|
|
678
|
+
tenga que acordarse de pasarlos.
|
|
679
|
+
|
|
680
|
+
### F3 — El resumen final solo se veía en pantalla, no quedaba en el Issue
|
|
681
|
+
|
|
682
|
+
**Qué pasaba:** al terminar, el agente comentaba "FINALIZADO" en el Issue
|
|
683
|
+
y lo cerraba, pero el resumen de ejecución (lo que sí le mostraba al
|
|
684
|
+
humano en pantalla) nunca quedaba escrito en el Issue — quien lo leyera
|
|
685
|
+
después no tenía ese resumen, solo el estado final.
|
|
686
|
+
**Resuelto:** `github-proyect-tracking` 0.7.0 agrega `finalize
|
|
687
|
+
--summary`/`--summary-file`, que publica un comentario aparte con el
|
|
688
|
+
resumen DESPUÉS del de FINALIZADO y ANTES de cerrar el Issue (orden
|
|
689
|
+
verificado con pruebas unitarias dedicadas, incluyendo que la ruta
|
|
690
|
+
`--force`/CANCELADO nunca publica resumen). `run_tracking.sh finalize`
|
|
691
|
+
acepta ahora un tercer argumento opcional (ruta a un archivo con el
|
|
692
|
+
resumen); `SKILL.md` (Etapa 3) ya le indica al agente que construya ese
|
|
693
|
+
resumen ANTES de llamar a `finalize` y lo pase como archivo.
|
|
694
|
+
|
|
695
|
+
---
|
|
696
|
+
|
|
614
697
|
## Qué significa esta lista para quien edite este skill después
|
|
615
698
|
|
|
616
699
|
Cada vez que se agregue un `build_component_<algo>` nuevo o se modifique
|
|
@@ -49,7 +49,6 @@ NEW_DIR="$REPO_ROOT/live/$NEW_ENV"
|
|
|
49
49
|
|
|
50
50
|
read -r APP_SOURCE ENTORNO_SOURCE < <(ksc_split_env "$SOURCE_ENV")
|
|
51
51
|
read -r APP_NEW ENTORNO_NEW < <(ksc_split_env "$NEW_ENV")
|
|
52
|
-
APP_SOURCE_CAP="$(ksc_capitalize "$APP_SOURCE")"
|
|
53
52
|
APP_NEW_CAP="$(ksc_capitalize "$APP_NEW")"
|
|
54
53
|
|
|
55
54
|
CIDR_FILE="$(ksc_cidr_file_path "$REPO_ROOT" "$NEW_ENV")"
|
|
@@ -169,17 +168,25 @@ ksc_copy_component_files() {
|
|
|
169
168
|
[[ -f "$SOURCE_DIR/$comp/$f" ]] && cp "$SOURCE_DIR/$comp/$f" "$NEW_DIR/$comp/$f"
|
|
170
169
|
done
|
|
171
170
|
# Sustitución estándar aplicada a TODO componente, conocido o no: el
|
|
172
|
-
# token completo del entorno, y el nombre corto de la app
|
|
173
|
-
#
|
|
174
|
-
# componentes — Review V1 (E-5) encontró texto real
|
|
175
|
-
# filtrado en service_accounts (display_name) y
|
|
176
|
-
# de un principal) porque esos dos nunca
|
|
177
|
-
# nombre de app, solo la del token
|
|
171
|
+
# token completo del entorno, y el nombre corto de la app en CUALQUIER
|
|
172
|
+
# capitalización (ver ksc_substr_replace_app). Antes esto solo se hacía
|
|
173
|
+
# a mano en algunos componentes — Review V1 (E-5) encontró texto real
|
|
174
|
+
# de SOURCE_ENV filtrado en service_accounts (display_name) y
|
|
175
|
+
# project_iam (namespace de un principal) porque esos dos nunca
|
|
176
|
+
# aplicaban la sustitución de nombre de app, solo la del token
|
|
177
|
+
# completo. Corregido ahí; después, un bug real encontrado usando el
|
|
178
|
+
# skill (no en revisión) mostró que la sustitución de nombre de app
|
|
179
|
+
# EXISTÍA pero solo cubría dos capitalizaciones exactas (minúscula y
|
|
180
|
+
# "Primera-mayúscula") — un nombre de marca estilizado como "XoloShop"
|
|
181
|
+
# (S interna mayúscula) no hacía match con ninguna de las dos y se
|
|
182
|
+
# colaba completo a service_accounts.display_name/description. Ver
|
|
183
|
+
# lessons-learned.md, sección F.
|
|
178
184
|
f="$NEW_DIR/$comp/terragrunt.hcl"
|
|
179
185
|
[[ -f "$f" ]] || return 0
|
|
180
186
|
ksc_substr_replace "$f" "$SOURCE_ENV" "$NEW_ENV"
|
|
181
|
-
[[ "$APP_SOURCE" != "$APP_NEW" ]]
|
|
182
|
-
|
|
187
|
+
if [[ "$APP_SOURCE" != "$APP_NEW" ]]; then
|
|
188
|
+
ksc_substr_replace_app "$f" "$APP_SOURCE" "$APP_NEW"
|
|
189
|
+
fi
|
|
183
190
|
}
|
|
184
191
|
|
|
185
192
|
# ---------------------------------------------------------------------------
|
|
@@ -429,7 +436,12 @@ build_component_artifact_registry() {
|
|
|
429
436
|
# era genérico (sin el nombre de la app), la sustitución automática no
|
|
430
437
|
# tiene nada que reemplazar y el resultado seguiría siendo genérico.
|
|
431
438
|
local was_generic=1
|
|
432
|
-
|
|
439
|
+
# -i: la app puede aparecer con cualquier capitalización (ver
|
|
440
|
+
# ksc_substr_replace_app) — un chequeo exacto-minúscula aquí marcaría
|
|
441
|
+
# "genérico" un archivo que en realidad sí tiene el nombre de la app
|
|
442
|
+
# (solo que estilizado, ej. "XoloShop"), y la sustitución automática de
|
|
443
|
+
# ksc_copy_component_files ya lo habría corregido de todas formas.
|
|
444
|
+
grep -qi "$APP_SOURCE" "$SOURCE_DIR/artifact_registry/terragrunt.hcl" && was_generic=0
|
|
433
445
|
|
|
434
446
|
ksc_copy_component_files artifact_registry
|
|
435
447
|
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Etapa: Validación — script 3.
|
|
3
|
+
# Busca, en TODO archivo de texto de live/<NEW_ENV>/, el nombre corto de
|
|
4
|
+
# la app de SOURCE_ENV (sin el sufijo de entorno -qa/-prod/-dev/etc.),
|
|
5
|
+
# en CUALQUIER capitalización. Complementa a 21_validate_project_pinning.sh
|
|
6
|
+
# (que solo busca el token completo "<app>-<entorno>", exacto): un nombre
|
|
7
|
+
# de marca estilizado como "XoloShop" (con una S interna mayúscula) no es
|
|
8
|
+
# "xoloshop-qa" ni "Xoloshop-QA" como substring, así que 21 nunca lo
|
|
9
|
+
# atrapa — y la sustitución de ksc_copy_component_files, si tiene un bug,
|
|
10
|
+
# tampoco. Caso real: service_accounts generado desde xoloshop-qa dejó
|
|
11
|
+
# "XoloShop" sin tocar en dos display_name/description, porque la
|
|
12
|
+
# sustitución de entonces solo cubría "xoloshop" y "Xoloshop" exactos.
|
|
13
|
+
#
|
|
14
|
+
# Uso: 22_validate_no_app_name_leftover.sh <NEW_ENV>
|
|
15
|
+
#
|
|
16
|
+
# Termina con exactamente uno de estos mensajes:
|
|
17
|
+
# "APP_NAME_LEFTOVER_OK: sin referencias a '<APP_SOURCE>'" (exit 0)
|
|
18
|
+
# "APP_NAME_LEFTOVER_FOUND: <n> archivo(s) (ver detalle arriba)" (exit 1)
|
|
19
|
+
|
|
20
|
+
set -uo pipefail
|
|
21
|
+
|
|
22
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
23
|
+
# shellcheck source=lib/common.sh
|
|
24
|
+
source "$SCRIPT_DIR/lib/common.sh"
|
|
25
|
+
|
|
26
|
+
NEW_ENV="${1:-}"
|
|
27
|
+
[[ -n "$NEW_ENV" ]] || ksc_die "uso: 22_validate_no_app_name_leftover.sh <NEW_ENV>"
|
|
28
|
+
|
|
29
|
+
REPO_ROOT="$(ksc_repo_root)"
|
|
30
|
+
NEW_DIR="$REPO_ROOT/live/$NEW_ENV"
|
|
31
|
+
[[ -d "$NEW_DIR" ]] || ksc_die "no existe $NEW_DIR"
|
|
32
|
+
|
|
33
|
+
CONFIG_FILE="$(ksc_config_path "$REPO_ROOT" "$NEW_ENV")"
|
|
34
|
+
SOURCE_ENV=""
|
|
35
|
+
[[ -f "$CONFIG_FILE" ]] && SOURCE_ENV="$(ksc_hcl_get "$CONFIG_FILE" SOURCE_ENV 2>/dev/null || true)"
|
|
36
|
+
[[ -n "$SOURCE_ENV" ]] || SOURCE_ENV="$(grep -m1 '^SOURCE_ENV=' "$CONFIG_FILE" 2>/dev/null | sed -E 's/^SOURCE_ENV="(.*)"$/\1/')"
|
|
37
|
+
[[ -n "$SOURCE_ENV" ]] || ksc_die "no pude determinar SOURCE_ENV (no está en $CONFIG_FILE) — no se puede validar sin saber qué nombre de app buscar"
|
|
38
|
+
|
|
39
|
+
read -r APP_SOURCE _ < <(ksc_split_env "$SOURCE_ENV")
|
|
40
|
+
|
|
41
|
+
FAILURES=()
|
|
42
|
+
|
|
43
|
+
# Mismo criterio que 21 (comentarios excluidos: la NOTA de project_iam
|
|
44
|
+
# menciona SOURCE_ENV completo a propósito, lo que incluye APP_SOURCE
|
|
45
|
+
# como substring) y misma cobertura de archivos (find, no una lista fija
|
|
46
|
+
# de nombres — Review V2, N-3).
|
|
47
|
+
while IFS= read -r -d '' file; do
|
|
48
|
+
hit="$(grep -v '^[[:space:]]*#' "$file" 2>/dev/null | grep -i -m1 -- "$APP_SOURCE" | sed -E 's/^[[:space:]]+//')"
|
|
49
|
+
if [[ -n "$hit" ]]; then
|
|
50
|
+
FAILURES+=("$file: \"$hit\"")
|
|
51
|
+
fi
|
|
52
|
+
done < <(find "$NEW_DIR" -maxdepth 2 -type f -print0)
|
|
53
|
+
|
|
54
|
+
if [[ "${#FAILURES[@]}" -eq 0 ]]; then
|
|
55
|
+
echo "APP_NAME_LEFTOVER_OK: sin referencias a '$APP_SOURCE' (ninguna capitalización) en $NEW_DIR"
|
|
56
|
+
exit 0
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
echo "Encontrado el nombre de la app fuente ('$APP_SOURCE', cualquier capitalización) fuera de comentarios:"
|
|
60
|
+
printf ' - %s\n' "${FAILURES[@]}"
|
|
61
|
+
echo "APP_NAME_LEFTOVER_FOUND: ${#FAILURES[@]} archivo(s) (ver detalle arriba) — corrige el shell script que genera ese componente para que sustituya '$APP_SOURCE' por el nombre de la app nueva, en cualquier capitalización (ver ksc_substr_replace_app en lib/common.sh)."
|
|
62
|
+
exit 1
|
|
@@ -745,3 +745,44 @@ ksc_substr_replace() {
|
|
|
745
745
|
local file="$1" old="$2" new="$3"
|
|
746
746
|
sed -i "s/${old}/${new}/g" "$file"
|
|
747
747
|
}
|
|
748
|
+
|
|
749
|
+
# ksc_substr_replace_app <file> <app_source_lower> <app_new_lower>
|
|
750
|
+
#
|
|
751
|
+
# Reemplaza TODAS las variantes de capitalización del nombre corto de la
|
|
752
|
+
# app fuente (no solo minúscula exacta ni "Primera-mayúscula" exacta) por
|
|
753
|
+
# la variante equivalente del nombre nuevo. Caso real encontrado
|
|
754
|
+
# construyendo `coreservices-qa` desde `xoloshop-qa`: `service_accounts`
|
|
755
|
+
# usa el nombre de marca estilizado `"XoloShop"` (con S interna
|
|
756
|
+
# mayúscula) en `display_name`/`description` — ni
|
|
757
|
+
# `ksc_substr_replace "$APP_SOURCE"` (minúscula exacta) ni
|
|
758
|
+
# `ksc_substr_replace "$(ksc_capitalize "$APP_SOURCE")"` ("Xoloshop",
|
|
759
|
+
# solo la primera letra) hacen match contra "XoloShop", así que ese
|
|
760
|
+
# leftover pasaba completo a `coreservices-qa` en silencio. Resuelto
|
|
761
|
+
# haciendo el match insensible a mayúsculas/minúsculas y eligiendo la
|
|
762
|
+
# capitalización del REEMPLAZO según cómo estaba escrita la coincidencia
|
|
763
|
+
# encontrada (todo minúsculas -> nuevo en minúsculas; todo mayúsculas ->
|
|
764
|
+
# nuevo en mayúsculas; cualquier otra mezcla, incluidos estilos de marca
|
|
765
|
+
# como "XoloShop" -> primera letra mayúscula del nuevo, mejor esfuerzo).
|
|
766
|
+
ksc_substr_replace_app() {
|
|
767
|
+
local file="$1" app_source="$2" app_new="$3"
|
|
768
|
+
[[ -f "$file" ]] || return 0
|
|
769
|
+
python3 - "$file" "$app_source" "$app_new" <<'PYEOF'
|
|
770
|
+
import re, sys
|
|
771
|
+
file, app_source, app_new = sys.argv[1], sys.argv[2], sys.argv[3]
|
|
772
|
+
with open(file) as f:
|
|
773
|
+
text = f.read()
|
|
774
|
+
pattern = re.compile(re.escape(app_source), re.IGNORECASE)
|
|
775
|
+
|
|
776
|
+
def repl(m):
|
|
777
|
+
s = m.group(0)
|
|
778
|
+
if s.isupper() and len(s) > 1:
|
|
779
|
+
return app_new.upper()
|
|
780
|
+
if s.islower():
|
|
781
|
+
return app_new.lower()
|
|
782
|
+
return app_new[:1].upper() + app_new[1:].lower()
|
|
783
|
+
|
|
784
|
+
new_text = pattern.sub(repl, text)
|
|
785
|
+
with open(file, "w") as f:
|
|
786
|
+
f.write(new_text)
|
|
787
|
+
PYEOF
|
|
788
|
+
}
|
|
@@ -10,7 +10,19 @@
|
|
|
10
10
|
# run_tracking.sh step-done <config_file> "<nombre del paso>"
|
|
11
11
|
# run_tracking.sh step-error <config_file> "<nombre del paso>" "<detalle>"
|
|
12
12
|
# run_tracking.sh status <config_file>
|
|
13
|
-
# run_tracking.sh finalize <config_file>
|
|
13
|
+
# run_tracking.sh finalize <config_file> [archivo_de_resumen]
|
|
14
|
+
#
|
|
15
|
+
# `init-plan` manda siempre --process-name "/kcreate-scaffolding-gcp-script"
|
|
16
|
+
# y --process-inputs con SOURCE_ENV/NEW_ENV (requiere github-proyect-tracking
|
|
17
|
+
# >= 0.5.0/0.6.0 — ver vendor/VENDORED_FROM.md) — así el Issue de tracking
|
|
18
|
+
# dice de entrada QUÉ skill lo está usando y CON QUÉ parámetros, sin que el
|
|
19
|
+
# humano tenga que adivinarlo leyendo los comentarios de los pasos.
|
|
20
|
+
#
|
|
21
|
+
# `finalize` acepta opcionalmente la ruta a un archivo de texto con el
|
|
22
|
+
# resumen final de la ejecución (requiere github-proyect-tracking >= 0.7.0):
|
|
23
|
+
# si se da, ese resumen queda como un comentario APARTE en el Issue,
|
|
24
|
+
# después del de FINALIZADO y antes de cerrarlo — el mismo resumen que el
|
|
25
|
+
# agente ya le muestra al humano en pantalla, también en el Issue.
|
|
14
26
|
#
|
|
15
27
|
# Si github-proyect-tracking no está instalado junto a este skill, todos los
|
|
16
28
|
# subcomandos salen con error claro (código 3) en vez de fallar a medias.
|
|
@@ -42,7 +54,10 @@ case "$SUBCOMMAND" in
|
|
|
42
54
|
init-plan)
|
|
43
55
|
PLAN_FILE="${3:-}"
|
|
44
56
|
[[ -n "$PLAN_FILE" && -f "$PLAN_FILE" ]] || ksc_die "init-plan requiere un archivo plan.json existente"
|
|
45
|
-
|
|
57
|
+
PROCESS_INPUTS="$(python3 -c 'import json,sys; print(json.dumps({"SOURCE_ENV": sys.argv[1], "NEW_ENV": sys.argv[2]}))' "$SOURCE_ENV" "$NEW_ENV")"
|
|
58
|
+
TP init --plan-file "$PLAN_FILE" \
|
|
59
|
+
--process-name "/kcreate-scaffolding-gcp-script" \
|
|
60
|
+
--process-inputs "$PROCESS_INPUTS"
|
|
46
61
|
;;
|
|
47
62
|
resume)
|
|
48
63
|
TP resume
|
|
@@ -64,7 +79,13 @@ case "$SUBCOMMAND" in
|
|
|
64
79
|
TP status --remote
|
|
65
80
|
;;
|
|
66
81
|
finalize)
|
|
67
|
-
|
|
82
|
+
SUMMARY_FILE="${3:-}"
|
|
83
|
+
if [[ -n "$SUMMARY_FILE" ]]; then
|
|
84
|
+
[[ -f "$SUMMARY_FILE" ]] || ksc_die "no existe el archivo de resumen: $SUMMARY_FILE"
|
|
85
|
+
TP finalize --summary-file "$SUMMARY_FILE"
|
|
86
|
+
else
|
|
87
|
+
TP finalize
|
|
88
|
+
fi
|
|
68
89
|
;;
|
|
69
90
|
*)
|
|
70
91
|
ksc_die "subcomando desconocido: $SUBCOMMAND"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: github-proyect-tracking
|
|
3
3
|
description: Track process execution state in GitHub Issues with resume.
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.7.0
|
|
5
5
|
author: Rommel (rommelmg), Hermes Agent
|
|
6
6
|
license: MIT
|
|
7
7
|
platforms: [linux, macos, windows]
|
|
@@ -69,7 +69,7 @@ python3 "$SKILL" doctor --url https://github.com/OWNER/REPO --token "$GITHUB_TOK
|
|
|
69
69
|
|
|
70
70
|
| Comando | Efecto |
|
|
71
71
|
|---|---|
|
|
72
|
-
| `init --url URL [--plan JSON] [--store-token]` | Valida URL+token, crea el Issue, guarda `active_process_id` |
|
|
72
|
+
| `init --url URL [--plan JSON] [--store-token] [--process-name N]` | Valida URL+token, crea el Issue (título `[Process Tracking][N] Execution ID: …` si se da N), guarda `active_process_id` y `process_name` |
|
|
73
73
|
| `resume [--url URL]` | Lee el ÚLTIMO comentario y dicta cómo continuar |
|
|
74
74
|
| `step-start --name N` | Comenta `[STATUS: INICIANDO] - Paso: N` (valida orden del plan) |
|
|
75
75
|
| `step-done --name N` | Comenta `[STATUS: COMPLETADO] - Paso: N` (exige paso en vuelo) |
|
|
@@ -95,9 +95,18 @@ exit 0) y `KeyboardInterrupt` (130).
|
|
|
95
95
|
lectura hace fallar `issues-write`; `UNVERIFIED` se reserva para permisos
|
|
96
96
|
granulares que la API no puede confirmar.
|
|
97
97
|
2. **Init.** `init --url URL --plan '["Paso 1: ...", ...]'` (plan
|
|
98
|
-
recomendado: nombres ÚNICOS; hace la reanudación determinista).
|
|
99
|
-
|
|
100
|
-
|
|
98
|
+
recomendado: nombres ÚNICOS; hace la reanudación determinista).
|
|
99
|
+
Si el proceso pertenece a un skill padre, añade
|
|
100
|
+
`--process-name "Mi Skill"`: el título del Issue pasa a
|
|
101
|
+
`[Process Tracking][Mi Skill] Execution ID: …` y el cuerpo incluye
|
|
102
|
+
`- **Proceso:** Mi Skill` — así el Issue identifica QUÉ se rastrea.
|
|
103
|
+
Para dejar constancia de CON QUÉ se ejecutó, añade `--process-inputs
|
|
104
|
+
'{"archivo":"x.csv","modo":"lint"}'` (objeto clave→valor; se publica
|
|
105
|
+
en la sección "Parámetros de entrada" del Issue, con secretos
|
|
106
|
+
redactados).
|
|
107
|
+
Si devuelve `RESUME_REQUIRED` (exit 7), NO crees otro Issue: ve al
|
|
108
|
+
paso 3. Criterio: `execution_id`, `process_name` e `issue_url` en la
|
|
109
|
+
salida.
|
|
101
110
|
3. **Reanudar (si aplica).** `resume` y sigue el `verdict.action`
|
|
102
111
|
(`RERUN_STEP`, `NEXT_STEP`, `START_FIRST`, `ALL_STEPS_DONE`/
|
|
103
112
|
`FINISH_CLEANUP` → finalize, `ABORTED_EXTERNALLY` → pregunta al
|
|
@@ -42,7 +42,7 @@ texto y los usa para reconciliar únicamente el intento que originó el POST.
|
|
|
42
42
|
|
|
43
43
|
## Comandos
|
|
44
44
|
|
|
45
|
-
### `init --url URL [--plan JSON | --plan-file F] [--token T] [--prompt-token] [--store-token]`
|
|
45
|
+
### `init --url URL [--plan JSON | --plan-file F] [--token T] [--prompt-token] [--store-token] [--process-name N] [--process-inputs JSON]`
|
|
46
46
|
|
|
47
47
|
- Valida la URL (regex, redactada en errores), resuelve el token, valida
|
|
48
48
|
`issues:write` ANTES de crear nada.
|
|
@@ -54,6 +54,20 @@ texto y los usa para reconciliar únicamente el intento que originó el POST.
|
|
|
54
54
|
`resume` determinista.
|
|
55
55
|
- `--store-token`: persistencia del token OPT-IN (por defecto NO se
|
|
56
56
|
guarda; `--no-store-token` quedó obsoleto).
|
|
57
|
+
- `--process-name N`: nombre del proceso/skill padre. Título resultante:
|
|
58
|
+
`[Process Tracking][N] Execution ID: {exec_id}`; el cuerpo incluye
|
|
59
|
+
`- **Proceso:** N`; estado y JSON llevan `process_name`. Sin `N`, se
|
|
60
|
+
conserva el formato legado `[Process Tracking] Execution ID: …`.
|
|
61
|
+
Un secreto en `N` se redacta; `[`/`]`/saltos → `VALIDATION_ERROR`.
|
|
62
|
+
- `--process-inputs JSON`: objeto clave→valor con los parámetros de
|
|
63
|
+
entrada del proceso. Se publican en la sección "Parámetros de entrada"
|
|
64
|
+
del cuerpo del Issue (ordenados por clave, valores entre backticks) y
|
|
65
|
+
se guardan en el estado (`process_inputs`) y en la salida de `init`
|
|
66
|
+
(`process_inputs`). Secretos en claves/valores se REDACTAN; límites:
|
|
67
|
+
50 pares, valores ≤ 2000 caracteres; claves vacías, JSON inválido o no
|
|
68
|
+
objeto → `VALIDATION_ERROR`. Sin la opción, el estado queda limpio
|
|
69
|
+
(`process_inputs: null`) y el cuerpo no tiene la sección.
|
|
70
|
+
- Salida de `init`: añade `process_name`, `process_inputs` e `issue_title`.
|
|
57
71
|
|
|
58
72
|
Salida: `execution_id`, `issue_number`, `issue_url`, `state_file`, `plan`,
|
|
59
73
|
`first_step`, `token_source`.
|
|
@@ -87,7 +101,7 @@ Publica `[STATUS: ERROR] - Paso: {N} - Detalles: {D}` y fija
|
|
|
87
101
|
reanudar exige `resume`). `--details-file` ideal para stacktraces largos
|
|
88
102
|
(trunca a 60000 chars).
|
|
89
103
|
|
|
90
|
-
### `finalize [--url URL] [--token T] [--force]`
|
|
104
|
+
### `finalize [--url URL] [--token T] [--force] [--summary TEXT | --summary-file F]`
|
|
91
105
|
|
|
92
106
|
1. Verifica el veredicto remoto: rechaza procesos con pasos pendientes
|
|
93
107
|
(`FINALIZE_BLOCKED`, exit 8) y abortes externos. Sin plan permite éxito
|
|
@@ -96,10 +110,18 @@ reanudar exige `resume`). `--details-file` ideal para stacktraces largos
|
|
|
96
110
|
exitosamente.` (idempotente). Con `--force`: `[STATUS: CANCELADO] -
|
|
97
111
|
El proceso fue cerrado anticipadamente (--force).` con los pasos
|
|
98
112
|
pendientes (cierre anticipado distinguible, nunca éxito falso).
|
|
99
|
-
3.
|
|
100
|
-
|
|
113
|
+
3. Si se dio `--summary`/`--summary-file` **y** la ruta es de éxito (no
|
|
114
|
+
`--force`): publica un comentario APARTE con ese texto (sanitizado y
|
|
115
|
+
truncado a 60000 chars, igual que `--details`), inmediatamente después
|
|
116
|
+
del de FINALIZADO. Sin marcador `[STATUS: ...]`, así que `COMMENT_RE`
|
|
117
|
+
lo ignora en lecturas futuras — no es parte de la máquina de estados,
|
|
118
|
+
solo queda como constancia legible en el Issue. `--summary`/
|
|
119
|
+
`--summary-file` vacío o en blanco no publica nada.
|
|
120
|
+
4. Cierra el Issue (PATCH state=closed) y **verifica re-leyendo**.
|
|
121
|
+
5. Limpia `active_process_id` (estado `FINALIZED`/`CANCELADO`) + historial
|
|
101
122
|
(con `outcome`). Un CANCELADO existente conserva ese resultado aunque el
|
|
102
|
-
reintento no lleve `--force`.
|
|
123
|
+
reintento no lleve `--force`. La salida incluye
|
|
124
|
+
`summary_comment_posted`/`summary_comment_url`.
|
|
103
125
|
|
|
104
126
|
### `status [--url URL] [--remote]`
|
|
105
127
|
|
|
@@ -95,9 +95,17 @@ def build_parser():
|
|
|
95
95
|
"init", help="nueva ejecución: valida, crea el Issue y guarda memoria")
|
|
96
96
|
common(subparser, url_required=True)
|
|
97
97
|
subparser.add_argument("--plan", default=None, metavar="JSON",
|
|
98
|
-
help='plan de pasos con nombres ÚNICOS, ej. \'["Paso 1","Paso 2"]\'')
|
|
98
|
+
help='plan de pasos con nombres ÚNICOS, ej. \'["Paso 1", "Paso 2"]\'')
|
|
99
99
|
subparser.add_argument("--plan-file", default=None, metavar="F",
|
|
100
100
|
help="archivo JSON con la lista de pasos")
|
|
101
|
+
subparser.add_argument("--process-name", default=None, metavar="N",
|
|
102
|
+
help="nombre del proceso/skill padre que se rastrea "
|
|
103
|
+
"(aparece en el título y cuerpo del Issue)")
|
|
104
|
+
subparser.add_argument("--process-inputs", default=None, metavar="JSON",
|
|
105
|
+
help='parámetros de entrada del proceso (objeto '
|
|
106
|
+
"clave→valor, se publican en el Issue; los "
|
|
107
|
+
"secretos se redactan), ej. \'{\"archivo\": "
|
|
108
|
+
'\"datos.csv\"}\'')
|
|
101
109
|
subparser.add_argument("--prompt-token", action="store_true",
|
|
102
110
|
help="pedir el token interactivamente (TTY)")
|
|
103
111
|
subparser.add_argument("--store-token", action="store_true",
|
|
@@ -139,6 +147,13 @@ def build_parser():
|
|
|
139
147
|
subparser.add_argument("--force", action="store_true",
|
|
140
148
|
help="cancelar aunque queden pasos pendientes o no haya trabajo "
|
|
141
149
|
"(no aplica si el Issue fue cerrado externamente)")
|
|
150
|
+
subparser.add_argument("--summary", default=None, metavar="TEXT",
|
|
151
|
+
help="resumen final del proceso (se publica como comentario "
|
|
152
|
+
"aparte, después del de FINALIZADO y antes de cerrar el "
|
|
153
|
+
"Issue; se sanitiza igual que --details). Solo aplica en "
|
|
154
|
+
"la ruta de éxito (no con --force)")
|
|
155
|
+
subparser.add_argument("--summary-file", default=None, metavar="F",
|
|
156
|
+
help="archivo con el resumen final (alternativa a --summary)")
|
|
142
157
|
subparser.set_defaults(func=cmd_finalize)
|
|
143
158
|
|
|
144
159
|
subparser = subparsers.add_parser(
|
|
@@ -153,6 +168,9 @@ def build_parser():
|
|
|
153
168
|
common(subparser, url_required=True)
|
|
154
169
|
subparser.add_argument("--execution-id", required=True, metavar="ID",
|
|
155
170
|
help="Execution ID EXACTO del título del Issue")
|
|
171
|
+
subparser.add_argument("--process-name", default=None, metavar="N",
|
|
172
|
+
help="nombre del proceso a adoptar (filtro adicional "
|
|
173
|
+
"opcional si hay ambigüedad)")
|
|
156
174
|
subparser.add_argument("--force", action="store_true",
|
|
157
175
|
help="reemplazar un proceso activo existente (conserva token "
|
|
158
176
|
"e historial)")
|
|
@@ -14,8 +14,9 @@ from .common import (
|
|
|
14
14
|
EXIT_RESUME_REQUIRED, EXIT_RUNTIME, EXIT_TOKEN_INSUFFICIENT,
|
|
15
15
|
EXIT_VALIDATION, MAX_DETAILS_LEN, MAX_PLAN_STEPS, SCHEMA_VERSION,
|
|
16
16
|
STATUS_CANCELADO, STATUS_COMPLETADO, STATUS_ERROR, STATUS_FINALIZADO,
|
|
17
|
-
STATUS_INICIANDO, SkillError, emit, new_execution_id,
|
|
18
|
-
|
|
17
|
+
STATUS_INICIANDO, TITLE_RE, SkillError, emit, new_execution_id,
|
|
18
|
+
normalize_process_name, normalize_step_name, now_iso, ordered_unique,
|
|
19
|
+
issue_title, redact_url, sanitize_text, validate_repo_url,
|
|
19
20
|
)
|
|
20
21
|
from .events import (
|
|
21
22
|
build_issue_body, events_from_comments, fmt_cancelado, fmt_completado,
|
|
@@ -96,6 +97,47 @@ def load_plan(args):
|
|
|
96
97
|
return plan
|
|
97
98
|
|
|
98
99
|
|
|
100
|
+
MAX_INPUTS_COUNT = 50
|
|
101
|
+
MAX_INPUT_VALUE_LEN = 2000
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def load_process_inputs(args):
|
|
105
|
+
"""Valida y normaliza --process-inputs (objeto JSON clave→valor).
|
|
106
|
+
|
|
107
|
+
- claves y valores se convierten a cadena; claves vacías se rechazan
|
|
108
|
+
- secretos en valores (y claves) se REDACTAN antes de guardar/publicar
|
|
109
|
+
- valores se truncan a MAX_INPUT_VALUE_LEN; máx MAX_INPUTS_COUNT pares
|
|
110
|
+
- devuelve None cuando no se pasó la opción (mantiene el estado limpio)
|
|
111
|
+
"""
|
|
112
|
+
raw = getattr(args, "process_inputs", None)
|
|
113
|
+
if raw is None:
|
|
114
|
+
return None
|
|
115
|
+
try:
|
|
116
|
+
data = json.loads(raw)
|
|
117
|
+
except json.JSONDecodeError as error:
|
|
118
|
+
raise SkillError(EXIT_VALIDATION, "VALIDATION_ERROR",
|
|
119
|
+
f"--process-inputs no es JSON válido: {error}")
|
|
120
|
+
if not isinstance(data, dict):
|
|
121
|
+
raise SkillError(EXIT_VALIDATION, "VALIDATION_ERROR",
|
|
122
|
+
"--process-inputs debe ser un objeto JSON "
|
|
123
|
+
"clave→valor, ej. '{\"archivo\": \"x.csv\", "
|
|
124
|
+
"\"modo\": \"lint\"}'")
|
|
125
|
+
if len(data) > MAX_INPUTS_COUNT:
|
|
126
|
+
raise SkillError(EXIT_VALIDATION, "VALIDATION_ERROR",
|
|
127
|
+
f"--process-inputs no puede exceder "
|
|
128
|
+
f"{MAX_INPUTS_COUNT} pares")
|
|
129
|
+
inputs = {}
|
|
130
|
+
for key, value in data.items():
|
|
131
|
+
clean_key, _ = sanitize_text(str(key).strip())
|
|
132
|
+
clean_key = clean_key.strip()
|
|
133
|
+
if not clean_key:
|
|
134
|
+
raise SkillError(EXIT_VALIDATION, "VALIDATION_ERROR",
|
|
135
|
+
"--process-inputs: las claves no pueden ser vacías")
|
|
136
|
+
clean_value, _ = sanitize_text(str(value))
|
|
137
|
+
inputs[clean_key] = clean_value[:MAX_INPUT_VALUE_LEN]
|
|
138
|
+
return inputs or None
|
|
139
|
+
|
|
140
|
+
|
|
99
141
|
def _remote_snapshot(owner, repo, number, token):
|
|
100
142
|
issue = github.fetch_issue(owner, repo, number, token)
|
|
101
143
|
comments = github.fetch_all_comments(owner, repo, number, token)
|
|
@@ -125,6 +167,8 @@ def cmd_init(args):
|
|
|
125
167
|
owner, repo = target
|
|
126
168
|
path = state_file_for(owner, repo)
|
|
127
169
|
plan = load_plan(args)
|
|
170
|
+
process_name = normalize_process_name(getattr(args, "process_name", None))
|
|
171
|
+
process_inputs = load_process_inputs(args)
|
|
128
172
|
|
|
129
173
|
lock = StateLock(path)
|
|
130
174
|
with lock:
|
|
@@ -154,8 +198,10 @@ def cmd_init(args):
|
|
|
154
198
|
store = bool(args.store_token) and source in ("cli", "prompt")
|
|
155
199
|
|
|
156
200
|
execution_id = new_execution_id()
|
|
157
|
-
title =
|
|
158
|
-
body = build_issue_body(args.url.strip(), execution_id, plan
|
|
201
|
+
title = issue_title(execution_id, process_name)
|
|
202
|
+
body = build_issue_body(args.url.strip(), execution_id, plan,
|
|
203
|
+
process_name=process_name,
|
|
204
|
+
process_inputs=process_inputs)
|
|
159
205
|
recovered = False
|
|
160
206
|
create_error = None
|
|
161
207
|
lock.ensure_owner()
|
|
@@ -202,6 +248,8 @@ def cmd_init(args):
|
|
|
202
248
|
"owner": owner, "repo": repo,
|
|
203
249
|
"active_process_id": number,
|
|
204
250
|
"execution_id": execution_id,
|
|
251
|
+
"process_name": process_name,
|
|
252
|
+
"process_inputs": process_inputs or None,
|
|
205
253
|
"issue_url": data.get("html_url"),
|
|
206
254
|
"process_state": "RUNNING",
|
|
207
255
|
"current_step": None,
|
|
@@ -224,6 +272,8 @@ def cmd_init(args):
|
|
|
224
272
|
"ok": True, "action": "INIT",
|
|
225
273
|
"execution_id": execution_id, "issue_number": number,
|
|
226
274
|
"issue_url": data.get("html_url"), "state_file": str(path),
|
|
275
|
+
"process_name": process_name, "issue_title": title,
|
|
276
|
+
"process_inputs": process_inputs or None,
|
|
227
277
|
"plan": plan, "first_step": plan[0] if plan else None,
|
|
228
278
|
"recovered": recovered,
|
|
229
279
|
"token_source": source, "token_stored": bool(state["github_token"]),
|
|
@@ -493,6 +543,25 @@ _PENDING_VERDICTS = ("RERUN_STEP", "NEXT_STEP", "START_FIRST",
|
|
|
493
543
|
|
|
494
544
|
|
|
495
545
|
def cmd_finalize(args):
|
|
546
|
+
summary = getattr(args, "summary", None)
|
|
547
|
+
summary_file = getattr(args, "summary_file", None)
|
|
548
|
+
if summary and summary_file:
|
|
549
|
+
raise SkillError(EXIT_VALIDATION, "VALIDATION_ERROR",
|
|
550
|
+
"usa --summary o --summary-file, no ambos")
|
|
551
|
+
if summary_file:
|
|
552
|
+
try:
|
|
553
|
+
summary = Path(summary_file).read_text(encoding="utf-8", errors="replace")
|
|
554
|
+
except OSError as error:
|
|
555
|
+
raise SkillError(EXIT_VALIDATION, "VALIDATION_ERROR",
|
|
556
|
+
f"no se pudo leer --summary-file: {error}")
|
|
557
|
+
if summary is not None:
|
|
558
|
+
summary = summary.strip()
|
|
559
|
+
if summary:
|
|
560
|
+
summary, _ = sanitize_text(summary)
|
|
561
|
+
summary = summary[:MAX_DETAILS_LEN]
|
|
562
|
+
else:
|
|
563
|
+
summary = None
|
|
564
|
+
|
|
496
565
|
owner, repo, path = resolve_target(args)
|
|
497
566
|
lock = StateLock(path)
|
|
498
567
|
with lock:
|
|
@@ -547,6 +616,19 @@ def cmd_finalize(args):
|
|
|
547
616
|
owner, repo, number, fmt_finalizado(), token,
|
|
548
617
|
baseline_ids=frozenset(item.get("id") for item in comments),
|
|
549
618
|
execution_id=state.get("execution_id"), sequence=len(events) + 1)
|
|
619
|
+
|
|
620
|
+
summary_comment = None
|
|
621
|
+
if summary and not forced and not already_cancel:
|
|
622
|
+
# Comentario aparte del resumen final, SOLO en la ruta de éxito
|
|
623
|
+
# (no al cancelar): va después del de FINALIZADO y antes de
|
|
624
|
+
# cerrar el Issue. No lleva el marcador [STATUS: ...], así que
|
|
625
|
+
# COMMENT_RE lo ignora en futuras lecturas de eventos — no
|
|
626
|
+
# interfiere con resume/plan_resume.
|
|
627
|
+
summary_comment = github.post_comment(
|
|
628
|
+
owner, repo, number, summary, token,
|
|
629
|
+
baseline_ids=frozenset(item.get("id") for item in comments),
|
|
630
|
+
execution_id=state.get("execution_id"), sequence=len(events) + 2)
|
|
631
|
+
|
|
550
632
|
github.close_issue(owner, repo, number, token)
|
|
551
633
|
issue = github.fetch_issue(owner, repo, number, token)
|
|
552
634
|
if issue.get("state") != "closed":
|
|
@@ -586,6 +668,8 @@ def cmd_finalize(args):
|
|
|
586
668
|
"forced": outcome == "CANCELADO",
|
|
587
669
|
"comment_posted": bool(comment),
|
|
588
670
|
"comment_url": comment["comment_url"] if comment else None,
|
|
671
|
+
"summary_comment_posted": bool(summary_comment),
|
|
672
|
+
"summary_comment_url": summary_comment["comment_url"] if summary_comment else None,
|
|
589
673
|
"completed_steps": completed,
|
|
590
674
|
"active_process_id": None,
|
|
591
675
|
"state_file": str(path),
|
|
@@ -653,8 +737,9 @@ def cmd_adopt(args):
|
|
|
653
737
|
existing = load_state(path)
|
|
654
738
|
token, _ = need_token(args, existing)
|
|
655
739
|
|
|
656
|
-
|
|
657
|
-
|
|
740
|
+
# v0.5.0: el título puede incluir [Nombre]; se busca por el execution_id
|
|
741
|
+
# (único) y el filtrado canónico exacto lo hace canonical_title_match.
|
|
742
|
+
query = f'repo:{owner}/{repo} "{execution_id}" in:title is:issue'
|
|
658
743
|
status, data, _ = github.api(
|
|
659
744
|
"GET", "/search/issues?" + urllib.parse.urlencode({"q": query, "per_page": 30}),
|
|
660
745
|
token)
|
|
@@ -662,8 +747,10 @@ def cmd_adopt(args):
|
|
|
662
747
|
raise SkillError(EXIT_RUNTIME, "SEARCH_FAILED",
|
|
663
748
|
f"búsqueda de Issues devolvió {status}: "
|
|
664
749
|
f"{data.get('message', '')}")
|
|
750
|
+
process_name = normalize_process_name(getattr(args, "process_name", None))
|
|
665
751
|
matches = [issue for issue in (data.get("items") or [])
|
|
666
|
-
if github.canonical_title_match(issue, execution_id
|
|
752
|
+
if github.canonical_title_match(issue, execution_id,
|
|
753
|
+
process_name=process_name)]
|
|
667
754
|
if not matches:
|
|
668
755
|
raise SkillError(EXIT_NOT_FOUND, "ISSUE_NOT_FOUND",
|
|
669
756
|
f"no se encontró un Issue con título canónico para "
|
|
@@ -691,6 +778,10 @@ def cmd_adopt(args):
|
|
|
691
778
|
plan = parse_plan_from_body(issue.get("body") or "")
|
|
692
779
|
verdict = plan_resume(events, plan, issue.get("state", "open"))
|
|
693
780
|
action = verdict["action"]
|
|
781
|
+
title_match = TITLE_RE.match((issue.get("title") or "").strip())
|
|
782
|
+
adopted_name = (title_match.group("process_name")
|
|
783
|
+
if title_match and title_match.group("process_name")
|
|
784
|
+
else None)
|
|
694
785
|
if issue.get("state") == "open":
|
|
695
786
|
active = number
|
|
696
787
|
process_state = ("ERROR" if events and events[-1]["status"] == STATUS_ERROR
|
|
@@ -708,6 +799,7 @@ def cmd_adopt(args):
|
|
|
708
799
|
"owner": owner, "repo": repo,
|
|
709
800
|
"active_process_id": active,
|
|
710
801
|
"execution_id": execution_id,
|
|
802
|
+
"process_name": process_name or adopted_name,
|
|
711
803
|
"issue_url": issue.get("html_url"),
|
|
712
804
|
"process_state": process_state,
|
|
713
805
|
"current_step": verdict.get("resume_from_step"),
|
|
@@ -727,7 +819,9 @@ def cmd_adopt(args):
|
|
|
727
819
|
"ok": True, "action": "ADOPT", "execution_id": execution_id,
|
|
728
820
|
"issue_number": number, "issue_url": issue.get("html_url"),
|
|
729
821
|
"issue_state": issue.get("state"), "verdict": verdict,
|
|
730
|
-
"process_state": process_state,
|
|
822
|
+
"process_state": process_state,
|
|
823
|
+
"process_name": process_name or adopted_name,
|
|
824
|
+
"state_file": str(path),
|
|
731
825
|
})
|
|
732
826
|
return EXIT_OK
|
|
733
827
|
|
|
@@ -7,7 +7,7 @@ import re
|
|
|
7
7
|
import uuid
|
|
8
8
|
from datetime import datetime, timezone
|
|
9
9
|
|
|
10
|
-
VERSION = "0.
|
|
10
|
+
VERSION = "0.6.0"
|
|
11
11
|
API_BASE = "https://api.github.com"
|
|
12
12
|
USER_AGENT = f"github-proyect-tracking/{VERSION}"
|
|
13
13
|
|
|
@@ -47,10 +47,18 @@ REPO_URL_RE = re.compile(
|
|
|
47
47
|
r"(?P<repo>[A-Za-z0-9._-]+?)(?:\.git)?/?$",
|
|
48
48
|
re.IGNORECASE,
|
|
49
49
|
)
|
|
50
|
+
# Título dual (v0.5.0): con o sin nombre de proceso.
|
|
51
|
+
# [Process Tracking][Mi Skill] Execution ID: exec-...
|
|
52
|
+
# [Process Tracking] Execution ID: exec-... (formato <=0.4.x)
|
|
50
53
|
TITLE_RE = re.compile(
|
|
51
|
-
r"^\[Process Tracking\]
|
|
54
|
+
r"^\[Process Tracking\]"
|
|
55
|
+
r"(?:\[(?P<process_name>"
|
|
56
|
+
r"(?:[^\[\]\r\n]|\[RED[Aa]ctado:[^\[\]]*\]){1,200}"
|
|
57
|
+
r")\])?"
|
|
58
|
+
r"\s*(?:-\s*)?Execution ID:\s*(?P<exec_id>\S+?)\s*$",
|
|
52
59
|
re.IGNORECASE,
|
|
53
60
|
)
|
|
61
|
+
MAX_PROCESS_NAME_LEN = 100
|
|
54
62
|
|
|
55
63
|
SECRET_REDACTIONS = [
|
|
56
64
|
(re.compile(r"(?i)\b(authorization|proxy-authorization)\s*[:=]\s*"
|
|
@@ -68,6 +76,43 @@ SECRET_REDACTIONS = [
|
|
|
68
76
|
]
|
|
69
77
|
|
|
70
78
|
|
|
79
|
+
def normalize_process_name(name):
|
|
80
|
+
"""Normaliza el nombre del proceso (skill padre) para título y cuerpo
|
|
81
|
+
del Issue. Orden V2-aprendido: validar la entrada CRUDA (longitud y
|
|
82
|
+
corchetes) ANTES de sanitizar — los marcadores [REDactado:...] que
|
|
83
|
+
introduce la sanitización son legítimos y TITLE_RE los acepta."""
|
|
84
|
+
if name is None:
|
|
85
|
+
return None
|
|
86
|
+
text = str(name).strip()
|
|
87
|
+
if not text:
|
|
88
|
+
return None
|
|
89
|
+
if len(text) > MAX_PROCESS_NAME_LEN:
|
|
90
|
+
raise SkillError(
|
|
91
|
+
EXIT_VALIDATION, "VALIDATION_ERROR",
|
|
92
|
+
f"el nombre del proceso no puede exceder {MAX_PROCESS_NAME_LEN} "
|
|
93
|
+
f"caracteres (recibido: {len(text)})")
|
|
94
|
+
if any(ch in text for ch in "[]\r\n"):
|
|
95
|
+
raise SkillError(EXIT_VALIDATION, "VALIDATION_ERROR",
|
|
96
|
+
"el nombre del proceso no puede contener '[', ']' "
|
|
97
|
+
"ni saltos de línea (rompe el título canónico)")
|
|
98
|
+
clean, _ = sanitize_text(text)
|
|
99
|
+
clean = clean.strip()
|
|
100
|
+
if not clean:
|
|
101
|
+
raise SkillError(EXIT_VALIDATION, "VALIDATION_ERROR",
|
|
102
|
+
"el nombre del proceso quedó vacío tras sanitizar")
|
|
103
|
+
return clean
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def issue_title(execution_id, process_name=None):
|
|
107
|
+
"""Título canónico del Issue con el nombre del proceso (v0.5.0).
|
|
108
|
+
El nombre debe venir YA normalizado (normalize_process_name); aquí no
|
|
109
|
+
se re-valida porque los marcadores [REDactado:...] son legítimos."""
|
|
110
|
+
if process_name:
|
|
111
|
+
return f"[Process Tracking][{process_name}] Execution ID: {execution_id}"
|
|
112
|
+
return f"[Process Tracking] Execution ID: {execution_id}"
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
|
|
71
116
|
class SkillError(Exception):
|
|
72
117
|
"""Error operativo que se serializa como un único objeto JSON."""
|
|
73
118
|
|
|
@@ -98,13 +98,42 @@ def events_from_comments(comments):
|
|
|
98
98
|
return events
|
|
99
99
|
|
|
100
100
|
|
|
101
|
-
def
|
|
101
|
+
def _normalized_inputs(process_inputs):
|
|
102
|
+
"""Normaliza los parámetros de entrada a pares (clave, valor) ordenados,
|
|
103
|
+
con ambos convertidos a cadena. Devuelve [] si no hay inputs."""
|
|
104
|
+
if not process_inputs:
|
|
105
|
+
return []
|
|
106
|
+
if not isinstance(process_inputs, dict):
|
|
107
|
+
return []
|
|
108
|
+
entries = []
|
|
109
|
+
for key in sorted(process_inputs, key=str):
|
|
110
|
+
value = process_inputs[key]
|
|
111
|
+
entries.append((str(key).strip(), str(value)))
|
|
112
|
+
return [(k, v) for k, v in entries if k]
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def build_issue_body(url, execution_id, plan, process_name=None,
|
|
116
|
+
process_inputs=None):
|
|
102
117
|
lines = [
|
|
103
118
|
"Proceso rastreado por el skill `github-proyect-tracking`.",
|
|
104
119
|
"",
|
|
105
120
|
f"- **Execution ID:** `{execution_id}`",
|
|
121
|
+
]
|
|
122
|
+
if process_name:
|
|
123
|
+
lines.append(f"- **Proceso:** {process_name}")
|
|
124
|
+
lines += [
|
|
106
125
|
f"- **Proyecto:** {url}",
|
|
107
126
|
f"- **Iniciado (UTC):** {now_iso()}",
|
|
127
|
+
]
|
|
128
|
+
|
|
129
|
+
# v0.6.0: parámetros de entrada del proceso — la única forma de saber
|
|
130
|
+
# exactamente qué tarea estaba realizando esta ejecución.
|
|
131
|
+
entries = _normalized_inputs(process_inputs)
|
|
132
|
+
if entries:
|
|
133
|
+
lines += ["", "## Parámetros de entrada", ""]
|
|
134
|
+
lines += [f"- **{key}:** `{value}`" for key, value in entries]
|
|
135
|
+
|
|
136
|
+
lines += [
|
|
108
137
|
"",
|
|
109
138
|
"## Plan de pasos",
|
|
110
139
|
]
|
|
@@ -14,7 +14,7 @@ import uuid
|
|
|
14
14
|
from .common import (
|
|
15
15
|
AMBIGUOUS_HTTP_STATUSES, API_BASE, EXIT_NOT_FOUND, EXIT_NO_TOKEN,
|
|
16
16
|
EXIT_RUNTIME, EXIT_TOKEN_INSUFFICIENT, MAX_COMMENT_PAGES, TITLE_RE,
|
|
17
|
-
USER_AGENT, SkillError,
|
|
17
|
+
USER_AGENT, SkillError, issue_title,
|
|
18
18
|
)
|
|
19
19
|
from .events import events_from_comments, transition_metadata, with_transition_metadata
|
|
20
20
|
|
|
@@ -295,6 +295,7 @@ def close_issue(owner, repo, number, token):
|
|
|
295
295
|
|
|
296
296
|
|
|
297
297
|
def find_issue_by_title(owner, repo, title, token):
|
|
298
|
+
# v0.5.0: title ya viene construido con issue_title()
|
|
298
299
|
url = (f"{API_BASE}/repos/{owner}/{repo}/issues?state=all&sort=created"
|
|
299
300
|
"&direction=desc&per_page=100")
|
|
300
301
|
seen = set()
|
|
@@ -310,6 +311,14 @@ def find_issue_by_title(owner, repo, title, token):
|
|
|
310
311
|
return None
|
|
311
312
|
|
|
312
313
|
|
|
313
|
-
def canonical_title_match(issue, execution_id):
|
|
314
|
+
def canonical_title_match(issue, execution_id, process_name=None):
|
|
315
|
+
"""v0.5.0: casa el título canónico con o sin nombre de proceso; el
|
|
316
|
+
exec_id debe coincidir EXACTO y, si se indica process_name, también."""
|
|
314
317
|
match = TITLE_RE.match((issue.get("title") or "").strip())
|
|
315
|
-
|
|
318
|
+
if not match or match.group("exec_id").lower() != execution_id.lower():
|
|
319
|
+
return False
|
|
320
|
+
if process_name:
|
|
321
|
+
found = match.group("process_name")
|
|
322
|
+
if (found or "").strip().lower() != str(process_name).strip().lower():
|
|
323
|
+
return False
|
|
324
|
+
return True
|