gitlab-skill 1.0.0
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/GITLAB_API_COMPREHENSIVE_GUIDE.md +3885 -0
- package/PRACTICAL_SCRIPTS.md +1351 -0
- package/QUICK_REFERENCE.md +682 -0
- package/README.md +461 -0
- package/SKILL.md +789 -0
- package/SKILL_SETUP.md +50 -0
- package/package.json +42 -0
- package/scripts/install-skill.mjs +119 -0
- package/skill-features/00-auth-and-bootstrap.md +81 -0
- package/skill-features/01-discovery-and-projects.md +73 -0
- package/skill-features/02-repository-and-code.md +101 -0
- package/skill-features/03-issues-and-merge-requests.md +69 -0
- package/skill-features/04-cicd-pipelines-jobs.md +86 -0
- package/skill-features/05-security-and-vulnerabilities.md +125 -0
- package/skill-features/06-graphql-and-glab-playbook.md +68 -0
|
@@ -0,0 +1,1351 @@
|
|
|
1
|
+
# GitLab API - Scripts Prácticos
|
|
2
|
+
|
|
3
|
+
> Colección de scripts completos y listos para usar
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Tabla de Contenidos
|
|
8
|
+
|
|
9
|
+
1. [Configuración Inicial](#configuración-inicial)
|
|
10
|
+
2. [Scripts de Proyectos](#scripts-de-proyectos)
|
|
11
|
+
3. [Scripts de Issues](#scripts-de-issues)
|
|
12
|
+
4. [Scripts de Merge Requests](#scripts-de-merge-requests)
|
|
13
|
+
5. [Scripts de Pipelines](#scripts-de-pipelines)
|
|
14
|
+
6. [Scripts de Repository](#scripts-de-repository)
|
|
15
|
+
7. [Scripts de Monitoreo](#scripts-de-monitoreo)
|
|
16
|
+
8. [Scripts de Automatización](#scripts-de-automatización)
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Configuración Inicial
|
|
21
|
+
|
|
22
|
+
### Script: setup-gitlab-env.sh
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
#!/bin/bash
|
|
26
|
+
|
|
27
|
+
# setup-gitlab-env.sh
|
|
28
|
+
# Configura variables de entorno para GitLab API
|
|
29
|
+
|
|
30
|
+
set -e
|
|
31
|
+
|
|
32
|
+
echo "=== GitLab API Setup ==="
|
|
33
|
+
echo ""
|
|
34
|
+
|
|
35
|
+
# Solicitar información
|
|
36
|
+
read -p "GitLab Host (default: https://gitlab.com): " GITLAB_HOST
|
|
37
|
+
GITLAB_HOST=${GITLAB_HOST:-https://gitlab.com}
|
|
38
|
+
|
|
39
|
+
read -sp "GitLab Token: " GITLAB_TOKEN
|
|
40
|
+
echo ""
|
|
41
|
+
|
|
42
|
+
# Validar token
|
|
43
|
+
echo "Validating token..."
|
|
44
|
+
RESPONSE=$(curl --silent --write-out "HTTPSTATUS:%{http_code}" \
|
|
45
|
+
--header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
|
|
46
|
+
--url "$GITLAB_HOST/api/v4/user")
|
|
47
|
+
|
|
48
|
+
HTTP_STATUS=$(echo "$RESPONSE" | tr -d '\n' | sed -e 's/.*HTTPSTATUS://')
|
|
49
|
+
BODY=$(echo "$RESPONSE" | sed -e 's/HTTPSTATUS\:.*//g')
|
|
50
|
+
|
|
51
|
+
if [ "$HTTP_STATUS" -eq 200 ]; then
|
|
52
|
+
USERNAME=$(echo "$BODY" | jq -r '.username')
|
|
53
|
+
echo "✅ Token válido. Usuario: $USERNAME"
|
|
54
|
+
|
|
55
|
+
# Guardar en archivo .env
|
|
56
|
+
cat > .gitlab-env << EOF
|
|
57
|
+
export GITLAB_HOST="$GITLAB_HOST"
|
|
58
|
+
export GITLAB_TOKEN="$GITLAB_TOKEN"
|
|
59
|
+
export GITLAB_API_URL="$GITLAB_HOST/api/v4"
|
|
60
|
+
EOF
|
|
61
|
+
|
|
62
|
+
echo ""
|
|
63
|
+
echo "✅ Configuración guardada en .gitlab-env"
|
|
64
|
+
echo "Para usar: source .gitlab-env"
|
|
65
|
+
else
|
|
66
|
+
echo "❌ Token inválido o error de conexión"
|
|
67
|
+
exit 1
|
|
68
|
+
fi
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Scripts de Proyectos
|
|
74
|
+
|
|
75
|
+
### Script: list-all-projects.sh
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
#!/bin/bash
|
|
79
|
+
|
|
80
|
+
# list-all-projects.sh
|
|
81
|
+
# Lista todos los proyectos con paginación automática
|
|
82
|
+
|
|
83
|
+
source .gitlab-env
|
|
84
|
+
|
|
85
|
+
OUTPUT_FILE="projects.json"
|
|
86
|
+
page=1
|
|
87
|
+
all_projects="[]"
|
|
88
|
+
|
|
89
|
+
echo "Obteniendo proyectos..."
|
|
90
|
+
|
|
91
|
+
while true; do
|
|
92
|
+
echo "Página $page..."
|
|
93
|
+
|
|
94
|
+
response=$(curl --silent \
|
|
95
|
+
--header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
|
|
96
|
+
--url "$GITLAB_API_URL/projects?page=$page&per_page=100&simple=true")
|
|
97
|
+
|
|
98
|
+
# Verificar si hay proyectos
|
|
99
|
+
count=$(echo "$response" | jq '. | length')
|
|
100
|
+
|
|
101
|
+
if [ "$count" -eq 0 ]; then
|
|
102
|
+
break
|
|
103
|
+
fi
|
|
104
|
+
|
|
105
|
+
# Agregar a la lista
|
|
106
|
+
all_projects=$(echo "$all_projects" | jq ". + $response")
|
|
107
|
+
|
|
108
|
+
page=$((page + 1))
|
|
109
|
+
done
|
|
110
|
+
|
|
111
|
+
# Guardar resultados
|
|
112
|
+
echo "$all_projects" > "$OUTPUT_FILE"
|
|
113
|
+
|
|
114
|
+
total=$(echo "$all_projects" | jq '. | length')
|
|
115
|
+
echo ""
|
|
116
|
+
echo "✅ Total de proyectos: $total"
|
|
117
|
+
echo "📄 Guardado en: $OUTPUT_FILE"
|
|
118
|
+
|
|
119
|
+
# Mostrar resumen
|
|
120
|
+
echo ""
|
|
121
|
+
echo "Top 10 proyectos:"
|
|
122
|
+
echo "$all_projects" | jq -r '.[:10] | .[] | "\(.id) - \(.name_with_namespace)"'
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Script: create-project.sh
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
#!/bin/bash
|
|
129
|
+
|
|
130
|
+
# create-project.sh
|
|
131
|
+
# Crea un nuevo proyecto con configuración completa
|
|
132
|
+
|
|
133
|
+
source .gitlab-env
|
|
134
|
+
|
|
135
|
+
# Parámetros
|
|
136
|
+
PROJECT_NAME="${1:-}"
|
|
137
|
+
PROJECT_PATH="${2:-}"
|
|
138
|
+
DESCRIPTION="${3:-}"
|
|
139
|
+
VISIBILITY="${4:-private}"
|
|
140
|
+
|
|
141
|
+
if [ -z "$PROJECT_NAME" ]; then
|
|
142
|
+
read -p "Nombre del proyecto: " PROJECT_NAME
|
|
143
|
+
fi
|
|
144
|
+
|
|
145
|
+
if [ -z "$PROJECT_PATH" ]; then
|
|
146
|
+
PROJECT_PATH=$(echo "$PROJECT_NAME" | tr '[:upper:]' '[:lower:]' | tr ' ' '-')
|
|
147
|
+
fi
|
|
148
|
+
|
|
149
|
+
if [ -z "$DESCRIPTION" ]; then
|
|
150
|
+
read -p "Descripción: " DESCRIPTION
|
|
151
|
+
fi
|
|
152
|
+
|
|
153
|
+
echo "Creando proyecto..."
|
|
154
|
+
echo " Nombre: $PROJECT_NAME"
|
|
155
|
+
echo " Path: $PROJECT_PATH"
|
|
156
|
+
echo " Visibilidad: $VISIBILITY"
|
|
157
|
+
|
|
158
|
+
response=$(curl --silent --write-out "HTTPSTATUS:%{http_code}" \
|
|
159
|
+
--request POST \
|
|
160
|
+
--header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
|
|
161
|
+
--header "Content-Type: application/json" \
|
|
162
|
+
--data "{
|
|
163
|
+
\"name\": \"$PROJECT_NAME\",
|
|
164
|
+
\"path\": \"$PROJECT_PATH\",
|
|
165
|
+
\"description\": \"$DESCRIPTION\",
|
|
166
|
+
\"visibility\": \"$VISIBILITY\",
|
|
167
|
+
\"initialize_with_readme\": true,
|
|
168
|
+
\"issues_enabled\": true,
|
|
169
|
+
\"merge_requests_enabled\": true,
|
|
170
|
+
\"wiki_enabled\": true,
|
|
171
|
+
\"snippets_enabled\": true,
|
|
172
|
+
\"builds_enabled\": true
|
|
173
|
+
}" \
|
|
174
|
+
--url "$GITLAB_API_URL/projects")
|
|
175
|
+
|
|
176
|
+
HTTP_STATUS=$(echo "$response" | tr -d '\n' | sed -e 's/.*HTTPSTATUS://')
|
|
177
|
+
BODY=$(echo "$response" | sed -e 's/HTTPSTATUS\:.*//g')
|
|
178
|
+
|
|
179
|
+
if [ "$HTTP_STATUS" -eq 201 ]; then
|
|
180
|
+
PROJECT_ID=$(echo "$BODY" | jq -r '.id')
|
|
181
|
+
WEB_URL=$(echo "$BODY" | jq -r '.web_url')
|
|
182
|
+
|
|
183
|
+
echo ""
|
|
184
|
+
echo "✅ Proyecto creado exitosamente"
|
|
185
|
+
echo " ID: $PROJECT_ID"
|
|
186
|
+
echo " URL: $WEB_URL"
|
|
187
|
+
else
|
|
188
|
+
echo ""
|
|
189
|
+
echo "❌ Error al crear proyecto"
|
|
190
|
+
echo "$BODY" | jq '.'
|
|
191
|
+
exit 1
|
|
192
|
+
fi
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Script: clone-project-settings.sh
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
#!/bin/bash
|
|
199
|
+
|
|
200
|
+
# clone-project-settings.sh
|
|
201
|
+
# Clona configuración de un proyecto a otro
|
|
202
|
+
|
|
203
|
+
source .gitlab-env
|
|
204
|
+
|
|
205
|
+
SOURCE_PROJECT_ID="$1"
|
|
206
|
+
TARGET_PROJECT_ID="$2"
|
|
207
|
+
|
|
208
|
+
if [ -z "$SOURCE_PROJECT_ID" ] || [ -z "$TARGET_PROJECT_ID" ]; then
|
|
209
|
+
echo "Uso: $0 <source_project_id> <target_project_id>"
|
|
210
|
+
exit 1
|
|
211
|
+
fi
|
|
212
|
+
|
|
213
|
+
echo "Clonando configuración..."
|
|
214
|
+
echo " Origen: $SOURCE_PROJECT_ID"
|
|
215
|
+
echo " Destino: $TARGET_PROJECT_ID"
|
|
216
|
+
|
|
217
|
+
# Obtener configuración del proyecto origen
|
|
218
|
+
source_config=$(curl --silent \
|
|
219
|
+
--header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
|
|
220
|
+
--url "$GITLAB_API_URL/projects/$SOURCE_PROJECT_ID")
|
|
221
|
+
|
|
222
|
+
# Extraer configuración relevante
|
|
223
|
+
visibility=$(echo "$source_config" | jq -r '.visibility')
|
|
224
|
+
issues_enabled=$(echo "$source_config" | jq -r '.issues_enabled')
|
|
225
|
+
merge_requests_enabled=$(echo "$source_config" | jq -r '.merge_requests_enabled')
|
|
226
|
+
wiki_enabled=$(echo "$source_config" | jq -r '.wiki_enabled')
|
|
227
|
+
builds_enabled=$(echo "$source_config" | jq -r '.builds_enabled')
|
|
228
|
+
auto_devops_enabled=$(echo "$source_config" | jq -r '.auto_devops_enabled')
|
|
229
|
+
|
|
230
|
+
# Aplicar configuración al proyecto destino
|
|
231
|
+
curl --request PUT \
|
|
232
|
+
--header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
|
|
233
|
+
--header "Content-Type: application/json" \
|
|
234
|
+
--data "{
|
|
235
|
+
\"visibility\": \"$visibility\",
|
|
236
|
+
\"issues_enabled\": $issues_enabled,
|
|
237
|
+
\"merge_requests_enabled\": $merge_requests_enabled,
|
|
238
|
+
\"wiki_enabled\": $wiki_enabled,
|
|
239
|
+
\"builds_enabled\": $builds_enabled,
|
|
240
|
+
\"auto_devops_enabled\": $auto_devops_enabled
|
|
241
|
+
}" \
|
|
242
|
+
--url "$GITLAB_API_URL/projects/$TARGET_PROJECT_ID"
|
|
243
|
+
|
|
244
|
+
echo ""
|
|
245
|
+
echo "✅ Configuración clonada exitosamente"
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
## Scripts de Issues
|
|
251
|
+
|
|
252
|
+
### Script: create-issue-from-template.sh
|
|
253
|
+
|
|
254
|
+
```bash
|
|
255
|
+
#!/bin/bash
|
|
256
|
+
|
|
257
|
+
# create-issue-from-template.sh
|
|
258
|
+
# Crea issues desde un template CSV
|
|
259
|
+
|
|
260
|
+
source .gitlab-env
|
|
261
|
+
|
|
262
|
+
PROJECT_ID="$1"
|
|
263
|
+
CSV_FILE="$2"
|
|
264
|
+
|
|
265
|
+
if [ -z "$PROJECT_ID" ] || [ -z "$CSV_FILE" ]; then
|
|
266
|
+
echo "Uso: $0 <project_id> <csv_file>"
|
|
267
|
+
echo ""
|
|
268
|
+
echo "Formato CSV: title,description,labels,assignee_username"
|
|
269
|
+
exit 1
|
|
270
|
+
fi
|
|
271
|
+
|
|
272
|
+
if [ ! -f "$CSV_FILE" ]; then
|
|
273
|
+
echo "❌ Archivo no encontrado: $CSV_FILE"
|
|
274
|
+
exit 1
|
|
275
|
+
fi
|
|
276
|
+
|
|
277
|
+
echo "Creando issues desde $CSV_FILE..."
|
|
278
|
+
echo ""
|
|
279
|
+
|
|
280
|
+
created=0
|
|
281
|
+
failed=0
|
|
282
|
+
|
|
283
|
+
# Leer CSV (skip header)
|
|
284
|
+
tail -n +2 "$CSV_FILE" | while IFS=',' read -r title description labels assignee_username; do
|
|
285
|
+
echo "Creando: $title"
|
|
286
|
+
|
|
287
|
+
# Obtener assignee ID si se especificó
|
|
288
|
+
assignee_id=""
|
|
289
|
+
if [ -n "$assignee_username" ]; then
|
|
290
|
+
assignee_id=$(curl --silent \
|
|
291
|
+
--header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
|
|
292
|
+
--url "$GITLAB_API_URL/users?username=$assignee_username" \
|
|
293
|
+
| jq -r '.[0].id')
|
|
294
|
+
fi
|
|
295
|
+
|
|
296
|
+
# Crear issue
|
|
297
|
+
response=$(curl --silent --write-out "HTTPSTATUS:%{http_code}" \
|
|
298
|
+
--request POST \
|
|
299
|
+
--header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
|
|
300
|
+
--header "Content-Type: application/json" \
|
|
301
|
+
--data "{
|
|
302
|
+
\"title\": \"$title\",
|
|
303
|
+
\"description\": \"$description\",
|
|
304
|
+
\"labels\": \"$labels\"
|
|
305
|
+
$([ -n "$assignee_id" ] && echo ", \"assignee_ids\": [$assignee_id]")
|
|
306
|
+
}" \
|
|
307
|
+
--url "$GITLAB_API_URL/projects/$PROJECT_ID/issues")
|
|
308
|
+
|
|
309
|
+
HTTP_STATUS=$(echo "$response" | tr -d '\n' | sed -e 's/.*HTTPSTATUS://')
|
|
310
|
+
|
|
311
|
+
if [ "$HTTP_STATUS" -eq 201 ]; then
|
|
312
|
+
created=$((created + 1))
|
|
313
|
+
echo " ✅ Creado"
|
|
314
|
+
else
|
|
315
|
+
failed=$((failed + 1))
|
|
316
|
+
echo " ❌ Error"
|
|
317
|
+
fi
|
|
318
|
+
|
|
319
|
+
# Rate limiting
|
|
320
|
+
sleep 0.5
|
|
321
|
+
done
|
|
322
|
+
|
|
323
|
+
echo ""
|
|
324
|
+
echo "Resumen:"
|
|
325
|
+
echo " ✅ Creados: $created"
|
|
326
|
+
echo " ❌ Fallidos: $failed"
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
### Script: export-issues.sh
|
|
330
|
+
|
|
331
|
+
```bash
|
|
332
|
+
#!/bin/bash
|
|
333
|
+
|
|
334
|
+
# export-issues.sh
|
|
335
|
+
# Exporta todas las issues de un proyecto a JSON/CSV
|
|
336
|
+
|
|
337
|
+
source .gitlab-env
|
|
338
|
+
|
|
339
|
+
PROJECT_ID="$1"
|
|
340
|
+
FORMAT="${2:-json}" # json o csv
|
|
341
|
+
|
|
342
|
+
if [ -z "$PROJECT_ID" ]; then
|
|
343
|
+
echo "Uso: $0 <project_id> [format]"
|
|
344
|
+
echo " format: json (default) o csv"
|
|
345
|
+
exit 1
|
|
346
|
+
fi
|
|
347
|
+
|
|
348
|
+
OUTPUT_FILE="issues-$PROJECT_ID.$FORMAT"
|
|
349
|
+
page=1
|
|
350
|
+
all_issues="[]"
|
|
351
|
+
|
|
352
|
+
echo "Exportando issues del proyecto $PROJECT_ID..."
|
|
353
|
+
|
|
354
|
+
while true; do
|
|
355
|
+
echo "Página $page..."
|
|
356
|
+
|
|
357
|
+
response=$(curl --silent \
|
|
358
|
+
--header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
|
|
359
|
+
--url "$GITLAB_API_URL/projects/$PROJECT_ID/issues?page=$page&per_page=100")
|
|
360
|
+
|
|
361
|
+
count=$(echo "$response" | jq '. | length')
|
|
362
|
+
|
|
363
|
+
if [ "$count" -eq 0 ]; then
|
|
364
|
+
break
|
|
365
|
+
fi
|
|
366
|
+
|
|
367
|
+
all_issues=$(echo "$all_issues" | jq ". + $response")
|
|
368
|
+
page=$((page + 1))
|
|
369
|
+
done
|
|
370
|
+
|
|
371
|
+
total=$(echo "$all_issues" | jq '. | length')
|
|
372
|
+
|
|
373
|
+
if [ "$FORMAT" = "json" ]; then
|
|
374
|
+
echo "$all_issues" > "$OUTPUT_FILE"
|
|
375
|
+
elif [ "$FORMAT" = "csv" ]; then
|
|
376
|
+
echo "iid,title,state,labels,assignee,created_at,updated_at,web_url" > "$OUTPUT_FILE"
|
|
377
|
+
echo "$all_issues" | jq -r '.[] | [
|
|
378
|
+
.iid,
|
|
379
|
+
.title,
|
|
380
|
+
.state,
|
|
381
|
+
(.labels | join(";")),
|
|
382
|
+
(.assignees[0].username // ""),
|
|
383
|
+
.created_at,
|
|
384
|
+
.updated_at,
|
|
385
|
+
.web_url
|
|
386
|
+
] | @csv' >> "$OUTPUT_FILE"
|
|
387
|
+
fi
|
|
388
|
+
|
|
389
|
+
echo ""
|
|
390
|
+
echo "✅ Exportadas $total issues"
|
|
391
|
+
echo "📄 Guardado en: $OUTPUT_FILE"
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
### Script: bulk-close-issues.sh
|
|
395
|
+
|
|
396
|
+
```bash
|
|
397
|
+
#!/bin/bash
|
|
398
|
+
|
|
399
|
+
# bulk-close-issues.sh
|
|
400
|
+
# Cierra issues en bulk por label o milestone
|
|
401
|
+
|
|
402
|
+
source .gitlab-env
|
|
403
|
+
|
|
404
|
+
PROJECT_ID="$1"
|
|
405
|
+
FILTER_TYPE="$2" # label o milestone
|
|
406
|
+
FILTER_VALUE="$3"
|
|
407
|
+
|
|
408
|
+
if [ -z "$PROJECT_ID" ] || [ -z "$FILTER_TYPE" ] || [ -z "$FILTER_VALUE" ]; then
|
|
409
|
+
echo "Uso: $0 <project_id> <filter_type> <filter_value>"
|
|
410
|
+
echo " filter_type: label o milestone"
|
|
411
|
+
echo ""
|
|
412
|
+
echo "Ejemplos:"
|
|
413
|
+
echo " $0 123 label bug"
|
|
414
|
+
echo " $0 123 milestone v1.0"
|
|
415
|
+
exit 1
|
|
416
|
+
fi
|
|
417
|
+
|
|
418
|
+
# Construir URL según filtro
|
|
419
|
+
if [ "$FILTER_TYPE" = "label" ]; then
|
|
420
|
+
FILTER_PARAM="labels=$FILTER_VALUE"
|
|
421
|
+
elif [ "$FILTER_TYPE" = "milestone" ]; then
|
|
422
|
+
FILTER_PARAM="milestone=$FILTER_VALUE"
|
|
423
|
+
else
|
|
424
|
+
echo "❌ Tipo de filtro inválido: $FILTER_TYPE"
|
|
425
|
+
exit 1
|
|
426
|
+
fi
|
|
427
|
+
|
|
428
|
+
echo "Obteniendo issues con $FILTER_TYPE=$FILTER_VALUE..."
|
|
429
|
+
|
|
430
|
+
issues=$(curl --silent \
|
|
431
|
+
--header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
|
|
432
|
+
--url "$GITLAB_API_URL/projects/$PROJECT_ID/issues?state=opened&$FILTER_PARAM&per_page=100")
|
|
433
|
+
|
|
434
|
+
count=$(echo "$issues" | jq '. | length')
|
|
435
|
+
|
|
436
|
+
if [ "$count" -eq 0 ]; then
|
|
437
|
+
echo "No se encontraron issues abiertas"
|
|
438
|
+
exit 0
|
|
439
|
+
fi
|
|
440
|
+
|
|
441
|
+
echo "Encontradas $count issues abiertas"
|
|
442
|
+
echo ""
|
|
443
|
+
read -p "¿Cerrar todas? (y/n): " confirm
|
|
444
|
+
|
|
445
|
+
if [ "$confirm" != "y" ]; then
|
|
446
|
+
echo "Cancelado"
|
|
447
|
+
exit 0
|
|
448
|
+
fi
|
|
449
|
+
|
|
450
|
+
closed=0
|
|
451
|
+
failed=0
|
|
452
|
+
|
|
453
|
+
echo "$issues" | jq -r '.[].iid' | while read -r iid; do
|
|
454
|
+
echo "Cerrando issue #$iid..."
|
|
455
|
+
|
|
456
|
+
response=$(curl --silent --write-out "HTTPSTATUS:%{http_code}" \
|
|
457
|
+
--request PUT \
|
|
458
|
+
--header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
|
|
459
|
+
--header "Content-Type: application/json" \
|
|
460
|
+
--data '{"state_event": "close"}' \
|
|
461
|
+
--url "$GITLAB_API_URL/projects/$PROJECT_ID/issues/$iid")
|
|
462
|
+
|
|
463
|
+
HTTP_STATUS=$(echo "$response" | tr -d '\n' | sed -e 's/.*HTTPSTATUS://')
|
|
464
|
+
|
|
465
|
+
if [ "$HTTP_STATUS" -eq 200 ]; then
|
|
466
|
+
closed=$((closed + 1))
|
|
467
|
+
else
|
|
468
|
+
failed=$((failed + 1))
|
|
469
|
+
fi
|
|
470
|
+
|
|
471
|
+
sleep 0.3
|
|
472
|
+
done
|
|
473
|
+
|
|
474
|
+
echo ""
|
|
475
|
+
echo "Resumen:"
|
|
476
|
+
echo " ✅ Cerradas: $closed"
|
|
477
|
+
echo " ❌ Fallidas: $failed"
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
---
|
|
481
|
+
|
|
482
|
+
## Scripts de Merge Requests
|
|
483
|
+
|
|
484
|
+
### Script: create-mr-from-branch.sh
|
|
485
|
+
|
|
486
|
+
```bash
|
|
487
|
+
#!/bin/bash
|
|
488
|
+
|
|
489
|
+
# create-mr-from-branch.sh
|
|
490
|
+
# Crea MR automáticamente desde branch actual
|
|
491
|
+
|
|
492
|
+
source .gitlab-env
|
|
493
|
+
|
|
494
|
+
# Detectar branch actual
|
|
495
|
+
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
|
496
|
+
|
|
497
|
+
if [ "$CURRENT_BRANCH" = "main" ] || [ "$CURRENT_BRANCH" = "master" ]; then
|
|
498
|
+
echo "❌ No puedes crear MR desde la rama principal"
|
|
499
|
+
exit 1
|
|
500
|
+
fi
|
|
501
|
+
|
|
502
|
+
# Detectar proyecto desde remote
|
|
503
|
+
REMOTE_URL=$(git config --get remote.origin.url)
|
|
504
|
+
PROJECT_PATH=$(echo "$REMOTE_URL" | sed -E 's|.*[:/]([^/]+/[^/]+)\.git|\1|')
|
|
505
|
+
|
|
506
|
+
echo "Creando MR..."
|
|
507
|
+
echo " Branch: $CURRENT_BRANCH"
|
|
508
|
+
echo " Proyecto: $PROJECT_PATH"
|
|
509
|
+
|
|
510
|
+
# Obtener project ID
|
|
511
|
+
PROJECT_ID=$(curl --silent \
|
|
512
|
+
--header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
|
|
513
|
+
--url "$GITLAB_API_URL/projects/$(echo "$PROJECT_PATH" | sed 's|/|%2F|g')" \
|
|
514
|
+
| jq -r '.id')
|
|
515
|
+
|
|
516
|
+
if [ "$PROJECT_ID" = "null" ]; then
|
|
517
|
+
echo "❌ No se pudo obtener ID del proyecto"
|
|
518
|
+
exit 1
|
|
519
|
+
fi
|
|
520
|
+
|
|
521
|
+
# Generar título desde nombre de branch
|
|
522
|
+
TITLE=$(echo "$CURRENT_BRANCH" | sed 's|-| |g' | sed 's|_| |g')
|
|
523
|
+
TITLE=$(echo "$TITLE" | awk '{for(i=1;i<=NF;i++)sub(/./,toupper(substr($i,1,1)),$i)}1')
|
|
524
|
+
|
|
525
|
+
read -p "Título del MR [$TITLE]: " INPUT_TITLE
|
|
526
|
+
TITLE=${INPUT_TITLE:-$TITLE}
|
|
527
|
+
|
|
528
|
+
read -p "Target branch [main]: " TARGET_BRANCH
|
|
529
|
+
TARGET_BRANCH=${TARGET_BRANCH:-main}
|
|
530
|
+
|
|
531
|
+
# Crear MR
|
|
532
|
+
response=$(curl --silent --write-out "HTTPSTATUS:%{http_code}" \
|
|
533
|
+
--request POST \
|
|
534
|
+
--header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
|
|
535
|
+
--header "Content-Type: application/json" \
|
|
536
|
+
--data "{
|
|
537
|
+
\"source_branch\": \"$CURRENT_BRANCH\",
|
|
538
|
+
\"target_branch\": \"$TARGET_BRANCH\",
|
|
539
|
+
\"title\": \"$TITLE\",
|
|
540
|
+
\"remove_source_branch\": true
|
|
541
|
+
}" \
|
|
542
|
+
--url "$GITLAB_API_URL/projects/$PROJECT_ID/merge_requests")
|
|
543
|
+
|
|
544
|
+
HTTP_STATUS=$(echo "$response" | tr -d '\n' | sed -e 's/.*HTTPSTATUS://')
|
|
545
|
+
BODY=$(echo "$response" | sed -e 's/HTTPSTATUS\:.*//g')
|
|
546
|
+
|
|
547
|
+
if [ "$HTTP_STATUS" -eq 201 ]; then
|
|
548
|
+
MR_IID=$(echo "$BODY" | jq -r '.iid')
|
|
549
|
+
WEB_URL=$(echo "$BODY" | jq -r '.web_url')
|
|
550
|
+
|
|
551
|
+
echo ""
|
|
552
|
+
echo "✅ MR creado exitosamente"
|
|
553
|
+
echo " IID: !$MR_IID"
|
|
554
|
+
echo " URL: $WEB_URL"
|
|
555
|
+
else
|
|
556
|
+
echo ""
|
|
557
|
+
echo "❌ Error al crear MR"
|
|
558
|
+
echo "$BODY" | jq '.'
|
|
559
|
+
exit 1
|
|
560
|
+
fi
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
### Script: auto-approve-mrs.sh
|
|
564
|
+
|
|
565
|
+
```bash
|
|
566
|
+
#!/bin/bash
|
|
567
|
+
|
|
568
|
+
# auto-approve-mrs.sh
|
|
569
|
+
# Aprueba automáticamente MRs que cumplen criterios
|
|
570
|
+
|
|
571
|
+
source .gitlab-env
|
|
572
|
+
|
|
573
|
+
PROJECT_ID="$1"
|
|
574
|
+
|
|
575
|
+
if [ -z "$PROJECT_ID" ]; then
|
|
576
|
+
echo "Uso: $0 <project_id>"
|
|
577
|
+
exit 1
|
|
578
|
+
fi
|
|
579
|
+
|
|
580
|
+
echo "Buscando MRs para aprobar en proyecto $PROJECT_ID..."
|
|
581
|
+
|
|
582
|
+
# Obtener MRs abiertos
|
|
583
|
+
mrs=$(curl --silent \
|
|
584
|
+
--header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
|
|
585
|
+
--url "$GITLAB_API_URL/projects/$PROJECT_ID/merge_requests?state=opened")
|
|
586
|
+
|
|
587
|
+
approved=0
|
|
588
|
+
skipped=0
|
|
589
|
+
|
|
590
|
+
echo "$mrs" | jq -c '.[]' | while read -r mr; do
|
|
591
|
+
iid=$(echo "$mr" | jq -r '.iid')
|
|
592
|
+
title=$(echo "$mr" | jq -r '.title')
|
|
593
|
+
pipeline_status=$(echo "$mr" | jq -r '.pipeline.status // "none"')
|
|
594
|
+
has_conflicts=$(echo "$mr" | jq -r '.has_conflicts')
|
|
595
|
+
|
|
596
|
+
echo ""
|
|
597
|
+
echo "MR !$iid: $title"
|
|
598
|
+
echo " Pipeline: $pipeline_status"
|
|
599
|
+
echo " Conflicts: $has_conflicts"
|
|
600
|
+
|
|
601
|
+
# Criterios de auto-aprobación
|
|
602
|
+
if [ "$pipeline_status" = "success" ] && [ "$has_conflicts" = "false" ]; then
|
|
603
|
+
echo " ✅ Aprobando..."
|
|
604
|
+
|
|
605
|
+
curl --silent \
|
|
606
|
+
--request POST \
|
|
607
|
+
--header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
|
|
608
|
+
--url "$GITLAB_API_URL/projects/$PROJECT_ID/merge_requests/$iid/approve" \
|
|
609
|
+
> /dev/null
|
|
610
|
+
|
|
611
|
+
approved=$((approved + 1))
|
|
612
|
+
else
|
|
613
|
+
echo " ⏭️ Omitiendo (no cumple criterios)"
|
|
614
|
+
skipped=$((skipped + 1))
|
|
615
|
+
fi
|
|
616
|
+
done
|
|
617
|
+
|
|
618
|
+
echo ""
|
|
619
|
+
echo "Resumen:"
|
|
620
|
+
echo " ✅ Aprobados: $approved"
|
|
621
|
+
echo " ⏭️ Omitidos: $skipped"
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
---
|
|
625
|
+
|
|
626
|
+
## Scripts de Pipelines
|
|
627
|
+
|
|
628
|
+
### Script: monitor-pipeline.sh
|
|
629
|
+
|
|
630
|
+
```bash
|
|
631
|
+
#!/bin/bash
|
|
632
|
+
|
|
633
|
+
# monitor-pipeline.sh
|
|
634
|
+
# Monitorea pipeline en tiempo real
|
|
635
|
+
|
|
636
|
+
source .gitlab-env
|
|
637
|
+
|
|
638
|
+
PROJECT_ID="$1"
|
|
639
|
+
PIPELINE_ID="$2"
|
|
640
|
+
|
|
641
|
+
if [ -z "$PROJECT_ID" ] || [ -z "$PIPELINE_ID" ]; then
|
|
642
|
+
echo "Uso: $0 <project_id> <pipeline_id>"
|
|
643
|
+
exit 1
|
|
644
|
+
fi
|
|
645
|
+
|
|
646
|
+
echo "Monitoreando pipeline $PIPELINE_ID..."
|
|
647
|
+
echo ""
|
|
648
|
+
|
|
649
|
+
previous_status=""
|
|
650
|
+
|
|
651
|
+
while true; do
|
|
652
|
+
response=$(curl --silent \
|
|
653
|
+
--header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
|
|
654
|
+
--url "$GITLAB_API_URL/projects/$PROJECT_ID/pipelines/$PIPELINE_ID")
|
|
655
|
+
|
|
656
|
+
status=$(echo "$response" | jq -r '.status')
|
|
657
|
+
duration=$(echo "$response" | jq -r '.duration // 0')
|
|
658
|
+
|
|
659
|
+
# Solo mostrar si cambió el estado
|
|
660
|
+
if [ "$status" != "$previous_status" ]; then
|
|
661
|
+
timestamp=$(date '+%Y-%m-%d %H:%M:%S')
|
|
662
|
+
echo "[$timestamp] Status: $status (Duration: ${duration}s)"
|
|
663
|
+
|
|
664
|
+
# Mostrar jobs
|
|
665
|
+
jobs=$(curl --silent \
|
|
666
|
+
--header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
|
|
667
|
+
--url "$GITLAB_API_URL/projects/$PROJECT_ID/pipelines/$PIPELINE_ID/jobs")
|
|
668
|
+
|
|
669
|
+
echo "$jobs" | jq -r '.[] | " - \(.name): \(.status)"'
|
|
670
|
+
echo ""
|
|
671
|
+
|
|
672
|
+
previous_status="$status"
|
|
673
|
+
fi
|
|
674
|
+
|
|
675
|
+
# Verificar si terminó
|
|
676
|
+
if [[ "$status" == "success" || "$status" == "failed" || "$status" == "canceled" ]]; then
|
|
677
|
+
echo "Pipeline finalizado con estado: $status"
|
|
678
|
+
|
|
679
|
+
if [ "$status" = "success" ]; then
|
|
680
|
+
exit 0
|
|
681
|
+
else
|
|
682
|
+
exit 1
|
|
683
|
+
fi
|
|
684
|
+
fi
|
|
685
|
+
|
|
686
|
+
sleep 10
|
|
687
|
+
done
|
|
688
|
+
```
|
|
689
|
+
|
|
690
|
+
### Script: trigger-pipeline-with-vars.sh
|
|
691
|
+
|
|
692
|
+
```bash
|
|
693
|
+
#!/bin/bash
|
|
694
|
+
|
|
695
|
+
# trigger-pipeline-with-vars.sh
|
|
696
|
+
# Ejecuta pipeline con variables personalizadas
|
|
697
|
+
|
|
698
|
+
source .gitlab-env
|
|
699
|
+
|
|
700
|
+
PROJECT_ID="$1"
|
|
701
|
+
REF="${2:-main}"
|
|
702
|
+
|
|
703
|
+
if [ -z "$PROJECT_ID" ]; then
|
|
704
|
+
echo "Uso: $0 <project_id> [ref]"
|
|
705
|
+
exit 1
|
|
706
|
+
fi
|
|
707
|
+
|
|
708
|
+
echo "Configurar variables para pipeline..."
|
|
709
|
+
echo ""
|
|
710
|
+
|
|
711
|
+
variables="[]"
|
|
712
|
+
|
|
713
|
+
while true; do
|
|
714
|
+
read -p "Variable key (Enter para terminar): " key
|
|
715
|
+
|
|
716
|
+
if [ -z "$key" ]; then
|
|
717
|
+
break
|
|
718
|
+
fi
|
|
719
|
+
|
|
720
|
+
read -p "Variable value: " value
|
|
721
|
+
|
|
722
|
+
variables=$(echo "$variables" | jq ". + [{\"key\": \"$key\", \"value\": \"$value\"}]")
|
|
723
|
+
echo " ✅ Agregada: $key"
|
|
724
|
+
done
|
|
725
|
+
|
|
726
|
+
if [ "$(echo "$variables" | jq '. | length')" -eq 0 ]; then
|
|
727
|
+
echo "No se agregaron variables"
|
|
728
|
+
variables_json=""
|
|
729
|
+
else
|
|
730
|
+
variables_json=", \"variables\": $variables"
|
|
731
|
+
fi
|
|
732
|
+
|
|
733
|
+
echo ""
|
|
734
|
+
echo "Ejecutando pipeline en $REF..."
|
|
735
|
+
|
|
736
|
+
response=$(curl --silent --write-out "HTTPSTATUS:%{http_code}" \
|
|
737
|
+
--request POST \
|
|
738
|
+
--header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
|
|
739
|
+
--header "Content-Type: application/json" \
|
|
740
|
+
--data "{\"ref\": \"$REF\"$variables_json}" \
|
|
741
|
+
--url "$GITLAB_API_URL/projects/$PROJECT_ID/pipeline")
|
|
742
|
+
|
|
743
|
+
HTTP_STATUS=$(echo "$response" | tr -d '\n' | sed -e 's/.*HTTPSTATUS://')
|
|
744
|
+
BODY=$(echo "$response" | sed -e 's/HTTPSTATUS\:.*//g')
|
|
745
|
+
|
|
746
|
+
if [ "$HTTP_STATUS" -eq 201 ]; then
|
|
747
|
+
PIPELINE_ID=$(echo "$BODY" | jq -r '.id')
|
|
748
|
+
WEB_URL=$(echo "$BODY" | jq -r '.web_url')
|
|
749
|
+
|
|
750
|
+
echo ""
|
|
751
|
+
echo "✅ Pipeline creado"
|
|
752
|
+
echo " ID: $PIPELINE_ID"
|
|
753
|
+
echo " URL: $WEB_URL"
|
|
754
|
+
echo ""
|
|
755
|
+
|
|
756
|
+
read -p "¿Monitorear pipeline? (y/n): " monitor
|
|
757
|
+
|
|
758
|
+
if [ "$monitor" = "y" ]; then
|
|
759
|
+
exec "$0/../monitor-pipeline.sh" "$PROJECT_ID" "$PIPELINE_ID"
|
|
760
|
+
fi
|
|
761
|
+
else
|
|
762
|
+
echo ""
|
|
763
|
+
echo "❌ Error al crear pipeline"
|
|
764
|
+
echo "$BODY" | jq '.'
|
|
765
|
+
exit 1
|
|
766
|
+
fi
|
|
767
|
+
```
|
|
768
|
+
|
|
769
|
+
### Script: retry-failed-jobs.sh
|
|
770
|
+
|
|
771
|
+
```bash
|
|
772
|
+
#!/bin/bash
|
|
773
|
+
|
|
774
|
+
# retry-failed-jobs.sh
|
|
775
|
+
# Reintenta todos los jobs fallidos de un pipeline
|
|
776
|
+
|
|
777
|
+
source .gitlab-env
|
|
778
|
+
|
|
779
|
+
PROJECT_ID="$1"
|
|
780
|
+
PIPELINE_ID="$2"
|
|
781
|
+
|
|
782
|
+
if [ -z "$PROJECT_ID" ] || [ -z "$PIPELINE_ID" ]; then
|
|
783
|
+
echo "Uso: $0 <project_id> <pipeline_id>"
|
|
784
|
+
exit 1
|
|
785
|
+
fi
|
|
786
|
+
|
|
787
|
+
echo "Obteniendo jobs fallidos del pipeline $PIPELINE_ID..."
|
|
788
|
+
|
|
789
|
+
jobs=$(curl --silent \
|
|
790
|
+
--header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
|
|
791
|
+
--url "$GITLAB_API_URL/projects/$PROJECT_ID/pipelines/$PIPELINE_ID/jobs?scope[]=failed")
|
|
792
|
+
|
|
793
|
+
count=$(echo "$jobs" | jq '. | length')
|
|
794
|
+
|
|
795
|
+
if [ "$count" -eq 0 ]; then
|
|
796
|
+
echo "No hay jobs fallidos"
|
|
797
|
+
exit 0
|
|
798
|
+
fi
|
|
799
|
+
|
|
800
|
+
echo "Encontrados $count jobs fallidos:"
|
|
801
|
+
echo "$jobs" | jq -r '.[] | " - \(.id): \(.name)"'
|
|
802
|
+
echo ""
|
|
803
|
+
|
|
804
|
+
read -p "¿Reintentar todos? (y/n): " confirm
|
|
805
|
+
|
|
806
|
+
if [ "$confirm" != "y" ]; then
|
|
807
|
+
echo "Cancelado"
|
|
808
|
+
exit 0
|
|
809
|
+
fi
|
|
810
|
+
|
|
811
|
+
retried=0
|
|
812
|
+
failed=0
|
|
813
|
+
|
|
814
|
+
echo "$jobs" | jq -r '.[].id' | while read -r job_id; do
|
|
815
|
+
echo "Reintentando job $job_id..."
|
|
816
|
+
|
|
817
|
+
response=$(curl --silent --write-out "HTTPSTATUS:%{http_code}" \
|
|
818
|
+
--request POST \
|
|
819
|
+
--header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
|
|
820
|
+
--url "$GITLAB_API_URL/projects/$PROJECT_ID/jobs/$job_id/retry")
|
|
821
|
+
|
|
822
|
+
HTTP_STATUS=$(echo "$response" | tr -d '\n' | sed -e 's/.*HTTPSTATUS://')
|
|
823
|
+
|
|
824
|
+
if [ "$HTTP_STATUS" -eq 201 ]; then
|
|
825
|
+
retried=$((retried + 1))
|
|
826
|
+
else
|
|
827
|
+
failed=$((failed + 1))
|
|
828
|
+
fi
|
|
829
|
+
|
|
830
|
+
sleep 0.5
|
|
831
|
+
done
|
|
832
|
+
|
|
833
|
+
echo ""
|
|
834
|
+
echo "Resumen:"
|
|
835
|
+
echo " ✅ Reintentados: $retried"
|
|
836
|
+
echo " ❌ Fallidos: $failed"
|
|
837
|
+
```
|
|
838
|
+
|
|
839
|
+
---
|
|
840
|
+
|
|
841
|
+
## Scripts de Repository
|
|
842
|
+
|
|
843
|
+
### Script: backup-repository.sh
|
|
844
|
+
|
|
845
|
+
```bash
|
|
846
|
+
#!/bin/bash
|
|
847
|
+
|
|
848
|
+
# backup-repository.sh
|
|
849
|
+
# Crea backup completo de un repositorio
|
|
850
|
+
|
|
851
|
+
source .gitlab-env
|
|
852
|
+
|
|
853
|
+
PROJECT_ID="$1"
|
|
854
|
+
BACKUP_DIR="${2:-./backups}"
|
|
855
|
+
|
|
856
|
+
if [ -z "$PROJECT_ID" ]; then
|
|
857
|
+
echo "Uso: $0 <project_id> [backup_dir]"
|
|
858
|
+
exit 1
|
|
859
|
+
fi
|
|
860
|
+
|
|
861
|
+
mkdir -p "$BACKUP_DIR"
|
|
862
|
+
|
|
863
|
+
echo "Creando backup del proyecto $PROJECT_ID..."
|
|
864
|
+
|
|
865
|
+
# Obtener información del proyecto
|
|
866
|
+
project=$(curl --silent \
|
|
867
|
+
--header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
|
|
868
|
+
--url "$GITLAB_API_URL/projects/$PROJECT_ID")
|
|
869
|
+
|
|
870
|
+
project_name=$(echo "$project" | jq -r '.path_with_namespace' | tr '/' '-')
|
|
871
|
+
timestamp=$(date '+%Y%m%d-%H%M%S')
|
|
872
|
+
backup_name="$project_name-$timestamp"
|
|
873
|
+
backup_path="$BACKUP_DIR/$backup_name"
|
|
874
|
+
|
|
875
|
+
mkdir -p "$backup_path"
|
|
876
|
+
|
|
877
|
+
echo " Nombre: $project_name"
|
|
878
|
+
echo " Backup: $backup_path"
|
|
879
|
+
echo ""
|
|
880
|
+
|
|
881
|
+
# 1. Clonar repositorio
|
|
882
|
+
echo "1. Clonando repositorio..."
|
|
883
|
+
git_url=$(echo "$project" | jq -r '.http_url_to_repo')
|
|
884
|
+
git clone --mirror "$git_url" "$backup_path/repository.git"
|
|
885
|
+
|
|
886
|
+
# 2. Exportar issues
|
|
887
|
+
echo "2. Exportando issues..."
|
|
888
|
+
page=1
|
|
889
|
+
all_issues="[]"
|
|
890
|
+
while true; do
|
|
891
|
+
issues=$(curl --silent \
|
|
892
|
+
--header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
|
|
893
|
+
--url "$GITLAB_API_URL/projects/$PROJECT_ID/issues?page=$page&per_page=100")
|
|
894
|
+
|
|
895
|
+
count=$(echo "$issues" | jq '. | length')
|
|
896
|
+
if [ "$count" -eq 0 ]; then break; fi
|
|
897
|
+
|
|
898
|
+
all_issues=$(echo "$all_issues" | jq ". + $issues")
|
|
899
|
+
page=$((page + 1))
|
|
900
|
+
done
|
|
901
|
+
echo "$all_issues" > "$backup_path/issues.json"
|
|
902
|
+
|
|
903
|
+
# 3. Exportar merge requests
|
|
904
|
+
echo "3. Exportando merge requests..."
|
|
905
|
+
page=1
|
|
906
|
+
all_mrs="[]"
|
|
907
|
+
while true; do
|
|
908
|
+
mrs=$(curl --silent \
|
|
909
|
+
--header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
|
|
910
|
+
--url "$GITLAB_API_URL/projects/$PROJECT_ID/merge_requests?page=$page&per_page=100")
|
|
911
|
+
|
|
912
|
+
count=$(echo "$mrs" | jq '. | length')
|
|
913
|
+
if [ "$count" -eq 0 ]; then break; fi
|
|
914
|
+
|
|
915
|
+
all_mrs=$(echo "$all_mrs" | jq ". + $mrs")
|
|
916
|
+
page=$((page + 1))
|
|
917
|
+
done
|
|
918
|
+
echo "$all_mrs" > "$backup_path/merge_requests.json"
|
|
919
|
+
|
|
920
|
+
# 4. Exportar pipelines (últimos 100)
|
|
921
|
+
echo "4. Exportando pipelines..."
|
|
922
|
+
pipelines=$(curl --silent \
|
|
923
|
+
--header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
|
|
924
|
+
--url "$GITLAB_API_URL/projects/$PROJECT_ID/pipelines?per_page=100")
|
|
925
|
+
echo "$pipelines" > "$backup_path/pipelines.json"
|
|
926
|
+
|
|
927
|
+
# 5. Exportar configuración del proyecto
|
|
928
|
+
echo "5. Exportando configuración..."
|
|
929
|
+
echo "$project" > "$backup_path/project.json"
|
|
930
|
+
|
|
931
|
+
# 6. Crear archivo comprimido
|
|
932
|
+
echo "6. Comprimiendo backup..."
|
|
933
|
+
cd "$BACKUP_DIR"
|
|
934
|
+
tar -czf "$backup_name.tar.gz" "$backup_name"
|
|
935
|
+
rm -rf "$backup_name"
|
|
936
|
+
|
|
937
|
+
echo ""
|
|
938
|
+
echo "✅ Backup completado"
|
|
939
|
+
echo " Archivo: $BACKUP_DIR/$backup_name.tar.gz"
|
|
940
|
+
```
|
|
941
|
+
|
|
942
|
+
### Script: sync-files-to-repo.sh
|
|
943
|
+
|
|
944
|
+
```bash
|
|
945
|
+
#!/bin/bash
|
|
946
|
+
|
|
947
|
+
# sync-files-to-repo.sh
|
|
948
|
+
# Sincroniza archivos locales con repositorio GitLab
|
|
949
|
+
|
|
950
|
+
source .gitlab-env
|
|
951
|
+
|
|
952
|
+
PROJECT_ID="$1"
|
|
953
|
+
LOCAL_DIR="$2"
|
|
954
|
+
BRANCH="${3:-main}"
|
|
955
|
+
|
|
956
|
+
if [ -z "$PROJECT_ID" ] || [ -z "$LOCAL_DIR" ]; then
|
|
957
|
+
echo "Uso: $0 <project_id> <local_dir> [branch]"
|
|
958
|
+
exit 1
|
|
959
|
+
fi
|
|
960
|
+
|
|
961
|
+
if [ ! -d "$LOCAL_DIR" ]; then
|
|
962
|
+
echo "❌ Directorio no encontrado: $LOCAL_DIR"
|
|
963
|
+
exit 1
|
|
964
|
+
fi
|
|
965
|
+
|
|
966
|
+
echo "Sincronizando archivos de $LOCAL_DIR a proyecto $PROJECT_ID..."
|
|
967
|
+
echo ""
|
|
968
|
+
|
|
969
|
+
# Crear commit con múltiples archivos
|
|
970
|
+
actions="[]"
|
|
971
|
+
|
|
972
|
+
find "$LOCAL_DIR" -type f | while read -r file; do
|
|
973
|
+
relative_path="${file#$LOCAL_DIR/}"
|
|
974
|
+
content=$(cat "$file" | base64)
|
|
975
|
+
|
|
976
|
+
echo " + $relative_path"
|
|
977
|
+
|
|
978
|
+
actions=$(echo "$actions" | jq ". + [{
|
|
979
|
+
\"action\": \"create\",
|
|
980
|
+
\"file_path\": \"$relative_path\",
|
|
981
|
+
\"content\": \"$content\",
|
|
982
|
+
\"encoding\": \"base64\"
|
|
983
|
+
}]")
|
|
984
|
+
done
|
|
985
|
+
|
|
986
|
+
echo ""
|
|
987
|
+
echo "Creando commit..."
|
|
988
|
+
|
|
989
|
+
response=$(curl --silent --write-out "HTTPSTATUS:%{http_code}" \
|
|
990
|
+
--request POST \
|
|
991
|
+
--header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
|
|
992
|
+
--header "Content-Type: application/json" \
|
|
993
|
+
--data "{
|
|
994
|
+
\"branch\": \"$BRANCH\",
|
|
995
|
+
\"commit_message\": \"Sync files from local directory\",
|
|
996
|
+
\"actions\": $actions
|
|
997
|
+
}" \
|
|
998
|
+
--url "$GITLAB_API_URL/projects/$PROJECT_ID/repository/commits")
|
|
999
|
+
|
|
1000
|
+
HTTP_STATUS=$(echo "$response" | tr -d '\n' | sed -e 's/.*HTTPSTATUS://')
|
|
1001
|
+
BODY=$(echo "$response" | sed -e 's/HTTPSTATUS\:.*//g')
|
|
1002
|
+
|
|
1003
|
+
if [ "$HTTP_STATUS" -eq 201 ]; then
|
|
1004
|
+
COMMIT_ID=$(echo "$BODY" | jq -r '.id')
|
|
1005
|
+
echo ""
|
|
1006
|
+
echo "✅ Archivos sincronizados"
|
|
1007
|
+
echo " Commit: $COMMIT_ID"
|
|
1008
|
+
else
|
|
1009
|
+
echo ""
|
|
1010
|
+
echo "❌ Error al sincronizar"
|
|
1011
|
+
echo "$BODY" | jq '.'
|
|
1012
|
+
exit 1
|
|
1013
|
+
fi
|
|
1014
|
+
```
|
|
1015
|
+
|
|
1016
|
+
---
|
|
1017
|
+
|
|
1018
|
+
## Scripts de Monitoreo
|
|
1019
|
+
|
|
1020
|
+
### Script: monitor-ci-status.sh
|
|
1021
|
+
|
|
1022
|
+
```bash
|
|
1023
|
+
#!/bin/bash
|
|
1024
|
+
|
|
1025
|
+
# monitor-ci-status.sh
|
|
1026
|
+
# Dashboard de estado de CI/CD para múltiples proyectos
|
|
1027
|
+
|
|
1028
|
+
source .gitlab-env
|
|
1029
|
+
|
|
1030
|
+
PROJECTS_FILE="${1:-projects.txt}"
|
|
1031
|
+
|
|
1032
|
+
if [ ! -f "$PROJECTS_FILE" ]; then
|
|
1033
|
+
echo "❌ Archivo no encontrado: $PROJECTS_FILE"
|
|
1034
|
+
echo ""
|
|
1035
|
+
echo "Crear archivo con IDs de proyectos (uno por línea):"
|
|
1036
|
+
echo " 123"
|
|
1037
|
+
echo " 456"
|
|
1038
|
+
echo " 789"
|
|
1039
|
+
exit 1
|
|
1040
|
+
fi
|
|
1041
|
+
|
|
1042
|
+
while true; do
|
|
1043
|
+
clear
|
|
1044
|
+
echo "=== GitLab CI/CD Dashboard ==="
|
|
1045
|
+
echo "Actualizado: $(date '+%Y-%m-%d %H:%M:%S')"
|
|
1046
|
+
echo ""
|
|
1047
|
+
|
|
1048
|
+
printf "%-40s %-15s %-10s %-10s\n" "PROYECTO" "ÚLTIMO PIPELINE" "ESTADO" "DURACIÓN"
|
|
1049
|
+
printf "%-40s %-15s %-10s %-10s\n" "========" "===============" "======" "========"
|
|
1050
|
+
|
|
1051
|
+
while read -r project_id; do
|
|
1052
|
+
# Obtener proyecto
|
|
1053
|
+
project=$(curl --silent \
|
|
1054
|
+
--header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
|
|
1055
|
+
--url "$GITLAB_API_URL/projects/$project_id")
|
|
1056
|
+
|
|
1057
|
+
project_name=$(echo "$project" | jq -r '.name')
|
|
1058
|
+
|
|
1059
|
+
# Obtener último pipeline
|
|
1060
|
+
pipeline=$(curl --silent \
|
|
1061
|
+
--header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
|
|
1062
|
+
--url "$GITLAB_API_URL/projects/$project_id/pipelines?per_page=1" \
|
|
1063
|
+
| jq '.[0]')
|
|
1064
|
+
|
|
1065
|
+
if [ "$pipeline" != "null" ]; then
|
|
1066
|
+
pipeline_id=$(echo "$pipeline" | jq -r '.id')
|
|
1067
|
+
status=$(echo "$pipeline" | jq -r '.status')
|
|
1068
|
+
duration=$(echo "$pipeline" | jq -r '.duration // 0')
|
|
1069
|
+
|
|
1070
|
+
# Colorear estado
|
|
1071
|
+
case "$status" in
|
|
1072
|
+
success)
|
|
1073
|
+
status_display="\033[32m$status\033[0m"
|
|
1074
|
+
;;
|
|
1075
|
+
failed)
|
|
1076
|
+
status_display="\033[31m$status\033[0m"
|
|
1077
|
+
;;
|
|
1078
|
+
running)
|
|
1079
|
+
status_display="\033[33m$status\033[0m"
|
|
1080
|
+
;;
|
|
1081
|
+
*)
|
|
1082
|
+
status_display="$status"
|
|
1083
|
+
;;
|
|
1084
|
+
esac
|
|
1085
|
+
|
|
1086
|
+
printf "%-40s %-15s %-20s %-10s\n" \
|
|
1087
|
+
"$project_name" \
|
|
1088
|
+
"#$pipeline_id" \
|
|
1089
|
+
"$(echo -e "$status_display")" \
|
|
1090
|
+
"${duration}s"
|
|
1091
|
+
else
|
|
1092
|
+
printf "%-40s %-15s %-10s %-10s\n" \
|
|
1093
|
+
"$project_name" \
|
|
1094
|
+
"N/A" \
|
|
1095
|
+
"N/A" \
|
|
1096
|
+
"N/A"
|
|
1097
|
+
fi
|
|
1098
|
+
done < "$PROJECTS_FILE"
|
|
1099
|
+
|
|
1100
|
+
echo ""
|
|
1101
|
+
echo "Presiona Ctrl+C para salir"
|
|
1102
|
+
sleep 30
|
|
1103
|
+
done
|
|
1104
|
+
```
|
|
1105
|
+
|
|
1106
|
+
### Script: check-pipeline-health.sh
|
|
1107
|
+
|
|
1108
|
+
```bash
|
|
1109
|
+
#!/bin/bash
|
|
1110
|
+
|
|
1111
|
+
# check-pipeline-health.sh
|
|
1112
|
+
# Analiza salud de pipelines de un proyecto
|
|
1113
|
+
|
|
1114
|
+
source .gitlab-env
|
|
1115
|
+
|
|
1116
|
+
PROJECT_ID="$1"
|
|
1117
|
+
DAYS="${2:-7}"
|
|
1118
|
+
|
|
1119
|
+
if [ -z "$PROJECT_ID" ]; then
|
|
1120
|
+
echo "Uso: $0 <project_id> [days]"
|
|
1121
|
+
exit 1
|
|
1122
|
+
fi
|
|
1123
|
+
|
|
1124
|
+
echo "Analizando pipelines de los últimos $DAYS días..."
|
|
1125
|
+
echo ""
|
|
1126
|
+
|
|
1127
|
+
# Calcular fecha
|
|
1128
|
+
date_from=$(date -d "$DAYS days ago" '+%Y-%m-%dT00:00:00Z')
|
|
1129
|
+
|
|
1130
|
+
# Obtener pipelines
|
|
1131
|
+
pipelines=$(curl --silent \
|
|
1132
|
+
--header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
|
|
1133
|
+
--url "$GITLAB_API_URL/projects/$PROJECT_ID/pipelines?updated_after=$date_from&per_page=100")
|
|
1134
|
+
|
|
1135
|
+
total=$(echo "$pipelines" | jq '. | length')
|
|
1136
|
+
success=$(echo "$pipelines" | jq '[.[] | select(.status == "success")] | length')
|
|
1137
|
+
failed=$(echo "$pipelines" | jq '[.[] | select(.status == "failed")] | length')
|
|
1138
|
+
canceled=$(echo "$pipelines" | jq '[.[] | select(.status == "canceled")] | length')
|
|
1139
|
+
|
|
1140
|
+
success_rate=0
|
|
1141
|
+
if [ "$total" -gt 0 ]; then
|
|
1142
|
+
success_rate=$(echo "scale=2; $success * 100 / $total" | bc)
|
|
1143
|
+
fi
|
|
1144
|
+
|
|
1145
|
+
echo "📊 Estadísticas de Pipelines"
|
|
1146
|
+
echo " Total: $total"
|
|
1147
|
+
echo " ✅ Exitosos: $success"
|
|
1148
|
+
echo " ❌ Fallidos: $failed"
|
|
1149
|
+
echo " ⏹️ Cancelados: $canceled"
|
|
1150
|
+
echo " 📈 Tasa de éxito: $success_rate%"
|
|
1151
|
+
echo ""
|
|
1152
|
+
|
|
1153
|
+
# Duración promedio
|
|
1154
|
+
avg_duration=$(echo "$pipelines" | jq '[.[] | select(.duration != null) | .duration] | add / length')
|
|
1155
|
+
echo "⏱️ Duración promedio: ${avg_duration}s"
|
|
1156
|
+
echo ""
|
|
1157
|
+
|
|
1158
|
+
# Pipelines más lentos
|
|
1159
|
+
echo "🐌 Top 5 pipelines más lentos:"
|
|
1160
|
+
echo "$pipelines" | jq -r '
|
|
1161
|
+
sort_by(.duration) | reverse | .[0:5] |
|
|
1162
|
+
.[] |
|
|
1163
|
+
" #\(.id): \(.duration)s - \(.ref)"
|
|
1164
|
+
'
|
|
1165
|
+
```
|
|
1166
|
+
|
|
1167
|
+
---
|
|
1168
|
+
|
|
1169
|
+
## Scripts de Automatización
|
|
1170
|
+
|
|
1171
|
+
### Script: auto-merge-approved-mrs.sh
|
|
1172
|
+
|
|
1173
|
+
```bash
|
|
1174
|
+
#!/bin/bash
|
|
1175
|
+
|
|
1176
|
+
# auto-merge-approved-mrs.sh
|
|
1177
|
+
# Merge automático de MRs aprobados
|
|
1178
|
+
|
|
1179
|
+
source .gitlab-env
|
|
1180
|
+
|
|
1181
|
+
PROJECT_ID="$1"
|
|
1182
|
+
MIN_APPROVALS="${2:-2}"
|
|
1183
|
+
|
|
1184
|
+
if [ -z "$PROJECT_ID" ]; then
|
|
1185
|
+
echo "Uso: $0 <project_id> [min_approvals]"
|
|
1186
|
+
exit 1
|
|
1187
|
+
fi
|
|
1188
|
+
|
|
1189
|
+
echo "Buscando MRs listos para merge..."
|
|
1190
|
+
echo " Aprobaciones mínimas: $MIN_APPROVALS"
|
|
1191
|
+
echo ""
|
|
1192
|
+
|
|
1193
|
+
# Obtener MRs abiertos
|
|
1194
|
+
mrs=$(curl --silent \
|
|
1195
|
+
--header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
|
|
1196
|
+
--url "$GITLAB_API_URL/projects/$PROJECT_ID/merge_requests?state=opened")
|
|
1197
|
+
|
|
1198
|
+
merged=0
|
|
1199
|
+
skipped=0
|
|
1200
|
+
|
|
1201
|
+
echo "$mrs" | jq -c '.[]' | while read -r mr; do
|
|
1202
|
+
iid=$(echo "$mr" | jq -r '.iid')
|
|
1203
|
+
title=$(echo "$mr" | jq -r '.title')
|
|
1204
|
+
|
|
1205
|
+
# Obtener detalles de aprobación
|
|
1206
|
+
approvals=$(curl --silent \
|
|
1207
|
+
--header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
|
|
1208
|
+
--url "$GITLAB_API_URL/projects/$PROJECT_ID/merge_requests/$iid/approvals")
|
|
1209
|
+
|
|
1210
|
+
approved_count=$(echo "$approvals" | jq -r '.approved_by | length')
|
|
1211
|
+
pipeline_status=$(echo "$mr" | jq -r '.pipeline.status // "none"')
|
|
1212
|
+
has_conflicts=$(echo "$mr" | jq -r '.has_conflicts')
|
|
1213
|
+
|
|
1214
|
+
echo "MR !$iid: $title"
|
|
1215
|
+
echo " Aprobaciones: $approved_count/$MIN_APPROVALS"
|
|
1216
|
+
echo " Pipeline: $pipeline_status"
|
|
1217
|
+
echo " Conflicts: $has_conflicts"
|
|
1218
|
+
|
|
1219
|
+
# Verificar criterios
|
|
1220
|
+
if [ "$approved_count" -ge "$MIN_APPROVALS" ] && \
|
|
1221
|
+
[ "$pipeline_status" = "success" ] && \
|
|
1222
|
+
[ "$has_conflicts" = "false" ]; then
|
|
1223
|
+
|
|
1224
|
+
echo " ✅ Mergeando..."
|
|
1225
|
+
|
|
1226
|
+
curl --silent \
|
|
1227
|
+
--request PUT \
|
|
1228
|
+
--header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
|
|
1229
|
+
--header "Content-Type: application/json" \
|
|
1230
|
+
--data '{"should_remove_source_branch": true}' \
|
|
1231
|
+
--url "$GITLAB_API_URL/projects/$PROJECT_ID/merge_requests/$iid/merge" \
|
|
1232
|
+
> /dev/null
|
|
1233
|
+
|
|
1234
|
+
merged=$((merged + 1))
|
|
1235
|
+
else
|
|
1236
|
+
echo " ⏭️ Omitiendo"
|
|
1237
|
+
skipped=$((skipped + 1))
|
|
1238
|
+
fi
|
|
1239
|
+
|
|
1240
|
+
echo ""
|
|
1241
|
+
done
|
|
1242
|
+
|
|
1243
|
+
echo "Resumen:"
|
|
1244
|
+
echo " ✅ Mergeados: $merged"
|
|
1245
|
+
echo " ⏭️ Omitidos: $skipped"
|
|
1246
|
+
```
|
|
1247
|
+
|
|
1248
|
+
### Script: cleanup-old-branches.sh
|
|
1249
|
+
|
|
1250
|
+
```bash
|
|
1251
|
+
#!/bin/bash
|
|
1252
|
+
|
|
1253
|
+
# cleanup-old-branches.sh
|
|
1254
|
+
# Limpia branches viejos y mergeados
|
|
1255
|
+
|
|
1256
|
+
source .gitlab-env
|
|
1257
|
+
|
|
1258
|
+
PROJECT_ID="$1"
|
|
1259
|
+
DAYS_OLD="${2:-30}"
|
|
1260
|
+
|
|
1261
|
+
if [ -z "$PROJECT_ID" ]; then
|
|
1262
|
+
echo "Uso: $0 <project_id> [days_old]"
|
|
1263
|
+
exit 1
|
|
1264
|
+
fi
|
|
1265
|
+
|
|
1266
|
+
echo "Buscando branches antiguos (>$DAYS_OLD días)..."
|
|
1267
|
+
echo ""
|
|
1268
|
+
|
|
1269
|
+
# Obtener branches
|
|
1270
|
+
branches=$(curl --silent \
|
|
1271
|
+
--header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
|
|
1272
|
+
--url "$GITLAB_API_URL/projects/$PROJECT_ID/repository/branches")
|
|
1273
|
+
|
|
1274
|
+
# Fecha límite
|
|
1275
|
+
cutoff_date=$(date -d "$DAYS_OLD days ago" '+%s')
|
|
1276
|
+
|
|
1277
|
+
to_delete=()
|
|
1278
|
+
|
|
1279
|
+
echo "$branches" | jq -c '.[]' | while read -r branch; do
|
|
1280
|
+
name=$(echo "$branch" | jq -r '.name')
|
|
1281
|
+
merged=$(echo "$branch" | jq -r '.merged')
|
|
1282
|
+
commit_date=$(echo "$branch" | jq -r '.commit.committed_date')
|
|
1283
|
+
|
|
1284
|
+
# Skip protected branches
|
|
1285
|
+
if [ "$name" = "main" ] || [ "$name" = "master" ] || [ "$name" = "develop" ]; then
|
|
1286
|
+
continue
|
|
1287
|
+
fi
|
|
1288
|
+
|
|
1289
|
+
# Convertir fecha
|
|
1290
|
+
commit_timestamp=$(date -d "$commit_date" '+%s')
|
|
1291
|
+
|
|
1292
|
+
# Verificar si es antiguo y mergeado
|
|
1293
|
+
if [ "$commit_timestamp" -lt "$cutoff_date" ] && [ "$merged" = "true" ]; then
|
|
1294
|
+
echo " 🗑️ $name (último commit: $commit_date)"
|
|
1295
|
+
to_delete+=("$name")
|
|
1296
|
+
fi
|
|
1297
|
+
done
|
|
1298
|
+
|
|
1299
|
+
if [ ${#to_delete[@]} -eq 0 ]; then
|
|
1300
|
+
echo "No hay branches para eliminar"
|
|
1301
|
+
exit 0
|
|
1302
|
+
fi
|
|
1303
|
+
|
|
1304
|
+
echo ""
|
|
1305
|
+
echo "Se eliminarán ${#to_delete[@]} branches"
|
|
1306
|
+
read -p "¿Continuar? (y/n): " confirm
|
|
1307
|
+
|
|
1308
|
+
if [ "$confirm" != "y" ]; then
|
|
1309
|
+
echo "Cancelado"
|
|
1310
|
+
exit 0
|
|
1311
|
+
fi
|
|
1312
|
+
|
|
1313
|
+
deleted=0
|
|
1314
|
+
for branch in "${to_delete[@]}"; do
|
|
1315
|
+
echo "Eliminando $branch..."
|
|
1316
|
+
|
|
1317
|
+
curl --silent \
|
|
1318
|
+
--request DELETE \
|
|
1319
|
+
--header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
|
|
1320
|
+
--url "$GITLAB_API_URL/projects/$PROJECT_ID/repository/branches/$(echo "$branch" | sed 's|/|%2F|g')" \
|
|
1321
|
+
> /dev/null
|
|
1322
|
+
|
|
1323
|
+
deleted=$((deleted + 1))
|
|
1324
|
+
sleep 0.3
|
|
1325
|
+
done
|
|
1326
|
+
|
|
1327
|
+
echo ""
|
|
1328
|
+
echo "✅ Eliminados $deleted branches"
|
|
1329
|
+
```
|
|
1330
|
+
|
|
1331
|
+
---
|
|
1332
|
+
|
|
1333
|
+
**Nota**: Todos estos scripts requieren:
|
|
1334
|
+
1. Archivo `.gitlab-env` con variables de entorno
|
|
1335
|
+
2. Permisos adecuados en GitLab
|
|
1336
|
+
3. `jq` instalado para procesamiento JSON
|
|
1337
|
+
4. `curl` para llamadas API
|
|
1338
|
+
|
|
1339
|
+
Para usar los scripts:
|
|
1340
|
+
```bash
|
|
1341
|
+
# 1. Configurar entorno
|
|
1342
|
+
./setup-gitlab-env.sh
|
|
1343
|
+
|
|
1344
|
+
# 2. Cargar variables
|
|
1345
|
+
source .gitlab-env
|
|
1346
|
+
|
|
1347
|
+
# 3. Ejecutar scripts
|
|
1348
|
+
./list-all-projects.sh
|
|
1349
|
+
./create-issue-from-template.sh 123 issues.csv
|
|
1350
|
+
./monitor-pipeline.sh 123 456
|
|
1351
|
+
```
|