-
Notifications
You must be signed in to change notification settings - Fork 8
431 lines (386 loc) · 18.3 KB
/
docker-readme-validation.yml
File metadata and controls
431 lines (386 loc) · 18.3 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
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
name: Docker Setup Integration Test
on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
push:
branches: [main]
workflow_dispatch:
permissions:
contents: read
concurrency:
group: docker-setup-integration-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
integration:
name: Simulate new-user setup (setup.sh → docker compose up)
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
# ── 1. Clone ─────────────────────────────────────────────────────────────
- name: Checkout
uses: actions/checkout@v6
# ── 2. setup.sh ──────────────────────────────────────────────────────────
- name: Run setup.sh (new-user step 1)
id: setup_sh
run: |
chmod +x ./setup.sh
./setup.sh
- name: Verify .env was generated with all required keys
id: env_keys
run: |
set -euo pipefail
required=(
BETTER_AUTH_SECRET
BETTER_AUTH_URL
DATABASE_URL
DB_USER
DB_PASSWORD
DB_NAME
S3_ENDPOINT
S3_ACCESS_KEY
S3_SECRET_KEY
S3_BUCKET
STORAGE_USER
STORAGE_PASSWORD
)
missing=()
for key in "${required[@]}"; do
if ! grep -q "^${key}=" .env; then
missing+=("$key")
fi
done
if [ ${#missing[@]} -gt 0 ]; then
echo "❌ Missing keys in .env: ${missing[*]}"
exit 1
fi
echo "✅ All required keys present in .env"
- name: Verify STORAGE_PASSWORD and S3_SECRET_KEY match (prevents credential mismatch bug)
id: storage_creds
run: |
set -euo pipefail
storage_pass="$(grep '^STORAGE_PASSWORD=' .env | cut -d= -f2-)"
s3_key="$(grep '^S3_SECRET_KEY=' .env | cut -d= -f2-)"
if [ "$storage_pass" != "$s3_key" ]; then
echo "❌ STORAGE_PASSWORD and S3_SECRET_KEY do not match — MinIO uploads will fail"
exit 1
fi
echo "✅ MinIO credentials are consistent"
- name: Verify BETTER_AUTH_SECRET is at least 32 characters
id: auth_secret
run: |
set -euo pipefail
secret="$(grep '^BETTER_AUTH_SECRET=' .env | cut -d= -f2-)"
len="${#secret}"
if [ "$len" -lt 32 ]; then
echo "❌ BETTER_AUTH_SECRET is only ${len} chars (minimum 32)"
exit 1
fi
echo "✅ BETTER_AUTH_SECRET is ${len} chars"
- name: Verify setup.sh refuses to overwrite existing .env
id: env_overwrite
run: |
set -euo pipefail
if ./setup.sh 2>&1; then
echo "❌ setup.sh should have refused to overwrite .env but exited 0"
exit 1
fi
echo "✅ setup.sh correctly refused to overwrite existing .env"
# ── 3. docker compose up --build ─────────────────────────────────────────
- name: Build image and start all services (new-user step 2)
id: docker_build
run: docker compose up --build -d
- name: Wait for db to be healthy
id: db_health
run: |
set -euo pipefail
echo "Waiting for db..."
for i in $(seq 60); do
state="$(docker inspect --format='{{.State.Health.Status}}' reqcore_db 2>/dev/null || echo 'not-started')"
echo " db: $state"
[ "$state" = "healthy" ] && break
if [ "$i" -eq 60 ]; then
echo "❌ db did not become healthy in time"
docker compose logs db --tail=50
exit 1
fi
sleep 3
done
echo "✅ db is healthy"
- name: Wait for minio to be healthy
id: minio_health
run: |
set -euo pipefail
echo "Waiting for minio..."
for i in $(seq 60); do
state="$(docker inspect --format='{{.State.Health.Status}}' reqcore_minio 2>/dev/null || echo 'not-started')"
echo " minio: $state"
[ "$state" = "healthy" ] && break
if [ "$i" -eq 60 ]; then
echo "❌ minio did not become healthy in time"
docker compose logs minio --tail=50
exit 1
fi
sleep 3
done
echo "✅ minio is healthy"
- name: Wait for app to be reachable on :3000
id: app_health
run: |
set -euo pipefail
echo "Waiting for app..."
for i in $(seq 60); do
if curl -fs http://localhost:3000 > /dev/null 2>&1; then
echo "✅ App is reachable on http://localhost:3000"
exit 0
fi
state="$(docker inspect --format='{{.State.Status}}' reqcore_app 2>/dev/null || echo 'missing')"
if [ "$state" = "exited" ] || [ "$state" = "dead" ]; then
echo "❌ App container exited unexpectedly"
docker compose logs app --tail=100
exit 1
fi
echo " attempt $i/60 — waiting..."
sleep 3
done
echo "❌ App did not become reachable in time"
docker compose logs app --tail=100
exit 1
# ── 4. Startup log assertions ─────────────────────────────────────────────
- name: Assert DB migrations ran successfully
id: db_migrations
run: |
set -euo pipefail
if ! docker compose logs app | grep -q "Database migrations applied successfully"; then
echo "❌ Migration success message not found in app logs"
docker compose logs app
exit 1
fi
echo "✅ Migrations applied successfully"
- name: Assert S3 bucket is ready
id: s3_bucket
run: |
set -euo pipefail
if ! docker compose logs app | grep -q 'S3 bucket "reqcore" is ready'; then
echo "❌ S3 bucket ready message not found in app logs"
docker compose logs app
exit 1
fi
echo "✅ S3 bucket is ready"
# ── 5. HTTP smoke tests ───────────────────────────────────────────────────
- name: HTTP smoke tests
id: http_smoke
run: |
set -euo pipefail
fail=0
check() {
local label="$1" url="$2" expected="$3"
local actual
actual="$(curl -s -o /dev/null -w "%{http_code}" "$url")"
if [ "$actual" = "$expected" ]; then
echo "✅ $label → $actual"
else
echo "❌ $label → expected $expected, got $actual"
fail=1
fi
}
check "Home page" "http://localhost:3000" "200"
check "Sign-in page" "http://localhost:3000/auth/sign-in" "200"
check "Sign-up page" "http://localhost:3000/auth/sign-up" "200"
check "Public job board" "http://localhost:3000/jobs" "200"
check "API/jobs (no auth→401)" "http://localhost:3000/api/jobs" "401"
check "API/candidates (no auth)" "http://localhost:3000/api/candidates" "401"
exit $fail
# ── 6. Seed command (optional step from README) ───────────────────────────
- name: Seed demo data (docker compose exec app npm run db:seed)
id: seed_data
run: |
set -euo pipefail
output="$(docker compose exec app npm run db:seed 2>&1)"
echo "$output"
if ! echo "$output" | grep -q "Seed complete"; then
echo "❌ Seed did not complete successfully"
exit 1
fi
echo "✅ Seed completed"
- name: Sign in with seeded demo account (auth smoke test)
id: seed_signin
run: |
set -euo pipefail
response="$(curl -s -X POST http://localhost:3000/api/auth/sign-in/email \
-H "Content-Type: application/json" \
-d '{"email":"demo@reqcore.com","password":"demo1234"}' \
-w "\n%{http_code}")"
body="$(echo "$response" | head -n -1)"
status="$(echo "$response" | tail -n 1)"
echo "Status: $status"
echo "Body: $body"
if [ "$status" != "200" ]; then
echo "❌ Sign-in failed — expected 200, got $status"
exit 1
fi
if ! echo "$body" | grep -q "demo@reqcore.com"; then
echo "❌ Response body does not contain expected email"
exit 1
fi
echo "✅ Demo account sign-in succeeded"
- name: Seed idempotency — re-running seed must not crash
id: seed_idempotent
run: |
set -euo pipefail
output="$(docker compose exec app npm run db:seed 2>&1)"
echo "$output"
if echo "$output" | grep -qi "^npm error\|unhandledRejection\|UnhandledPromiseRejection"; then
echo "❌ Seed second run produced an error"
exit 1
fi
echo "✅ Seed is idempotent"
# ── 7. Adminer --profile tools ────────────────────────────────────────────
- name: Start Adminer via --profile tools
id: adminer_start
run: docker compose --profile tools up -d adminer
- name: Wait for Adminer to respond on :8080
id: adminer_health
run: |
set -euo pipefail
for i in $(seq 20); do
if curl -fs http://localhost:8080 > /dev/null 2>&1; then
echo "✅ Adminer is reachable on http://localhost:8080"
exit 0
fi
sleep 2
done
echo "❌ Adminer did not become reachable"
docker compose logs adminer --tail=30
exit 1
# ── 8. Restart resilience (migrations must be idempotent) ────────────────
- name: Restart app and verify it comes back clean
id: restart_resilience
run: |
set -euo pipefail
docker compose restart app
for i in $(seq 30); do
if curl -fs http://localhost:3000 > /dev/null 2>&1; then
echo "✅ App reachable again after restart"
break
fi
if [ "$i" -eq 30 ]; then
echo "❌ App did not come back after restart"
docker compose logs app --tail=50
exit 1
fi
sleep 3
done
if docker compose logs app | grep -q "Migration failed"; then
echo "❌ Migration error found in logs after restart"
docker compose logs app
exit 1
fi
echo "✅ Restart handled cleanly — no migration errors"
# ── Always: dump logs on failure ──────────────────────────────────────────
- name: Docker Integration Summary
if: ${{ !cancelled() }}
run: |
echo "## Docker Setup Integration Results" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "Simulates the full new-user onboarding: \`setup.sh → docker compose up → seed → smoke tests\`" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "| # | Check | Status |" >> "$GITHUB_STEP_SUMMARY"
echo "|---|-------|--------|" >> "$GITHUB_STEP_SUMMARY"
row() {
local n="$1" name="$2" outcome="$3"
local icon="❌"
[ "$outcome" = "success" ] && icon="✅"
[ "$outcome" = "skipped" ] && icon="⏭️"
echo "| $n | $name | $icon |" >> "$GITHUB_STEP_SUMMARY"
}
row 1 "Run setup.sh" "${{ steps.setup_sh.outcome }}"
row 2 ".env has all required keys" "${{ steps.env_keys.outcome }}"
row 3 "Storage credentials match" "${{ steps.storage_creds.outcome }}"
row 4 "Auth secret ≥ 32 chars" "${{ steps.auth_secret.outcome }}"
row 5 "setup.sh refuses overwrite" "${{ steps.env_overwrite.outcome }}"
row 6 "Docker build & start" "${{ steps.docker_build.outcome }}"
row 7 "Database healthy" "${{ steps.db_health.outcome }}"
row 8 "MinIO healthy" "${{ steps.minio_health.outcome }}"
row 9 "App reachable on :3000" "${{ steps.app_health.outcome }}"
row 10 "DB migrations applied" "${{ steps.db_migrations.outcome }}"
row 11 "S3 bucket ready" "${{ steps.s3_bucket.outcome }}"
row 12 "HTTP smoke tests" "${{ steps.http_smoke.outcome }}"
row 13 "Seed demo data" "${{ steps.seed_data.outcome }}"
row 14 "Demo account sign-in" "${{ steps.seed_signin.outcome }}"
row 15 "Seed idempotency" "${{ steps.seed_idempotent.outcome }}"
row 16 "Adminer starts" "${{ steps.adminer_start.outcome }}"
row 17 "Adminer reachable on :8080" "${{ steps.adminer_health.outcome }}"
row 18 "Restart resilience" "${{ steps.restart_resilience.outcome }}"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "_Run: \`${{ github.run_id }}\` · Commit: \`${{ github.sha }}\`_" >> "$GITHUB_STEP_SUMMARY"
- name: Generate JUnit XML report
if: ${{ !cancelled() }}
run: |
# Generate JUnit XML so test results can be consumed by dashboards/reporters
cat > docker-integration-results.xml << 'XMLEOF'
<?xml version="1.0" encoding="UTF-8"?>
<testsuites name="Docker Setup Integration" tests="18">
<testsuite name="Setup" tests="5">
XMLEOF
junit_case() {
local name="$1" outcome="$2"
if [ "$outcome" = "success" ]; then
echo " <testcase name=\"$name\" classname=\"docker-integration\" />" >> docker-integration-results.xml
elif [ "$outcome" = "skipped" ]; then
echo " <testcase name=\"$name\" classname=\"docker-integration\"><skipped /></testcase>" >> docker-integration-results.xml
else
echo " <testcase name=\"$name\" classname=\"docker-integration\"><failure message=\"Step failed\" /></testcase>" >> docker-integration-results.xml
fi
}
junit_case "Run setup.sh" "${{ steps.setup_sh.outcome }}"
junit_case ".env has all required keys" "${{ steps.env_keys.outcome }}"
junit_case "Storage credentials match" "${{ steps.storage_creds.outcome }}"
junit_case "Auth secret >= 32 chars" "${{ steps.auth_secret.outcome }}"
junit_case "setup.sh refuses overwrite" "${{ steps.env_overwrite.outcome }}"
echo " </testsuite>" >> docker-integration-results.xml
echo ' <testsuite name="Infrastructure" tests="4">' >> docker-integration-results.xml
junit_case "Docker build and start" "${{ steps.docker_build.outcome }}"
junit_case "Database healthy" "${{ steps.db_health.outcome }}"
junit_case "MinIO healthy" "${{ steps.minio_health.outcome }}"
junit_case "App reachable on :3000" "${{ steps.app_health.outcome }}"
echo " </testsuite>" >> docker-integration-results.xml
echo ' <testsuite name="Startup Assertions" tests="2">' >> docker-integration-results.xml
junit_case "DB migrations applied" "${{ steps.db_migrations.outcome }}"
junit_case "S3 bucket ready" "${{ steps.s3_bucket.outcome }}"
echo " </testsuite>" >> docker-integration-results.xml
echo ' <testsuite name="Smoke Tests" tests="4">' >> docker-integration-results.xml
junit_case "HTTP smoke tests" "${{ steps.http_smoke.outcome }}"
junit_case "Seed demo data" "${{ steps.seed_data.outcome }}"
junit_case "Demo account sign-in" "${{ steps.seed_signin.outcome }}"
junit_case "Seed idempotency" "${{ steps.seed_idempotent.outcome }}"
echo " </testsuite>" >> docker-integration-results.xml
echo ' <testsuite name="Optional Services" tests="3">' >> docker-integration-results.xml
junit_case "Adminer starts" "${{ steps.adminer_start.outcome }}"
junit_case "Adminer reachable on :8080" "${{ steps.adminer_health.outcome }}"
junit_case "Restart resilience" "${{ steps.restart_resilience.outcome }}"
echo " </testsuite>" >> docker-integration-results.xml
echo " </testsuites>" >> docker-integration-results.xml
- name: Upload JUnit XML results
uses: actions/upload-artifact@v7
if: ${{ !cancelled() }}
with:
name: docker-integration-junit
path: docker-integration-results.xml
retention-days: 30
- name: Dump all service logs
if: always()
run: |
echo "=== docker compose ps ==="
docker compose --profile tools ps || true
echo ""
echo "=== app ==="
docker compose logs app --no-color --tail=200 || true
echo ""
echo "=== db ==="
docker compose logs db --no-color --tail=50 || true
echo ""
echo "=== minio ==="
docker compose logs minio --no-color --tail=50 || true
- name: Tear down all services and volumes
if: always()
run: docker compose --profile tools down -v || true