kcreate-scaffolding-gcp-script 0.0.0-stage → 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.
@@ -0,0 +1,706 @@
1
+ # Lecciones reales incorporadas en este skill
2
+
3
+ Seis fuentes:
4
+
5
+ - **A — descubiertas desplegando `xoloshop-qa` a mano** (sesión previa,
6
+ documentadas también en `docs/CreateScriptsNewGCPProjectInstructions.md`
7
+ y `docs/DeployNewGCPProjectInstructions.md` de `club-tijuana/infra-cloud`).
8
+ - **B — descubiertas probando este skill de punta a punta** contra una
9
+ copia descartable de `infra-cloud` real, generando `xoloshop-prod` desde
10
+ `boletera-qa`, antes de considerarlo terminado. Varias de estas B
11
+ **nunca se habían notado** ni siquiera en el proceso manual (ej. los
12
+ archivos de nivel raíz, el dominio DNS) — la automatización las hizo
13
+ visibles por primera vez.
14
+ - **C — encontradas por `Review_V1.md`**, una revisión independiente que
15
+ también ejecutó el skill de punta a punta (contra `boletera-qa` y, de
16
+ forma crucial, contra `xoloshop-qa` — el entorno que las pruebas B no
17
+ habían usado como fuente). Ver `Review_V1.md` en la raíz de este repo
18
+ para el texto completo de la revisión y la respuesta punto por punto.
19
+ - **D — encontradas por `Review_V2.md`**, una re-revisión después de
20
+ aplicar C, con reintentos reales sobre entornos a medio construir
21
+ (encontró una regresión crítica del propio fix de reanudación de C1).
22
+ Ver `Review_V2.md`.
23
+ - **E — encontradas por `Review_V3.md`**, una tercera revisión que
24
+ reprodujo la cadena crítica de N-1 paso a paso y encontró que el
25
+ propio fix de `artifact_registry` (una función nueva de esa misma
26
+ ronda) se había quedado con la misma contaminación que D1/D2 ya habían
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.
32
+
33
+ Cada lección dice **qué pasaba**, **por qué**, y **en qué script/función
34
+ de este skill queda resuelta**.
35
+
36
+ ---
37
+
38
+ ## A1 — Fórmula de CIDR sin validar produce octetos inválidos
39
+
40
+ **Qué pasaba:** extender a mano `10.N.0.0/22` sin revisar los 4 bloques
41
+ derivados produjo `10.335.0.0/17` (octeto `335`, inválido).
42
+ **Resuelto en:** `lib/common.sh:ksc_cidr_pick_free` — usa
43
+ `ipaddress.ip_network()` de Python (nunca aritmética a mano) y prueba
44
+ `N=1..249` hasta encontrar uno sin colisión; si no hay ninguno libre,
45
+ falla con un mensaje explícito en vez de devolver algo inválido.
46
+
47
+ ## A2 — Coma faltante editando `project_services` a mano
48
+
49
+ **Qué pasaba:** un elemento sin coma al final de la lista rompe
50
+ `terragrunt hcl validate` con un error de sintaxis poco claro.
51
+ **Resuelto en:** `build_component_project_services` — el archivo se copia
52
+ **completo, sin tocarlo**. Este skill nunca edita esa lista campo por
53
+ campo, así que esta clase de bug queda eliminada por diseño, no detectada
54
+ después.
55
+
56
+ ## A3 — Valores genéricos/org-wide en `artifact_registry`
57
+
58
+ **Qué pasaba:** `repository_id`/`description` quedaron como
59
+ `"club-tijuana-docker-repo"` / `"Official Club Tijuana docker
60
+ repository."` en un despliegue real, sin adaptar.
61
+ **Resuelto en:** `build_component_artifact_registry` — sustituye el
62
+ nombre de la app si lo encuentra en el valor. **Lección B encontrada
63
+ probando esto:** `boletera-qa`, la fuente real usada para probar este
64
+ skill, **todavía tiene ese mismo valor genérico sin corregir**. Como no
65
+ hay substring de la app para reemplazar, el script detecta el caso y
66
+ emite un `[WARN]` explícito en vez de copiar el problema en silencio —
67
+ nunca asume que "no encontré nada que sustituir" significa "está bien".
68
+
69
+ ## A4 — IP de `cloudadmin` transcrita mal
70
+
71
+ **Qué pasaba:** `177.226.233.64` en vez de `177.226.232.136` al copiar a
72
+ mano.
73
+ **Resuelto en:** `01_init_config.sh` extrae las entradas fijas (no
74
+ `dev-*`) de `authorized_networks` directo del `terragrunt.hcl` real de
75
+ `SOURCE_ENV` con `grep`/`sed`, nunca de memoria ni de una tabla.
76
+
77
+ ## A5 — IP de `dev-<usuario>` vieja o reusada
78
+
79
+ **Qué pasaba:** el ISP cambia; una IP de otra sesión queda obsoleta y
80
+ deja al usuario sin acceso.
81
+ **Resuelto en:** `lib/common.sh:ksc_fresh_public_ip`, llamada dentro de
82
+ `build_component_sql_postgres` — siempre `curl` en el momento de
83
+ construir, nunca cacheada entre corridas.
84
+
85
+ ## A6 — `gke_version`/`database_version` copiados a ciegas
86
+
87
+ **Qué pasaba:** no verificar si ya existe una versión estable más
88
+ reciente antes de copiar la de `SOURCE_ENV`.
89
+ **Resuelto en:** el config los autodetecta con el sufijo
90
+ `_NEEDS_VERIFICATION` en el nombre del campo — el skill los usa tal cual
91
+ estén en el config al construir, pero el nombre del campo mismo recuerda
92
+ que deben verificarse contra la documentación oficial antes de confiar en
93
+ ellos ciegamente (esto sí requiere criterio humano/del agente — no es
94
+ mecanizable sin consultar una fuente externa).
95
+
96
+ ## A7 — Falta `orgpolicy.googleapis.com` + sus 3 overrides
97
+
98
+ **Qué pasaba:** sin esta API y sus 3 overrides en `org_policies`, el
99
+ despliegue falla varios pasos después, con minutos de espera por
100
+ propagación cada vez que se corrige una política a la vez.
101
+ **Resuelto en:** `build_component_project_services` y
102
+ `build_component_org_policies` copian ambos archivos **completos** de
103
+ `SOURCE_ENV` — si `SOURCE_ENV` ya tiene la API y los 3 overrides (todo
104
+ entorno desplegado después de 2026-02-17 los tiene), `NEW_ENV` los hereda
105
+ automáticamente, sin que este skill necesite saber los nombres exactos de
106
+ las constraints.
107
+
108
+ ## A8 — `roles/orgpolicy.policyAdmin` en `project_iam`
109
+
110
+ **Qué pasaba:** GCP rechaza ese rol a nivel proyecto
111
+ (`Role ... is not supported for this resource`); solo existe a nivel
112
+ Organización/Carpeta.
113
+ **Resuelto en:** `build_component_project_iam` hace `grep` de ese string
114
+ después de copiar y **falla la construcción** (no solo advierte) si lo
115
+ encuentra — a diferencia de los demás `[WARN]`, este es un error real
116
+ conocido, no una zona gris.
117
+
118
+ ## A9 — Cambiar el casing de `project_name` sin que lo pidieran
119
+
120
+ **Qué pasaba:** confusión real entre `"XoloShop-QA"` y `"Xoloshop-QA"` en
121
+ el mismo PR.
122
+ **Resuelto en:** `lib/common.sh:ksc_infer_project_name` aplica una regla
123
+ fija y determinística (primera letra de la app en mayúscula, entorno
124
+ `QA`/`Prod`) y la deja en el config para que el humano la confirme en la
125
+ Etapa 1 — nunca se improvisa un casing distinto después de eso.
126
+
127
+ ## A10 — Bindings de `project_iam` específicos de otro equipo/app
128
+
129
+ **Qué pasaba:** copiar usuarios de un dominio externo o un rol
130
+ personalizado que no existe en el proyecto nuevo.
131
+ **Resuelto en:** `build_component_project_iam` copia el archivo completo
132
+ (no hay forma mecánica confiable de distinguir "binding específico de la
133
+ app" de "binding que debería estar en todo entorno" sin entender el
134
+ negocio) pero **siempre** emite un `[WARN]` pidiendo revisión humana
135
+ explícita antes de confiar en el archivo. Este es, a propósito, el punto
136
+ donde este skill deja más criterio al agente/humano — es el mismo punto
137
+ donde la guía manual original tampoco pudo mecanizarlo del todo.
138
+
139
+ ---
140
+
141
+ ## B1 — `live/<NEW_ENV>/` nunca se crea antes de copiar `project.hcl`
142
+
143
+ **Qué pasaba (encontrado probando):** la primera versión de
144
+ `10_build_components.sh` fallaba en el primer paso (`cp: cannot create
145
+ regular file ... No such file or directory`) porque nunca se hacía
146
+ `mkdir -p` del directorio del entorno nuevo antes de copiar `project.hcl`
147
+ dentro de él.
148
+ **Resuelto en:** `mkdir -p "$NEW_DIR"` explícito justo después de
149
+ descubrir los componentes, antes de cualquier copia.
150
+
151
+ ## B2 — Orden de construcción por alfabeto, no por dependencia real
152
+
153
+ **Qué pasaba (encontrado probando):** los componentes se descubren y
154
+ procesan alfabéticamente. `sql_private_access` necesita el rango CIDR que
155
+ calcula `vpc`, pero `"sql_private_access" < "vpc"` alfabéticamente, así
156
+ que se procesaba primero y fallaba por falta del archivo de CIDR.
157
+ **Resuelto en:** después de descubrir los componentes, si `vpc` está
158
+ presente se reordena al frente de la lista, antes que cualquier otro.
159
+
160
+ ## B3 — Variable de shell reusada como variable de loop
161
+
162
+ **Qué pasaba (encontrado probando):** dentro de `build_component_vpc`, la
163
+ variable local `f` (la ruta del `vpc/terragrunt.hcl` nuevo) se declaraba
164
+ una vez, pero más abajo un `for f in .../sql_private_access/terragrunt.hcl`
165
+ reusaba el mismo nombre como variable de loop — al terminar el loop, `f`
166
+ quedaba apuntando al **último archivo de otro entorno** recorrido, no al
167
+ `vpc/terragrunt.hcl` que se quería editar. Las siguientes llamadas
168
+ editaban el archivo equivocado (de otro entorno ya desplegado).
169
+ **Resuelto en:** la variable de loop se renombró a `sp_file`, y se agregó
170
+ `local f` explícito en `ksc_copy_component_files` como defensa adicional.
171
+ **Por qué importa documentarlo:** si agregas lógica nueva a cualquier
172
+ `build_component_*`, nunca reuses el nombre de una variable `local` ya
173
+ declarada en la misma función como variable de loop — bash no lo detecta
174
+ ni avisa, simplemente sobreescribe en silencio.
175
+
176
+ ## B4 — `sql_private_access.address` no lleva el prefijo CIDR
177
+
178
+ **Qué pasaba (encontrado probando):** el campo `address` de
179
+ `sql_private_access/terragrunt.hcl` es una IP **sin** sufijo `/NN` (el
180
+ prefijo va en un campo separado, `prefix_length`). El cálculo de rangos
181
+ nuevos producía strings `ipaddress.ip_network` completos
182
+ (`"10.0.0.0/16"`), y escribirlos tal cual en `address` genera un valor
183
+ inválido para ese módulo. Además, la recolección de rangos *existentes*
184
+ para evitar colisiones nunca leía estos `address`/`prefix_length`
185
+ separados — solo los CIDR completos de `vpc/terragrunt.hcl` — así que un
186
+ rango nuevo podía colisionar con un `sql_private_access` real sin que el
187
+ picker lo notara.
188
+ **Resuelto en:** `build_component_vpc` reconstruye el CIDR completo de
189
+ cada `sql_private_access/terragrunt.hcl` existente (`address` +
190
+ `prefix_length`) antes de buscar uno libre; `build_component_sql_private_access`
191
+ separa el rango elegido en `address` (sin sufijo) y `prefix_length` antes
192
+ de escribirlos.
193
+
194
+ ## B5 — Archivos de nivel raíz de `live/<entorno>/` (no de un componente)
195
+
196
+ **Qué pasaba (encontrado probando, con `20_validate_file_parity.sh`):**
197
+ `boletera-qa` tiene un `Makefile` (corre `terragrunt run --all ...`) y un
198
+ `README.md` directamente en `live/boletera-qa/`, fuera de cualquier
199
+ componente. Nada en el proceso — ni este skill en su primera versión, ni
200
+ el proceso manual que generó `xoloshop-qa` en la sesión anterior — los
201
+ copiaba. El propio `xoloshop-qa` real de `infra-cloud` sigue sin estos dos
202
+ archivos hoy.
203
+ **Resuelto en:** paso `top-level-files` dedicado en
204
+ `10_build_components.sh`, antes del loop de componentes.
205
+ **Por qué importa:** es la prueba de por qué existe la Etapa 3 — el
206
+ proceso manual original nunca detectó esta falta en su momento porque
207
+ nadie comparó cantidades de archivo a conciencia; el script de validación
208
+ de paridad sí.
209
+
210
+ ## B6 — Falso positivo en la validación de "project_id pinneado"
211
+
212
+ **Qué pasaba (encontrado probando):** la primera versión de
213
+ `21_validate_project_pinning.sh` exigía que **todo** componente tuviera
214
+ una clave `inputs.project_id = local.project_id`. Pero `org_policies` usa
215
+ `parent = "projects/${local.project_id}"` y
216
+ `storage_assets`/`storage_docs` usan `project = local.project_id` — nunca
217
+ una clave llamada `project_id` en `inputs`. El check marcaba estos 3
218
+ componentes como fallidos aunque estaban perfectamente bien pinneados.
219
+ **Resuelto en:** el check se generalizó a "¿aparece la substring exacta
220
+ `local.project_id` en algún lado del archivo?" — cubre cualquier forma en
221
+ que un módulo use el project id, sin asumir un nombre de clave fijo.
222
+
223
+ ## B7 — `dns_record_name`/`dns_domain_name` no son un patrón de texto genérico
224
+
225
+ **Qué pasaba (encontrado probando):** `sql_postgres` de varios entornos
226
+ (no `xoloshop-qa`, que nunca los configuró) tiene
227
+ `dns_record_name = "boletera.db"` y
228
+ `dns_domain_name = "qa.boletera.ninja."`. A simple vista parece el mismo
229
+ patrón `<app>.algo` que el resto de los campos, pero en realidad codifica
230
+ un **dominio DNS real que la organización tiene registrado para esa app
231
+ específica** (`boletera.ninja`, `cashless.ninja`) — no hay garantía de que
232
+ la app nueva vaya a vivir bajo `<NEW_ENV_APP>.ninja`.
233
+ **Resuelto en:** `build_component_sql_postgres` genera un valor por el
234
+ mismo patrón visual **solo si el campo ya existía en `SOURCE_ENV`**, pero
235
+ **siempre** lo marca con un `[WARN]` pidiendo confirmación humana — a
236
+ diferencia de los demás campos (nombres de red, SA, CIDR), donde la
237
+ sustitución mecánica sí es segura porque no representa una decisión de
238
+ negocio externa al repo.
239
+
240
+ ## B8 — `github_workload_identity` puede manejar varios repos de la misma app
241
+
242
+ **Qué pasaba (encontrado probando):** se asumió que este componente
243
+ siempre tiene exactamente 2 bindings (`terraform` + un `github-manager`).
244
+ `boletera-qa` real tiene **6**: el de `terraform` y 5 bindings
245
+ `github-manager` distintos, uno por cada repo de su propia arquitectura
246
+ multi-repo (`xbol-web-admin`, `xbol-api-client`, etc.). La primera versión
247
+ de este skill solo corregía el *segundo* binding encontrado y dejaba los
248
+ otros 4 con nombres de repo obsoletos de `SOURCE_ENV`, sin avisar.
249
+ **Resuelto en (versión original):** contar los bindings; si eran
250
+ exactamente 2 (caso simple) ajustar automáticamente, y si eran más, no
251
+ adivinar — solo `[WARN]`. **Superado por C6** (ver sección C): ahora el
252
+ bloque completo se reconstruye siempre, soportando una lista de repos en
253
+ `APP_GITHUB_REPO` — ya no hace falta elegir entre "caso simple" y
254
+ "advertir sin tocar".
255
+
256
+ ---
257
+
258
+ ## C1 — La construcción moría en el reintento (reanudación prometida pero rota)
259
+
260
+ **Qué pasaba:** `mkdir -p "$NEW_DIR"` corría antes que cualquier
261
+ componente, así que un fallo posterior dejaba `live/<NEW_ENV>/` creado —
262
+ y una guarda previa (`[[ ! -d "$NEW_DIR" ]] || ksc_die ...`) trataba eso
263
+ como "ya existe, no tocar" en el siguiente intento. La reanudación que
264
+ `SKILL.md`/`IDEA.md` prometían no existía en la práctica.
265
+ **Resuelto en:** se quitó la guarda. `10_build_components.sh` ahora
266
+ reconstruye cada componente siempre desde `SOURCE_DIR` (nunca incrementa
267
+ sobre `NEW_DIR`), así que el reintento es naturalmente idempotente — no
268
+ hace falta un motor de reanudación por paso, solo que nada dependa de un
269
+ estado parcial previo.
270
+
271
+ ## C2 — El picker de CIDR podía asignar un rango que se solapa consigo mismo
272
+
273
+ **Qué pasaba:** la fórmula probaba un solo `N` para los 4 bloques
274
+ (subred, pods, svc, `sql_private_access`) y solo los validaba contra los
275
+ rangos **ya existentes**, nunca entre sí. Cuando `N` resultaba múltiplo de
276
+ 10, la subred (`10.N.0.0/22`) quedaba **contenida** en su propio
277
+ `sql_private_access` (`10.N.0.0/16`) — un solapamiento real que solo se
278
+ detecta desplegando. Reproducido: el primer rango libre daba `N=1`
279
+ (válido), pero el segundo consecutivo daba `N=10` (inválido).
280
+ **Resuelto en:** `ksc_cidr_pick_free` ahora busca por década
281
+ (`sql_private_access = 10.<década>.0.0/16`) y, dentro de cada década, por
282
+ un offset ≥ 1 para la subred (preferido `+3`, la convención real
283
+ dominante del repo) — la subred nunca puede caer en la década exacta de
284
+ su propio `/16` por construcción, no solo por un chequeo adicional (que
285
+ también se agregó).
286
+
287
+ ## C3 — `boletera-qa` (el ejemplo del propio README) rompía el build
288
+
289
+ **Qué pasaba:** `build_top_level_files` fallaba la construcción completa
290
+ si `SOURCE_ENV` no tenía alguno de los 3 archivos de nivel raíz
291
+ (`Makefile`/`README.md`/`.gitignore`) — el caso real de `xoloshop-qa`,
292
+ que es justo el entorno más "sano" del repo (el único con
293
+ `orgpolicy.googleapis.com` y las 6 `org_policies`).
294
+ **Resuelto en:** copiar cada archivo de nivel raíz es ahora
295
+ condicional-sin-fallar (`if [[ -f ... ]]; then cp ...; fi`, nunca un `&&`
296
+ encadenado cuyo valor de verdad se propague como el de toda la función) —
297
+ que falte uno de los 3 es normal, no un error.
298
+
299
+ ## C4 — Project_iam/org_policies/project_services de `SOURCE_ENV` se copiaban sin verificar que estuvieran sanos
300
+
301
+ **Qué pasaba:** la estrategia "copiar tal cual" de A2/A7 asume
302
+ implícitamente que `SOURCE_ENV` ya tiene todo lo que un entorno nuevo
303
+ necesita. En la práctica, 4 de los 5 entornos reales de este repo NO
304
+ tienen `orgpolicy.googleapis.com` ni las 6 `org_policies` — construir
305
+ desde cualquiera de ellos (incluido `boletera-qa`, el ejemplo del README)
306
+ reproduce en silencio exactamente el error #7 de
307
+ `CreateScriptsNewGCPProjectInstructions.md`.
308
+ **Resuelto en:** `ksc_check_source_health` (nueva, en `lib/common.sh`,
309
+ invocada desde `02_print_config.sh` antes de confirmar) compara
310
+ `SOURCE_ENV` contra el resto de `live/` y advierte si le falta una API o
311
+ le faltan entradas de `org_policies` que **el entorno modificado más
312
+ recientemente** ya tiene. Se usa "el más reciente" y no "la mayoría"
313
+ porque un requisito nuevo de la organización, por definición, todavía NO
314
+ lo tiene la mayoría de entornos viejos — una votación por mayoría nunca
315
+ lo habría detectado (se probó: con mayoría, el hueco real de
316
+ `boletera-qa` no se veía; con "más reciente" sí). La misma función
317
+ también advierte si la IP fija de `cloudadmin` de `SOURCE_ENV` difiere de
318
+ la mayoría del resto de `live/` (ahí sí conviene mayoría, no "más
319
+ reciente" — una IP de oficina compartida rara vez cambia legítimamente).
320
+ Nunca bloquea nada — solo advierte, porque un `SOURCE_ENV` minimalista a
321
+ propósito es una posibilidad legítima que el humano puede confirmar.
322
+
323
+ ## C5 — Sustitución incompleta del nombre de la app fuera de `artifact_registry`
324
+
325
+ **Qué pasaba:** solo se sustituía el token completo del entorno
326
+ (`boletera-qa` → `shopdemo-qa`), nunca el nombre corto de la app
327
+ (`boletera`) ni su capitalizado (`Boletera`), salvo en
328
+ `artifact_registry`. Resultado real: `service_accounts` dejaba
329
+ `display_name = "Boletera QA Node Service Account"` y `project_iam`
330
+ dejaba namespaces/bindings con `boletera` sin tocar.
331
+ **Resuelto en:** `ksc_copy_component_files` (la función que TODO
332
+ `build_component_*` usa para copiar) ahora aplica, para cualquier
333
+ componente, tres sustituciones en orden: el token completo del entorno,
334
+ el nombre corto de la app en minúscula, y el nombre corto capitalizado —
335
+ una sola vez, en un solo lugar, en vez de que cada componente decidiera
336
+ por su cuenta cuáles aplicar (y se le olvidara a varios).
337
+
338
+ ## C6 — Editar `github_workload_identity` por posición era frágil, y no soportaba apps multi-repo
339
+
340
+ **Qué pasaba:** el binding de `github-manager` se identificaba por ser
341
+ la "2.ª coincidencia" de `principal_set_suffix` en el archivo — si
342
+ `SOURCE_ENV` tuviera los 2 bindings en el orden inverso, se habría
343
+ corrompido el binding de `terraform`/`infra-cloud` en vez del de la app.
344
+ Además, cuando `SOURCE_ENV` tenía más de 2 bindings (apps multi-repo como
345
+ `boletera-qa`, con 5), el skill solo advertía y dejaba los 5 nombres de
346
+ repo viejos sin tocar.
347
+ **Resuelto en:** `ksc_set_workload_identity_bindings` reconstruye el
348
+ bloque completo: ubica el binding de `terraform` **por contenido**
349
+ (`service_account_email` que empieza con `"terraform@"`, nunca por
350
+ posición) y genera un binding `github-manager` por cada repo listado en
351
+ `APP_GITHUB_REPO` (separados por `;`) — soporta tanto el caso simple (1
352
+ repo) como el multi-repo, sin advertir-y-no-tocar.
353
+
354
+ ## C6b — No todo entorno real tiene un binding de terraform en `github_workload_identity`
355
+
356
+ **Qué pasaba (encontrado verificando el fix de C6 con una CUARTA fuente,
357
+ `cashless-qa`, que ninguna ronda anterior de pruebas había usado):**
358
+ `cashless-qa` real solo tiene un binding `github-manager` — nunca tuvo
359
+ uno de `terraform`. `ksc_set_workload_identity_bindings` (y la extracción
360
+ previa en `build_component_github_workload_identity`) asumían que ese
361
+ binding siempre existe y fallaban la construcción completa si no lo
362
+ encontraban.
363
+ **Resuelto en:** si no hay binding de `terraform@...`, no es un error —
364
+ se usa `"infra-cloud"` como `principal_set_suffix` por defecto (el valor
365
+ que la guía §3.15 pide siempre) y se avisa con `[WARN]`, en vez de morir.
366
+
367
+ ## C7 — Lecciones menores de la misma revisión
368
+
369
+ - **Componentes opcionales copiados siempre:** el skill copiaba *todos*
370
+ los componentes que `SOURCE_ENV` tuviera, incluyendo los que la guía
371
+ (§1.2) pide explícitamente no incluir por defecto
372
+ (`cloud_armor`/`identity_platform`/`monitoring_alerts`/`secrets`).
373
+ Resuelto con `ksc_base_components` (intersección real de componentes en
374
+ TODOS los entornos de `live/`, sin lista hardcodeada — y excluyendo
375
+ explícitamente cualquier entorno que no siga el patrón `<app>-qa|prod>`,
376
+ como un proyecto de infraestructura compartida) + el campo
377
+ `EXTRA_COMPONENTS` del config para pedirlos de vuelta explícitamente.
378
+ - **Tracking como dependencia dura:** si `github-proyect-tracking` no
379
+ podía inicializar (sin red, token, URL inválida), toda la construcción
380
+ moría — aunque es 100% local. Resuelto: el tracking es ahora
381
+ best-effort (`[WARN]` + seguir sin reanudación remota).
382
+ - **`deletion_protection` en `*-prod`:** se copiaba de `SOURCE_ENV` sin
383
+ aviso; si `SOURCE_ENV` es qa (`"false"`) y `NEW_ENV` es prod, quedaba
384
+ `"false"` en prod en silencio. Resuelto con un `[WARN]` específico.
385
+ - **Clave HCL duplicada heredada de `SOURCE_ENV`** (ej.
386
+ `boletera-qa/sql_postgres` trae `update_timeout` dos veces dentro del
387
+ mismo bloque `inputs`): ni `terragrunt hcl fmt` ni `hcl validate` lo
388
+ detectan. Resuelto con `ksc_check_duplicate_keys`, corrido al final de
389
+ la construcción sobre cada componente (cuidado: tiene que ser
390
+ consciente de bloques `{ }` — una primera versión confundía
391
+ `locals.project_id` con `inputs.project_id` como "duplicado" por tener
392
+ la misma indentación).
393
+ - **Dos falsos positivos de los propios chequeos de este skill, encontrados
394
+ recién al probar con `xoloshop-qa` como fuente** (no con `boletera-qa`):
395
+ el comentario que el propio skill agrega en `project_iam` explicando por
396
+ qué no se puede usar `roles/orgpolicy.policyAdmin` ahí contiene ese
397
+ mismo string textualmente, y el chequeo de "¿quedó algo de SOURCE_ENV
398
+ sin sustituir?" no distinguía comentarios de código real. Ambos
399
+ chequeos ahora ignoran líneas que empiezan con `#`.
400
+ - **`terragrunt hcl validate` por componente** (guía §3.17) no se corría,
401
+ solo `hcl fmt`. Se agregó al final de la construcción, tolerando el
402
+ falso positivo conocido de `gke_private`.
403
+ - Bug de texto: un carácter de guion suave invisible (U+00AD) dentro de
404
+ "realmente" en un mensaje `[WARN]`, y la plantilla del config decía
405
+ "separadas por coma" cuando el formato real usa `;`. Ambos corregidos.
406
+
407
+ ---
408
+
409
+ ## D — Encontradas por `Review_V2.md` (re-revisión después de aplicar C)
410
+
411
+ `Review_V2.md` volvió a ejecutar el skill de punta a punta contra las
412
+ correcciones de la sección C, con varias fuentes y, de forma crítica,
413
+ con reintentos reales sobre entornos a medio construir. Encontró un bug
414
+ crítico nuevo — una regresión directa de C1 — y varios menores.
415
+
416
+ ## D1 (N-1) — CRÍTICO: la base de componentes se contaminaba con entornos sin comitear
417
+
418
+ **Qué pasaba:** `ksc_base_components` contaba cualquier directorio de
419
+ `live/` que pareciera un entorno (`^[a-z0-9]+-(qa|prod)$`) — incluido un
420
+ `live/<NEW_ENV>/` a medio construir (el estado normal de un reintento
421
+ desde C1) y, peor, cualquier OTRO entorno abandonado de una corrida
422
+ anterior, con cualquier nombre. Reproducido en cadena: un entorno
423
+ mutilado a 2 componentes hacía que un entorno nuevo e inocente **también**
424
+ construyera solo 2 componentes, con `FILE_PARITY_OK`/`PROJECT_PINNING_OK`
425
+ en verde — exactamente el defecto silencioso que este skill existe para
426
+ eliminar.
427
+ **Resuelto en:** `ksc_real_envs` (nueva, consolidada): un entorno solo
428
+ cuenta si además de cumplir el patrón de nombre tiene **al menos un
429
+ archivo comiteado en git** bajo su `live/<env>/`. Este skill nunca
430
+ comitea nada, así que cualquier intento (parcial o completo, propio o de
431
+ otro `NEW_ENV`) queda excluido automáticamente, sin tener que conocer su
432
+ nombre ni asumir que "bien formado" significa "completo". Se usa en
433
+ `ksc_base_components`, `ksc_check_source_health` y
434
+ `ksc_majority_fixed_network` — las tres tenían el mismo filtro
435
+ incompleto (D2).
436
+
437
+ ## D2 — El mismo filtro incompleto estaba duplicado en 3 funciones
438
+
439
+ **Qué pasaba (encontrado aplicando el fix de D1):** el filtro
440
+ "solo entornos reales" estaba copiado a mano en `ksc_base_components`,
441
+ `ksc_check_source_health` y `ksc_majority_fixed_network` por separado.
442
+ Corregirlo solo en la primera (como un primer intento de fix hizo)
443
+ dejaba las otras dos igual de contaminables — confirmado probando: con
444
+ dos entornos de prueba sin comitear en `live/`, el doctor reportaba "6/7
445
+ entornos reales" en vez de "4/5".
446
+ **Resuelto en:** las tres ahora llaman a `ksc_real_envs`, una sola
447
+ fuente de verdad. Lección operativa: cuando un fix corrige una función,
448
+ `grep` el resto del archivo por lógica duplicada antes de dar el fix por
449
+ completo.
450
+
451
+ ## D3 — Bug de `set -e` introducido por el propio fix de N-6
452
+
453
+ **Qué pasaba (encontrado probando el fix, antes de llegar a reportarlo):**
454
+ el código nuevo para preferir la IP mayoritaria (ver D5) usaba
455
+ `[[ condición ]] && variable=valor` como declaración suelta dentro de un
456
+ `for`, en un script con `set -e`. Cuando la condición daba falso para
457
+ **cualquier** entorno (ej. `live/shared/`, que no matchea
458
+ `<app>-qa|prod>`), el script completo moría ahí mismo — y como venía
459
+ ANTES del bloque que autodetecta `ORG_ID`/`GKE_VERSION`/etc., esos
460
+ campos quedaban `__PENDIENTE__` sin ninguna pista de por qué.
461
+ **Resuelto:** `if`/`fi` en vez de `&&` para cualquier asignación con
462
+ efecto secundario que dependa de una condición que puede ser falsa,
463
+ dentro de un script con `set -e`. Lección operativa: `A && B` es seguro
464
+ bajo `set -e` si B puede fallar (A fallar está exento), pero mezclar
465
+ asignaciones de esta forma invita a confundirse — mejor `if` siempre que
466
+ el cuerpo no sea un one-liner trivial como `continue`.
467
+
468
+ ## D4 (N-2) — `01_init_config.sh` moría en un rescate legítimo
469
+
470
+ **Qué pasaba:** después del fix de reanudación de C1, `10` ya no
471
+ fallaba por que `live/<NEW_ENV>/` existiera — pero `01` sí, con un
472
+ mensaje que literalmente decía "bórralo tú mismo primero", contradiciendo
473
+ la instrucción de `SKILL.md` de simplemente re-correr `10`.
474
+ **Resuelto en:** `01` ahora distingue "`live/<NEW_ENV>/` existe y el
475
+ config de este skill YA existe" (reintento legítimo, no es error) de
476
+ "existe pero no hay config" (colisión real con otra cosa, sí es error).
477
+
478
+ ## D5 (N-6) — El doctor avisaba del typo pero el config igual escribía el valor malo
479
+
480
+ **Qué pasaba:** `ksc_check_source_health` ya sabía que la mayoría de
481
+ `live/` usa `177.226.232.136/32` para `cloudadmin` y que `xoloshop-qa`
482
+ tiene un valor distinto — pero solo avisaba; `01_init_config.sh` seguía
483
+ autodetectando y escribiendo el valor de `SOURCE_ENV` tal cual.
484
+ **Resuelto en:** `ksc_majority_fixed_network` (extraída del doctor,
485
+ reusada) + lógica en `01_init_config.sh` que adopta el valor mayoritario
486
+ en el config cuando hay una mayoría clara (>mitad de los entornos
487
+ reales) y difiere del de `SOURCE_ENV`, con un `[WARN]` explicando el
488
+ cambio (no en silencio).
489
+
490
+ ## D6 (N-7) — El picker de CIDR preferiría la década 0 o rellenaría huecos bajos
491
+
492
+ **Qué pasaba:** el picker probaba décadas en orden `0, 10, 20, ...` —
493
+ técnicamente sin colisión, pero `10.0.x.x` se reserva convencionalmente
494
+ (VPN/defaults/on-prem) y rellenar huecos bajos (`10, 50, ...`) en vez de
495
+ seguir la secuencia real (`20, 30, 40, 130, 140`) es menos predecible
496
+ para quien revisa el PR.
497
+ **Resuelto en:** el orden de búsqueda ahora prioriza la primera década
498
+ libre **por encima** de la mayor ya usada, y la década 0 queda como
499
+ último recurso, no primera opción.
500
+
501
+ **Corrección posterior (Review V3, P-4):** la primera versión de esta
502
+ lección decía "el siguiente lógico es 150" — eso es matemáticamente
503
+ imposible con la fórmula real: `svc = 10.<década+off+106>.0.0/23`
504
+ necesita `década+off ≤ 149` para que el segundo octeto no pase de 255,
505
+ lo que limita la década máxima usable a **140** sin importar el offset.
506
+ El código ya se comportaba bien (`candidate()` devuelve `None` para
507
+ cualquier década inválida y el loop sigue), pero probaba décadas hasta
508
+ 240 sin necesidad; se acotó el rango generado a `0..140` para que el
509
+ código y el comentario coincidan con el techo real de la fórmula.
510
+
511
+ ## D7 — Mejora pendiente resuelta: `artifact_registry` determinista sin hardcodear la organización
512
+
513
+ Encontrada como mejora abierta en la propia `Review_V2.md` (no un bug):
514
+ cuando `SOURCE_ENV` tiene `repository_id`/`description` genéricos (4 de
515
+ 5 entornos reales), en vez de solo advertir, `ksc_learn_artifact_registry_template`
516
+ busca OTRO entorno de `live/` que sí tenga su propio nombre de app en su
517
+ `artifact_registry`, extrae el patrón reemplazando ese nombre por un
518
+ marcador, y lo aplica con el nombre de la app nueva. Nunca se hardcodea
519
+ "club-tijuana" ni ningún otro literal de la organización en el código
520
+ del skill — se aprende de los datos reales de `live/`, igual que el
521
+ doctor.
522
+
523
+ ## D8 — Lecciones menores de la misma re-revisión
524
+
525
+ - **Cobertura del leftover-check reducida (N-3):** V1 revisaba todos los
526
+ archivos de `live/<NEW_ENV>/`; el fix de ignorar comentarios (B-algo)
527
+ de la ronda anterior redujo por accidente la lista a solo
528
+ `terragrunt.hcl`/`project.hcl`/`README.md`, perdiendo
529
+ `Makefile`/`.gitignore` de cada componente. Restaurado con `find` sobre
530
+ todo el árbol (2 niveles), filtrando comentarios línea por línea en vez
531
+ de limitar por nombre de archivo.
532
+ - **WARNs del doctor no sobrevivían a la Etapa 1 (N-5):** `KSC_WARNINGS_FILE`
533
+ solo se fijaba en `10_build_components.sh`; si la sesión moría entre
534
+ `02` y `10`, los avisos del doctor se perdían. Ahora `02` también la fija
535
+ (truncando, fresca cada corrida) y `10` hace *append* en vez de truncar.
536
+ - **Código muerto (N-4):** una variable (`DUPLICATE_ISSUES`) que se
537
+ asignaba y nunca se leía. Eliminada.
538
+ - **`SKILL.md` no cubría `finalize` sin tracking (N-8):** si el tracking
539
+ nunca se inicializó, la instrucción de cerrar con `run_tracking.sh
540
+ finalize` fallaría sin explicación. Se agregó la condición explícita.
541
+ - **Cosmético (N-9):** `uninstall.sh --clean-gitignore` dejaba una línea
542
+ en blanco suelta al final del `.gitignore`.
543
+
544
+ ---
545
+
546
+ ## E1 (P-1) — La función nueva de D7 nació con la misma contaminación que D1/D2 ya habían corregido
547
+
548
+ **Qué pasaba:** `ksc_learn_artifact_registry_template` (agregada en la
549
+ misma ronda que D7) iteraba `live/*/` directamente, sin pasar por
550
+ `ksc_real_envs` — exactamente el filtro incompleto que D1/D2 ya habían
551
+ consolidado en las otras tres funciones. Reproducido: un entorno sin
552
+ comitear (`live/aaaajunk-qa/`) con `repository_id =
553
+ "aaaajunk-mega-custom-repo"` se "aprendía" como si fuera una plantilla
554
+ real, con un `[OK]` que además afirmaba (incorrectamente) que era un
555
+ "entorno real".
556
+ **Resuelto en:** la función ahora usa `ksc_real_envs` como las demás, y
557
+ de paso adoptó el mismo criterio de "preferir el más reciente" del
558
+ doctor en vez de "el primero alfabético" — dos entornos específicos con
559
+ nombres distintos ya no dependen de en qué orden los liste el
560
+ filesystem.
561
+ **Lección operativa, otra vez:** cada función nueva que itere `live/*/`
562
+ tiene que pasar por `ksc_real_envs` desde el día uno — no basta con
563
+ haberlo corregido en las funciones que ya existían (D2 ya dejó esta
564
+ lección una vez; aquí se repitió con una función que no existía
565
+ todavía cuando D2 se escribió).
566
+
567
+ ## E2 (P-2) — Rescate imposible si se perdía el config
568
+
569
+ **Qué pasaba:** si `.kcreate-scaffolding/<env>.config` se perdía (ej.
570
+ por `uninstall.sh --clean-gitignore`, que la borra a propósito) con
571
+ `live/<NEW_ENV>/` todavía a medias, `01_init_config.sh` trataba cualquier
572
+ `live/<NEW_ENV>/` sin config como colisión real y recomendaba borrar
573
+ exactamente lo que se quería rescatar — sin camino automático de vuelta.
574
+ **Resuelto en:** `01` ahora reconoce una firma mínima
575
+ (`project.hcl` con `project_id` == `NEW_ENV`, que `build_project_hcl`
576
+ siempre deja) para distinguir "rescate legítimo" de "colisión real", y
577
+ regenera el config desde cero en el primer caso.
578
+
579
+ ## E3 (P-3) — Los warnings de la Construcción se duplicaban en cada reintento
580
+
581
+ **Qué pasaba:** el fix de persistencia de D-faltante-17/N-5 usó un solo
582
+ archivo en modo *append* para doctor + build. Correcto para que el
583
+ doctor sobreviva entre `02` y `10`, pero cada reintento de `10` volvía a
584
+ escribir sus propios WARN sobre los de la corrida anterior — un archivo
585
+ que pasaba de 8 a 14 líneas con un solo retry, sin límite.
586
+ **Resuelto en:** dos archivos separados
587
+ (`ksc_warnings_file_path` para el doctor, `ksc_build_warnings_file_path`
588
+ para la construcción). El del doctor se trunca en cada `02` (sigue
589
+ fresco); el de la construcción se trunca al INICIO de cada `10` (no
590
+ repite sus propios WARN en un retry). El reporte final combina ambos.
591
+
592
+ ## E4 (P-4) — Comentario/lección describía un comportamiento matemáticamente imposible
593
+
594
+ **Qué pasaba:** D6 (y el comentario del código) decían que el picker de
595
+ CIDR, al no encontrar una década libre cerca, seguiría hasta "150, 160,
596
+ ...". La fórmula real (`svc = 10.<década+off+106>.0.0/23`) hace que
597
+ cualquier década ≥ 150 exceda 255 en el segundo octeto para todo
598
+ offset — esas décadas nunca se usan, sin importar qué tan lejos llegue
599
+ el rango de búsqueda. El código ya se comportaba bien (`candidate()`
600
+ devuelve `None` y el loop sigue), solo el comentario/lección prometían
601
+ algo que no podía pasar.
602
+ **Resuelto en:** el rango de búsqueda ahora se acota a `0..140` (el
603
+ techo real de la fórmula) y el comentario/D6 se corrigieron para
604
+ describir el comportamiento real, no uno aspiracional.
605
+
606
+ ## E5 — Observación: repo destino sin ningún commit
607
+
608
+ Si `live/` no tiene NINGÚN entorno comiteado (un repo de infra recién
609
+ creado, sin commit inicial), `ksc_real_envs` da una lista vacía y la
610
+ base de componentes también queda vacía — el build excluiría todo salvo
611
+ lo que `EXTRA_COMPONENTS` pida. No ocurre en un repo de infra real (que
612
+ siempre tiene historia), pero se agregó un `[WARN]` explícito en
613
+ `ksc_base_components` para ese caso, en vez de dejar que "todo se
614
+ excluye" pase sin ninguna pista de la causa.
615
+
616
+ ---
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
+
697
+ ## Qué significa esta lista para quien edite este skill después
698
+
699
+ Cada vez que se agregue un `build_component_<algo>` nuevo o se modifique
700
+ uno existente: **pruébalo contra una copia descartable de un repo real**
701
+ (nunca contra el repo de trabajo del usuario) antes de considerarlo
702
+ terminado, igual que se hizo para construir las lecciones B1-B8. La mitad
703
+ de los bugs reales de este skill nunca se habrían notado solo leyendo el
704
+ código — se encontraron ejecutándolo de punta a punta contra datos reales
705
+ y revisando el resultado campo por campo, no solo confiando en que los
706
+ checks automatizados pasaran.