Repository navigation
795 lines (773 loc) · 43.7 KB
/
Copy pathci.yml
File metadata and controls
795 lines (773 loc) · 43.7 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
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
name: CI
on:
pull_request:
push:
branches: [master]
permissions:
contents: read
concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
format:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup-pnpm
- run: pnpm format
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup-pnpm
- run: pnpm lint
typecheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup-pnpm
- run: pnpm typecheck
unit-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup-pnpm
# Tenant-Integrationstests laufen hier im Local-Mode (Skip mit Warnung);
# das PFLICHT-Gate dafür ist der Job db-gates (fail-closed, EYT-66/57).
- run: pnpm test
# Dass die Zeitrechnung unabhängig von der Maschinenzone ist, folgte
# bisher nur aus dem Quelltext (überall explizites `timeZone` in `Intl`,
# sonst nur `Date.UTC`/`getUTC*`). Gelesen ist nicht gemessen — und der
# Runner steht auf UTC, also würde ein Rückfall auf die Maschinenzone dort
# gar nicht auffallen. Zwei Extremzonen, je eine pro Vorzeichen:
# Kiritimati liegt auf UTC+14, Niue auf UTC-11. Ein Kalendertag, der aus
# der Maschinenzone kommt, geht unter mindestens einer von beiden kaputt.
- name: Domain-Suite unter fremder Maschinenzeitzone (EYT-61 AK6)
run: |
TZ=Pacific/Kiritimati pnpm --filter @easytree/domain test
TZ=Pacific/Niue pnpm --filter @easytree/domain test
build-web:
runs-on: ubuntu-latest
# Kein EASYTREE_API_PROXY_TARGET mehr: seit EYT-126 loest der Same-Origin-
# Proxy das Ziel zur LAUFZEIT auf (Route Handler statt `rewrites()`). Dass
# die Variable hier fehlt, ist die dauerhafte Gegenprobe — brauchte der
# Build sie noch, waere dieser Pflichtjob rot.
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup-pnpm
- run: pnpm --filter @easytree/web... build
# EYT-142: OpenNext-Build plus Dry-Run, und die gzip-Groesse wandert in
# die Zusammenfassung. Das Free-Tier-Limit liegt bei 3 MiB gzip; ohne
# eine dauerhaft sichtbare Zahl faellt ein langsames Anwachsen erst auf,
# wenn das Deploy abgelehnt wird (Risiko R-BUNDLE).
- name: Cloudflare Web-Worker buendelbar + Groesse (EYT-142)
run: |
set -euo pipefail
pnpm --filter @easytree/web run build:cf
pnpm --filter @easytree/web run cf:dry-run | tee /tmp/cf-web-dry.log
# Drei Stufen, absichtlich in dieser Reihenfolge. Wrangler ist ein
# Fremdwerkzeug; sein Ausgabeformat ist keine Zusage an uns. Eine
# Formataenderung soll die Zusammenfassung verschlechtern, nicht den
# Build faellen — aber ein voelliges Fehlen der Messung MUSS laut sein.
erwartet='Total Upload: [0-9.]+ KiB / gzip: [0-9.]+ KiB'
groesse="$(grep -oE "$erwartet" /tmp/cf-web-dry.log | tail -1 || true)"
hinweis=""
if [ -z "$groesse" ]; then
# Stufe 2: Zeile existiert, aber anders formatiert — verbatim melden.
groesse="$(grep -aF 'Total Upload' /tmp/cf-web-dry.log | tail -1 | sed 's/^[[:space:]]*//' || true)"
if [ -n "$groesse" ]; then
hinweis="> Hinweis: Wrangler hat das Format geaendert; erwartet war \`$erwartet\`. Die Zeile steht unveraendert oben — die Schwellenpruefung unten ist damit NICHT automatisiert."
fi
fi
if [ -z "$groesse" ]; then
# Stufe 3: gar keine Groessenangabe — der Schritt haette nichts gemessen.
echo "Keine 'Total Upload'-Zeile im Wrangler-Output. Der Schritt misst nichts und ist deshalb rot." >&2
exit 1
fi
{
echo "### Cloudflare Web-Worker (EYT-142)"
echo ""
echo "\`$groesse\`"
echo ""
echo "Free-Tier-Grenze: 3072 KiB gzip."
# Bewusst ein `if` und keine `&&`-Kette: unter `set -e` liefert
# `[ -n "" ] && …` den Status 1 und faellt den ganzen Schritt — im
# NORMALFALL, in dem gar kein Hinweis noetig ist. Lokal gemessen.
if [ -n "$hinweis" ]; then
echo ""
echo "$hinweis"
fi
} >> "$GITHUB_STEP_SUMMARY"
# Real-Browser-Smoke der Web-Shell (EYT-58b): Tastatur, Fokus,
# Viewports und axe inkl. color-contrast in echtem Chromium.
web-smoke:
runs-on: ubuntu-latest
# Hier laeuft KEINE API: der Smoke prueft den gehandelten Ausfall und die
# Shell, nicht den Proxy. Er ist ausdruecklich KEIN Proxy-Nachweis — der
# kommt aus dem integrierten Harness.
#
# Auf Job-Ebene, damit `next start` sie ERBT: seit EYT-126 ist sie eine
# LAUFZEIT-Variable. Beim Bauen wird sie nicht mehr gelesen; der Bauschritt
# unten liefe auch ohne sie durch.
env:
EASYTREE_API_PROXY_TARGET: http://127.0.0.1:3001
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup-pnpm
- run: pnpm --filter @easytree/web... build
- run: pnpm --filter @easytree/web exec playwright install --with-deps chromium
- run: pnpm --filter @easytree/web run test:e2e
build-api:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup-pnpm
- run: pnpm --filter @easytree/api... build
# EYT-142: der Cloudflare-Entry muss buendelbar BLEIBEN. Ohne diesen
# Schritt faellt ein Bruch erst beim Deploy auf — und der Build davor
# war gruen. Kein eigener Job: ein neuer Job waere ein Status-Check, den
# das Ruleset nicht kennt, und setup-/verify-branch-protection.sh
# meldeten Drift.
- name: Cloudflare API-Worker buendelbar (EYT-142)
run: pnpm --filter @easytree/api run cf:dry-run
secret-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Fail if real .env files are tracked
run: |
tracked="$(git ls-files | grep -E '(^|/)\.env(\.local|\.production|\.development|\.test)?$' || true)"
if [ -n "$tracked" ]; then
echo "::error::Tracked .env files found:"; echo "$tracked"; exit 1
fi
- name: Fail if private key files are tracked
run: |
keys="$(git ls-files | grep -E '\.(pem|p12|pfx|key)$' || true)"
if [ -n "$keys" ]; then
echo "::error::Tracked key material found:"; echo "$keys"; exit 1
fi
- name: Run gitleaks (pinned)
env:
GITLEAKS_VERSION: "8.24.3"
run: |
curl -sSfL "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" -o /tmp/gitleaks.tgz
tar -xzf /tmp/gitleaks.tgz -C /tmp gitleaks
/tmp/gitleaks git --redact --no-banner --exit-code 1 .
db-gates:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup-pnpm
- name: Start local Supabase stack (Docker)
run: pnpm exec supabase start -x studio
- name: Reset 1 — migrations + seed on empty DB
run: pnpm exec supabase db reset
- name: pgTAP suite (run 1)
run: pnpm exec supabase test db
- name: Reset 2 — reproducibility proof
run: pnpm exec supabase db reset
- name: pgTAP suite (run 2, must be identical)
run: pnpm exec supabase test db
# Das Metagate in supabase/tests/0005_schema_meta_gate.sql fragt den
# Katalog statt Tabellen aufzuzaehlen. Diese Gegenprobe beweist, dass es
# nicht selbst vakuoes ist: eine absichtlich offene Tabelle MUSS es rot
# machen. Ohne diesen Schritt waere "alles gruen" auch dann moeglich,
# wenn das Gate gar nichts prueft.
- name: Metagate-Gegenprobe — offene Tabelle muss rot machen (EYT-86)
shell: bash
run: |
set -Eeuo pipefail
PSQL='psql postgresql://postgres:postgres@127.0.0.1:54322/postgres -v ON_ERROR_STOP=1'
$PSQL -c "create table public.eyt_meta_gate_probe (id uuid primary key, org_id uuid not null);"
$PSQL -c "grant select, insert on table public.eyt_meta_gate_probe to authenticated;"
if pnpm exec supabase test db >/tmp/metagate.log 2>&1; then
echo "::error::Metagate blieb gruen, obwohl eine offene Tabelle existiert — es prueft nichts."
tail -40 /tmp/metagate.log
$PSQL -c "drop table public.eyt_meta_gate_probe;"
exit 1
fi
# Nicht irgendein Fehlschlag: es muss DIESE Tabelle sein.
if ! grep -q "eyt_meta_gate_probe" /tmp/metagate.log; then
echo "::error::Suite ist rot, aber nicht wegen der offenen Tabelle:"
tail -40 /tmp/metagate.log
$PSQL -c "drop table public.eyt_meta_gate_probe;"
exit 1
fi
echo "Metagate-Gegenprobe OK: offene Tabelle wurde erkannt."
grep -o "eyt_meta_gate_probe" /tmp/metagate.log | head -1
$PSQL -c "drop table public.eyt_meta_gate_probe;"
# Nach der Gegenprobe ist der Katalog wieder sauber; ein Reset stellt
# zusaetzlich den Seed-Stand her, auf dem die Tenant-Gates aufbauen.
- name: Reset 3 — nach der Gegenprobe
run: pnpm exec supabase db reset
# Rollen sind clusterweit, Migration 0003 ist deshalb idempotent. Bewiesen
# war bisher nur, dass sie sich wiederholen laesst — nicht, welcher Zweig
# lief. Diese Probe schliesst die Luecke: Rolle loeschen, Reset, Existenz
# pruefen. Damit ist beides belegt, der create- und der alter-Pfad.
- name: Rollen-Probe — 0003 legt easytree_app auch neu an (EYT-86)
shell: bash
run: |
set -Eeuo pipefail
PSQL='psql postgresql://postgres:postgres@127.0.0.1:54322/postgres -v ON_ERROR_STOP=1'
# `postgres` ist in Supabase KEIN Superuser (es traegt BYPASSRLS, das ist
# nicht dasselbe). reassign/drop owned by scheitern deshalb mit
# Only roles with privileges of role "easytree_app" may ...
# Gebraucht werden sie hier auch nicht: 0003 legt die Rolle an und gibt
# ihr Rechte, sie BESITZT kein Objekt. Was DROP ROLE blockiert, ist
# ausschliesslich das Datenbankrecht aus 0003:
# role "easytree_app" cannot be dropped because some objects depend on it
# DETAIL: privileges for database postgres
# Das darf postgres entziehen, weil postgres es erteilt hat.
#
# Kein `|| true`: bekaeme easytree_app spaeter Eigentum an einem Objekt,
# muss dieser Schritt laut werden statt es zu verschlucken. DROP ROLE
# benennt die Abhaengigkeit dann selbst.
$PSQL -c "revoke all on database postgres from easytree_app;"
$PSQL -c "drop role easytree_app;"
test "$($PSQL -Atc "select count(*) from pg_roles where rolname='easytree_app'")" = "0"
echo "Rolle geloescht — jetzt muss der Reset sie neu anlegen."
pnpm exec supabase db reset
test "$($PSQL -Atc "select count(*) from pg_roles where rolname='easytree_app'")" = "1"
echo "Rollen-Probe OK: create-Zweig von 0003 belegt."
- name: Restart stack — re-provision Supavisor tenant (EYT-15)
run: |
pnpm exec supabase stop
pnpm exec supabase start -x studio
# EYT-107 P1: der Publish-Pfad braucht den LAUFZEITKANAL, nicht irgendeine
# Verbindung. `plan_versions_update_in_org` verlangt seit Migration 0015
# zusaetzlich `app.is_runtime_channel()`, also `session_user =
# 'easytree_app'`. Ueber `postgres` filtert die Policy jede Zeile heraus.
#
# Der Schritt steht hier und nicht weiter unten, weil die Rollen-Probe
# darueber `easytree_app` loescht und der Reset sie ohne Passwort neu
# anlegt. Der gleichlautende Schritt vor den Prozess-Smokes bleibt
# bestehen: `alter role ... password` ist idempotent, und dort belegt er
# die Voraussetzung der Smokes an Ort und Stelle.
- name: Provision local password for easytree_app (EYT-45, EYT-107)
run: |
PGPASSWORD=postgres psql "host=127.0.0.1 port=54322 dbname=postgres user=postgres sslmode=disable" \
-v ON_ERROR_STOP=1 -c "alter role easytree_app password 'ci-local-app-password';"
# Gegenprobe: die Anwendungsrolle darf RLS nicht umgehen koennen.
# Stand frueher unten beim Prozess-Smoke, zusammen mit einer zweiten,
# wirkungslosen Provisionierung derselben Rolle (Reviewbefund F5).
# Zwischen beiden Stellen liegt kein Stack-Neustart, also genuegt
# diese eine — und sie steht dort, wo die Rolle zuerst gebraucht wird.
PGPASSWORD=ci-local-app-password psql "host=127.0.0.1 port=54322 dbname=postgres user=easytree_app sslmode=disable" \
-v ON_ERROR_STOP=1 -Atc \
"select current_user || ' super=' || rolsuper || ' bypassrls=' || rolbypassrls || ' inherit=' || rolinherit from pg_roles where rolname = current_user" \
| tee -a "$GITHUB_STEP_SUMMARY"
# ALLE Workspace-Abhaengigkeiten von @easytree/api, nicht eine namentlich
# genannte. Die frueher hier hartkodierte Liste (@easytree/config) waere in
# dem Moment still veraltet, in dem apps/api eine zweite Workspace-Abhaengigkeit
# bekommt — und genau das passiert mit @easytree/domain (EYT-46). Die Folge
# waere kein sichtbarer Fehler, sondern ein fail-closed Tenant-Gate, das an
# einem ungebauten dist/ scheitert.
- name: Build workspace deps for api tests
run: pnpm --filter @easytree/api^... build
- name: Tenant isolation gate (fail-closed, direct connection)
env:
EASYTREE_TENANT_TESTS: required
EASYTREE_TEST_DB_URL: postgresql://postgres:postgres@127.0.0.1:54322/postgres
shell: bash
run: |
pnpm --filter @easytree/api exec vitest run test/tenant-isolation.integration.test.ts 2>&1 | tee /tmp/tenant-direct.log
# Prueft executed>=1, passed==executed und skipped==0 (EYT-66/EYT-71).
bash scripts/assert-tenant-report.sh tenant-isolation /tmp/tenant-direct.log
# Nebenlaeufigkeit ist der Teil von EYT-49 AK6, den pgTAP nicht leisten
# kann: eine Session zeigt Ablehnung, nicht Serialisierung. Diese Suite
# oeffnet zwei echte Verbindungen und committet bewusst nichts —
# veroeffentlichte Zeilen sind laut 0010 unveraenderlich UND unloeschbar,
# ein commit hier liesse jeden folgenden Schritt auf einer veraenderten
# Datenbank laufen.
# EYT-107 P1: diese Suite VEROEFFENTLICHT (EXCLUDE, Unveraenderlichkeit,
# Race gegen `for share`). Sie arbeitet ausschliesslich innerhalb von
# `set local role authenticated` und laeuft deshalb vollstaendig ueber
# `easytree_app` — ueber `postgres` waere jedes ihrer UPDATEs seit
# Migration 0015 wirkungslos und die Suite still vakuos.
- name: Planning invariants gate (fail-closed, zwei Verbindungen, EYT-49)
env:
EASYTREE_TENANT_TESTS: required
EASYTREE_TEST_DB_URL: postgresql://easytree_app:ci-local-app-password@127.0.0.1:54322/postgres
shell: bash
run: |
pnpm --filter @easytree/api exec vitest run test/planning-invariants.integration.test.ts 2>&1 | tee /tmp/planning-invariants.log
# Gleiche Bedingungen wie bei den Tenant-Gates: executed>=1,
# passed==executed, skipped==0 (EYT-90).
bash scripts/assert-tenant-report.sh planning-invariants /tmp/planning-invariants.log
# EYT-92: Idempotenz, Transaktionsklammer und Serialisierung des
# Schreibpfads. Eigener Schritt im BESTEHENDEN Job db-gates — kein neuer
# Job und kein neuer Status-Check, sonst entstuende ein Pflichtcheck, den
# das Ruleset nicht kennt und den scripts/setup-branch-protection.sh als
# Drift meldet.
# EYT-136: ZWEI Verbindungszeichenketten, dieselbe Bauart wie beim
# Publish-Gate. `EASYTREE_TEST_DB_URL` ist die Verwaltungsverbindung
# (`postgres`) fuer Fixtures und Aufraeumen — sie ist seit Migration 0017
# die einzige, die Entwuerfe wieder loeschen kann.
# `EASYTREE_TEST_APP_DB_URL` ist der Laufzeitkanal (`easytree_app`), ueber
# den der Entwurfs-Command laeuft: `assignments_insert_in_org` verlangt
# seit 0017 `app.is_runtime_channel()`, und eine `postgres`-Sitzung
# erfuellt das nicht. Beide Zeichenketten stammen ausschliesslich aus der
# in diesem Job provisionierten lokalen Supabase-Instanz.
- name: Planning write gate (fail-closed, zwei Verbindungen, EYT-92/EYT-136)
env:
EASYTREE_TENANT_TESTS: required
EASYTREE_TEST_DB_URL: postgresql://postgres:postgres@127.0.0.1:54322/postgres
EASYTREE_TEST_APP_DB_URL: postgresql://easytree_app:ci-local-app-password@127.0.0.1:54322/postgres
shell: bash
run: |
pnpm --filter @easytree/api exec vitest run test/planning-write.integration.test.ts 2>&1 | tee /tmp/planning-write.log
bash scripts/assert-tenant-report.sh planning-write /tmp/planning-write.log
# Der Kanal gehoert ins Protokoll, nicht in eine Behauptung — dieselbe
# Zusicherung wie beim Publish-Gate unten. Ohne diesen Grep waere die
# Kanalzeile nur innerhalb von vitest wirksam; hier wird sie auf
# Jobebene tragend.
grep -F '[planning-write] session_user=easytree_app current_user=authenticated runtime_channel=true' /tmp/planning-write.log
# EYT-136: und der Riegel, der bei fehlendem planning.write faellt.
grep -F '[planning-write] member-probe ziel=plan_versions kanal=true mandant_alpha=true recht_alpha=false recht_beta=true' /tmp/planning-write.log
grep -F '[planning-write] member-probe ziel=assignments kanal=true mandant_alpha=true recht_alpha=false recht_beta=true' /tmp/planning-write.log
# EYT-136 (Kanalmatrix 08.08.2026): der Definer-Nachweis. `sperrlesbar=0`
# bei `lesbar=1` ist die ganze Aussage — die sperrende Lesung der
# Triggerfunktion faende als INVOKER nichts, `security definer` traegt
# sie. Ohne diesen Grep waere die Sonde nur innerhalb von vitest
# wirksam; hier wird sie auf Jobebene tragend.
grep -F '[planning-write] definer-probe kanal=true mandant_alpha=true recht_write=true recht_publish=false lesbar=1 sperrlesbar=0 definer=true' /tmp/planning-write.log
# EYT-107: das Veroeffentlichen. Wieder ein Schritt im BESTEHENDEN Job,
# kein neuer Pflichtcheck.
#
# Was hier gemessen wird, kann kein Unittest: der Sync-Trigger aus
# Migration 0010, die Exclusion-Constraint, die erst beim
# Veroeffentlichen greift, die Serialisierung ueber ZWEI echte
# Verbindungen und der Rollback nach einem erzwungenen Fehler an drei
# Punkten. Eine Sitzung kann eine Sperre nicht beobachten — sie wartet
# nie auf sich selbst.
#
# EYT-107 P1: ZWEI Verbindungszeichenketten. `EASYTREE_TEST_DB_URL` legt
# als `postgres` die Fixtures an und zaehlt Audit-, Outbox- und
# Idempotenzzeilen; `EASYTREE_TEST_APP_DB_URL` ist der Laufzeitkanal, ueber
# den der Publish-Command selbst laeuft. Fehlt die zweite, ist die Suite
# rot statt still uebersprungen — der Nachweis der Kanalgrenze soll nicht
# an einer vergessenen Variablen vorbeigruenen.
- name: Planning publish gate (fail-closed, zwei Verbindungen, EYT-107)
env:
EASYTREE_TENANT_TESTS: required
EASYTREE_TEST_DB_URL: postgresql://postgres:postgres@127.0.0.1:54322/postgres
EASYTREE_TEST_APP_DB_URL: postgresql://easytree_app:ci-local-app-password@127.0.0.1:54322/postgres
shell: bash
run: |
pnpm --filter @easytree/api exec vitest run test/planning-publish.integration.test.ts 2>&1 | tee /tmp/planning-publish.log
bash scripts/assert-tenant-report.sh planning-publish /tmp/planning-publish.log
# Der Kanal gehoert ins Protokoll, nicht in eine Behauptung.
grep -F '[planning-publish] session_user=easytree_app runtime_channel=true' /tmp/planning-publish.log
# EYT-106 AK4: die DATENBANKSEITIGE Haelfte der Kostenautorisierung.
# Eigener Schritt im BESTEHENDEN Job db-gates — kein neuer Job und damit
# kein neuer Pflichtcheck, den das Ruleset nicht kennt (dieselbe
# Begruendung wie beim Planning-Write-Gate).
#
# Was hier gemessen wird, koennen die Policy-Unit-Tests prinzipiell
# nicht: sie sehen PostgreSQL nie und blieben gruen, wenn RLS jedem
# alles zeigte. Erst dieser Lauf belegt, dass BEIDE Haelften unabhaengig
# ablehnen.
- name: Cost access gate (fail-closed, RLS ohne Anwendungsschicht, EYT-106)
env:
EASYTREE_TENANT_TESTS: required
EASYTREE_TEST_DB_URL: postgresql://postgres:postgres@127.0.0.1:54322/postgres
shell: bash
run: |
pnpm --filter @easytree/api exec vitest run test/costs/cost-access.integration.test.ts 2>&1 | tee /tmp/cost-access.log
bash scripts/assert-tenant-report.sh cost-access /tmp/cost-access.log
# EYT-108: die Zeitstempelform der Kostenantworten. Ein Produktionsfehler
# kam genau hier durch — die HTTP-Nahttests nutzen Stubs mit
# wohlgeformten Zeitstempeln und konnten die SQL-Projektion nie sehen.
# Eigener Schritt im BESTEHENDEN Job db-gates, kein neuer Pflichtcheck.
- name: Rate timestamp contract gate (fail-closed, echtes PostgreSQL, EYT-108)
env:
EASYTREE_TENANT_TESTS: required
EASYTREE_TEST_DB_URL: postgresql://postgres:postgres@127.0.0.1:54322/postgres
shell: bash
run: |
pnpm --filter @easytree/api exec vitest run test/costs/rate-timestamp.integration.test.ts 2>&1 | tee /tmp/rate-timestamp.log
bash scripts/assert-tenant-report.sh rate-timestamp /tmp/rate-timestamp.log
pnpm --filter @easytree/api exec vitest run test/costs/rate-http-contract.integration.test.ts 2>&1 | tee /tmp/rate-http-contract.log
bash scripts/assert-tenant-report.sh rate-http-contract /tmp/rate-http-contract.log
# EYT-108: Abloesung eines offenen Satzes unter echter Nebenlaeufigkeit
# und Wiederholung. Braucht ZWEI unabhaengige Verbindungen — eine
# Verbindung sieht ihre eigene Zeilensperre nie als Wartezeit und koennte
# deshalb nicht zeigen, dass `for update` den kritischen Abschnitt
# serialisiert. Eigener Schritt im BESTEHENDEN Job db-gates, kein neuer
# Pflichtcheck: ein neuer Job waere ein Status-Check, den das Ruleset
# nicht kennt.
- name: Rate succession gate (fail-closed, zwei Verbindungen, EYT-108)
env:
EASYTREE_TENANT_TESTS: required
EASYTREE_TEST_DB_URL: postgresql://postgres:postgres@127.0.0.1:54322/postgres
shell: bash
run: |
pnpm --filter @easytree/api exec vitest run test/costs/rate-succession.integration.test.ts 2>&1 | tee /tmp/rate-succession.log
bash scripts/assert-tenant-report.sh rate-succession /tmp/rate-succession.log
# EYT-109: die Leseoperationen fuer veroeffentlichte Staende. Zwei
# Verbindungen wie beim Publish-Gate — das Repository laeuft auf dem
# Laufzeitkanal (easytree_app), Fixtures und Zaehlungen auf `postgres`.
# Eine Beobachtung ueber den Laufzeitkanal saehe genau das nicht, dessen
# Abwesenheit sie belegen soll.
#
# Eigener Schritt im BESTEHENDEN Job db-gates, kein neuer Job: ein neuer
# Job waere ein Status-Check, den das Ruleset nicht kennt, und beide
# Listen in setup-/verify-branch-protection.sh muessten mitwachsen.
#
# `assert-tenant-report.sh` statt eines rohen `grep`: das Skript prueft
# zusaetzlich `passed == executed`, was sich als regulaerer Ausdruck nicht
# formulieren laesst. Ein Lauf mit executed=7 passed=2 skipped=0 waere
# unter einem blossen grep gruen.
- name: Planungslesungen fuer veroeffentlichte Staende (fail-closed, zwei Verbindungen, EYT-109)
env:
EASYTREE_TENANT_TESTS: required
EASYTREE_TEST_DB_URL: postgresql://postgres:postgres@127.0.0.1:54322/postgres
EASYTREE_TEST_APP_DB_URL: postgresql://easytree_app:ci-local-app-password@127.0.0.1:54322/postgres
shell: bash
run: |
pnpm --filter @easytree/api exec vitest run test/planning-published-reads.integration.test.ts 2>&1 | tee /tmp/planning-published-reads.log
bash scripts/assert-tenant-report.sh planning-published-reads /tmp/planning-published-reads.log
# EYT-138: der Snapshot-Schreibweg gegen die echte Datenbank. Wieder zwei
# Verbindungen — das Repository auf dem Laufzeitkanal (easytree_app),
# Fixtures und Zaehlungen auf `postgres`. Eine Zaehlung ueber den
# Laufzeitkanal saehe genau das nicht, dessen Abwesenheit sie belegen soll,
# und scheiterte ausserdem an fehlenden Rechten.
#
# Hier entstehen die Aussagen, die der datenbankfreie Zwilling
# (test/costs/cost-snapshot-repository.test.ts) strukturell nicht treffen
# kann: echte Atomizitaet, RLS, Spaltenrechte, mandantengebundene
# Fremdschluessel und die Kanalgrenze aus Migration 0018. Auf der
# Entwicklungsmaschine laeuft kein Docker; dieser Schritt ist der einzige
# Ort, an dem die Suite je ausgefuehrt wird.
- name: Kosten-Snapshot — Persistenz, Atomizitaet, Rechte, Kanal (fail-closed, zwei Verbindungen, EYT-138)
env:
EASYTREE_TENANT_TESTS: required
EASYTREE_TEST_DB_URL: postgresql://postgres:postgres@127.0.0.1:54322/postgres
EASYTREE_TEST_APP_DB_URL: postgresql://easytree_app:ci-local-app-password@127.0.0.1:54322/postgres
shell: bash
run: |
pnpm --filter @easytree/api exec vitest run test/costs/cost-snapshot.integration.test.ts 2>&1 | tee /tmp/cost-snapshot.log
bash scripts/assert-tenant-report.sh cost-snapshot /tmp/cost-snapshot.log
# EYT-139: die HTTP-Naht des Snapshots gegen echtes PostgreSQL. Was hier
# gemessen wird, kann der Nahttest mit gestellten Repositories
# (test/costs/snapshot-http.test.ts) strukturell nicht: dass RLS die
# fremde Organisation wirklich ausblendet, dass ein Montagefehler KEINE
# Zeile hinterlaesst, dass ein Retry keinen zweiten Snapshot anlegt, und
# dass ein gelesener Snapshot eine spaetere Satzversion nicht sieht.
#
# Eigener Schritt im BESTEHENDEN Job db-gates, kein neuer Job: ein neuer
# Job waere ein Status-Check, den das Ruleset nicht kennt, und beide
# Listen in setup-/verify-branch-protection.sh muessten mitwachsen.
- name: Snapshot-HTTP-Naht (fail-closed, zwei Verbindungen, EYT-139)
env:
EASYTREE_TENANT_TESTS: required
EASYTREE_TEST_DB_URL: postgresql://postgres:postgres@127.0.0.1:54322/postgres
EASYTREE_TEST_APP_DB_URL: postgresql://easytree_app:ci-local-app-password@127.0.0.1:54322/postgres
shell: bash
run: |
pnpm --filter @easytree/api exec vitest run test/costs/snapshot-http.integration.test.ts 2>&1 | tee /tmp/snapshot-http.log
bash scripts/assert-tenant-report.sh snapshot-http /tmp/snapshot-http.log
# EYT-143: ein gespeicherter Snapshot ueberlebt eine RUECKWIRKEND wirksame
# Satzversion. Der HTTP-Fall I8 im Schritt darueber misst dasselbe
# Szenario; diese Suite ergaenzt genau die zwei Aussagen, die er aus
# Bauart nicht treffen kann:
#
# 1. Sie vergleicht die GESPEICHERTEN Zeilen selbst, ueber die
# Beobachterverbindung. I8 sieht nur die Leseausgabe — ein Defekt, der
# die Zeilen umschriebe UND sie konsistent zurueckliese, liesse ihn
# gruen.
# 2. Sie belegt die aktuelle Satzauflage ueber den PRODUKTIONSPFAD
# (versionsFor + effectiveRateVersion) statt ueber rohes Admin-SQL.
#
# Eigener Schritt im BESTEHENDEN Job db-gates, kein neuer Job: ein neuer
# Job waere ein Status-Check, den das Ruleset nicht kennt, und beide
# Listen in setup-/verify-branch-protection.sh muessten mitwachsen.
- name: Snapshot-Unveraenderlichkeit gegen spaetere Saetze (fail-closed, zwei Verbindungen, EYT-143)
env:
EASYTREE_TENANT_TESTS: required
EASYTREE_TEST_DB_URL: postgresql://postgres:postgres@127.0.0.1:54322/postgres
EASYTREE_TEST_APP_DB_URL: postgresql://easytree_app:ci-local-app-password@127.0.0.1:54322/postgres
shell: bash
run: |
pnpm --filter @easytree/api exec vitest run test/costs/snapshot-immutability.integration.test.ts 2>&1 | tee /tmp/snapshot-immutability.log
bash scripts/assert-tenant-report.sh snapshot-immutability /tmp/snapshot-immutability.log
- name: Resolve pooler URL + pre-flight (Supavisor, EYT-15)
shell: bash
run: |
set -Eeuo pipefail
summary() { printf '%s\n' "$1" | tee -a "$GITHUB_STEP_SUMMARY"; }
pooler_ct="$(docker ps --filter 'name=supabase_pooler_' --format '{{.Names}}' | head -1 || true)"
if [ -z "$pooler_ct" ]; then
summary "FAIL-CLOSED: Kein laufender Supavisor-Container gefunden."
docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}' | tee -a "$GITHUB_STEP_SUMMARY"
exit 1
fi
pooler_env="$(docker inspect --format '{{range .Config.Env}}{{println .}}{{end}}' "$pooler_ct" 2>/dev/null || true)"
runtime_tenant="$(printf '%s\n' "$pooler_env" | sed -n 's/^POOLER_TENANT_ID=//p' | head -1 || true)"
runtime_password="$(printf '%s\n' "$pooler_env" | sed -n 's/^POSTGRES_PASSWORD=//p' | head -1 || true)"
published="$(docker port "$pooler_ct" 6543/tcp 2>/dev/null | head -1 || true)"
runtime_port="${published##*:}"
# Belegte lokale Supabase-CLI-Fallbacks. Die Direct-Connection desselben
# Jobs verwendet bereits postgres/postgres; config.toml bindet 54329.
postgres_password="${runtime_password:-postgres}"
pooler_port="${runtime_port:-54329}"
tenant_candidates=()
[ -n "$runtime_tenant" ] && tenant_candidates+=("$runtime_tenant")
if [ "$runtime_tenant" != "pooler-dev" ]; then
tenant_candidates+=("pooler-dev")
fi
summary "Pooler-Diagnose: container=${pooler_ct}, runtime_tenant=${runtime_tenant:-nicht-exportiert}, published=${published:-nicht-exportiert}, effective_port=${pooler_port}, candidates=${#tenant_candidates[@]}."
if ! [[ "$pooler_port" =~ ^[0-9]+$ ]] || [ "${#tenant_candidates[@]}" -eq 0 ]; then
summary "FAIL-CLOSED: Keine valide lokale Pooler-Konfiguration ableitbar."
exit 1
fi
working_tenant=""
for tenant_id in "${tenant_candidates[@]}"; do
pooler_user="postgres.${tenant_id}"
for attempt in 1 2 3 4 5 6; do
if PGPASSWORD="$postgres_password" PGCONNECT_TIMEOUT=5 psql \
"host=127.0.0.1 port=${pooler_port} dbname=postgres user=${pooler_user} sslmode=disable" \
-Atc 'select 1' >/tmp/pooler-preflight.out 2>/tmp/pooler-preflight.err; then
working_tenant="$tenant_id"
break 2
fi
summary "Pre-flight tenant=${tenant_id}, Versuch ${attempt}/6: $(tr '\n' ' ' </tmp/pooler-preflight.err)"
sleep 2
done
done
if [ -z "$working_tenant" ]; then
summary "FAIL-CLOSED: Kein belegter Supavisor-Tenant ist erreichbar."
docker inspect --format '{{json .NetworkSettings.Ports}}' "$pooler_ct" | tee -a "$GITHUB_STEP_SUMMARY" || true
docker logs --tail 100 "$pooler_ct" 2>&1 | tee -a "$GITHUB_STEP_SUMMARY" || true
exit 1
fi
pooler_user="postgres.${working_tenant}"
encoded_user="$(node -e 'process.stdout.write(encodeURIComponent(process.argv[1]))' "$pooler_user")"
encoded_password="$(node -e 'process.stdout.write(encodeURIComponent(process.argv[1]))' "$postgres_password")"
pooler_url="postgresql://${encoded_user}:${encoded_password}@127.0.0.1:${pooler_port}/postgres?sslmode=disable"
summary "Pooler-Pre-flight OK: tenant=${working_tenant}, port=${pooler_port}, sslmode=disable."
echo "EASYTREE_TEST_POOLER_URL=$pooler_url" >> "$GITHUB_ENV"
- name: Tenant isolation gate (fail-closed, TRANSACTION POOLER)
env:
EASYTREE_TENANT_TESTS: required
shell: bash
run: |
pnpm --filter @easytree/api exec vitest run test/tenant-pooling.integration.test.ts 2>&1 | tee /tmp/tenant-pooled.log
# Gleiche Bedingungen wie beim Direct-Gate (EYT-66/EYT-71).
bash scripts/assert-tenant-report.sh tenant-pooling /tmp/tenant-pooled.log
- name: Tenant report -> step summary
if: always()
run: |
grep "\[tenant-isolation\]" /tmp/tenant-direct.log >> "$GITHUB_STEP_SUMMARY" || true
grep "\[tenant-pooling\]" /tmp/tenant-pooled.log >> "$GITHUB_STEP_SUMMARY" || true
- name: Build api for runtime smokes
run: pnpm --filter @easytree/api... build
# Passwort und Rollen-Gegenprobe stehen weiter oben, direkt nach dem
# Stack-Neustart (Reviewbefund F5): dort werden sie zuerst gebraucht, und
# zwischen beiden Stellen gibt es keinen Neustart, der sie zuruecksetzen
# koennte. Die Rolle legt Migration 0003 an; ein Passwort steht bewusst
# NICHT in der Migration (das laege in git), produktiv kommt es aus dem
# Secret Manager.
- name: API smoke — healthy runtime (ready must be 200)
env:
NODE_ENV: test
API_PORT: "3001"
EXPECT_READY: "200"
DATABASE_URL: "postgresql://easytree_app:ci-local-app-password@127.0.0.1:54322/postgres"
SUPABASE_URL: "http://127.0.0.1:54321"
SUPABASE_ANON_KEY: "ci-local-anon-key"
run: bash scripts/smoke-api.sh
# Loest den frueheren Smoke "dependency down -> ready 503" ab. Seit EYT-45
# verweigert der Bootstrap den Start, wenn die Rollenattribute nicht
# ermittelbar sind — eine unerreichbare Datenbank darf die
# Sicherheitspruefung nicht ueberspringen. Das 503-Verhalten der Readiness
# bleibt in apps/api/test/health.e2e.test.ts abgedeckt (zwei Faelle).
- name: API smoke — refuses to start when the role cannot be verified
env:
NODE_ENV: test
API_PORT: "3002"
DATABASE_URL: "postgresql://easytree_app:ci-local-app-password@127.0.0.1:59999/postgres"
SUPABASE_URL: "http://127.0.0.1:54321"
SUPABASE_ANON_KEY: "ci-local-anon-key"
run: bash scripts/smoke-api-role-gate.sh
# Gegenprobe zur Migration: mit der Superuser-Rolle MUSS der Start
# scheitern. Ohne diesen Fall koennte die Pruefung auch dann gruen sein,
# wenn sie gar nichts prueft.
- name: API smoke — refuses to start as superuser
env:
NODE_ENV: test
API_PORT: "3004"
DATABASE_URL: "postgresql://postgres:postgres@127.0.0.1:54322/postgres"
SUPABASE_URL: "http://127.0.0.1:54321"
SUPABASE_ANON_KEY: "ci-local-anon-key"
run: bash scripts/smoke-api-role-gate.sh
- name: Worker smoke — no port, graceful shutdown
env:
NODE_ENV: test
API_PORT: "3003"
DATABASE_URL: "postgresql://easytree_app:ci-local-app-password@127.0.0.1:54322/postgres"
SUPABASE_URL: "http://127.0.0.1:54321"
SUPABASE_ANON_KEY: "ci-local-anon-key"
run: bash scripts/smoke-worker.sh
# EYT-126 — Container-Smoke: dieselbe Codebasis als OCI-Workloads.
#
# Er steht am ENDE von db-gates und bekommt bewusst KEINEN eigenen Job.
# Ein neuer Job waere so lange kein Pflichtcheck, bis jemand mit
# Repository-Adminrechten das Ruleset neu anwendet, und beide Kopien der
# Pflichtcheck-Liste (setup-/verify-branch-protection.sh) muessten
# mitgezogen werden. Dieselbe Begruendung steht schon bei den
# Cloudflare-Schritten in build-web und build-api.
#
# Und nur HIER gibt es, was der Smoke sonst nirgends bekommt: eine echte
# PostgreSQL-Instanz mit der Rolle easytree_app. Ohne sie waere `/ready`
# kein Nachweis, sondern eine Zusicherung ueber sich selbst.
- name: Head und CI-SHA muessen uebereinstimmen (EYT-126)
shell: bash
run: |
set -Eeuo pipefail
ist="$(git rev-parse HEAD)"
echo "checkout=${ist} github.sha=${{ github.sha }}"
test "${ist}" = "${{ github.sha }}"
- name: Container-Smoke — Web und API als OCI-Workloads (EYT-126)
shell: bash
env:
EASYTREE_CONTAINER_SMOKE: required
GIT_SHA: ${{ github.sha }}
# Aus Sicht des CONTAINERS, nicht des Runners: host.docker.internal
# zeigt ueber --add-host=...:host-gateway auf den Hostrechner, auf
# dem der Supabase-Stack seine Ports veroeffentlicht.
EASYTREE_SMOKE_DATABASE_URL: "postgresql://easytree_app:ci-local-app-password@host.docker.internal:54322/postgres"
EASYTREE_SMOKE_SUPABASE_URL: "http://host.docker.internal:54321"
EASYTREE_SMOKE_ANON_KEY: "ci-local-anon-key"
run: |
set -Eeuo pipefail
bash scripts/smoke-container.sh 2>&1 | tee /tmp/container-smoke.log
- name: Container-Gate-Zeile pruefen und berichten (EYT-126)
if: always()
shell: bash
run: |
set -Eeuo pipefail
# Die Zeile, nicht der gruene Haken: `skipped=0` ist die eigentliche
# Aussage. Ein Smoke, der sich mangels Docker weggeduckt hat, waere
# sonst nicht von einem bestandenen zu unterscheiden.
grep -E '\[container-smoke\] mode=required executed=[0-9]+ passed=[0-9]+ skipped=0' /tmp/container-smoke.log
grep -F '[container-smoke]' /tmp/container-smoke.log >> "$GITHUB_STEP_SUMMARY"
# Integrierter Read-Through-Nachweis (EYT-50): Browser -> Same-Origin-Rewrite
# -> NestJS -> TenantQueryRunner -> RLS -> PostgreSQL.
#
# Der Job ist bewusst duenn. Die Orchestrierung liegt in einem Skript, weil
# ein `trap` nur im eigenen Prozess gilt: ueber mehrere Steps verteilt raeumte
# ein Fehlschlag die zuvor gestarteten Prozesse nicht ab.
read-through:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup-pnpm
- run: bash scripts/read-through-harness.sh
- name: Logs und Traces sichern
if: always()
uses: actions/upload-artifact@v4
with:
name: read-through-diagnose
path: |
harness-logs/
apps/web/test-results/
retention-days: 7
if-no-files-found: ignore
# Reale Auth-Kostenreise (EYT-106 AK8, EYT-134): echte Loginseite -> GoTrue ->
# HttpOnly-Cookies -> Next-Rewrite -> ECHTE NestJS-API (dist/main.js, nicht
# dist-harness) -> Policy -> RLS -> PostgreSQL.
#
# Anders als `read-through` startet Playwright hier API und Web selbst
# (webServer-Block der Reisekonfiguration) und beendet sie garantiert wieder,
# auch wenn ein Nachweis abstuerzt. Der Job besitzt nur Supabase — Docker
# lebt laenger als ein Playwright-Lauf und wird unten mit `if: always()`
# gestoppt.
auth-journey:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup-pnpm
- name: Start local Supabase stack (Docker)
run: pnpm exec supabase start -x studio
- name: Reset — Migrationen und Seed
run: pnpm exec supabase db reset
- name: Provision local password for easytree_app (EYT-45)
run: |
PGPASSWORD=postgres psql "host=127.0.0.1 port=54322 dbname=postgres user=postgres sslmode=disable" \
-v ON_ERROR_STOP=1 -c "alter role easytree_app password 'ci-local-app-password';"
- name: Supabase-Koordinaten uebernehmen (URL und Anon-Key)
shell: bash
run: |
set -Eeuo pipefail
# Aus dem LAUFENDEN Stack, nicht hartkodiert: der Issuer wird exakt
# verglichen (`${SUPABASE_URL}/auth/v1`), und `localhost` statt
# `127.0.0.1` liefe in ISSUER_MISMATCH statt in eine klare Meldung.
# Zwei getrennte Aufrufe, jeder direkt in ein `sed` gepipt, statt
# `eval` ueber die ganze Ausgabe: die enthaelt auch SERVICE_ROLE_KEY
# und JWT_SECRET. So haelt zu keinem Zeitpunkt eine Shell-Variable
# den vollstaendigen Satz — dasselbe Prinzip, das der Waechter aus
# Arbeitspaket A statisch durchsetzt.
API_URL="$(pnpm exec supabase status -o env | sed -n 's/^API_URL="\(.*\)"$/\1/p')"
ANON_KEY="$(pnpm exec supabase status -o env | sed -n 's/^ANON_KEY="\(.*\)"$/\1/p')"
test -n "${API_URL:-}" || { echo "::error::API_URL fehlt aus supabase status"; exit 1; }
test -n "${ANON_KEY:-}" || { echo "::error::ANON_KEY fehlt aus supabase status"; exit 1; }
echo "::add-mask::${ANON_KEY}"
{
echo "EASYTREE_JOURNEY_SUPABASE_URL=${API_URL}"
echo "EASYTREE_JOURNEY_ANON_KEY=${ANON_KEY}"
echo "EASYTREE_JOURNEY_DATABASE_URL=postgresql://easytree_app:ci-local-app-password@127.0.0.1:54322/postgres"
echo "EASYTREE_JOURNEY_ADMIN_DB_URL=postgresql://postgres:postgres@127.0.0.1:54322/postgres"
} >> "$GITHUB_ENV"
echo "Supabase-API unter ${API_URL} uebernommen (Anon-Key maskiert)."
# Das Proxyziel steht NICHT mehr hier: seit EYT-126 setzt es
# apps/web/e2e/auth-journey/config.ts in der webServer-Umgebung, also zur
# Laufzeit. Genau dieser Job beweist damit zugleich, dass Login, POST-
# Koerper und HttpOnly-Cookies durch die neue Durchreiche gehen.
- name: Bauen — ECHTE API und Web (das Proxyziel kommt erst beim Start)
run: |
pnpm --filter @easytree/api... build
pnpm --filter @easytree/web... build
- name: Chromium installieren
run: pnpm --filter @easytree/web exec playwright install --with-deps chromium
- name: Reise fahren
run: pnpm --filter @easytree/web exec playwright test -c e2e/auth-journey/config.ts
- name: Testdaten entfernen und Leere ZWEIMAL belegen (auch nach Fehlschlag)
if: always()
shell: bash
run: |
set -Eeuo pipefail
# Zweimal, weil Idempotenz behauptet ist: der erste Lauf raeumt ab
# (oder findet dank globalTeardown nichts mehr), der zweite muss
# trotzdem restzeilen=0 melden. Der Marker wird ERZWUNGEN — fehlt er,
# ist der Schritt rot. Genau das fehlte vorher: das Skript druckte
# sein eigenes Versagen und der Job endete mit 0.
for lauf in 1 2; do
ausgabe="$(psql "postgresql://postgres:postgres@127.0.0.1:54322/postgres" \
-v ON_ERROR_STOP=1 \
-v reisender_a=auth-journey-a@easytree.test \
-v reisender_b=auth-journey-b@easytree.test \
-f apps/web/e2e/auth-journey/teardown.sql 2>&1)"
zeile="$(printf '%s\n' "$ausgabe" | grep -F '[auth-journey-teardown]' || true)"
if [ -z "$zeile" ]; then
echo "::error::Teardown-Lauf ${lauf}: Marker fehlt — Nachbedingung unbelegt."
exit 1
fi
echo "Lauf ${lauf}: ${zeile}"
printf '%s\n' "$zeile" | grep -q 'restzeilen=0' || {
echo "::error::Teardown-Lauf ${lauf} meldet Restzeilen."; exit 1; }
done
- name: Screenshots, Zusammenfassung und Traces sichern
if: always()
uses: actions/upload-artifact@v4
with:
name: auth-journey-nachweise
path: |
apps/web/test-results/auth-journey/
retention-days: 7
if-no-files-found: warn
- name: Supabase stoppen
if: always()
run: pnpm exec supabase stop --no-backup || true