@cat-indev/catops-cli 0.0.1-alpha.6 → 0.0.1-alpha.8
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 +308 -211
- package/dist/core/messages.d.ts +63 -0
- package/dist/core/messages.js +84 -2
- package/dist/core/messages.js.map +1 -1
- package/dist/services/ansible.d.ts +7 -6
- package/dist/services/ansible.js +10 -10
- package/dist/services/ansible.js.map +1 -1
- package/dist/services/archive.d.ts +4 -4
- package/dist/services/archive.js +6 -6
- package/dist/services/archive.js.map +1 -1
- package/dist/services/argocd.d.ts +11 -8
- package/dist/services/argocd.js +14 -14
- package/dist/services/argocd.js.map +1 -1
- package/dist/services/az.d.ts +13 -8
- package/dist/services/az.js +17 -14
- package/dist/services/az.js.map +1 -1
- package/dist/services/docker.d.ts +10 -7
- package/dist/services/docker.js +12 -12
- package/dist/services/docker.js.map +1 -1
- package/dist/services/git.d.ts +9 -9
- package/dist/services/git.js +16 -16
- package/dist/services/git.js.map +1 -1
- package/dist/services/helm.d.ts +4 -4
- package/dist/services/helm.js +6 -6
- package/dist/services/helm.js.map +1 -1
- package/dist/services/kubectl.d.ts +4 -2
- package/dist/services/kubectl.js +13 -8
- package/dist/services/kubectl.js.map +1 -1
- package/dist/services/npm.d.ts +5 -5
- package/dist/services/npm.js +8 -8
- package/dist/services/npm.js.map +1 -1
- package/dist/services/oc.d.ts +12 -2
- package/dist/services/oc.js +22 -9
- package/dist/services/oc.js.map +1 -1
- package/dist/services/shell.d.ts +8 -0
- package/dist/services/shell.js +11 -0
- package/dist/services/shell.js.map +1 -1
- package/dist/services/tekton.d.ts +8 -6
- package/dist/services/tekton.js +10 -10
- package/dist/services/tekton.js.map +1 -1
- package/dist/services/terraform.d.ts +13 -8
- package/dist/services/terraform.js +14 -14
- package/dist/services/terraform.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,114 +1,107 @@
|
|
|
1
|
-
#
|
|
1
|
+
# catops-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
|
|
|
7
16
|
**Opción A — publicado en tu registro npm (público o privado tipo Verdaccio/Artifactory/GitHub Packages):**
|
|
8
17
|
|
|
9
18
|
```bash
|
|
10
|
-
npm install
|
|
11
|
-
# o si lo publicas con scope propio, p.ej. @miorg/
|
|
12
|
-
npm install @miorg/
|
|
19
|
+
npm install catops-cli
|
|
20
|
+
# o si lo publicas con scope propio, p.ej. @miorg/catops-cli
|
|
21
|
+
npm install @miorg/catops-cli
|
|
13
22
|
```
|
|
14
23
|
|
|
15
24
|
**Opción B — sin publicar, directo desde este proyecto (útil mientras lo maduras):**
|
|
16
25
|
|
|
17
26
|
```bash
|
|
18
|
-
# Dentro del repo de
|
|
19
|
-
npm pack # genera
|
|
27
|
+
# Dentro del repo de catops-cli
|
|
28
|
+
npm pack # genera catops-cli-<version>.tgz
|
|
20
29
|
|
|
21
30
|
# Dentro del proyecto que lo va a consumir
|
|
22
|
-
npm install /ruta/a/
|
|
31
|
+
npm install /ruta/a/catops-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):**
|
|
26
35
|
|
|
27
36
|
```bash
|
|
28
|
-
# Dentro del repo de
|
|
37
|
+
# Dentro del repo de catops-cli
|
|
29
38
|
npm link
|
|
30
39
|
|
|
31
40
|
# Dentro del proyecto consumidor
|
|
32
|
-
npm link
|
|
41
|
+
npm link catops-cli
|
|
33
42
|
```
|
|
34
43
|
|
|
35
44
|
**Opción D — como dependencia de Git (monorepo o repo privado, sin registro npm):**
|
|
36
45
|
|
|
37
46
|
```bash
|
|
38
|
-
npm install git+https://github.com/tu-org/
|
|
47
|
+
npm install git+https://github.com/tu-org/catops-cli.git
|
|
39
48
|
```
|
|
40
49
|
|
|
41
50
|
Cualquiera de las 4 deja disponibles dos cosas en el proyecto consumidor:
|
|
42
51
|
|
|
43
|
-
1. La librería
|
|
44
|
-
|
|
52
|
+
1. La librería, tanto desde TS como desde JS puro:
|
|
53
|
+
```typescript
|
|
54
|
+
import { Context, Menu, services, type MenuDefinition } from "catops-cli";
|
|
55
|
+
```
|
|
56
|
+
```javascript
|
|
57
|
+
const { Context, Menu, services } = require("catops-cli");
|
|
58
|
+
```
|
|
59
|
+
2. El binario: `npx catops-cli` (o `catops-cli` si lo instalaste global con `-g`).
|
|
45
60
|
|
|
46
61
|
## Publicar una nueva versión
|
|
47
62
|
|
|
48
63
|
```bash
|
|
49
64
|
npm version patch # o minor / major
|
|
50
|
-
npm publish # agrega --access public si usas un scope (@miorg/
|
|
65
|
+
npm publish # agrega --access public si usas un scope (@miorg/catops-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
|
+
catops-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, exec-options-passthrough.test.js
|
|
105
100
|
```
|
|
106
101
|
|
|
107
|
-
|
|
102
|
+
## Uso rápido: menú con `devops.pipeline.js` + el bin
|
|
108
103
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
Crea un `devops.pipeline.js` (o `devops.config.js` / `.devops-cli.js`) en la raíz de tu proyecto:
|
|
104
|
+
Crea un `devops.pipeline.js` (o `devops.config.js` / `.catops-cli.js`) en la raíz de tu proyecto:
|
|
112
105
|
|
|
113
106
|
```javascript
|
|
114
107
|
// devops.pipeline.js
|
|
@@ -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
|
-
npx
|
|
122
|
+
npx catops-cli --debug --env=prod
|
|
135
123
|
```
|
|
136
124
|
|
|
137
|
-
`
|
|
125
|
+
`catops-cli` detecta el archivo, arma el `Context` a partir de los flags/params de `argv`, y renderiza el menú.
|
|
126
|
+
|
|
127
|
+
## Uso directo en tu propio script (p. ej. con `tsx`)
|
|
138
128
|
|
|
139
|
-
|
|
129
|
+
```typescript
|
|
130
|
+
// src/index.ts
|
|
131
|
+
import { Context, Menu, type MenuDefinition } from "catops-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("catops-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
|
+
catops-cli --menu-selector=docker --docker-action=build
|
|
199
214
|
|
|
200
|
-
|
|
215
|
+
# un solo nivel:
|
|
216
|
+
catops-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
|
+
catops-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 });
|
|
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.
|
|
219
223
|
|
|
220
|
-
|
|
224
|
+
### Callbacks de éxito/error por item
|
|
221
225
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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,113 +245,101 @@ 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 y en TODOS los servicios
|
|
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
|
-
|
|
264
|
-
|
|
265
|
-
// 1. ¿A qué área de TI pertenece este error?
|
|
266
|
-
ctx.notifier.classify(classifiers.byCommand({
|
|
267
|
-
docker: "containers",
|
|
268
|
-
kubectl: "kubernetes",
|
|
269
|
-
oc: "kubernetes",
|
|
270
|
-
terraform: "infra",
|
|
271
|
-
ansible: "infra",
|
|
272
|
-
git: "scm",
|
|
273
|
-
argocd: "cd-pipeline",
|
|
274
|
-
tkn: "cd-pipeline",
|
|
275
|
-
az: "cloud-azure"
|
|
276
|
-
}));
|
|
255
|
+
await ctx.services.docker.push(image); // igual que siempre
|
|
277
256
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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
|
+
```
|
|
284
264
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
265
|
+
**Todos los comandos de todos los servicios** (`docker`, `git`, `kubectl`, `helm`, `npm`, `archive`, `terraform`, `ansible`, `argocd`, `tekton`, `oc`, `az`) aceptan este mismo control, sin que cambie nada de lo que ya usabas:
|
|
266
|
+
|
|
267
|
+
- Si la función ya recibía un **objeto de opciones** (la mayoría), agregale la clave `exec`:
|
|
268
|
+
```javascript
|
|
269
|
+
await ctx.services.terraform.apply({ autoApprove: true, exec: { retry: 3 } });
|
|
270
|
+
await ctx.services.argocd.appSync("mi-app", { prune: true, exec: { retry: 3 } });
|
|
271
|
+
```
|
|
272
|
+
- Si la función recibe **argumentos posicionales** (strings sueltos), `exec` va como el **último argumento**:
|
|
273
|
+
```javascript
|
|
274
|
+
await ctx.services.docker.push("registry/app:v1", { retry: 3 });
|
|
275
|
+
await ctx.services.git.push({ retry: 3 });
|
|
276
|
+
await ctx.services.helm.uninstall("mi-app", { retry: 3 });
|
|
277
|
+
```
|
|
278
|
+
- `kubectl` y `oc` combinan `exec` en el **mismo objeto** que ya usás para `kubeconfig`/`namespace`:
|
|
279
|
+
```javascript
|
|
280
|
+
// 10 reintentos en un login inestable de OpenShift
|
|
281
|
+
await ctx.services.oc.login({
|
|
282
|
+
server: "https://api.cluster:6443",
|
|
283
|
+
token: process.env.OC_TOKEN,
|
|
284
|
+
namespace: "prod",
|
|
285
|
+
exec: { retry: 10, retryDelay: 2000 }
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
await ctx.services.kubectl.apply("deploy.yaml", { namespace: "prod", exec: { retry: 5, timeout: 30000 } });
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
Defaults globales para todo el proceso (afecta a todos los comandos que no pasen su propio `exec`/opciones puntuales):
|
|
289
292
|
|
|
290
|
-
|
|
291
|
-
ctx.
|
|
293
|
+
```javascript
|
|
294
|
+
ctx.services.shell.configure({ retry: 3, timeout: 30000 });
|
|
292
295
|
```
|
|
293
296
|
|
|
294
|
-
|
|
297
|
+
`Context.parseArgv()` ya conecta flags de línea de comandos automáticamente a esos defaults globales:
|
|
295
298
|
|
|
299
|
+
```bash
|
|
300
|
+
npx catops-cli --dry-run # activa dryRun global
|
|
301
|
+
npx catops-cli --retry=3 --timeout=15000
|
|
296
302
|
```
|
|
297
|
-
const { classifiers, messages } = require("devops-cli");
|
|
298
|
-
|
|
299
|
-
ctx.notifier.classify(classifiers.byCommand({ docker: "containers" }));
|
|
300
303
|
|
|
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
|
-
]));
|
|
304
|
+
## Servicios de infraestructura incluidos
|
|
305
305
|
|
|
306
|
-
|
|
307
|
-
|
|
306
|
+
```javascript
|
|
307
|
+
await ctx.services.terraform.plan({ varFile: "prod.tfvars" });
|
|
308
|
+
await ctx.services.terraform.apply();
|
|
308
309
|
|
|
309
|
-
|
|
310
|
+
await ctx.services.ansible.playbook("site.yml", { inventory: "hosts.ini" });
|
|
310
311
|
|
|
311
|
-
|
|
312
|
+
await ctx.services.argocd.appSync("mi-app", { prune: true });
|
|
312
313
|
|
|
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) |
|
|
314
|
+
await ctx.services.tekton.pipelineStart("build-pipeline", { params: { image: "app:v1" } });
|
|
320
315
|
|
|
321
|
-
|
|
316
|
+
await ctx.services.az.acrBuild({ registry: "miregistro", image: "app:v1" });
|
|
317
|
+
```
|
|
322
318
|
|
|
323
|
-
## kubectl / oc: kubeconfig, namespace y espera cíclica del rollout
|
|
319
|
+
## kubectl / oc: kubeconfig, namespace, retry/timeout, y espera cíclica del rollout
|
|
324
320
|
|
|
325
|
-
`kubectl` y `oc`
|
|
321
|
+
`kubectl` y `oc` aceptan `{ kubeconfig, namespace, exec }` como último argumento en **todos** sus comandos (retrocompatible, sigue funcionando sin ese argumento — ver la sección anterior para el detalle de `exec`):
|
|
326
322
|
|
|
327
323
|
```javascript
|
|
328
324
|
await ctx.services.kubectl.apply("deploy.yaml", { kubeconfig: "/etc/kube/prod.yaml", namespace: "prod" });
|
|
329
|
-
await ctx.services.kubectl.get("pods", "-o", "wide", { namespace: "staging" });
|
|
325
|
+
await ctx.services.kubectl.get("pods", "-o", "wide", { namespace: "staging", exec: { retry: 3 } });
|
|
330
326
|
|
|
331
|
-
await ctx.services.oc.login({ server: "https://api.cluster:6443", token, namespace: "prod" });
|
|
327
|
+
await ctx.services.oc.login({ server: "https://api.cluster:6443", token, namespace: "prod", exec: { retry: 10 } });
|
|
332
328
|
await ctx.services.oc.apply("deploy.yaml", { namespace: "prod" });
|
|
333
329
|
```
|
|
334
330
|
|
|
335
331
|
### `waitForDeployment` — validación cíclica del rollout
|
|
336
332
|
|
|
337
|
-
Sondea el Deployment (o `DeploymentConfig` con `oc`) hasta que:
|
|
333
|
+
Sondea el Deployment (o `DeploymentConfig` con `oc` + `resourceType: "dc"`) hasta que:
|
|
338
334
|
|
|
339
335
|
- llega a **estado exitoso** (réplicas listas/actualizadas == deseadas) → resuelve con `{ status: "success", ... }`,
|
|
340
336
|
- se queda en **estado Failed** más de `failedGracePeriod` sin recuperarse → lanza `DeploymentRolloutError`,
|
|
341
337
|
- supera **`maxRestarts`** reinicios acumulados entre todos sus pods → lanza `DeploymentRolloutError` de inmediato, sin esperar el grace period,
|
|
342
338
|
- o se cumple el **`timeout`** global sin éxito → lanza `DeploymentRolloutError`.
|
|
343
339
|
|
|
344
|
-
En los tres casos de fallo, el polling se detiene
|
|
340
|
+
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
341
|
|
|
346
342
|
```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
343
|
await ctx.run("deploy-api", async () => {
|
|
353
344
|
await ctx.services.kubectl.apply("deployment.yaml", { namespace: "prod" });
|
|
354
345
|
|
|
@@ -363,11 +354,9 @@ await ctx.run("deploy-api", async () => {
|
|
|
363
354
|
});
|
|
364
355
|
```
|
|
365
356
|
|
|
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
357
|
### `waitForDeploymentGroup` — validar todas las instancias de un mismo despliegue GitOps
|
|
369
358
|
|
|
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
|
|
359
|
+
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
360
|
|
|
372
361
|
```yaml
|
|
373
362
|
metadata:
|
|
@@ -375,7 +364,7 @@ metadata:
|
|
|
375
364
|
deployment-group: repository-14
|
|
376
365
|
```
|
|
377
366
|
|
|
378
|
-
|
|
367
|
+
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
368
|
|
|
380
369
|
```javascript
|
|
381
370
|
await ctx.run("deploy-repo-14", () =>
|
|
@@ -391,10 +380,107 @@ await ctx.run("deploy-repo-14", () =>
|
|
|
391
380
|
```
|
|
392
381
|
|
|
393
382
|
- Si **todas** llegan a estado exitoso → resuelve con `{ status: "success", deployments: [...] }` (el detalle de cada una).
|
|
394
|
-
- Si **alguna falla**
|
|
383
|
+
- 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
384
|
- Si el label **no matchea ningún deployment**, también lanza `DeploymentGroupRolloutError` (grupo vacío = error, no éxito silencioso).
|
|
396
385
|
|
|
397
|
-
|
|
386
|
+
Con `oc`, ambas funciones aceptan `resourceType: "dc"` para apuntar a `DeploymentConfig` clásico en vez de `Deployment` nativo (default: `"deployment"`).
|
|
387
|
+
|
|
388
|
+
## Logging commands de Azure Pipelines (`ctx.services.azdo`)
|
|
389
|
+
|
|
390
|
+
```javascript
|
|
391
|
+
ctx.services.azdo.setVariable("BUILD_TAG", "v1.2.3");
|
|
392
|
+
ctx.services.azdo.logWarning("El caché de npm no se encontró, se reconstruye desde cero.");
|
|
393
|
+
ctx.services.azdo.group("Build");
|
|
394
|
+
// ... pasos ...
|
|
395
|
+
ctx.services.azdo.endGroup();
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
## Notificaciones: clasificar errores por área de TI, personalizar el mensaje, y enviarlos
|
|
399
|
+
|
|
400
|
+
`ctx.notifier` tiene tres responsabilidades independientes:
|
|
401
|
+
|
|
402
|
+
1. **`classify()`** — decide a qué **área de TI** pertenece un error (para elegir a qué canal mandarlo).
|
|
403
|
+
2. **`describeError()`** — decide el **mensaje** a reportar (reemplaza el stderr crudo por algo humano).
|
|
404
|
+
3. **`channel()`** / **`onSuccess()`** — a qué **senders** se manda cada área.
|
|
405
|
+
|
|
406
|
+
```javascript
|
|
407
|
+
const { classifiers, messages, senders } = require("catops-cli");
|
|
408
|
+
|
|
409
|
+
// 1. ¿A qué área de TI pertenece este error?
|
|
410
|
+
ctx.notifier.classify(classifiers.byCommand({
|
|
411
|
+
docker: "containers",
|
|
412
|
+
kubectl: "kubernetes",
|
|
413
|
+
oc: "kubernetes",
|
|
414
|
+
terraform: "infra",
|
|
415
|
+
ansible: "infra",
|
|
416
|
+
git: "scm",
|
|
417
|
+
argocd: "cd-pipeline",
|
|
418
|
+
tkn: "cd-pipeline",
|
|
419
|
+
az: "cloud-azure"
|
|
420
|
+
}));
|
|
421
|
+
|
|
422
|
+
// también podés clasificar por el texto del error:
|
|
423
|
+
ctx.notifier.classify(classifiers.byPattern([
|
|
424
|
+
[/permission denied|unauthorized/i, "security"],
|
|
425
|
+
[/timeout|ECONNREFUSED/i, "networking"],
|
|
426
|
+
[/no space left|ENOSPC/i, "infra"]
|
|
427
|
+
]));
|
|
428
|
+
|
|
429
|
+
// 2. ¿qué mensaje se reporta? (opcional — sin esto, se usa el stderr crudo)
|
|
430
|
+
ctx.notifier.describeError(messages.byRule([
|
|
431
|
+
{
|
|
432
|
+
command: "docker", args: "push", pattern: /500 Internal Server Error/,
|
|
433
|
+
message: "Se ha reportado a infraestructura: falta de espacio en el registry"
|
|
434
|
+
},
|
|
435
|
+
{
|
|
436
|
+
command: "kubectl", pattern: /500/,
|
|
437
|
+
message: "El API server de Kubernetes devolvió 500, reintenta en unos minutos"
|
|
438
|
+
},
|
|
439
|
+
{
|
|
440
|
+
command: "terraform", pattern: /500/,
|
|
441
|
+
message: (error, ctx) => `Backend remoto de Terraform no respondió (env: ${ctx.params.env ?? "?"})`
|
|
442
|
+
}
|
|
443
|
+
]));
|
|
444
|
+
|
|
445
|
+
// 3. ¿a dónde se manda cada área?
|
|
446
|
+
ctx.notifier.channel("kubernetes", senders.webhook({ url: process.env.TEAMS_WEBHOOK }));
|
|
447
|
+
ctx.notifier.channel("security", senders.http({ url: "https://security.miempresa.com/incidents" }));
|
|
448
|
+
ctx.notifier.channel("*", senders.file({ path: "./catops-cli-errors.log" })); // TODO error, sin importar el área
|
|
449
|
+
|
|
450
|
+
// (opcional) éxito, sin clasificación de área
|
|
451
|
+
ctx.notifier.onSuccess(senders.log());
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
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.
|
|
455
|
+
|
|
456
|
+
### Clasificadores de área (`classifiers`)
|
|
457
|
+
|
|
458
|
+
| Fábrica | Uso |
|
|
459
|
+
|---|---|
|
|
460
|
+
| `classifiers.byCommand({ docker: "containers", ... })` | Mapea el comando que falló (adjunto automáticamente por `shell.exec`) a un área |
|
|
461
|
+
| `classifiers.byPattern([[regex, area], ...])` | Matchea contra el `stderr`/mensaje del error |
|
|
462
|
+
|
|
463
|
+
### Formateadores de mensaje (`messages`)
|
|
464
|
+
|
|
465
|
+
| Fábrica | Uso |
|
|
466
|
+
|---|---|
|
|
467
|
+
| `messages.byPattern([[regex, mensaje], ...])` | Mismo mensaje sin importar el comando — solo mira el texto del error |
|
|
468
|
+
| `messages.byCommand({ docker: "mensaje fijo" })` | Mensaje fijo por comando, sin importar el detalle del error |
|
|
469
|
+
| `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". |
|
|
470
|
+
|
|
471
|
+
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.
|
|
472
|
+
|
|
473
|
+
### Senders incluidos
|
|
474
|
+
|
|
475
|
+
| Sender | Uso |
|
|
476
|
+
|---|---|
|
|
477
|
+
| `senders.log()` | Usa el logger interno (consola) |
|
|
478
|
+
| `senders.file({ path })` | Agrega el evento como una línea JSON al archivo |
|
|
479
|
+
| `senders.http({ url, method?, headers?, formatBody? })` | `POST` genérico del evento como JSON |
|
|
480
|
+
| `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 |
|
|
481
|
+
| `senders.websocket({ url, timeout? })` | Abre una conexión WS, manda el evento como JSON y cierra. Requiere Node ≥21 (usa el `WebSocket` global) |
|
|
482
|
+
|
|
483
|
+
Podés escribir tu propio sender: es cualquier función `(event) => void | Promise<void>` — recibe `{ type, taskId, area?, error?, result?, message, timestamp }`.
|
|
398
484
|
|
|
399
485
|
## Tests
|
|
400
486
|
|
|
@@ -402,11 +488,22 @@ Igual que con `waitForDeployment`, el error lleva `command: "kubectl"` / `comman
|
|
|
402
488
|
npm test
|
|
403
489
|
```
|
|
404
490
|
|
|
405
|
-
|
|
491
|
+
`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:
|
|
492
|
+
|
|
493
|
+
- `context.test.js` — Context, flags, vars, parseArgv, dryRun global
|
|
494
|
+
- `shell.test.js` — retry, timeout, dryRun del motor shell.exec
|
|
495
|
+
- `services.test.js` — que docker/terraform/argocd arman bien sus argumentos
|
|
496
|
+
- `menu-selector.test.js` — selección automática por flag, en cascada de varios niveles
|
|
497
|
+
- `hooks-integration.test.js` — ctx.run() y items de menú con onSuccess/onError
|
|
498
|
+
- `notifier.test.js` — classify/channel/onSuccess/describeError/byRule
|
|
499
|
+
- `senders.test.js` — file/http/webhook/log contra servidores reales en localhost
|
|
500
|
+
- `kubectl.test.js`, `oc.test.js` — kubeconfig/namespace, waitForDeployment (éxito, timeout, Failed, maxRestarts)
|
|
501
|
+
- `exec-options-passthrough.test.js` — retry/timeout/dryRun (`exec`) llegando a todos los servicios, incluyendo un retry real que se recupera tras 2 fallos
|
|
502
|
+
- `deployment-group.test.js` — waitForDeploymentGroup (éxito total, fallo parcial, label sin matches, oc con `dc`)
|
|
406
503
|
|
|
407
504
|
## Siguientes pasos posibles
|
|
408
505
|
|
|
409
|
-
- Publicar en un registro privado (Verdaccio/Artifactory/GitHub Packages) para instalarlo con scope, p.ej. `@miorg/
|
|
410
|
-
- Agregar
|
|
411
|
-
- Agregar más plugins (`ansible-lint`, `trivy`, `sonar-scanner`) con el mismo patrón.
|
|
506
|
+
- Publicar en un registro privado (Verdaccio/Artifactory/GitHub Packages) para instalarlo con scope, p.ej. `@miorg/catops-cli`.
|
|
507
|
+
- Agregar más plugins (`ansible-lint`, `trivy`, `sonar-scanner`) con el mismo patrón que `terraform.ts`/`docker.ts`.
|
|
412
508
|
- CI propio (GitHub Actions/Azure Pipelines) que corra `npm test` en cada PR antes de `npm publish`.
|
|
509
|
+
- `--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.
|