-
Notifications
You must be signed in to change notification settings - Fork 14
749 lines (653 loc) · 29.5 KB
/
Copy pathaws-web-release.yml
File metadata and controls
749 lines (653 loc) · 29.5 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
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
name: AWS/Web Release
on:
push:
branches: [main]
paths:
- 'apps/auth/**'
- 'apps/backend/**'
- 'apps/web/**'
- 'apps/admin/**'
- 'db/**'
- 'infra/**'
- 'scripts/checks/check-agent-api-smoke.sh'
- 'scripts/checks/check-api-health.sh'
- 'scripts/checks/check-demo-cognito-users.sh'
- 'scripts/checks/check-multipart-completion-reconciliation-schedule.sh'
- 'scripts/checks/check-mcp-smoke.sh'
- 'scripts/checks/check-public-endpoints.sh'
- 'scripts/deploy/deploy-admin.sh'
- 'scripts/deploy/deploy-web.sh'
- 'scripts/generate/generate-catalog-dump.sh'
- 'scripts/generate/generate-global-metrics-snapshot.sh'
- 'scripts/deploy/migrate-aws.sh'
- 'scripts/generate/write-ci-cdk-context.py'
- '.github/workflows/aws-web-release.yml'
workflow_dispatch:
concurrency:
group: main-release
cancel-in-progress: false
permissions:
actions: read
checks: read
contents: read
id-token: write
env:
AWS_REGION: ${{ vars.AWS_REGION }}
STACK_NAME: FlashcardsOpenSourceApp
jobs:
changes:
name: Detect changed areas
runs-on: ubuntu-24.04
outputs:
aws_changed: ${{ steps.detect.outputs.aws_changed }}
auth_changed: ${{ steps.detect.outputs.auth_changed }}
backend_changed: ${{ steps.detect.outputs.backend_changed }}
web_changed: ${{ steps.detect.outputs.web_changed }}
admin_changed: ${{ steps.detect.outputs.admin_changed }}
infra_changed: ${{ steps.detect.outputs.infra_changed }}
changed_files: ${{ steps.detect.outputs.changed_files }}
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Detect changed files
id: detect
env:
PUSH_BEFORE_SHA: ${{ github.event.before }}
run: |
set -euo pipefail
aws_release_script_pattern='^scripts/(checks/(check-agent-api-smoke|check-api-health|check-demo-cognito-users|check-mcp-smoke|check-multipart-completion-reconciliation-schedule|check-public-endpoints)\.sh|deploy/(deploy-admin|deploy-web|migrate-aws)\.sh|generate/(generate-catalog-dump\.sh|generate-global-metrics-snapshot\.sh|write-ci-cdk-context\.py))$'
changed_files_path="${RUNNER_TEMP}/changed-files.txt"
before_sha="${PUSH_BEFORE_SHA:-}"
if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then
printf '%s\n' 'workflow_dispatch (forced AWS deploy)' > "${changed_files_path}"
elif [[ "${GITHUB_EVENT_NAME}" == "push" ]] && [[ -n "${before_sha}" ]] && [[ "${before_sha}" != "0000000000000000000000000000000000000000" ]]; then
git diff --name-only "${before_sha}" "${GITHUB_SHA}" > "${changed_files_path}"
elif git rev-parse HEAD^ >/dev/null 2>&1; then
git diff --name-only HEAD^ HEAD > "${changed_files_path}"
else
git ls-files > "${changed_files_path}"
fi
aws_changed=false
if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then
aws_changed=true
elif grep -Eq "^(apps/auth/|apps/backend/|apps/web/|apps/admin/|db/|infra/|\\.github/workflows/aws-web-release\\.yml$)" "${changed_files_path}" || grep -Eq "${aws_release_script_pattern}" "${changed_files_path}"; then
aws_changed=true
fi
auth_changed=false
backend_changed=false
web_changed=false
admin_changed=false
infra_changed=false
if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then
auth_changed=true
backend_changed=true
web_changed=true
admin_changed=true
infra_changed=true
else
if grep -Eq '^(apps/auth/|\.github/workflows/aws-web-release\.yml$)' "${changed_files_path}"; then
auth_changed=true
fi
if grep -Eq '^(apps/backend/|\.github/workflows/aws-web-release\.yml$)' "${changed_files_path}"; then
backend_changed=true
fi
if grep -Eq '^(apps/web/|apps/backend/src/scheduling/|\.github/workflows/aws-web-release\.yml$)' "${changed_files_path}"; then
web_changed=true
fi
if grep -Eq '^(apps/admin/|\.github/workflows/aws-web-release\.yml$)' "${changed_files_path}"; then
admin_changed=true
fi
if grep -Eq '^(infra/|\.github/workflows/aws-web-release\.yml$)' "${changed_files_path}"; then
infra_changed=true
fi
fi
{
echo "aws_changed=${aws_changed}"
echo "auth_changed=${auth_changed}"
echo "backend_changed=${backend_changed}"
echo "web_changed=${web_changed}"
echo "admin_changed=${admin_changed}"
echo "infra_changed=${infra_changed}"
echo 'changed_files<<EOF'
cat "${changed_files_path}"
echo 'EOF'
} >> "${GITHUB_OUTPUT}"
- name: Summarize changed areas
env:
CHANGED_FILES: ${{ steps.detect.outputs.changed_files }}
run: |
{
echo "## Release scope"
echo ""
echo "- AWS changed: \`${{ steps.detect.outputs.aws_changed }}\`"
echo "- Auth changed: \`${{ steps.detect.outputs.auth_changed }}\`"
echo "- Backend changed: \`${{ steps.detect.outputs.backend_changed }}\`"
echo "- Web changed: \`${{ steps.detect.outputs.web_changed }}\`"
echo "- Admin changed: \`${{ steps.detect.outputs.admin_changed }}\`"
echo "- Infra changed: \`${{ steps.detect.outputs.infra_changed }}\`"
echo ""
echo "### Changed files"
echo ""
if [[ -n "${CHANGED_FILES}" ]]; then
printf '%s\n' "${CHANGED_FILES}"
else
echo "No changed files detected."
fi
} >> "${GITHUB_STEP_SUMMARY}"
pre_deploy_checks:
name: Pre-deploy checks
needs: changes
if: ${{ needs.changes.outputs.aws_changed == 'true' }}
runs-on: ubuntu-24.04
timeout-minutes: 45
services:
postgres:
image: postgres:18
env:
POSTGRES_DB: postgres
POSTGRES_PASSWORD: postgres-integration-${{ github.run_id }}-${{ github.run_attempt }}
POSTGRES_USER: postgres_integration
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U postgres_integration -d postgres"
--health-interval 5s
--health-timeout 5s
--health-retries 10
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 24
cache: npm
cache-dependency-path: |
apps/auth/package-lock.json
apps/backend/package-lock.json
apps/web/package-lock.json
apps/admin/package-lock.json
infra/aws/package-lock.json
- name: Install dependencies
run: |
npm ci --prefix apps/auth
npm ci --prefix apps/backend
npm ci --prefix apps/web
npm ci --prefix apps/admin
npm ci --prefix infra/aws
- name: Build auth app
if: ${{ needs.changes.outputs.auth_changed == 'true' }}
run: npm run build --prefix apps/auth
- name: Test auth app
if: ${{ needs.changes.outputs.auth_changed == 'true' }}
run: npm run test --prefix apps/auth
- name: Lint backend
if: ${{ needs.changes.outputs.backend_changed == 'true' }}
run: npm run lint --prefix apps/backend
- name: Build backend
if: ${{ needs.changes.outputs.backend_changed == 'true' }}
run: npm run build --prefix apps/backend
- name: Test backend MCP protocol
if: ${{ needs.changes.outputs.backend_changed == 'true' }}
run: npm run test:mcp --prefix apps/backend
- name: Test PostgreSQL 18 lifecycle and 0096 to 0097 security boundaries
env:
POSTGRES_INTEGRATION_ADMIN_URL: postgresql://postgres_integration:postgres-integration-${{ github.run_id }}-${{ github.run_attempt }}@127.0.0.1:5432/postgres?sslmode=disable
run: npm run test:postgres-integration --prefix apps/backend
- name: Build web app
if: ${{ needs.changes.outputs.web_changed == 'true' }}
run: npm run build --prefix apps/web
- name: Build admin app
if: ${{ needs.changes.outputs.admin_changed == 'true' }}
run: npm run build --prefix apps/admin
- name: Build infra
if: ${{ needs.changes.outputs.infra_changed == 'true' }}
run: npm run build --prefix infra/aws
- name: Test infra
if: ${{ needs.changes.outputs.infra_changed == 'true' }}
run: npm run test --prefix infra/aws
- name: Set up Docker QEMU for CDK bundling
uses: docker/setup-qemu-action@v4
with:
platforms: arm64
- name: Synthesize infra
working-directory: infra/aws
env:
AWS_DEPLOY_ROLE_ARN: ${{ secrets.AWS_DEPLOY_ROLE_ARN }}
CDK_CONTEXT_ALERT_EMAIL: ${{ vars.CDK_ALERT_EMAIL }}
CDK_CONTEXT_ANALYTICS_ACCESS_ENABLED: ${{ vars.CDK_ANALYTICS_ACCESS_ENABLED }}
CDK_CONTEXT_ANTHROPIC_API_KEY_SECRET_ARN: ${{ vars.CDK_ANTHROPIC_API_KEY_SECRET_ARN }}
CDK_CONTEXT_API_CERTIFICATE_ARN: ${{ vars.CDK_API_CERTIFICATE_ARN }}
CDK_CONTEXT_APEX_REDIRECT_CERTIFICATE_ARN_US_EAST_1: ${{ vars.CDK_APEX_REDIRECT_CERTIFICATE_ARN_US_EAST_1 }}
CDK_CONTEXT_AUTH_CERTIFICATE_ARN: ${{ vars.CDK_AUTH_CERTIFICATE_ARN }}
CDK_CONTEXT_MCP_CERTIFICATE_ARN: ${{ vars.CDK_MCP_CERTIFICATE_ARN }}
CDK_CONTEXT_ADMIN_CERTIFICATE_ARN_US_EAST_1: ${{ vars.CDK_ADMIN_CERTIFICATE_ARN_US_EAST_1 }}
CDK_CONTEXT_ADMIN_EMAILS: ${{ vars.CDK_ADMIN_EMAILS }}
CDK_CONTEXT_DEMO_EMAIL_DOSTIP: ${{ vars.CDK_DEMO_EMAIL_DOSTIP }}
CDK_CONTEXT_DEMO_PASSWORD_SECRET_ARN: ${{ vars.CDK_DEMO_PASSWORD_SECRET_ARN }}
CDK_CONTEXT_DOMAIN_NAME: ${{ vars.CDK_DOMAIN_NAME }}
CDK_CONTEXT_GITHUB_REPO: ${{ vars.CDK_GITHUB_REPO }}
CDK_CONTEXT_GLOBAL_METRICS_VISIBLE: ${{ vars.CDK_GLOBAL_METRICS_VISIBLE }}
CDK_CONTEXT_GUEST_AI_WEIGHTED_MONTHLY_TOKEN_CAP: ${{ vars.CDK_GUEST_AI_WEIGHTED_MONTHLY_TOKEN_CAP }}
CDK_CONTEXT_LANGFUSE_BASE_URL: ${{ vars.CDK_LANGFUSE_BASE_URL }}
CDK_CONTEXT_LANGFUSE_PUBLIC_KEY_SECRET_ARN: ${{ vars.CDK_LANGFUSE_PUBLIC_KEY_SECRET_ARN }}
CDK_CONTEXT_LANGFUSE_SECRET_KEY_SECRET_ARN: ${{ vars.CDK_LANGFUSE_SECRET_KEY_SECRET_ARN }}
CDK_CONTEXT_OPENAI_API_KEY_SECRET_ARN: ${{ vars.CDK_OPENAI_API_KEY_SECRET_ARN }}
CDK_CONTEXT_REGION: ${{ vars.AWS_REGION }}
CDK_CONTEXT_RESEND_API_KEY_SECRET_ARN: ${{ vars.CDK_RESEND_API_KEY_SECRET_ARN }}
CDK_CONTEXT_RESEND_SENDER_EMAIL: ${{ vars.CDK_RESEND_SENDER_EMAIL }}
CDK_CONTEXT_SENTRY_DSN_SECRET_ARN: ${{ vars.CDK_SENTRY_DSN_SECRET_ARN }}
CDK_CONTEXT_SENTRY_ENVIRONMENT: ${{ vars.CDK_SENTRY_ENVIRONMENT }}
CDK_CONTEXT_SENTRY_RELEASE: ${{ github.sha }}
CDK_CONTEXT_SENTRY_TRACES_SAMPLE_RATE: ${{ vars.CDK_SENTRY_TRACES_SAMPLE_RATE }}
CDK_CONTEXT_SITE_BASE_URL: ${{ vars.CDK_SITE_BASE_URL }}
CDK_CONTEXT_WEB_CERTIFICATE_ARN_US_EAST_1: ${{ vars.CDK_WEB_CERTIFICATE_ARN_US_EAST_1 }}
run: |
python3 ../../scripts/generate/write-ci-cdk-context.py \
--output cdk.context.local.json \
--aws-deploy-role-arn "${AWS_DEPLOY_ROLE_ARN}"
npm run synth
aws_deploy:
name: Deploy AWS release
needs:
- changes
- pre_deploy_checks
if: ${{ needs.changes.outputs.aws_changed == 'true' && needs.pre_deploy_checks.result == 'success' }}
runs-on: ubuntu-24.04-arm
timeout-minutes: 30
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 24
cache: npm
cache-dependency-path: |
apps/auth/package-lock.json
apps/backend/package-lock.json
apps/web/package-lock.json
apps/admin/package-lock.json
infra/aws/package-lock.json
- name: Install dependencies
run: |
npm ci --prefix apps/auth
npm ci --prefix apps/backend
npm ci --prefix apps/web
npm ci --prefix apps/admin
npm ci --prefix infra/aws
- name: Build web app
env:
VITE_SENTRY_DSN: ${{ vars.VITE_SENTRY_DSN }}
VITE_SENTRY_ENVIRONMENT: production
VITE_SENTRY_TRACES_SAMPLE_RATE: ${{ vars.VITE_SENTRY_TRACES_SAMPLE_RATE }}
VITE_APP_BUILD: ${{ github.sha }}
SENTRY_UPLOAD_SOURCEMAPS: ${{ vars.VITE_SENTRY_DSN != '' && 'true' || 'false' }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: ${{ vars.SENTRY_ORG }}
SENTRY_PROJECT: ${{ vars.SENTRY_WEB_PROJECT }}
run: npm run build --prefix apps/web
- name: Build admin app
run: npm run build --prefix apps/admin
- name: Configure AWS credentials (OIDC)
uses: aws-actions/configure-aws-credentials@v6.3.0
with:
role-to-assume: ${{ secrets.AWS_DEPLOY_ROLE_ARN }}
aws-region: ${{ env.AWS_REGION }}
- name: CDK deploy with migration-gated runtime and reconciliation schedule disabled
working-directory: infra/aws
env:
AWS_DEPLOY_ROLE_ARN: ${{ secrets.AWS_DEPLOY_ROLE_ARN }}
CDK_CONTEXT_ALERT_EMAIL: ${{ vars.CDK_ALERT_EMAIL }}
CDK_CONTEXT_ANALYTICS_ACCESS_ENABLED: ${{ vars.CDK_ANALYTICS_ACCESS_ENABLED }}
CDK_CONTEXT_ANTHROPIC_API_KEY_SECRET_ARN: ${{ vars.CDK_ANTHROPIC_API_KEY_SECRET_ARN }}
CDK_CONTEXT_API_CERTIFICATE_ARN: ${{ vars.CDK_API_CERTIFICATE_ARN }}
CDK_CONTEXT_APEX_REDIRECT_CERTIFICATE_ARN_US_EAST_1: ${{ vars.CDK_APEX_REDIRECT_CERTIFICATE_ARN_US_EAST_1 }}
CDK_CONTEXT_AUTH_CERTIFICATE_ARN: ${{ vars.CDK_AUTH_CERTIFICATE_ARN }}
CDK_CONTEXT_MCP_CERTIFICATE_ARN: ${{ vars.CDK_MCP_CERTIFICATE_ARN }}
CDK_CONTEXT_ADMIN_CERTIFICATE_ARN_US_EAST_1: ${{ vars.CDK_ADMIN_CERTIFICATE_ARN_US_EAST_1 }}
CDK_CONTEXT_ADMIN_EMAILS: ${{ vars.CDK_ADMIN_EMAILS }}
CDK_CONTEXT_DEMO_EMAIL_DOSTIP: ${{ vars.CDK_DEMO_EMAIL_DOSTIP }}
CDK_CONTEXT_DEMO_PASSWORD_SECRET_ARN: ${{ vars.CDK_DEMO_PASSWORD_SECRET_ARN }}
CDK_CONTEXT_DOMAIN_NAME: ${{ vars.CDK_DOMAIN_NAME }}
CDK_CONTEXT_GITHUB_REPO: ${{ vars.CDK_GITHUB_REPO }}
CDK_CONTEXT_GLOBAL_METRICS_VISIBLE: ${{ vars.CDK_GLOBAL_METRICS_VISIBLE }}
CDK_CONTEXT_GUEST_AI_WEIGHTED_MONTHLY_TOKEN_CAP: ${{ vars.CDK_GUEST_AI_WEIGHTED_MONTHLY_TOKEN_CAP }}
CDK_CONTEXT_LANGFUSE_BASE_URL: ${{ vars.CDK_LANGFUSE_BASE_URL }}
CDK_CONTEXT_LANGFUSE_PUBLIC_KEY_SECRET_ARN: ${{ vars.CDK_LANGFUSE_PUBLIC_KEY_SECRET_ARN }}
CDK_CONTEXT_LANGFUSE_SECRET_KEY_SECRET_ARN: ${{ vars.CDK_LANGFUSE_SECRET_KEY_SECRET_ARN }}
CDK_CONTEXT_OPENAI_API_KEY_SECRET_ARN: ${{ vars.CDK_OPENAI_API_KEY_SECRET_ARN }}
CDK_CONTEXT_REGION: ${{ vars.AWS_REGION }}
CDK_CONTEXT_RESEND_API_KEY_SECRET_ARN: ${{ vars.CDK_RESEND_API_KEY_SECRET_ARN }}
CDK_CONTEXT_RESEND_SENDER_EMAIL: ${{ vars.CDK_RESEND_SENDER_EMAIL }}
CDK_CONTEXT_SENTRY_DSN_SECRET_ARN: ${{ vars.CDK_SENTRY_DSN_SECRET_ARN }}
CDK_CONTEXT_SENTRY_ENVIRONMENT: ${{ vars.CDK_SENTRY_ENVIRONMENT }}
CDK_CONTEXT_SENTRY_RELEASE: ${{ github.sha }}
CDK_CONTEXT_SENTRY_TRACES_SAMPLE_RATE: ${{ vars.CDK_SENTRY_TRACES_SAMPLE_RATE }}
CDK_CONTEXT_SITE_BASE_URL: ${{ vars.CDK_SITE_BASE_URL }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: ${{ vars.SENTRY_ORG }}
SENTRY_PROJECT: ${{ vars.SENTRY_BACKEND_PROJECT }}
SENTRY_RELEASE: ${{ github.sha }}
SENTRY_UPLOAD_BACKEND_SOURCEMAPS: true
CDK_CONTEXT_WEB_CERTIFICATE_ARN_US_EAST_1: ${{ vars.CDK_WEB_CERTIFICATE_ARN_US_EAST_1 }}
run: |
python3 ../../scripts/generate/write-ci-cdk-context.py \
--output cdk.context.local.json \
--aws-deploy-role-arn "${AWS_DEPLOY_ROLE_ARN}"
npx cdk deploy --all --require-approval never \
-c generatedMediaPromotionScheduleState=DISABLED \
-c mediaBlobCleanupEnabled=false \
-c multipartCompletionReconciliationScheduleState=DISABLED
- name: Verify required database migration
run: |
bash scripts/deploy/migrate-aws.sh \
--stack-name ${{ env.STACK_NAME }} \
--require-latest-migration
- name: CDK deploy with reconciliation schedule enabled
working-directory: infra/aws
env:
SENTRY_UPLOAD_BACKEND_SOURCEMAPS: false
run: |
npx cdk deploy --all --require-approval never \
-c generatedMediaPromotionScheduleState=ENABLED \
-c mediaBlobCleanupEnabled=true \
-c multipartCompletionReconciliationScheduleState=ENABLED
- name: Verify cleanup-capable reconciliation schedules are enabled
run: |
bash scripts/checks/check-multipart-completion-reconciliation-schedule.sh \
--stack-name ${{ env.STACK_NAME }} \
--region ${{ env.AWS_REGION }}
- name: Check demo Cognito users
run: bash scripts/checks/check-demo-cognito-users.sh --stack-name ${{ env.STACK_NAME }} --region ${{ env.AWS_REGION }}
- name: Seed global metrics snapshot
run: bash scripts/generate/generate-global-metrics-snapshot.sh --stack-name ${{ env.STACK_NAME }}
- name: Seed public catalog dump
run: bash scripts/generate/generate-catalog-dump.sh --stack-name ${{ env.STACK_NAME }}
- name: Deploy web assets
run: bash scripts/deploy/deploy-web.sh --stack-name ${{ env.STACK_NAME }}
- name: Deploy admin assets
run: bash scripts/deploy/deploy-admin.sh --stack-name ${{ env.STACK_NAME }}
- name: Check API health
run: bash scripts/checks/check-api-health.sh --stack-name ${{ env.STACK_NAME }}
- name: Check public custom domains
run: bash scripts/checks/check-public-endpoints.sh --stack-name ${{ env.STACK_NAME }}
- name: Summarize deployed release
run: |
{
echo "## AWS deploy"
echo ""
echo "- Deployed SHA: \`${GITHUB_SHA}\`"
echo "- Web post-deploy smoke: pending next job"
echo "- Agent API post-deploy smoke: pending next job"
echo "- MCP post-deploy smoke: pending next job"
echo ""
echo "Manual admin smoke remains required for \`https://admin.<domain>\`."
} >> "${GITHUB_STEP_SUMMARY}"
# Scope note: this job never loads the deployed web assets. It builds `dist`
# from the merge commit, points `app.flashcards-open-source-app.com` at
# 127.0.0.1 through `/etc/hosts`, and serves that fresh build over a
# self-signed certificate. Only `auth.` and `api.` reach real deployed
# infrastructure, so a green run means the merge commit's web client works
# against the deployed backend, not that the hosted web deployment is healthy.
web_smoke:
name: Web post-deploy smoke
needs:
- changes
- aws_deploy
if: ${{ needs.changes.outputs.aws_changed == 'true' }}
runs-on: ubuntu-24.04
container:
image: mcr.microsoft.com/playwright:v1.63.0-noble
options: --init --ipc=host
timeout-minutes: 45
steps:
- uses: actions/checkout@v7
timeout-minutes: 5
- uses: actions/setup-node@v7
timeout-minutes: 5
with:
node-version: 24
cache: npm
cache-dependency-path: apps/web/package-lock.json
- name: Install web dependencies
timeout-minutes: 5
run: npm ci --prefix apps/web
- name: Build web app for live smoke
timeout-minutes: 5
env:
VITE_APP_BASE_URL: https://app.flashcards-open-source-app.com
VITE_API_BASE_URL: https://api.flashcards-open-source-app.com/v1
VITE_AUTH_BASE_URL: https://auth.flashcards-open-source-app.com
run: npm run build --prefix apps/web
- name: Run live web smoke test
id: live_smoke
working-directory: apps/web
shell: bash
env:
FLASHCARDS_E2E_APP_BASE_URL: https://app.flashcards-open-source-app.com
FLASHCARDS_E2E_AUTH_BASE_URL: https://auth.flashcards-open-source-app.com
FLASHCARDS_LIVE_REVIEW_EMAIL: google-review@example.com
run: |
set -euo pipefail
cert_dir="${RUNNER_TEMP}/web-live-e2e-cert"
cert_path="${cert_dir}/app.flashcards-open-source-app.com.crt"
key_path="${cert_dir}/app.flashcards-open-source-app.com.key"
server_log_path="${RUNNER_TEMP}/web-live-e2e-server.log"
mkdir -p "${cert_dir}"
openssl req \
-x509 \
-newkey rsa:2048 \
-sha256 \
-nodes \
-days 1 \
-subj "/CN=app.flashcards-open-source-app.com" \
-addext "subjectAltName=DNS:app.flashcards-open-source-app.com" \
-keyout "${key_path}" \
-out "${cert_path}"
printf '127.0.0.1 app.flashcards-open-source-app.com\n' | tee -a /etc/hosts >/dev/null
env PATH="${PATH}" node scripts/serve-dist-https.mjs \
--host 0.0.0.0 \
--port 443 \
--dir dist \
--cert "${cert_path}" \
--key "${key_path}" >"${server_log_path}" 2>&1 &
server_pid=$!
cleanup() {
kill "${server_pid}" >/dev/null 2>&1 || true
}
trap cleanup EXIT
for _ in $(seq 1 30); do
if curl --silent --show-error --fail --insecure https://app.flashcards-open-source-app.com/ >/dev/null; then
break
fi
sleep 1
done
curl --silent --show-error --fail --insecure https://app.flashcards-open-source-app.com/ >/dev/null
npm run test:e2e
- name: Upload Playwright report
if: always()
uses: actions/upload-artifact@v7
with:
name: web-playwright-report
if-no-files-found: ignore
path: apps/web/playwright-report
- name: Upload Playwright test results
if: always()
uses: actions/upload-artifact@v7
with:
name: web-playwright-test-results
if-no-files-found: ignore
path: apps/web/test-results
# The smoke already records console, page-error and network evidence, but it
# only reaches the run when it is collected into an artifact of its own.
- name: Collect live smoke failure diagnostics
if: failure()
shell: bash
run: |
set -euo pipefail
diagnostics_dir="web-live-smoke-failure-diagnostics"
server_log_path="${RUNNER_TEMP}/web-live-e2e-server.log"
mkdir -p "${diagnostics_dir}"
if [[ -f "${server_log_path}" ]]; then
cp "${server_log_path}" "${diagnostics_dir}/static-server.log"
echo "::group::Static web server log"
cat "${server_log_path}"
echo "::endgroup::"
else
echo "No static web server log at ${server_log_path}."
fi
if [[ -d apps/web/test-results ]]; then
find apps/web/test-results -type f \
\( -name 'failure-diagnostics.json' -o -name 'failure-summary.txt' \) \
-exec cp --parents {} "${diagnostics_dir}/" \;
fi
find "${diagnostics_dir}" -type f
- name: Upload live smoke failure diagnostics
if: failure()
uses: actions/upload-artifact@v7
with:
name: web-live-smoke-failure-diagnostics
if-no-files-found: warn
path: web-live-smoke-failure-diagnostics
- name: Summarize smoke result
if: always()
run: |
{
echo "## Web post-deploy smoke"
echo ""
echo "- Deployed SHA: \`${GITHUB_SHA}\`"
echo "- Result: \`${{ steps.live_smoke.outcome }}\`"
echo "- Artifacts: \`web-playwright-report\`, \`web-playwright-test-results\`, \`web-live-smoke-failure-diagnostics\` (uploaded on failure)"
} >> "${GITHUB_STEP_SUMMARY}"
agent_api_smoke:
name: Agent API post-deploy smoke
needs:
- changes
- aws_deploy
if: ${{ needs.changes.outputs.aws_changed == 'true' }}
runs-on: ubuntu-24.04
timeout-minutes: 30
steps:
- uses: actions/checkout@v7
- name: Run agent API smoke test
id: agent_smoke
env:
FLASHCARDS_AGENT_SMOKE_AUTH_BASE_URL: https://auth.flashcards-open-source-app.com
FLASHCARDS_AGENT_SMOKE_API_BASE_URL: https://api.flashcards-open-source-app.com/v1
FLASHCARDS_AGENT_SMOKE_DEMO_EMAIL: google-review@example.com
FLASHCARDS_AGENT_SMOKE_WORKSPACE_PREFIX: "E2E agent api "
FLASHCARDS_AGENT_SMOKE_CONNECTION_LABEL_PREFIX: "E2E agent api "
run: bash scripts/checks/check-agent-api-smoke.sh
- name: Summarize smoke result
if: always()
run: |
{
echo "## Agent API post-deploy smoke"
echo ""
echo "- Deployed SHA: \`${GITHUB_SHA}\`"
echo "- Result: \`${{ steps.agent_smoke.outcome }}\`"
} >> "${GITHUB_STEP_SUMMARY}"
mcp_smoke:
name: MCP post-deploy smoke
needs:
- changes
- aws_deploy
if: ${{ needs.changes.outputs.aws_changed == 'true' }}
runs-on: ubuntu-24.04
timeout-minutes: 30
steps:
- uses: actions/checkout@v7
- name: Run MCP smoke test
id: mcp_smoke
env:
FLASHCARDS_MCP_SMOKE_AUTH_BASE_URL: https://auth.flashcards-open-source-app.com
FLASHCARDS_MCP_SMOKE_API_BASE_URL: https://api.flashcards-open-source-app.com/v1
FLASHCARDS_MCP_SMOKE_MCP_BASE_URL: https://mcp.flashcards-open-source-app.com
FLASHCARDS_MCP_SMOKE_DEMO_EMAIL: google-review@example.com
FLASHCARDS_MCP_SMOKE_WORKSPACE_PREFIX: "E2E mcp "
FLASHCARDS_MCP_SMOKE_CONNECTION_LABEL_PREFIX: "E2E mcp "
run: bash scripts/checks/check-mcp-smoke.sh
- name: Summarize smoke result
if: always()
run: |
{
echo "## MCP post-deploy smoke"
echo ""
echo "- Deployed SHA: \`${GITHUB_SHA}\`"
echo "- Result: \`${{ steps.mcp_smoke.outcome }}\`"
} >> "${GITHUB_STEP_SUMMARY}"
aws_release_status:
name: Resolve AWS release outcome
needs:
- changes
- pre_deploy_checks
- aws_deploy
- web_smoke
- agent_api_smoke
- mcp_smoke
if: ${{ always() }}
runs-on: ubuntu-24.04
outputs:
aws_release_status: ${{ steps.resolve.outputs.aws_release_status }}
steps:
- name: Resolve AWS release status
id: resolve
run: |
set -euo pipefail
aws_release_status="no_aws_changes"
if [[ "${{ needs.changes.outputs.aws_changed }}" == "true" ]]; then
if [[ "${{ needs.pre_deploy_checks.result }}" != "success" ]]; then
aws_release_status="pre_deploy_checks_failed"
elif [[ "${{ needs.aws_deploy.result }}" != "success" ]]; then
aws_release_status="deployment_failed"
elif [[ "${{ needs.web_smoke.result }}" != "success" ]]; then
aws_release_status="web_smoke_failed"
elif [[ "${{ needs.agent_api_smoke.result }}" != "success" ]]; then
aws_release_status="agent_api_smoke_failed"
elif [[ "${{ needs.mcp_smoke.result }}" != "success" ]]; then
aws_release_status="mcp_smoke_failed"
else
aws_release_status="released"
fi
fi
echo "aws_release_status=${aws_release_status}" >> "${GITHUB_OUTPUT}"
- name: Summarize AWS release outcome
run: |
{
echo "## AWS release outcome"
echo ""
echo "- AWS release status: \`${{ steps.resolve.outputs.aws_release_status }}\`"
} >> "${GITHUB_STEP_SUMMARY}"
release_summary:
name: AWS/Web release summary
if: ${{ always() }}
needs:
- changes
- pre_deploy_checks
- aws_deploy
- web_smoke
- agent_api_smoke
- mcp_smoke
- aws_release_status
runs-on: ubuntu-24.04
steps:
- name: Summarize release outcome
run: |
{
echo "## AWS/Web release summary"
echo ""
echo "- AWS changed: \`${{ needs.changes.outputs.aws_changed }}\`"
echo "- Pre-deploy checks job: \`${{ needs.pre_deploy_checks.result }}\`"
echo "- AWS release status: \`${{ needs.aws_release_status.outputs.aws_release_status }}\`"
echo "- AWS deploy job: \`${{ needs.aws_deploy.result }}\`"
echo "- Web smoke job: \`${{ needs.web_smoke.result }}\`"
echo "- Agent API smoke job: \`${{ needs.agent_api_smoke.result }}\`"
echo "- MCP smoke job: \`${{ needs.mcp_smoke.result }}\`"
echo "- Run details: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
} >> "${GITHUB_STEP_SUMMARY}"
- name: Fail workflow when AWS/Web release is not healthy
run: |
set -euo pipefail
aws_release_status="${{ needs.aws_release_status.outputs.aws_release_status }}"
if [[ "${aws_release_status}" != "no_aws_changes" ]] && [[ "${aws_release_status}" != "released" ]]; then
echo "AWS release finished in non-healthy state: ${aws_release_status}" >&2
exit 1
fi