-
Notifications
You must be signed in to change notification settings - Fork 0
188 lines (167 loc) · 7.32 KB
/
Copy pathci.yml
File metadata and controls
188 lines (167 loc) · 7.32 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
# Pipeline CI/CD: test + build en cada push/PR; en main, además espera el
# deploy de Vercel (auto-deploy por integración git), lo verifica con
# /api/health y hace rollback automático si la versión nueva no está sana.
#
# Secrets opcionales (el pipeline degrada con gracia si faltan):
# VERCEL_TOKEN → habilita el rollback automático
# LAB_INGEST_TOKEN → reporta métricas del run a /api/lab/ingest (panel LAB)
# NTFY_TOPIC → push al teléfono cuando hay rollback
# Variables opcionales (Settings → Variables):
# PROD_URL → URL de producción (default https://codebymike.tech)
name: CI
on:
push:
branches: [main]
pull_request:
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: false
jobs:
quality:
name: Test + Build
runs-on: ubuntu-latest
outputs:
coverage: ${{ steps.metrics.outputs.coverage }}
tests_passed: ${{ steps.metrics.outputs.tests_passed }}
tests_failed: ${{ steps.metrics.outputs.tests_failed }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- name: Instalar dependencias
run: npm ci
- name: Tests con cobertura
run: npx vitest run --coverage --reporter=default --reporter=json --outputFile=vitest-report.json
- name: Build
run: npm run build
- name: Extraer métricas
id: metrics
if: always()
run: |
COVERAGE=$(node -e "try{console.log(require('./coverage/coverage-summary.json').total.lines.pct)}catch{console.log('')}")
PASSED=$(node -e "try{console.log(require('./vitest-report.json').numPassedTests)}catch{console.log('')}")
FAILED=$(node -e "try{console.log(require('./vitest-report.json').numFailedTests)}catch{console.log('')}")
echo "coverage=$COVERAGE" >> "$GITHUB_OUTPUT"
echo "tests_passed=$PASSED" >> "$GITHUB_OUTPUT"
echo "tests_failed=$FAILED" >> "$GITHUB_OUTPUT"
echo "Cobertura: $COVERAGE% · Tests: $PASSED ok / $FAILED fallidos"
e2e:
name: E2E (Playwright)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- name: Instalar dependencias
run: npm ci
- name: Instalar Chromium
run: npx playwright install --with-deps chromium
- name: Tests e2e
# Las bases son archivos libsql desechables que siembra el propio
# webServer (ver playwright.config.ts). No hace falta ningún secret.
run: npm run test:e2e
- name: Subir reporte si algo falla
if: failure()
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: playwright-report/
retention-days: 7
verify-production:
name: Verificar deploy + rollback
needs: quality
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
runs-on: ubuntu-latest
env:
PROD_URL: ${{ vars.PROD_URL || 'https://codebymike.tech' }}
VERCEL_ORG_ID: team_GwKd4hurNRGdWHhXUHiQ9HeM
VERCEL_PROJECT_ID: prj_vdfQibXACk3SvwYWEmKdEpZ4NaGh
steps:
- name: Esperar a que Vercel despliegue este commit
id: wait
run: |
echo "Esperando deploy de ${GITHUB_SHA} en ${PROD_URL} (máx 8 min)…"
for i in $(seq 1 48); do
SHA=$(curl -sf --max-time 10 "$PROD_URL/api/health" | node -e "let d='';process.stdin.on('data',c=>d+=c).on('end',()=>{try{console.log(JSON.parse(d).sha||'')}catch{console.log('')}})" || true)
if [ "$SHA" = "$GITHUB_SHA" ]; then
echo "Deploy activo tras $((i*10))s"
echo "deployed=true" >> "$GITHUB_OUTPUT"
exit 0
fi
sleep 10
done
echo "deployed=false" >> "$GITHUB_OUTPUT"
echo "::warning::El deploy de este commit no apareció en 8 min; se omite la verificación."
- name: Health check post-deploy
id: health
if: steps.wait.outputs.deployed == 'true'
run: |
OK=0
for i in 1 2 3; do
STATUS=$(curl -s -o /tmp/health.json -w '%{http_code}' --max-time 15 "$PROD_URL/api/health" || echo 000)
echo "Check $i: HTTP $STATUS $(cat /tmp/health.json 2>/dev/null)"
if [ "$STATUS" = "200" ]; then OK=$((OK+1)); fi
sleep 5
done
if [ "$OK" -ge 2 ]; then
echo "healthy=true" >> "$GITHUB_OUTPUT"
else
echo "healthy=false" >> "$GITHUB_OUTPUT"
echo "::error::Health check falló ($OK/3 intentos sanos)"
fi
- name: Rollback automático
id: rollback
if: steps.wait.outputs.deployed == 'true' && steps.health.outputs.healthy == 'false'
env:
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
run: |
if [ -z "$VERCEL_TOKEN" ]; then
echo "::warning::Sin VERCEL_TOKEN: no se puede hacer rollback automático."
exit 0
fi
npx vercel@latest rollback --yes --token "$VERCEL_TOKEN"
echo "rolled_back=true" >> "$GITHUB_OUTPUT"
- name: Notificar rollback (ntfy)
if: steps.rollback.outputs.rolled_back == 'true'
env:
NTFY_TOPIC: ${{ secrets.NTFY_TOPIC }}
run: |
[ -z "$NTFY_TOPIC" ] && exit 0
curl -s -X POST "https://ntfy.sh/$NTFY_TOPIC" \
-H "Title: Rollback automatico ejecutado" \
-H "Priority: 5" -H "Tags: rotating_light" \
-d "El deploy ${GITHUB_SHA:0:7} fallo el health check y se revirtio a la version anterior. Run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
- name: Reportar run al panel LAB
if: always()
env:
LAB_INGEST_TOKEN: ${{ secrets.LAB_INGEST_TOKEN }}
run: |
[ -z "$LAB_INGEST_TOKEN" ] && { echo "Sin LAB_INGEST_TOKEN: no se reporta."; exit 0; }
if [ "${{ steps.rollback.outputs.rolled_back }}" = "true" ]; then CONCLUSION=rolled_back;
elif [ "${{ steps.health.outputs.healthy }}" = "false" ]; then CONCLUSION=failure;
else CONCLUSION=success; fi
HEALTH=$([ "${{ steps.health.outputs.healthy }}" = "true" ] && echo true || echo false)
DURATION=$(( ($(date +%s) - $(date -d "${{ github.event.head_commit.timestamp }}" +%s)) * 1000 ))
curl -s -X POST "$PROD_URL/api/lab/ingest" \
-H "Authorization: Bearer $LAB_INGEST_TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"kind\": \"ci_run\",
\"sha\": \"$GITHUB_SHA\",
\"branch\": \"${GITHUB_REF_NAME}\",
\"runId\": \"${{ github.run_id }}\",
\"url\": \"${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}\",
\"conclusion\": \"$CONCLUSION\",
\"testsPassed\": \"${{ needs.quality.outputs.tests_passed }}\",
\"testsFailed\": \"${{ needs.quality.outputs.tests_failed }}\",
\"coveragePct\": \"${{ needs.quality.outputs.coverage }}\",
\"durationMs\": \"$DURATION\",
\"healthOk\": $HEALTH
}"
- name: Fallar el job si el deploy quedó insano
if: steps.health.outputs.healthy == 'false'
run: exit 1