@cat-indev/catops-cli 0.0.1-alpha.6 → 0.0.1-alpha.7
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 +264 -194
- package/dist/core/messages.d.ts +63 -0
- package/dist/core/messages.js +84 -2
- package/dist/core/messages.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
# devops-cli
|
|
2
2
|
|
|
3
|
-
Framework
|
|
3
|
+
Framework para pipelines DevOps, escrito en **TypeScript** (100% usable desde JavaScript puro), empaquetado como librería npm instalable en cualquier proyecto.
|
|
4
|
+
|
|
5
|
+
Trae:
|
|
6
|
+
|
|
7
|
+
- Un **ExecutionContext** compartido (`flags`, `params`, `env`, `vars`, `results`, `logger`, `services`, `notifier`) para que ninguna task tenga que recibir parámetros manualmente.
|
|
8
|
+
- **14 servicios** listos (`shell`, `docker`, `git`, `kubectl`, `helm`, `npm`, `archive`, `terraform`, `ansible`, `argocd`, `tekton`, `oc`, `az`, `azdo`).
|
|
9
|
+
- **Menús interactivos** con navegación anidada y **selección automática por flag** (para correr pipelines sin prompts, ideal para CI).
|
|
10
|
+
- **retry / timeout / dryRun** en cada comando de shell.
|
|
11
|
+
- Validación cíclica de rollouts de Kubernetes/OpenShift (`waitForDeployment`, `waitForDeploymentGroup`).
|
|
12
|
+
- **Callbacks de éxito/error por tarea** + un sistema de **notificaciones clasificadas por área de TI**, con mensajes personalizables y senders (`log`, `file`, `http`, `webhook`, `websocket`).
|
|
4
13
|
|
|
5
14
|
## Instalación
|
|
6
15
|
|
|
@@ -16,10 +25,10 @@ npm install @miorg/devops-cli
|
|
|
16
25
|
|
|
17
26
|
```bash
|
|
18
27
|
# Dentro del repo de devops-cli
|
|
19
|
-
npm pack # genera devops-cli
|
|
28
|
+
npm pack # genera devops-cli-<version>.tgz
|
|
20
29
|
|
|
21
30
|
# Dentro del proyecto que lo va a consumir
|
|
22
|
-
npm install /ruta/a/devops-cli
|
|
31
|
+
npm install /ruta/a/devops-cli-<version>.tgz
|
|
23
32
|
```
|
|
24
33
|
|
|
25
34
|
**Opción C — enlazado local con `npm link` (para desarrollar la librería y el proyecto que la consume al mismo tiempo):**
|
|
@@ -40,7 +49,13 @@ npm install git+https://github.com/tu-org/devops-cli.git
|
|
|
40
49
|
|
|
41
50
|
Cualquiera de las 4 deja disponibles dos cosas en el proyecto consumidor:
|
|
42
51
|
|
|
43
|
-
1. La librería
|
|
52
|
+
1. La librería, tanto desde TS como desde JS puro:
|
|
53
|
+
```typescript
|
|
54
|
+
import { Context, Menu, services, type MenuDefinition } from "devops-cli";
|
|
55
|
+
```
|
|
56
|
+
```javascript
|
|
57
|
+
const { Context, Menu, services } = require("devops-cli");
|
|
58
|
+
```
|
|
44
59
|
2. El binario: `npx devops-cli` (o `devops-cli` si lo instalaste global con `-g`).
|
|
45
60
|
|
|
46
61
|
## Publicar una nueva versión
|
|
@@ -50,63 +65,41 @@ npm version patch # o minor / major
|
|
|
50
65
|
npm publish # agrega --access public si usas un scope (@miorg/devops-cli)
|
|
51
66
|
```
|
|
52
67
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
Une dos piezas que ya tenías:
|
|
68
|
+
`prepublishOnly` corre el build y los tests automáticamente antes de publicar.
|
|
56
69
|
|
|
57
|
-
|
|
58
|
-
2. Los **servicios de shell** (`shell`, `docker`, `git`, `kubectl`, `helm`, `npm`, `archive`), integrados **tal cual** los diste, sin reescribir su lógica.
|
|
70
|
+
## TypeScript
|
|
59
71
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
## Estructura
|
|
72
|
+
Todo `src/` está escrito en TypeScript, con `strict: true`. `npm run build` compila a `dist/` (JS + `.d.ts` + source maps por archivo) — eso es lo único que se publica (ver `files` en `package.json`).
|
|
63
73
|
|
|
64
74
|
```
|
|
65
75
|
src/
|
|
66
76
|
core/
|
|
67
|
-
Context.
|
|
68
|
-
Menu.
|
|
69
|
-
|
|
70
|
-
|
|
77
|
+
Context.ts -> ExecutionContext singleton (Context.current() / Context.parseArgv())
|
|
78
|
+
Menu.ts -> Menu.render() con navegación anidada + selección automática por flag
|
|
79
|
+
Notifier.ts -> clasificación de errores por área + canales + senders
|
|
80
|
+
classifiers.ts -> fábricas de ErrorClassifier: byCommand, byPattern
|
|
81
|
+
messages.ts -> fábricas de ErrorMessageFormatter: byPattern, byCommand, byRule
|
|
82
|
+
senders.ts -> fábricas de Sender: log, file, http, webhook, websocket
|
|
83
|
+
prompt.ts -> ctx.ask / ctx.confirm / ctx.select (sin dependencias externas)
|
|
84
|
+
logger.ts -> logger usado por Context y por shell.ts
|
|
85
|
+
types.ts -> tipos compartidos (MenuDefinition, ExecOptions, NotificationEvent, ...)
|
|
71
86
|
services/
|
|
72
|
-
shell.
|
|
73
|
-
docker.
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
terraform.js -> init/plan/apply/destroy/output/validate/fmt
|
|
80
|
-
ansible.js -> playbook/adhoc/vaultEncrypt/vaultDecrypt/galaxyInstall
|
|
81
|
-
argocd.js -> login/appSync/appGet/appWait/appSet/appList/appRollback
|
|
82
|
-
tekton.js -> pipelineStart/pipelinerunList/pipelinerunLogs/taskStart/taskrunLogs
|
|
83
|
-
oc.js -> login/project/apply/get/rollout/newApp/startBuild/logs
|
|
84
|
-
az.js -> loginServicePrincipal/acrBuild/webappDeploy/aksGetCredentials/...
|
|
85
|
-
azdo.js -> logging commands de Azure Pipelines (##vso)
|
|
86
|
-
index.js -> registra todos los servicios anteriores
|
|
87
|
-
index.js -> exporta { Context, Menu, logger, prompt, services }
|
|
87
|
+
shell.ts -> motor base (spawn), con retry/timeout/dryRun
|
|
88
|
+
docker.ts, git.ts, kubectl.ts, helm.ts, npm.ts, archive.ts
|
|
89
|
+
terraform.ts, ansible.ts, argocd.ts, tekton.ts, oc.ts, az.ts, azdo.ts
|
|
90
|
+
index.ts -> registra todos los servicios anteriores (ServicesRegistry)
|
|
91
|
+
index.ts -> entry point público: Context, Menu, Notifier, senders, classifiers, messages, services, tipos
|
|
92
|
+
bin/
|
|
93
|
+
devops-cli.ts -> CLI ejecutable (busca devops.pipeline.js en el proyecto consumidor)
|
|
88
94
|
examples/
|
|
89
|
-
pipeline-example.js
|
|
95
|
+
pipeline-example.js -> pipeline + menú + notificaciones de ejemplo, corre contra dist/
|
|
90
96
|
test/
|
|
91
|
-
context.test.js
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
## Qué se corrigió para que "convivan"
|
|
97
|
-
|
|
98
|
-
- `shell.js` usaba `logger.info(...)` / `logger.error(...)` sin importarlo. Se agregó `const logger = require("../core/logger")`.
|
|
99
|
-
- `Context.js` hacía `this.logger = logger` sin importar `logger` tampoco. Ahora importa `./logger`.
|
|
100
|
-
- Se agregó `this.services = services` dentro del constructor de `Context`, cableando exactamente el "Registry" que proponías al final de tu mensaje:
|
|
101
|
-
|
|
102
|
-
```javascript
|
|
103
|
-
ctx.services.git.clone(...)
|
|
104
|
-
ctx.services.docker.build(...)
|
|
97
|
+
context.test.js, shell.test.js, services.test.js, menu-selector.test.js,
|
|
98
|
+
notifier.test.js, senders.test.js, hooks-integration.test.js,
|
|
99
|
+
kubectl.test.js, oc.test.js, deployment-group.test.js
|
|
105
100
|
```
|
|
106
101
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
## Uso dentro de un proyecto que lo instaló
|
|
102
|
+
## Uso rápido: menú con `devops.pipeline.js` + el bin
|
|
110
103
|
|
|
111
104
|
Crea un `devops.pipeline.js` (o `devops.config.js` / `.devops-cli.js`) en la raíz de tu proyecto:
|
|
112
105
|
|
|
@@ -116,34 +109,53 @@ module.exports = (ctx) => ({
|
|
|
116
109
|
title: "Pipeline",
|
|
117
110
|
options: {
|
|
118
111
|
Build: async () => {
|
|
119
|
-
await ctx.services.docker.build({
|
|
120
|
-
image: "registry/app:v1",
|
|
121
|
-
dockerfile: "Dockerfile"
|
|
122
|
-
});
|
|
112
|
+
await ctx.services.docker.build({ image: "registry/app:v1", dockerfile: "Dockerfile" });
|
|
123
113
|
},
|
|
124
114
|
Deploy: async () => {
|
|
125
|
-
await ctx.services.kubectl.apply("deployment.yaml");
|
|
115
|
+
await ctx.services.kubectl.apply("deployment.yaml", { namespace: "prod" });
|
|
126
116
|
}
|
|
127
117
|
}
|
|
128
118
|
});
|
|
129
119
|
```
|
|
130
120
|
|
|
131
|
-
Y ejecuta:
|
|
132
|
-
|
|
133
121
|
```bash
|
|
134
122
|
npx devops-cli --debug --env=prod
|
|
135
123
|
```
|
|
136
124
|
|
|
137
125
|
`devops-cli` detecta el archivo, arma el `Context` a partir de los flags/params de `argv`, y renderiza el menú.
|
|
138
126
|
|
|
139
|
-
## Uso
|
|
127
|
+
## Uso directo en tu propio script (p. ej. con `tsx`)
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
// src/index.ts
|
|
131
|
+
import { Context, Menu, type MenuDefinition } from "devops-cli";
|
|
132
|
+
|
|
133
|
+
const ctx = Context.parseArgv();
|
|
134
|
+
|
|
135
|
+
const mainMenu: MenuDefinition = {
|
|
136
|
+
title: "Pipeline",
|
|
137
|
+
"flag-selector": "--menu-selector",
|
|
138
|
+
options: {
|
|
139
|
+
Build: { selector: "build", action: () => ctx.services.docker.build({ image: "app:v1" }) }
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
Menu.render(mainMenu, ctx);
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
```json
|
|
147
|
+
{ "scripts": { "dev": "tsx src/index.ts" } }
|
|
148
|
+
```
|
|
140
149
|
|
|
141
150
|
```bash
|
|
142
|
-
|
|
151
|
+
npx tsx src/index.ts --menu-selector=build
|
|
152
|
+
npm run dev -- --menu-selector=build # con npm hace falta el "--" para reenviar flags
|
|
143
153
|
```
|
|
144
154
|
|
|
155
|
+
## Uso como librería sin menú (pipeline lineal)
|
|
156
|
+
|
|
145
157
|
```javascript
|
|
146
|
-
const { Context
|
|
158
|
+
const { Context } = require("devops-cli"); // o require("./dist") dentro de este repo
|
|
147
159
|
|
|
148
160
|
const ctx = Context.parseArgv(); // llena flags/params desde argv
|
|
149
161
|
|
|
@@ -153,83 +165,74 @@ await ctx.services.git.checkout("develop");
|
|
|
153
165
|
await ctx.services.npm.ci();
|
|
154
166
|
await ctx.services.docker.build({ image: ctx.get("image"), dockerfile: "Dockerfile" });
|
|
155
167
|
await ctx.services.docker.push(ctx.get("image"));
|
|
156
|
-
await ctx.services.kubectl.apply("deployment.yaml");
|
|
168
|
+
await ctx.services.kubectl.apply("deployment.yaml", { namespace: "prod" });
|
|
157
169
|
```
|
|
158
170
|
|
|
159
|
-
|
|
171
|
+
## Menús: definición, anidamiento y selectores automáticos por flag
|
|
160
172
|
|
|
161
|
-
|
|
162
|
-
await Menu.render({
|
|
163
|
-
title: "Deploy",
|
|
164
|
-
options: {
|
|
165
|
-
Build: buildTask,
|
|
166
|
-
Docker: dockerMenu, // submenú anidado
|
|
167
|
-
Publish: publishTask
|
|
168
|
-
}
|
|
169
|
-
});
|
|
170
|
-
```
|
|
173
|
+
Un `MenuDefinition` es `{ title, options }`, donde cada entrada de `options` puede ser:
|
|
171
174
|
|
|
172
|
-
|
|
175
|
+
- una **función** — task directa: `Build: () => {...}`
|
|
176
|
+
- otro **`MenuDefinition`** — submenú directo: `Docker: dockerMenu`
|
|
177
|
+
- un **objeto largo** — para poder darle `selector`, `onSuccess`/`onError`, o envolver un submenú:
|
|
178
|
+
```javascript
|
|
179
|
+
Build: { selector: "build", action: () => {...}, onSuccess: (r, ctx) => {...}, onError: (e, ctx) => {...} }
|
|
180
|
+
Docker: { selector: "docker", menu: dockerMenu }
|
|
181
|
+
// también podés inlinear el submenú directo con su propio selector al lado:
|
|
182
|
+
Docker: { selector: "docker", title: "Docker", "flag-selector": "--docker-action", options: {...} }
|
|
183
|
+
```
|
|
173
184
|
|
|
174
|
-
|
|
185
|
+
### Selección automática por flag
|
|
175
186
|
|
|
176
|
-
|
|
177
|
-
await ctx.services.docker.push(image); // igual que siempre
|
|
187
|
+
Cualquier `MenuDefinition` puede declarar `"flag-selector": "--algun-flag"`. Si el `Context` trae un param que matchea el `selector` de alguno de sus items, esa opción se ejecuta **automáticamente, sin ningún prompt**:
|
|
178
188
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
}
|
|
185
|
-
|
|
189
|
+
```javascript
|
|
190
|
+
const dockerMenu = {
|
|
191
|
+
title: "Docker",
|
|
192
|
+
"flag-selector": "--docker-action",
|
|
193
|
+
options: {
|
|
194
|
+
Build: { selector: "build", action: buildTask },
|
|
195
|
+
Push: { selector: "push", action: pushTask }
|
|
196
|
+
}
|
|
197
|
+
};
|
|
186
198
|
|
|
187
|
-
|
|
199
|
+
const mainMenu = {
|
|
200
|
+
title: "Pipeline",
|
|
201
|
+
"flag-selector": "--menu-selector",
|
|
202
|
+
options: {
|
|
203
|
+
Docker: { selector: "docker", menu: dockerMenu },
|
|
204
|
+
Deploy: { selector: "deploy", action: deployTask }
|
|
205
|
+
}
|
|
206
|
+
};
|
|
188
207
|
|
|
189
|
-
|
|
190
|
-
ctx.services.shell.configure({ retry: 3, timeout: 30000 });
|
|
208
|
+
Menu.render(mainMenu, ctx);
|
|
191
209
|
```
|
|
192
210
|
|
|
193
|
-
Y `Context.parseArgv()` ya conecta flags de línea de comandos automáticamente:
|
|
194
|
-
|
|
195
211
|
```bash
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
```
|
|
212
|
+
# encadena ambos niveles en un solo comando, sin ningún prompt interactivo:
|
|
213
|
+
devops-cli --menu-selector=docker --docker-action=build
|
|
199
214
|
|
|
200
|
-
|
|
215
|
+
# un solo nivel:
|
|
216
|
+
devops-cli --menu-selector=deploy
|
|
201
217
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
ctx.services.azdo.logWarning("El caché de npm no se encontró, se reconstruye desde cero.");
|
|
205
|
-
ctx.services.azdo.group("Build");
|
|
206
|
-
// ... pasos ...
|
|
207
|
-
ctx.services.azdo.endGroup();
|
|
218
|
+
# sin flags -> menú interactivo normal
|
|
219
|
+
devops-cli
|
|
208
220
|
```
|
|
209
221
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
```javascript
|
|
213
|
-
await ctx.services.terraform.plan({ varFile: "prod.tfvars" });
|
|
214
|
-
await ctx.services.terraform.apply();
|
|
215
|
-
|
|
216
|
-
await ctx.services.ansible.playbook("site.yml", { inventory: "hosts.ini" });
|
|
217
|
-
|
|
218
|
-
await ctx.services.argocd.appSync("mi-app", { prune: true });
|
|
219
|
-
|
|
220
|
-
await ctx.services.tekton.pipelineStart("build-pipeline", { params: { image: "app:v1" } });
|
|
222
|
+
Si el valor del flag no matchea ningún `selector` del nivel actual, cae de vuelta al menú interactivo (con un warning), en vez de fallar en seco. Cada submenú revisa su **propio** `flag-selector` de forma independiente, así que podés automatizar tantos niveles como quieras encadenando flags.
|
|
221
223
|
|
|
222
|
-
|
|
223
|
-
await ctx.services.oc.rollout("mi-app");
|
|
224
|
+
### Callbacks de éxito/error por item
|
|
224
225
|
|
|
225
|
-
|
|
226
|
+
```javascript
|
|
227
|
+
Deploy: {
|
|
228
|
+
selector: "deploy",
|
|
229
|
+
action: () => ctx.services.kubectl.apply("deployment.yaml"),
|
|
230
|
+
onSuccess: (result, ctx) => ctx.logger.success("Deploy OK"),
|
|
231
|
+
onError: (error, ctx) => ctx.logger.error(`Deploy falló: ${error.message}`)
|
|
232
|
+
}
|
|
226
233
|
```
|
|
227
234
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
Cada tarea (`ctx.run()` o un item de menú) puede llevar sus propios callbacks, y además reporta automáticamente al `notifier` global del `Context`.
|
|
231
|
-
|
|
232
|
-
### Callbacks por tarea
|
|
235
|
+
Mismo patrón con `ctx.run()` fuera de un menú:
|
|
233
236
|
|
|
234
237
|
```javascript
|
|
235
238
|
await ctx.run(
|
|
@@ -242,87 +245,54 @@ await ctx.run(
|
|
|
242
245
|
);
|
|
243
246
|
```
|
|
244
247
|
|
|
245
|
-
En
|
|
246
|
-
|
|
247
|
-
```javascript
|
|
248
|
-
options: {
|
|
249
|
-
Deploy: {
|
|
250
|
-
selector: "deploy",
|
|
251
|
-
action: () => ctx.services.kubectl.apply("deployment.yaml"),
|
|
252
|
-
onSuccess: (result, ctx) => {...},
|
|
253
|
-
onError: (error, ctx) => {...}
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
```
|
|
248
|
+
En ambos casos, además de tus callbacks, el resultado se reporta automáticamente al `ctx.notifier` (ver más abajo) — no hay que llamarlo a mano.
|
|
257
249
|
|
|
258
|
-
|
|
250
|
+
## retry / timeout / dryRun en shell.exec
|
|
259
251
|
|
|
260
|
-
`
|
|
252
|
+
`shell.exec(command, ...args)` sigue aceptando exactamente los mismos argumentos de siempre. Si el último argumento es un objeto plano, se interpreta como opciones **solo para esa llamada**:
|
|
261
253
|
|
|
262
254
|
```javascript
|
|
263
|
-
|
|
255
|
+
await ctx.services.docker.push(image); // igual que siempre
|
|
264
256
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
git: "scm",
|
|
273
|
-
argocd: "cd-pipeline",
|
|
274
|
-
tkn: "cd-pipeline",
|
|
275
|
-
az: "cloud-azure"
|
|
276
|
-
}));
|
|
257
|
+
await ctx.services.shell.exec("curl", "https://flaky-api.internal", {
|
|
258
|
+
retry: 3, // reintentos totales (default: 1 = sin retry)
|
|
259
|
+
retryDelay: 1000, // ms entre reintentos
|
|
260
|
+
timeout: 5000, // ms antes de matar el proceso con SIGTERM
|
|
261
|
+
dryRun: true // solo loguea el comando, no lo ejecuta
|
|
262
|
+
});
|
|
263
|
+
```
|
|
277
264
|
|
|
278
|
-
|
|
279
|
-
ctx.notifier.classify(classifiers.byPattern([
|
|
280
|
-
[/permission denied|unauthorized/i, "security"],
|
|
281
|
-
[/timeout|ECONNREFUSED/i, "networking"],
|
|
282
|
-
[/no space left|ENOSPC/i, "infra"]
|
|
283
|
-
]));
|
|
265
|
+
Defaults globales para todo el proceso:
|
|
284
266
|
|
|
285
|
-
|
|
286
|
-
ctx.
|
|
287
|
-
ctx.notifier.channel("security", senders.http({ url: "https://security.miempresa.com/incidents" }));
|
|
288
|
-
ctx.notifier.channel("*", senders.file({ path: "./devops-cli-errors.log" })); // TODO error, sin importar el área
|
|
289
|
-
|
|
290
|
-
// 3. (opcional) éxito, sin clasificación de área
|
|
291
|
-
ctx.notifier.onSuccess(senders.log());
|
|
267
|
+
```javascript
|
|
268
|
+
ctx.services.shell.configure({ retry: 3, timeout: 30000 });
|
|
292
269
|
```
|
|
293
270
|
|
|
294
|
-
|
|
271
|
+
`Context.parseArgv()` ya conecta flags de línea de comandos automáticamente:
|
|
295
272
|
|
|
273
|
+
```bash
|
|
274
|
+
npx devops-cli --dry-run # activa dryRun global
|
|
275
|
+
npx devops-cli --retry=3 --timeout=15000
|
|
296
276
|
```
|
|
297
|
-
const { classifiers, messages } = require("devops-cli");
|
|
298
|
-
|
|
299
|
-
ctx.notifier.classify(classifiers.byCommand({ docker: "containers" }));
|
|
300
277
|
|
|
301
|
-
|
|
302
|
-
[/500 Internal Server Error/, "Se ha reportado a infraestructura: falta de espacio en el registry"],
|
|
303
|
-
[/unauthorized|403/i, "Credenciales inválidas contra el registry, revisa el secret"]
|
|
304
|
-
]));
|
|
278
|
+
## Servicios de infraestructura incluidos
|
|
305
279
|
|
|
306
|
-
|
|
307
|
-
|
|
280
|
+
```javascript
|
|
281
|
+
await ctx.services.terraform.plan({ varFile: "prod.tfvars" });
|
|
282
|
+
await ctx.services.terraform.apply();
|
|
308
283
|
|
|
309
|
-
|
|
284
|
+
await ctx.services.ansible.playbook("site.yml", { inventory: "hosts.ini" });
|
|
310
285
|
|
|
311
|
-
|
|
286
|
+
await ctx.services.argocd.appSync("mi-app", { prune: true });
|
|
312
287
|
|
|
313
|
-
|
|
314
|
-
|---|---|
|
|
315
|
-
| `senders.log()` | Usa el logger interno (consola) |
|
|
316
|
-
| `senders.file({ path })` | Agrega el evento como una línea JSON al archivo |
|
|
317
|
-
| `senders.http({ url, method?, headers?, formatBody? })` | `POST` genérico del evento como JSON |
|
|
318
|
-
| `senders.webhook({ url, format? })` | Como `http`, pero formatea `{ text: "❌ ..." }` por defecto (Slack/Teams/Discord-friendly) |
|
|
319
|
-
| `senders.websocket({ url, timeout? })` | Abre una conexión WS, manda el evento como JSON y cierra. Requiere Node ≥21 (usa el `WebSocket` global) |
|
|
288
|
+
await ctx.services.tekton.pipelineStart("build-pipeline", { params: { image: "app:v1" } });
|
|
320
289
|
|
|
321
|
-
|
|
290
|
+
await ctx.services.az.acrBuild({ registry: "miregistro", image: "app:v1" });
|
|
291
|
+
```
|
|
322
292
|
|
|
323
293
|
## kubectl / oc: kubeconfig, namespace y espera cíclica del rollout
|
|
324
294
|
|
|
325
|
-
`kubectl` y `oc`
|
|
295
|
+
`kubectl` y `oc` aceptan `{ kubeconfig, namespace }` como último argumento en **todos** sus comandos (retrocompatible, sigue funcionando sin ese argumento):
|
|
326
296
|
|
|
327
297
|
```javascript
|
|
328
298
|
await ctx.services.kubectl.apply("deploy.yaml", { kubeconfig: "/etc/kube/prod.yaml", namespace: "prod" });
|
|
@@ -334,21 +304,16 @@ await ctx.services.oc.apply("deploy.yaml", { namespace: "prod" });
|
|
|
334
304
|
|
|
335
305
|
### `waitForDeployment` — validación cíclica del rollout
|
|
336
306
|
|
|
337
|
-
Sondea el Deployment (o `DeploymentConfig` con `oc`) hasta que:
|
|
307
|
+
Sondea el Deployment (o `DeploymentConfig` con `oc` + `resourceType: "dc"`) hasta que:
|
|
338
308
|
|
|
339
309
|
- llega a **estado exitoso** (réplicas listas/actualizadas == deseadas) → resuelve con `{ status: "success", ... }`,
|
|
340
310
|
- se queda en **estado Failed** más de `failedGracePeriod` sin recuperarse → lanza `DeploymentRolloutError`,
|
|
341
311
|
- supera **`maxRestarts`** reinicios acumulados entre todos sus pods → lanza `DeploymentRolloutError` de inmediato, sin esperar el grace period,
|
|
342
312
|
- o se cumple el **`timeout`** global sin éxito → lanza `DeploymentRolloutError`.
|
|
343
313
|
|
|
344
|
-
En los tres casos de fallo, el polling se detiene
|
|
314
|
+
En los tres casos de fallo, el polling se detiene y el error se re-lanza — listo para que `ctx.run(...)` lo capture y lo reporte automáticamente vía `ctx.notifier` (el error ya trae `command: "kubectl"` / `command: "oc"`, así que `classifiers.byCommand({ kubectl: "kubernetes" })` lo clasifica sin configuración extra).
|
|
345
315
|
|
|
346
316
|
```javascript
|
|
347
|
-
const { classifiers } = require("devops-cli");
|
|
348
|
-
|
|
349
|
-
ctx.notifier.classify(classifiers.byCommand({ kubectl: "kubernetes", oc: "kubernetes" }));
|
|
350
|
-
ctx.notifier.channel("kubernetes", senders.webhook({ url: process.env.SLACK_K8S_WEBHOOK }));
|
|
351
|
-
|
|
352
317
|
await ctx.run("deploy-api", async () => {
|
|
353
318
|
await ctx.services.kubectl.apply("deployment.yaml", { namespace: "prod" });
|
|
354
319
|
|
|
@@ -363,11 +328,9 @@ await ctx.run("deploy-api", async () => {
|
|
|
363
328
|
});
|
|
364
329
|
```
|
|
365
330
|
|
|
366
|
-
Con `oc`, usa `resourceType: "dc"` para apuntar a un `DeploymentConfig` clásico de OpenShift en vez de un `Deployment` nativo (default: `"deployment"`).
|
|
367
|
-
|
|
368
331
|
### `waitForDeploymentGroup` — validar todas las instancias de un mismo despliegue GitOps
|
|
369
332
|
|
|
370
|
-
Pensada para el caso de GitOps donde un mismo repo termina desplegado como **varios Deployments** (una instancia por región/config/cliente, etc.), todos marcados con un label común
|
|
333
|
+
Pensada para el caso de GitOps donde un mismo repo termina desplegado como **varios Deployments** (una instancia por región/config/cliente, etc.), todos marcados con un label común:
|
|
371
334
|
|
|
372
335
|
```yaml
|
|
373
336
|
metadata:
|
|
@@ -375,7 +338,7 @@ metadata:
|
|
|
375
338
|
deployment-group: repository-14
|
|
376
339
|
```
|
|
377
340
|
|
|
378
|
-
|
|
341
|
+
Descubre todas las instancias que compartan ese label y corre `waitForDeployment` sobre **cada una en paralelo**, con el mismo `timeout`/`pollInterval`/`failedGracePeriod`/`maxRestarts` para todas:
|
|
379
342
|
|
|
380
343
|
```javascript
|
|
381
344
|
await ctx.run("deploy-repo-14", () =>
|
|
@@ -391,10 +354,107 @@ await ctx.run("deploy-repo-14", () =>
|
|
|
391
354
|
```
|
|
392
355
|
|
|
393
356
|
- Si **todas** llegan a estado exitoso → resuelve con `{ status: "success", deployments: [...] }` (el detalle de cada una).
|
|
394
|
-
- Si **alguna falla**
|
|
357
|
+
- Si **alguna falla** → espera a que las demás terminen, y lanza `DeploymentGroupRolloutError` con `succeeded` (nombres que sí llegaron) y `failed` (nombre + status + mensaje de cada una que no).
|
|
395
358
|
- Si el label **no matchea ningún deployment**, también lanza `DeploymentGroupRolloutError` (grupo vacío = error, no éxito silencioso).
|
|
396
359
|
|
|
397
|
-
|
|
360
|
+
Con `oc`, ambas funciones aceptan `resourceType: "dc"` para apuntar a `DeploymentConfig` clásico en vez de `Deployment` nativo (default: `"deployment"`).
|
|
361
|
+
|
|
362
|
+
## Logging commands de Azure Pipelines (`ctx.services.azdo`)
|
|
363
|
+
|
|
364
|
+
```javascript
|
|
365
|
+
ctx.services.azdo.setVariable("BUILD_TAG", "v1.2.3");
|
|
366
|
+
ctx.services.azdo.logWarning("El caché de npm no se encontró, se reconstruye desde cero.");
|
|
367
|
+
ctx.services.azdo.group("Build");
|
|
368
|
+
// ... pasos ...
|
|
369
|
+
ctx.services.azdo.endGroup();
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
## Notificaciones: clasificar errores por área de TI, personalizar el mensaje, y enviarlos
|
|
373
|
+
|
|
374
|
+
`ctx.notifier` tiene tres responsabilidades independientes:
|
|
375
|
+
|
|
376
|
+
1. **`classify()`** — decide a qué **área de TI** pertenece un error (para elegir a qué canal mandarlo).
|
|
377
|
+
2. **`describeError()`** — decide el **mensaje** a reportar (reemplaza el stderr crudo por algo humano).
|
|
378
|
+
3. **`channel()`** / **`onSuccess()`** — a qué **senders** se manda cada área.
|
|
379
|
+
|
|
380
|
+
```javascript
|
|
381
|
+
const { classifiers, messages, senders } = require("devops-cli");
|
|
382
|
+
|
|
383
|
+
// 1. ¿A qué área de TI pertenece este error?
|
|
384
|
+
ctx.notifier.classify(classifiers.byCommand({
|
|
385
|
+
docker: "containers",
|
|
386
|
+
kubectl: "kubernetes",
|
|
387
|
+
oc: "kubernetes",
|
|
388
|
+
terraform: "infra",
|
|
389
|
+
ansible: "infra",
|
|
390
|
+
git: "scm",
|
|
391
|
+
argocd: "cd-pipeline",
|
|
392
|
+
tkn: "cd-pipeline",
|
|
393
|
+
az: "cloud-azure"
|
|
394
|
+
}));
|
|
395
|
+
|
|
396
|
+
// también podés clasificar por el texto del error:
|
|
397
|
+
ctx.notifier.classify(classifiers.byPattern([
|
|
398
|
+
[/permission denied|unauthorized/i, "security"],
|
|
399
|
+
[/timeout|ECONNREFUSED/i, "networking"],
|
|
400
|
+
[/no space left|ENOSPC/i, "infra"]
|
|
401
|
+
]));
|
|
402
|
+
|
|
403
|
+
// 2. ¿qué mensaje se reporta? (opcional — sin esto, se usa el stderr crudo)
|
|
404
|
+
ctx.notifier.describeError(messages.byRule([
|
|
405
|
+
{
|
|
406
|
+
command: "docker", args: "push", pattern: /500 Internal Server Error/,
|
|
407
|
+
message: "Se ha reportado a infraestructura: falta de espacio en el registry"
|
|
408
|
+
},
|
|
409
|
+
{
|
|
410
|
+
command: "kubectl", pattern: /500/,
|
|
411
|
+
message: "El API server de Kubernetes devolvió 500, reintenta en unos minutos"
|
|
412
|
+
},
|
|
413
|
+
{
|
|
414
|
+
command: "terraform", pattern: /500/,
|
|
415
|
+
message: (error, ctx) => `Backend remoto de Terraform no respondió (env: ${ctx.params.env ?? "?"})`
|
|
416
|
+
}
|
|
417
|
+
]));
|
|
418
|
+
|
|
419
|
+
// 3. ¿a dónde se manda cada área?
|
|
420
|
+
ctx.notifier.channel("kubernetes", senders.webhook({ url: process.env.TEAMS_WEBHOOK }));
|
|
421
|
+
ctx.notifier.channel("security", senders.http({ url: "https://security.miempresa.com/incidents" }));
|
|
422
|
+
ctx.notifier.channel("*", senders.file({ path: "./devops-cli-errors.log" })); // TODO error, sin importar el área
|
|
423
|
+
|
|
424
|
+
// (opcional) éxito, sin clasificación de área
|
|
425
|
+
ctx.notifier.onSuccess(senders.log());
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
A partir de aquí, cualquier `ctx.run(...)` o item de menú con `action` reporta automáticamente al notifier — no hay que llamarlo a mano en cada task.
|
|
429
|
+
|
|
430
|
+
### Clasificadores de área (`classifiers`)
|
|
431
|
+
|
|
432
|
+
| Fábrica | Uso |
|
|
433
|
+
|---|---|
|
|
434
|
+
| `classifiers.byCommand({ docker: "containers", ... })` | Mapea el comando que falló (adjunto automáticamente por `shell.exec`) a un área |
|
|
435
|
+
| `classifiers.byPattern([[regex, area], ...])` | Matchea contra el `stderr`/mensaje del error |
|
|
436
|
+
|
|
437
|
+
### Formateadores de mensaje (`messages`)
|
|
438
|
+
|
|
439
|
+
| Fábrica | Uso |
|
|
440
|
+
|---|---|
|
|
441
|
+
| `messages.byPattern([[regex, mensaje], ...])` | Mismo mensaje sin importar el comando — solo mira el texto del error |
|
|
442
|
+
| `messages.byCommand({ docker: "mensaje fijo" })` | Mensaje fijo por comando, sin importar el detalle del error |
|
|
443
|
+
| `messages.byRule([{ command?, args?, pattern?, message }, ...])` | **La opción avanzada**: combina comando + sub-comando (`args`, distingue `docker push` de `docker build`) + patrón de texto, todo en modo AND. `message` puede ser un string fijo o una función `(error, ctx) => string`. Resuelve el caso de "el mismo 500 puede venir de docker, kubectl o terraform, y cada uno necesita su propio mensaje". |
|
|
444
|
+
|
|
445
|
+
Si ningún classifier/formatter matchea, se usa el área `"unclassified"` y el mensaje crudo del error, respectivamente — nada se rompe si no configurás nada de esto.
|
|
446
|
+
|
|
447
|
+
### Senders incluidos
|
|
448
|
+
|
|
449
|
+
| Sender | Uso |
|
|
450
|
+
|---|---|
|
|
451
|
+
| `senders.log()` | Usa el logger interno (consola) |
|
|
452
|
+
| `senders.file({ path })` | Agrega el evento como una línea JSON al archivo |
|
|
453
|
+
| `senders.http({ url, method?, headers?, formatBody? })` | `POST` genérico del evento como JSON |
|
|
454
|
+
| `senders.webhook({ url, format? })` | Como `http`, pero formatea `{ text: "❌ ..." }` por defecto — compatible con Slack/Discord y con **Microsoft Teams** vía Workflows (Power Automate), pasando un `format` que arme el payload de Adaptive Card que Teams espera |
|
|
455
|
+
| `senders.websocket({ url, timeout? })` | Abre una conexión WS, manda el evento como JSON y cierra. Requiere Node ≥21 (usa el `WebSocket` global) |
|
|
456
|
+
|
|
457
|
+
Podés escribir tu propio sender: es cualquier función `(event) => void | Promise<void>` — recibe `{ type, taskId, area?, error?, result?, message, timestamp }`.
|
|
398
458
|
|
|
399
459
|
## Tests
|
|
400
460
|
|
|
@@ -402,11 +462,21 @@ Igual que con `waitForDeployment`, el error lleva `command: "kubectl"` / `comman
|
|
|
402
462
|
npm test
|
|
403
463
|
```
|
|
404
464
|
|
|
405
|
-
|
|
465
|
+
`pretest` corre el build automáticamente, así los tests validan el `dist/` real que se publica (no el código fuente). Usa `node:test`, sin dependencias externas — interceptando `shell.exec` en vez de ejecutar binarios reales:
|
|
466
|
+
|
|
467
|
+
- `context.test.js` — Context, flags, vars, parseArgv, dryRun global
|
|
468
|
+
- `shell.test.js` — retry, timeout, dryRun del motor shell.exec
|
|
469
|
+
- `services.test.js` — que docker/terraform/argocd arman bien sus argumentos
|
|
470
|
+
- `menu-selector.test.js` — selección automática por flag, en cascada de varios niveles
|
|
471
|
+
- `hooks-integration.test.js` — ctx.run() y items de menú con onSuccess/onError
|
|
472
|
+
- `notifier.test.js` — classify/channel/onSuccess/describeError/byRule
|
|
473
|
+
- `senders.test.js` — file/http/webhook/log contra servidores reales en localhost
|
|
474
|
+
- `kubectl.test.js`, `oc.test.js` — kubeconfig/namespace, waitForDeployment (éxito, timeout, Failed, maxRestarts)
|
|
475
|
+
- `deployment-group.test.js` — waitForDeploymentGroup (éxito total, fallo parcial, label sin matches, oc con `dc`)
|
|
406
476
|
|
|
407
477
|
## Siguientes pasos posibles
|
|
408
478
|
|
|
409
479
|
- Publicar en un registro privado (Verdaccio/Artifactory/GitHub Packages) para instalarlo con scope, p.ej. `@miorg/devops-cli`.
|
|
410
|
-
- Agregar
|
|
411
|
-
- Agregar más plugins (`ansible-lint`, `trivy`, `sonar-scanner`) con el mismo patrón.
|
|
480
|
+
- Agregar más plugins (`ansible-lint`, `trivy`, `sonar-scanner`) con el mismo patrón que `terraform.ts`/`docker.ts`.
|
|
412
481
|
- CI propio (GitHub Actions/Azure Pipelines) que corra `npm test` en cada PR antes de `npm publish`.
|
|
482
|
+
- `--catch=throw` (o similar) para que un item de menú fallido mate el proceso completo en vez de solo loguear y seguir — útil corriendo vía `--menu-selector` dentro de un step de Azure Pipelines.
|
package/dist/core/messages.d.ts
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
|
+
import type { Context } from "./Context";
|
|
1
2
|
import type { ErrorMessageFormatter } from "./types";
|
|
2
3
|
/**
|
|
3
4
|
* Formateador de fábrica: prueba una lista de [regex, mensaje] contra el
|
|
4
5
|
* stderr/mensaje del error y devuelve el mensaje personalizado del primer
|
|
5
6
|
* patrón que matchee, en vez del stderr crudo.
|
|
6
7
|
*
|
|
8
|
+
* Ojo: esto NO distingue por comando — un mismo patrón (ej. "500 Internal
|
|
9
|
+
* Server Error") aplica igual venga de `docker`, `kubectl` o `terraform`.
|
|
10
|
+
* Si necesitas distinguir por comando (y opcionalmente por sub-comando),
|
|
11
|
+
* usa `byRule` más abajo.
|
|
12
|
+
*
|
|
7
13
|
* Ejemplo:
|
|
8
14
|
* notifier.describeError(messages.byPattern([
|
|
9
15
|
* [/500 Internal Server Error/, "Se reportó a infraestructura: falta de espacio en el registry"],
|
|
@@ -24,3 +30,60 @@ export declare function byPattern(rules: Array<[RegExp, string]>): ErrorMessageF
|
|
|
24
30
|
* }));
|
|
25
31
|
*/
|
|
26
32
|
export declare function byCommand(map: Record<string, string>): ErrorMessageFormatter;
|
|
33
|
+
/**
|
|
34
|
+
* Una regla de `byRule`: TODAS las condiciones que definas deben cumplirse
|
|
35
|
+
* (AND) para que aplique. Omitir una condición equivale a "no filtrar por
|
|
36
|
+
* eso" (matchea cualquier valor).
|
|
37
|
+
*/
|
|
38
|
+
export interface MessageRule {
|
|
39
|
+
/**
|
|
40
|
+
* Comando exacto (o lista de comandos) que debe haber fallado, tal como
|
|
41
|
+
* lo ve `shell.exec` (el primer argumento: "docker", "kubectl", "oc",
|
|
42
|
+
* "terraform", "git", etc.). Sin esto, la regla no filtra por comando.
|
|
43
|
+
*/
|
|
44
|
+
command?: string | string[];
|
|
45
|
+
/**
|
|
46
|
+
* Sub-comando / argumento que debe estar presente, por ejemplo "push"
|
|
47
|
+
* para distinguir `docker push` de `docker build`. Puede ser un string
|
|
48
|
+
* exacto (se busca entre los args, o como substring de los args unidos)
|
|
49
|
+
* o un RegExp contra los args unidos con espacios.
|
|
50
|
+
*/
|
|
51
|
+
args?: string | RegExp;
|
|
52
|
+
/** Patrón contra el stderr/mensaje del error. Sin esto, no filtra por texto. */
|
|
53
|
+
pattern?: RegExp;
|
|
54
|
+
/** Mensaje final. Puede ser un string fijo o una función que lo arma dinámicamente. */
|
|
55
|
+
message: string | ((error: unknown, ctx: Context) => string);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Formateador de fábrica avanzado: combina comando + sub-comando/args +
|
|
59
|
+
* patrón de texto (todo opcional, en modo AND) para distinguir el MISMO
|
|
60
|
+
* error de red/HTTP según de dónde vino. Resuelve justo el caso de "un 500
|
|
61
|
+
* puede venir de docker push, de kubectl, o de terraform, y cada uno
|
|
62
|
+
* necesita su propio mensaje".
|
|
63
|
+
*
|
|
64
|
+
* Se prueban las reglas en orden; gana la primera que matchee todas sus
|
|
65
|
+
* condiciones. Una regla sin `command` ni `args` ni `pattern` matchea
|
|
66
|
+
* cualquier error (útil como catch-all al final de la lista).
|
|
67
|
+
*
|
|
68
|
+
* Ejemplo — el caso concreto de varios comandos devolviendo el mismo 500:
|
|
69
|
+
*
|
|
70
|
+
* notifier.describeError(messages.byRule([
|
|
71
|
+
* {
|
|
72
|
+
* command: "docker", args: "push", pattern: /500 Internal Server Error/,
|
|
73
|
+
* message: "Se ha reportado a infraestructura: falta de espacio en el registry"
|
|
74
|
+
* },
|
|
75
|
+
* {
|
|
76
|
+
* command: "kubectl", pattern: /500/,
|
|
77
|
+
* message: "El API server de Kubernetes devolvió 500, reintenta en unos minutos"
|
|
78
|
+
* },
|
|
79
|
+
* {
|
|
80
|
+
* command: "terraform", pattern: /500/,
|
|
81
|
+
* message: (error, ctx) => `El backend remoto de Terraform State no respondió (env: ${ctx.params.env ?? "?"})`
|
|
82
|
+
* },
|
|
83
|
+
* {
|
|
84
|
+
* pattern: /500 Internal Server Error/,
|
|
85
|
+
* message: "Error 500 no clasificado por comando, revisar logs crudos"
|
|
86
|
+
* }
|
|
87
|
+
* ]));
|
|
88
|
+
*/
|
|
89
|
+
export declare function byRule(rules: MessageRule[]): ErrorMessageFormatter;
|
package/dist/core/messages.js
CHANGED
|
@@ -2,11 +2,21 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.byPattern = byPattern;
|
|
4
4
|
exports.byCommand = byCommand;
|
|
5
|
+
exports.byRule = byRule;
|
|
6
|
+
function textOf(error) {
|
|
7
|
+
const execError = error;
|
|
8
|
+
return execError?.stderr || execError?.message || String(error);
|
|
9
|
+
}
|
|
5
10
|
/**
|
|
6
11
|
* Formateador de fábrica: prueba una lista de [regex, mensaje] contra el
|
|
7
12
|
* stderr/mensaje del error y devuelve el mensaje personalizado del primer
|
|
8
13
|
* patrón que matchee, en vez del stderr crudo.
|
|
9
14
|
*
|
|
15
|
+
* Ojo: esto NO distingue por comando — un mismo patrón (ej. "500 Internal
|
|
16
|
+
* Server Error") aplica igual venga de `docker`, `kubectl` o `terraform`.
|
|
17
|
+
* Si necesitas distinguir por comando (y opcionalmente por sub-comando),
|
|
18
|
+
* usa `byRule` más abajo.
|
|
19
|
+
*
|
|
10
20
|
* Ejemplo:
|
|
11
21
|
* notifier.describeError(messages.byPattern([
|
|
12
22
|
* [/500 Internal Server Error/, "Se reportó a infraestructura: falta de espacio en el registry"],
|
|
@@ -16,8 +26,7 @@ exports.byCommand = byCommand;
|
|
|
16
26
|
*/
|
|
17
27
|
function byPattern(rules) {
|
|
18
28
|
return (error) => {
|
|
19
|
-
const
|
|
20
|
-
const text = execError?.stderr || execError?.message || String(error);
|
|
29
|
+
const text = textOf(error);
|
|
21
30
|
for (const [pattern, message] of rules) {
|
|
22
31
|
if (pattern.test(text)) {
|
|
23
32
|
return message;
|
|
@@ -45,4 +54,77 @@ function byCommand(map) {
|
|
|
45
54
|
return map[command];
|
|
46
55
|
};
|
|
47
56
|
}
|
|
57
|
+
function commandMatches(rule, command) {
|
|
58
|
+
if (!rule.command)
|
|
59
|
+
return true;
|
|
60
|
+
if (!command)
|
|
61
|
+
return false;
|
|
62
|
+
const candidates = Array.isArray(rule.command) ? rule.command : [rule.command];
|
|
63
|
+
return candidates.includes(command);
|
|
64
|
+
}
|
|
65
|
+
function argsMatch(rule, args) {
|
|
66
|
+
if (!rule.args)
|
|
67
|
+
return true;
|
|
68
|
+
if (!args || !args.length)
|
|
69
|
+
return false;
|
|
70
|
+
const joined = args.join(" ");
|
|
71
|
+
if (rule.args instanceof RegExp) {
|
|
72
|
+
return rule.args.test(joined);
|
|
73
|
+
}
|
|
74
|
+
return args.includes(rule.args) || joined.includes(rule.args);
|
|
75
|
+
}
|
|
76
|
+
function patternMatches(rule, text) {
|
|
77
|
+
if (!rule.pattern)
|
|
78
|
+
return true;
|
|
79
|
+
return rule.pattern.test(text);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Formateador de fábrica avanzado: combina comando + sub-comando/args +
|
|
83
|
+
* patrón de texto (todo opcional, en modo AND) para distinguir el MISMO
|
|
84
|
+
* error de red/HTTP según de dónde vino. Resuelve justo el caso de "un 500
|
|
85
|
+
* puede venir de docker push, de kubectl, o de terraform, y cada uno
|
|
86
|
+
* necesita su propio mensaje".
|
|
87
|
+
*
|
|
88
|
+
* Se prueban las reglas en orden; gana la primera que matchee todas sus
|
|
89
|
+
* condiciones. Una regla sin `command` ni `args` ni `pattern` matchea
|
|
90
|
+
* cualquier error (útil como catch-all al final de la lista).
|
|
91
|
+
*
|
|
92
|
+
* Ejemplo — el caso concreto de varios comandos devolviendo el mismo 500:
|
|
93
|
+
*
|
|
94
|
+
* notifier.describeError(messages.byRule([
|
|
95
|
+
* {
|
|
96
|
+
* command: "docker", args: "push", pattern: /500 Internal Server Error/,
|
|
97
|
+
* message: "Se ha reportado a infraestructura: falta de espacio en el registry"
|
|
98
|
+
* },
|
|
99
|
+
* {
|
|
100
|
+
* command: "kubectl", pattern: /500/,
|
|
101
|
+
* message: "El API server de Kubernetes devolvió 500, reintenta en unos minutos"
|
|
102
|
+
* },
|
|
103
|
+
* {
|
|
104
|
+
* command: "terraform", pattern: /500/,
|
|
105
|
+
* message: (error, ctx) => `El backend remoto de Terraform State no respondió (env: ${ctx.params.env ?? "?"})`
|
|
106
|
+
* },
|
|
107
|
+
* {
|
|
108
|
+
* pattern: /500 Internal Server Error/,
|
|
109
|
+
* message: "Error 500 no clasificado por comando, revisar logs crudos"
|
|
110
|
+
* }
|
|
111
|
+
* ]));
|
|
112
|
+
*/
|
|
113
|
+
function byRule(rules) {
|
|
114
|
+
return (error, ctx) => {
|
|
115
|
+
const execError = error;
|
|
116
|
+
const text = textOf(error);
|
|
117
|
+
for (const rule of rules) {
|
|
118
|
+
const matches = commandMatches(rule, execError?.command)
|
|
119
|
+
&& argsMatch(rule, execError?.args)
|
|
120
|
+
&& patternMatches(rule, text);
|
|
121
|
+
if (matches) {
|
|
122
|
+
return typeof rule.message === "function"
|
|
123
|
+
? rule.message(error, ctx)
|
|
124
|
+
: rule.message;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return undefined;
|
|
128
|
+
};
|
|
129
|
+
}
|
|
48
130
|
//# sourceMappingURL=messages.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"messages.js","sourceRoot":"","sources":["../../src/core/messages.ts"],"names":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"messages.js","sourceRoot":"","sources":["../../src/core/messages.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cat-indev/catops-cli",
|
|
3
|
-
"version": "0.0.1-alpha.
|
|
3
|
+
"version": "0.0.1-alpha.7",
|
|
4
4
|
"description": "Framework CLI para pipelines DevOps (shell, docker, git, kubectl, helm, npm, terraform, ansible, argocd, tekton, oc, az) sobre un ExecutionContext compartido, con menus interactivos y selectores automaticos por flag. Escrito en TypeScript, 100% usable desde JavaScript.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|