Block internal-flavored paths / Block forbidden paths (push) Skipped
lint-env-coupling-dismissal / lint-env-coupling-dismissal (push) Skipped
verify-providers-gen / Regenerate providers artifact and fail on drift (push) Skipped
Block integration-tester contamination artifacts / Block staging-trigger / invalid manifest contamination (push) Skipped
boot-to-registration-e2e / Minimal cell (SSOT runtime + molecules-server) (push) Skipped
ci-required-drift / drift (push) Skipped
CI / Detect changes (push) Skipped
CI / Platform (Go) (push) Skipped
CI / Canvas (Next.js) (push) Skipped
CI / Shellcheck (E2E scripts) (push) Skipped
CI / Canvas Deploy Status (push) Skipped
CI / Python Lint & Test (push) Skipped
CI / all-required (push) Skipped
Concierge Creates Workspace Hermetic / Concierge Creates Workspace Hermetic (push) Skipped
contract-ssot-sync / Verify vendored plugin-manifest schema matches the molecule-ai-sdk SSOT (push) Skipped
cron-fixtures-ssot-drift / Verify vendored cron fixtures are byte-identical to the molecule-ai-sdk SSOT (push) Skipped
design-token-drift / Canvas ↔ app design-token SSOT drift (push) Skipped
E2E API Smoke Test / detect-changes (push) Skipped
E2E API Smoke Test / E2E API Smoke Test (push) Skipped
E2E Chat / E2E Chat (push) Skipped
E2E Ephemeral CP Happy Path / detect (push) Skipped
E2E Ephemeral CP Happy Path / E2E Ephemeral CP Happy Path (push) Skipped
E2E External Connect-Snippet + Leak-Lint / E2E External Connect-Snippet + Leak-Lint (push) Skipped
E2E Workspace Lifecycle (staginge2e) / E2E Workspace Lifecycle (compile+skip) (push) Skipped
Handlers Postgres Integration / detect-changes (push) Skipped
Handlers Postgres Integration / Handlers Postgres Integration (push) Skipped
Harness Replays / detect-changes (push) Skipped
Harness Replays / Harness Replays (push) Skipped
lint-bp-context-emit-match / lint-bp-context-emit-match (push) Skipped
lint-continue-on-error-tracking / lint-continue-on-error-tracking (push) Skipped
Lint curl status-code capture / Scan workflows for curl status-capture pollution (push) Skipped
Lint workflow YAML (repository compatibility policy) / Lint workflow YAML for repository compatibility policy (push) Skipped
Local Provision Lifecycle E2E / Local Provision Lifecycle E2E (stub) (push) Skipped
template-delivery-e2e / detect-changes (push) Skipped
template-delivery-e2e / Template-asset delivery (fresh seo-agent — config+prompts via asset channel, seo-all via plugin reconcile) (push) Skipped
Ops Scripts Tests / Ops scripts (unittest) (push) Skipped
Lint forbidden tenant-env keys / Scan workspace_secrets writers for forbidden env keys (push) Skipped
Lint forbidden tenant-env keys / Scan for repo-host token write into tenant workspace surface (push) Skipped
Lint forbidden hand-written mcp__ tool-id literals / Scan for hand-written mcp__ tool-id literals (push) Skipped
lint-no-coe-on-required / lint-no-coe-on-required (push) Skipped
lint-phantom-gate / lint-phantom-gate (push) Skipped
Lint publish-runner timeout-minutes / Lint publish-runner timeout-minutes (push) Skipped
lint-required-workflows-docker-host-pinned / Lint docker-host pin on docker-touching workflows (push) Skipped
Lint schedule budget / Zero-cron ratchet (push) Skipped
lint-setup-go-cache / lint-setup-go-cache (push) Skipped
lint-staging-tenant-cd-gate-chain / lint-staging-tenant-cd-gate-chain (push) Skipped
Local Provision Lifecycle E2E / Local Provision Lifecycle E2E (real image + MiniMax LLM, advisory) (push) Skipped
main-canary / canary (push) Skipped
mcp-plugin-delivery-contract-drift / Compare MCP plugin delivery contract against template and runtime canonicals (push) Skipped
mcp-verb-published-manifest / Published mcp-server manifest ⊇ contract required verbs (push) Skipped
merge-commit-guard / Verify merged PR commits are on main (push) Skipped
nodup-lint / no-duplication (push) Skipped
pin-provenance-guard / Pin provenance (push) Skipped
provision-request-ssot-sync / Verify core's provision-request mirror matches the molecule-ai-sdk SSOT (push) Skipped
publish-canvas-image / Build & push canvas image (push) Skipped
publish-desktop-image / Build & push desktop image (push) Skipped
publish-desktop-image / Promote desktop :latest to CI-green build (push) Skipped
publish-workspace-server-image / Build & push tenant image (push) Skipped
publish-workspace-server-image / Promote tenant :latest to CI-green build (push) Skipped
runtime-default-flip-gate / flip-gate (push) Skipped
sdk-pin-drift / SDK consumer pin vs molecule-ai-sdk main (push) Skipped
sdk-route-milestone-contract-drift / SDK route + milestone derive-gates (push) Skipped
SECRET_PATTERNS drift lint / Detect SECRET_PATTERNS drift (push) Skipped
Secret scan / Scan diff for credential-shaped strings (push) Skipped
Self-Host Concierge Schedules E2E / Self-Host Concierge Schedules E2E (stub) (push) Skipped
staging-tenant-cd / await-image (staging-<sha> published) (push) Skipped
staging-tenant-cd / advance-pin (staging tenant image DB pin) (push) Skipped
staging-tenant-cd / redeploy-fleet (roll staging tenants, session-preserving) (push) Skipped
staging-tenant-cd / runtime-image-readiness (exact promoted digests on provisioner daemon) (push) Skipped
staging-tenant-cd / e2e-smoke (staging real provision + A2A HARD GATE) (push) Skipped
staging-tenant-cd / rollback-pin (revert pin if the fleet roll failed) (push) Skipped
staging-tenant-cd / rollback-audit (prove rollback-pin itself actually executed) (push) Skipped
Sweep stale e2e-* orgs (staging) / Sweep e2e orgs (push) Skipped
sync-providers-yaml / Verify SDK registry dependency (push) Skipped
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 4s
Block integration-tester contamination artifacts / Block staging-trigger / invalid manifest contamination (pull_request) Successful in 4s
CI / Python Lint & Test (pull_request) Successful in 4s
sync-providers-yaml / Verify SDK registry dependency (pull_request) Skipped
CI / Detect changes (pull_request) Successful in 5s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 4s
Harness Replays / detect-changes (pull_request) Successful in 5s
E2E Ephemeral CP Happy Path / detect (pull_request) Successful in 7s
boot-to-registration-e2e / Minimal cell (SSOT runtime + molecules-server) (pull_request) Skipped
Check migration collisions / Migration version collision check (pull_request) Skipped
contract-ssot-sync / Verify vendored plugin-manifest schema matches the molecule-ai-sdk SSOT (pull_request) Skipped
cron-fixtures-ssot-drift / Verify vendored cron fixtures are byte-identical to the molecule-ai-sdk SSOT (pull_request) Skipped
design-token-drift / Canvas ↔ app design-token SSOT drift (pull_request) Skipped
E2E External Connect-Snippet + Leak-Lint / E2E External Connect-Snippet + Leak-Lint (pull_request) Skipped
E2E Workspace Lifecycle (staginge2e) / E2E Workspace Lifecycle (compile+skip) (pull_request) Skipped
lint-mask-pr-atomicity / lint-mask-pr-atomicity (pull_request) Skipped
manifest-entry-existence-check / Verify manifest entries exist on Gitea (pull_request) Skipped
Lint curl status-code capture / Scan workflows for curl status-capture pollution (pull_request) Successful in 5s
mcp-plugin-delivery-contract-drift / Compare MCP plugin delivery contract against template and runtime canonicals (pull_request) Skipped
mcp-verb-published-manifest / Published mcp-server manifest ⊇ contract required verbs (pull_request) Skipped
nodup-lint / no-duplication (pull_request) Skipped
provision-request-ssot-sync / Verify core's provision-request mirror matches the molecule-ai-sdk SSOT (pull_request) Skipped
SECRET_PATTERNS drift lint / Detect SECRET_PATTERNS drift (pull_request) Skipped
Self-Host Concierge Schedules E2E / Self-Host Concierge Schedules E2E (stub) (pull_request) Skipped
verify-providers-gen / Regenerate providers artifact and fail on drift (pull_request) Skipped
Lint forbidden tenant-env keys / Scan workspace_secrets writers for forbidden env keys (pull_request) Successful in 5s
Lint forbidden tenant-env keys / Scan for repo-host token write into tenant workspace surface (pull_request) Successful in 4s
Lint forbidden hand-written mcp__ tool-id literals / Scan for hand-written mcp__ tool-id literals (pull_request) Successful in 4s
ci-required-drift / drift (pull_request) Successful in 21s
Concierge Creates Workspace Hermetic / Concierge Creates Workspace Hermetic (pull_request) Successful in 27s
lint-continue-on-error-tracking / lint-continue-on-error-tracking (pull_request) Successful in 25s
lint-phantom-gate / lint-phantom-gate (pull_request) Successful in 17s
Lint publish-runner timeout-minutes / Lint publish-runner timeout-minutes (pull_request) Successful in 17s
lint-bp-context-emit-match / lint-bp-context-emit-match (pull_request) Successful in 27s
lint-no-coe-on-required / lint-no-coe-on-required (pull_request) Successful in 24s
lint-env-coupling-dismissal / lint-env-coupling-dismissal (pull_request) Successful in 28s
lint-required-workflows-docker-host-pinned / Lint docker-host pin on docker-touching workflows (pull_request) Successful in 6s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 5s
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 19s
lint-required-context-exists-in-bp / lint-required-context-exists-in-bp (pull_request) Successful in 26s
Lint schedule budget / Zero-cron ratchet (pull_request) Successful in 16s
lint-staging-tenant-cd-gate-chain / lint-staging-tenant-cd-gate-chain (pull_request) Successful in 13s
lint-setup-go-cache / lint-setup-go-cache (pull_request) Successful in 16s
Lint workflow YAML (repository compatibility policy) / Lint workflow YAML for repository compatibility policy (pull_request) Successful in 20s
sdk-pin-drift / SDK consumer pin vs molecule-ai-sdk main (pull_request) Successful in 16s
Secret scan / Scan diff for credential-shaped strings (pull_request) No credential-shaped strings (deterministic re-assert)
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 6s
pin-provenance-guard / Pin provenance (pull_request) Successful in 29s
template-delivery-e2e / detect-changes (pull_request) Successful in 8s
sdk-route-milestone-contract-drift / SDK route + milestone derive-gates (pull_request) Successful in 17s
template-delivery-e2e / Template-asset delivery (fresh seo-agent — config+prompts via asset channel, seo-all via plugin reconcile) (pull_request) Successful in 3s
lint-context-set-shrink / lint-context-set-shrink (pull_request) Successful in 1m24s
Local Provision Lifecycle E2E / Local Provision Lifecycle E2E (stub) (pull_request) Successful in 54s
Local Provision Lifecycle E2E / Local Provision Lifecycle E2E (real image + MiniMax LLM, advisory) (pull_request) Successful in 47s
gitea-merge-queue / queue (pull_request_review) Successful in 15s
Ops Scripts Tests / Ops scripts (unittest) (pull_request) Successful in 2m1s
Harness Replays / Harness Replays (pull_request) Successful in 2m40s
E2E Chat / E2E Chat (pull_request) Successful in 3m32s
Lint pre-flip continue-on-error / Verify continue-on-error flips have run-log proof (pull_request) Successful in 12s
CI / Canvas (Next.js) (pull_request) Successful in 5m40s
CI / Canvas Deploy Status (pull_request) Successful in 1s
CI / Platform (Go) (pull_request) Successful in 5m53s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 6m44s
CI / all-required (pull_request) Successful in 3s
E2E API Smoke Test / detect-changes (pull_request) Successful in 7s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Successful in 2s
E2E Ephemeral CP Happy Path / E2E Ephemeral CP Happy Path (pull_request) Successful in 23m32s
audit-force-merge / audit (pull_request_target) Successful in 6s
668 lines
36 KiB
YAML
668 lines
36 KiB
YAML
name: E2E Chat
|
||
|
||
# Comprehensive Playwright E2E for the unified chat stack (desktop
|
||
# ChatTab + mobile MobileChat). Every pull request, main/staging push, and
|
||
# manual dispatch runs the browser suite; there is no label gate, paths filter,
|
||
# or success-without-tests arm.
|
||
#
|
||
# Architecture:
|
||
# 1. Ephemeral Postgres + Redis (docker, unique container names)
|
||
# 2. workspace-server built from source, started with
|
||
# MOLECULE_ENV=development with fail-closed ADMIN_TOKEN auth
|
||
# 3. canvas served from a PRODUCTION build (`next build` + the
|
||
# `output: "standalone"` node server — the same artifact
|
||
# canvas/Dockerfile ships), NOT `next dev --turbopack`. See
|
||
# "Why a production build" below.
|
||
# 4. Playwright tests create workspaces via API, point them at an
|
||
# in-process echo runtime, and exercise the full send/receive
|
||
# round-trip through the browser.
|
||
#
|
||
# Why a production build (2026-08-05, core#5079 blocker)
|
||
# ------------------------------------------------------
|
||
# This lane used `npx next dev --turbopack`. It failed non-deterministically
|
||
# in ONE mode: React Flow rendered UNSTYLED, so `.react-flow__background`
|
||
# kept its browser-default `pointer-events: auto` and swallowed every click
|
||
# on the canvas; specs died with `<rect …> from <svg data-testid=
|
||
# "rf__background"> subtree intercepts pointer events`. Diagnostics captured
|
||
# on a stuck page (run 623425, job 921290):
|
||
#
|
||
# react-flow background pointer-events = auto
|
||
# stylesheets = 1 (1 readable), .react-flow__background rule present = false
|
||
# viewport transform = translate(314px, -204px) scale(2)
|
||
#
|
||
# i.e. a Turbopack CSS chunk never arrived and the page NEVER recovered.
|
||
# Document requests were returning in 40–77 ms, so it was not slow
|
||
# compilation. Observed 2 failures in 4 runs on core#5079
|
||
# (623210 ✗, 623299 ✓, 623425 ✗, 622625 ✓).
|
||
#
|
||
# `next build` emits immutable, content-hashed CSS files that are NAMED by
|
||
# the initial document. There is no on-demand chunk compilation left, which
|
||
# removes the COMPILATION mechanism behind the flake.
|
||
#
|
||
# This paragraph used to add that the React Flow sheet arrives "as `<link
|
||
# rel="preload" as="style">` plus an RSC-payload stylesheet entry React
|
||
# inserts at hydration". That was true when written and is FALSE NOW: #5105
|
||
# moved the import into the layout stylesheet precisely so the sheet is a
|
||
# plain, render-blocking `<link rel="stylesheet">` in the initial document,
|
||
# and the preflight below ASSERTS that shape and fails the job if it ever
|
||
# regresses. Left corrected rather than deleted because a stale claim about
|
||
# how an asset is delivered is not inert — it is what a future investigator
|
||
# reasons from, and core#5106 has now been misdirected twice by exactly that
|
||
# (see the note on the diagnostic in canvas/e2e/helpers/canvas.ts).
|
||
#
|
||
# It does NOT remove every mechanism, and this file will not pretend
|
||
# otherwise. A stylesheet request that is merely SLOW makes React suspend the
|
||
# subtree (`pointer-events: none`, no unstyled window); a request that FAILS
|
||
# makes React render anyway, reproducing the same unstyled window. Measured
|
||
# over this lane’s own runs: ~1 occurrence in 29 production-build runs
|
||
# against 3 in 30 dev runs. Substantially more reliable, not immune.
|
||
#
|
||
# What this costs / what it changes:
|
||
# - `next build` is added to the job. MEASURED on this runner: 23-29s
|
||
# across 9 runs; a whole green job is ~2.0 min wall (it was ~2.0 min
|
||
# on the dev lane, which paid a comparable cold Turbopack compile).
|
||
# timeout-minutes stays 15 and was NOT raised: the worst case seen
|
||
# here is a totally broken canvas (every click times out at 30s),
|
||
# which finished RED in 11.7 min.
|
||
# - NEXT_PUBLIC_* are INLINED AT BUILD TIME by Next.js, so the build MUST
|
||
# run AFTER the platform port and admin token are chosen. The build step
|
||
# asserts the inline actually happened — a bundle built with the wrong
|
||
# port starts fine and fails subtly.
|
||
# - NO build cache is added. The bundle embeds per-run values (platform
|
||
# port, per-run admin token), so a restored bundle would be a WRONG
|
||
# bundle that still boots. `actions/setup-node`'s npm cache (lockfile
|
||
# keyed) is the only cache here and is unchanged.
|
||
# - The suite now runs against the PRODUCTION CSP from src/middleware.ts
|
||
# (`NODE_ENV=production` → nonce + strict-dynamic + a connect-src
|
||
# allowlist) instead of the permissive dev CSP (`connect-src *`). That
|
||
# is the policy real users get; a CSP regression is now caught here.
|
||
# - `next build` type-checks and lints (next.config.ts leaves
|
||
# eslint.ignoreDuringBuilds / typescript.ignoreBuildErrors at false, as
|
||
# production wants), so a canvas type/lint error now reds this lane too.
|
||
# That mirrors what canvas/Dockerfile and publish-canvas-image.yml
|
||
# already do; nothing was loosened to accommodate it.
|
||
#
|
||
# Nothing in the suite depended on dev-only behaviour: no spec references
|
||
# fast refresh, the Next dev error overlay, source maps, or NODE_ENV. The
|
||
# `MOLECULE_ENV=development` the specs DO assert on belongs to the Go
|
||
# platform-server and is untouched by this change.
|
||
#
|
||
# Parallel-safety: per-run containers and ephemeral loopback ports. Chromium
|
||
# has its own bridge network namespace: host Docker churn cannot invalidate
|
||
# browser sockets (core#5106). Playwright forwards client loopback destinations.
|
||
|
||
on:
|
||
# ALWAYS RUNS — 2026-07-14. No paths filter, no detect-changes job, no no-op arm.
|
||
#
|
||
# This lane carried the SAME bug as e2e-staging-canvas (core#4323). Its
|
||
# detect-changes hand-rolled a paths filter in shell and then, on pull_request
|
||
# only, demanded the PR carry a `merge-queue` LABEL:
|
||
#
|
||
# if printf '%s\n' "$labels" | grep -qx "$QUEUE_LABEL"; then chat=true
|
||
# else "PR is not in merge-queue; skipping heavy E2E Chat" chat=false
|
||
#
|
||
# But the merge queue ABANDONED that label. gitea-merge-queue.yml sets
|
||
# AUTO_DISCOVER=1 and calls it "OPTIONAL metadata" — agent Gitea tokens lack
|
||
# write:issue and could never self-label. Nothing applies it. So on any PR the
|
||
# only reachable arm was the "No-op pass" step, which echoed and exit 0 —
|
||
# posting SUCCESS for the `E2E Chat` context without running a single test.
|
||
# Under main's `['*']` branch protection every posted context is required, so
|
||
# this was a merge gate whose pass arm was "I did not run".
|
||
#
|
||
# The lane WORKS — it is green whenever it actually executes (139s / 141s / 129s
|
||
# on recent main pushes, all success). It simply never executed on the one event
|
||
# where it could have blocked a bad change. Now it does.
|
||
push:
|
||
branches: [main, staging]
|
||
pull_request:
|
||
branches: [main, staging]
|
||
workflow_dispatch:
|
||
|
||
concurrency:
|
||
group: e2e-chat-${{ github.event.pull_request.head.sha || github.sha }}
|
||
cancel-in-progress: false
|
||
|
||
env:
|
||
GITHUB_SERVER_URL: https://git.moleculesai.app
|
||
|
||
jobs:
|
||
# bp-exempt: helper job; real gate is E2E Chat / E2E Chat (pull_request)
|
||
# bp-required: pending #1142 — new E2E check; add to branch protection after 3 green runs.
|
||
e2e-chat:
|
||
name: E2E Chat
|
||
# mc#1529 follow-on: docker run/exec for postgres + redis containers.
|
||
# Must land on the self-hosted Linux docker-host lane.
|
||
runs-on: docker-host
|
||
# mc#2654 mask REMOVED — this lane is now FAIL-CLOSED (gating). It runs the
|
||
# LOCAL platform-server (MOLECULE_ORG_ID unset → the local Docker /
|
||
# molecules-server provisioner path, main.go:230) + canvas + the in-process
|
||
# echo runtime entirely on-box: NO EC2, NO remote CP, NO external cloud
|
||
# dependency, so there is no infra-flake surface to justify an advisory mask.
|
||
#
|
||
# Empirical basis for the flip (run-log-grep-before-flip, per
|
||
# lint-pre-flip-continue-on-error): the last real main runs pass green with
|
||
# NO masked failures — run 430897 "26 passed", run 430812 "26 passed",
|
||
# run 430653 no-op pass. The prior continue-on-error:true was hiding nothing;
|
||
# keeping it only let a future regression roll up to a false "success"
|
||
# (Gitea Quirk #10). Removing it makes a real chat regression go RED.
|
||
#
|
||
# Assertions stay honest and fail-closed (unchanged by this flip):
|
||
# - Postgres/Redis/platform/canvas readiness are already bounded
|
||
# readiness-polls that hard-fail (and dump logs) at their deadline,
|
||
# not fixed sleeps — preserved.
|
||
# - passWithNoTests:false + forbidOnly (playwright.config.ts) → a
|
||
# renamed/moved spec or stray test.only can no longer green the lane.
|
||
# - REQUIRE-LIVE guard in "Run Playwright E2E tests" requires every
|
||
# invocation to execute >=1 test, else exit 1.
|
||
# - chat-desktop.spec.ts asserts echo-runtime.lastRequest after each
|
||
# round-trip, so an optimistic client-side render cannot pass without
|
||
# a real A2A request reaching the server (core#2796 follow-up).
|
||
# - There is no paths-filter/no-op arm; every workflow invocation runs
|
||
# the browser suite.
|
||
#
|
||
# Wiring this context into branch_protections/main (bp-required #1142) stays
|
||
# an operator step; removing the mask here only stops the masking.
|
||
continue-on-error: false
|
||
timeout-minutes: 15
|
||
env:
|
||
PG_CONTAINER: pg-e2e-chat-${{ github.run_id }}-${{ github.run_attempt }}
|
||
REDIS_CONTAINER: redis-e2e-chat-${{ github.run_id }}-${{ github.run_attempt }}
|
||
PLAYWRIGHT_CONTAINER: browser-e2e-chat-${{ github.run_id }}-${{ github.run_attempt }}
|
||
steps:
|
||
-
|
||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||
|
||
-
|
||
uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
|
||
with:
|
||
go-version: 'stable'
|
||
# cache:false — the self-hosted runner bind-mounts a persistent
|
||
# GOCACHE/GOMODCACHE (/var/cache/ci-go-{build,mod}); actions/cache is
|
||
# redundant and corrupts it by untarring over the bind mount ("File
|
||
# exists" -> "Failed to restore" -> partial cache -> linker/typecheck
|
||
# errors on heavy jobs, e.g. test -race link "too many errors" and
|
||
# go-arch-lint "without types"). Fleet sweep after the cp ci.yml find.
|
||
cache: false
|
||
cache-dependency-path: workspace-server/go.sum
|
||
|
||
-
|
||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||
with:
|
||
node-version: '22'
|
||
cache: 'npm'
|
||
cache-dependency-path: canvas/package-lock.json
|
||
|
||
- name: Sweep stale e2e-chat testcontainers (self-heal prior leaks)
|
||
run: |
|
||
# Prior e2e-chat runs that were cancelled/killed — or whose always()
|
||
# cleanup hit a wedged docker daemon — leak their pg-/redis-e2e-chat-*
|
||
# containers, which then pile up on the shared runner host (observed: 13
|
||
# such containers, up to 2 weeks old, on the operator daemon). Reap any
|
||
# e2e-chat container older than the job window so leaks self-heal every
|
||
# run instead of relying on each run's own cleanup succeeding. Age-based
|
||
# (>2h, well beyond the 15m job) so a CONCURRENT e2e-chat job's fresh
|
||
# containers are never touched. See controlplane#646.
|
||
now=$(date -u +%s)
|
||
docker ps -a --filter name=e2e-chat --format '{{.Names}}' | while read -r c; do
|
||
[ -n "$c" ] || continue
|
||
created=$(docker inspect -f '{{.Created}}' "$c" 2>/dev/null) || continue
|
||
cts=$(date -u -d "$created" +%s 2>/dev/null) || continue
|
||
if [ $(( now - cts )) -gt 7200 ]; then
|
||
echo "sweeping stale e2e-chat container $c (created $created)"
|
||
timeout 30 docker rm -fv "$c" >/dev/null 2>&1 || true
|
||
fi
|
||
done
|
||
|
||
- name: Start Postgres (docker)
|
||
run: |
|
||
# tmpfs PGDATA + run label + `rm -fv` pre-clean: see the script header.
|
||
bash .gitea/scripts/ci-service-container.sh postgres "$PG_CONTAINER" postgres:16 \
|
||
-e POSTGRES_USER=dev -e POSTGRES_PASSWORD=dev -e POSTGRES_DB=molecule \
|
||
-p 127.0.0.1::5432 >/dev/null
|
||
PG_PORT=$(docker port "$PG_CONTAINER" 5432/tcp | awk -F: '/^(0\.0\.0\.0|127\.0\.0\.1):/ {print $2; exit}')
|
||
if [ -z "$PG_PORT" ]; then
|
||
PG_PORT=$(docker port "$PG_CONTAINER" 5432/tcp | head -1 | awk -F: '{print $NF}')
|
||
fi
|
||
if [ -z "$PG_PORT" ]; then
|
||
echo "::error::Could not resolve host port for $PG_CONTAINER"
|
||
exit 1
|
||
fi
|
||
echo "PG_PORT=${PG_PORT}" >> "$GITHUB_ENV"
|
||
echo "DATABASE_URL=postgres://dev:dev@127.0.0.1:${PG_PORT}/molecule?sslmode=disable" >> "$GITHUB_ENV"
|
||
echo "E2E_DATABASE_URL=postgres://dev:dev@127.0.0.1:${PG_PORT}/molecule?sslmode=disable" >> "$GITHUB_ENV"
|
||
for i in $(seq 1 30); do
|
||
if docker exec "$PG_CONTAINER" pg_isready -U dev >/dev/null 2>&1; then
|
||
echo "Postgres ready after ${i}s"
|
||
exit 0
|
||
fi
|
||
sleep 1
|
||
done
|
||
echo "::error::Postgres did not become ready in 30s"
|
||
exit 1
|
||
|
||
- name: Start Redis (docker)
|
||
run: |
|
||
bash .gitea/scripts/ci-service-container.sh redis "$REDIS_CONTAINER" redis:7 -p 127.0.0.1::6379 >/dev/null
|
||
REDIS_PORT=$(docker port "$REDIS_CONTAINER" 6379/tcp | awk -F: '/^(0\.0\.0\.0|127\.0\.0\.1):/ {print $2; exit}')
|
||
if [ -z "$REDIS_PORT" ]; then
|
||
REDIS_PORT=$(docker port "$REDIS_CONTAINER" 6379/tcp | head -1 | awk -F: '{print $NF}')
|
||
fi
|
||
if [ -z "$REDIS_PORT" ]; then
|
||
echo "::error::Could not resolve host port for $REDIS_CONTAINER"
|
||
exit 1
|
||
fi
|
||
echo "REDIS_PORT=${REDIS_PORT}" >> "$GITHUB_ENV"
|
||
echo "REDIS_URL=redis://127.0.0.1:${REDIS_PORT}" >> "$GITHUB_ENV"
|
||
for i in $(seq 1 15); do
|
||
if docker exec "$REDIS_CONTAINER" redis-cli ping 2>/dev/null | grep -q PONG; then
|
||
echo "Redis ready after ${i}s"
|
||
exit 0
|
||
fi
|
||
sleep 1
|
||
done
|
||
echo "::error::Redis did not become ready in 15s"
|
||
exit 1
|
||
|
||
- name: Build platform
|
||
working-directory: workspace-server
|
||
run: go build -o platform-server ./cmd/server
|
||
|
||
- name: Pick platform port
|
||
run: |
|
||
PLATFORM_PORT=$(python3 - <<'PY'
|
||
import socket
|
||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||
s.bind(("127.0.0.1", 0))
|
||
print(s.getsockname()[1])
|
||
PY
|
||
)
|
||
echo "PLATFORM_PORT=${PLATFORM_PORT}" >> "$GITHUB_ENV"
|
||
echo "E2E_PLATFORM_URL=http://127.0.0.1:${PLATFORM_PORT}" >> "$GITHUB_ENV"
|
||
echo "Platform host port: ${PLATFORM_PORT}"
|
||
|
||
- name: Pick canvas port
|
||
run: |
|
||
CANVAS_PORT=$(python3 - <<'PY'
|
||
import socket
|
||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||
s.bind(("127.0.0.1", 0))
|
||
print(s.getsockname()[1])
|
||
PY
|
||
)
|
||
echo "CANVAS_PORT=${CANVAS_PORT}" >> "$GITHUB_ENV"
|
||
echo "Canvas host port: ${CANVAS_PORT}"
|
||
|
||
- name: Set deterministic admin token
|
||
run: |
|
||
# PR #2291 made auth fail-closed everywhere (no dev-mode escape).
|
||
# This ephemeral test platform requires ADMIN_TOKEN; its test Canvas
|
||
# uses the matching NEXT_PUBLIC_ADMIN_TOKEN or every API call 401s.
|
||
# Production SaaS leaves the public value empty and uses sessions.
|
||
# We set a deterministic per-run value so the ephemeral platform
|
||
# and canvas are paired correctly.
|
||
E2E_ADMIN_TOKEN="e2e-chat-admin-${{ github.run_id }}-${{ github.run_attempt }}"
|
||
echo "ADMIN_TOKEN=${E2E_ADMIN_TOKEN}" >> "$GITHUB_ENV"
|
||
echo "MOLECULE_ADMIN_TOKEN=${E2E_ADMIN_TOKEN}" >> "$GITHUB_ENV"
|
||
echo "NEXT_PUBLIC_ADMIN_TOKEN=${E2E_ADMIN_TOKEN}" >> "$GITHUB_ENV"
|
||
echo "Admin token configured for e2e-chat platform + canvas."
|
||
|
||
- name: Start platform (background)
|
||
working-directory: workspace-server
|
||
run: |
|
||
export MOLECULE_ENV=development
|
||
export DATABASE_URL="${DATABASE_URL}"
|
||
export REDIS_URL="${REDIS_URL}"
|
||
export PORT="${PLATFORM_PORT}"
|
||
export CORS_ORIGINS="http://localhost:3000,http://localhost:3001,http://localhost:${CANVAS_PORT},http://127.0.0.1:${CANVAS_PORT}"
|
||
./platform-server > platform.log 2>&1 &
|
||
echo $! > platform.pid
|
||
|
||
- name: Wait for /health
|
||
run: |
|
||
# Readiness signal: the platform binds /health only AFTER the full
|
||
# migration chain has been applied on cold start (it prints
|
||
# "Platform starting on :PORT" at that point). So a 200 from /health
|
||
# is the real "migrations done + server listening" signal.
|
||
#
|
||
# The migration chain grows every release, so a fixed ~30s budget is
|
||
# brittle by construction. Use a generous wall-clock budget that
|
||
# comfortably exceeds cold-start + full-migration time, polling fast.
|
||
# Robust to a growing chain WITHOUT masking a dead platform: if the
|
||
# background platform-server process has exited, fail loudly at once.
|
||
DEADLINE_SECS=180 # cold-start + full migration chain headroom
|
||
PLATFORM_PID="$(cat workspace-server/platform.pid 2>/dev/null || true)"
|
||
start=$(date +%s)
|
||
while :; do
|
||
if curl -sf "http://127.0.0.1:${PLATFORM_PORT}/health" > /dev/null; then
|
||
echo "Platform healthy after $(( $(date +%s) - start ))s"
|
||
exit 0
|
||
fi
|
||
if [ -n "$PLATFORM_PID" ] && ! kill -0 "$PLATFORM_PID" 2>/dev/null; then
|
||
echo "::error::platform-server (pid ${PLATFORM_PID}) exited before /health became reachable — see log below"
|
||
cat workspace-server/platform.log || true
|
||
exit 1
|
||
fi
|
||
if [ "$(( $(date +%s) - start ))" -ge "$DEADLINE_SECS" ]; then
|
||
echo "::error::Platform did not become healthy within ${DEADLINE_SECS}s — see log below"
|
||
cat workspace-server/platform.log || true
|
||
exit 1
|
||
fi
|
||
sleep 1
|
||
done
|
||
|
||
- name: Install canvas dependencies
|
||
working-directory: canvas
|
||
run: npm ci
|
||
|
||
- name: Start isolated Playwright browser
|
||
run: |
|
||
set -euo pipefail
|
||
endpoint="$(bash .gitea/scripts/start-playwright-server.sh "$PLAYWRIGHT_CONTAINER")"
|
||
echo "PLAYWRIGHT_WS_ENDPOINT=$endpoint" >> "$GITHUB_ENV"
|
||
|
||
- name: Build canvas (production bundle)
|
||
working-directory: canvas
|
||
env:
|
||
NEXT_TELEMETRY_DISABLED: "1"
|
||
run: |
|
||
set -euo pipefail
|
||
# NEXT_PUBLIC_* are INLINED INTO THE CLIENT BUNDLE BY `next build`.
|
||
# They are NOT read at runtime, so this step must run AFTER "Pick
|
||
# platform port" and "Set deterministic admin token" — building
|
||
# first and exporting later yields a bundle that boots happily and
|
||
# talks to the wrong port / presents the wrong bearer token.
|
||
export NEXT_PUBLIC_PLATFORM_URL="http://127.0.0.1:${PLATFORM_PORT}"
|
||
export NEXT_PUBLIC_WS_URL="ws://127.0.0.1:${PLATFORM_PORT}/ws"
|
||
start=$(date +%s)
|
||
npx next build
|
||
echo "next build completed in $(( $(date +%s) - start ))s"
|
||
|
||
# --- Prove the build is the build we asked for -------------------
|
||
# A wrong-but-bootable bundle is the failure mode a production
|
||
# build introduces, so assert the inlining instead of trusting it.
|
||
#
|
||
# Each assertion first proves its own INPUT is non-empty. `set -u`
|
||
# only catches an UNSET variable; an EMPTY one sails straight
|
||
# through and turns `grep -qF "$X"` into a match-everything no-op
|
||
# that reports success. A guard that passes on empty input is
|
||
# exactly the vacuous-pass class this lane exists to keep out of
|
||
# the tree, so it must not live in the lane’s own guards. Both
|
||
# cases have an empty-input proof recorded on the PR.
|
||
if [ -z "${PLATFORM_PORT:-}" ]; then
|
||
echo "::error::PLATFORM_PORT is empty — 'Pick platform port' did not run, and the inlining check below would collapse to a search for the bare '127.0.0.1:' prefix. Refusing to assert vacuously."
|
||
exit 1
|
||
fi
|
||
if [ -z "${NEXT_PUBLIC_ADMIN_TOKEN:-}" ]; then
|
||
echo "::error::NEXT_PUBLIC_ADMIN_TOKEN is empty — 'Set deterministic admin token' did not run, and grepping for the empty string matches EVERY file, so the check below would pass having asserted nothing."
|
||
exit 1
|
||
fi
|
||
if ! grep -rqF "127.0.0.1:${PLATFORM_PORT}" .next/static/chunks; then
|
||
echo "::error::NEXT_PUBLIC_PLATFORM_URL (127.0.0.1:${PLATFORM_PORT}) was not inlined into the client bundle — the canvas would call the wrong platform. Check step ordering: the build must run after 'Pick platform port'."
|
||
exit 1
|
||
fi
|
||
# -q so the token value is never echoed into the log.
|
||
if ! grep -rqF "${NEXT_PUBLIC_ADMIN_TOKEN}" .next/static/chunks; then
|
||
echo "::error::NEXT_PUBLIC_ADMIN_TOKEN was not inlined into the client bundle — every workspace API call would 401. Check step ordering: the build must run after 'Set deterministic admin token'."
|
||
exit 1
|
||
fi
|
||
# The regression this lane switched build modes to kill: React Flow
|
||
# rendering unstyled because its stylesheet never reached the page.
|
||
# In a production build the rule lives in a content-hashed CSS file
|
||
# on disk, so its ABSENCE is a build defect and must be loud here
|
||
# rather than showing up as a 15s "node never became hittable".
|
||
RF_CSS=$(grep -rlF 'react-flow__background' .next/static/css | head -1 || true)
|
||
if [ -z "$RF_CSS" ]; then
|
||
echo "::error::No built stylesheet contains .react-flow__background — @xyflow/react/dist/style.css did not make it into the production CSS output; every canvas click would be swallowed by the unstyled background."
|
||
exit 1
|
||
fi
|
||
echo "RF_CSS_PATH=/_next/static/css/$(basename "$RF_CSS")" >> "$GITHUB_ENV"
|
||
echo "React Flow stylesheet: $(basename "$RF_CSS")"
|
||
|
||
# Assemble the standalone runtime exactly as canvas/Dockerfile does
|
||
# (`COPY .next/standalone`, `.next/static`, `public`). `next start`
|
||
# is NOT usable here — Next 15 prints "next start does not work with
|
||
# output: standalone" — so we run the same `node server.js` entry
|
||
# point production runs.
|
||
cp -r .next/static .next/standalone/.next/static
|
||
cp -r public .next/standalone/public
|
||
|
||
- name: Start canvas (production server, background)
|
||
working-directory: canvas
|
||
run: |
|
||
set -euo pipefail
|
||
# Single node process (not an `npx` wrapper), so the pid we record
|
||
# is the pid the cleanup step actually kills.
|
||
#
|
||
# HOSTNAME MUST be set explicitly. Next's standalone server.js does
|
||
# `process.env.HOSTNAME || '0.0.0.0'`, and on this runner HOSTNAME
|
||
# is the container's own name — the server then bound
|
||
# `http://robot-2:35783` and every `localhost` probe got HTTP 000
|
||
# (observed: run 623567). Pin the loopback address the rest of this
|
||
# lane already uses (Postgres/Redis/platform all bind 127.0.0.1);
|
||
# `localhost` resolves to it on this runner, which is exactly how
|
||
# the dev server was being reached before.
|
||
#
|
||
# --require the core#5106 server probe. On the one captured
|
||
# recurrence (run 632484) this log contained four lines — the banner
|
||
# and "Ready in 85ms" — for the whole run, so it could establish that
|
||
# the server had not crashed and NOTHING else. It could not say
|
||
# whether the request for the stylesheet that died ever reached this
|
||
# process, nor whether this process was able to run at the time. The
|
||
# probe answers both (see canvas/e2e/support/server-probe.cjs) and is
|
||
# silent on the happy path. Absolute path: the server runs with
|
||
# cwd=.next/standalone, and a relative --require resolves against cwd.
|
||
PROBE="${PWD}/e2e/support/server-probe.cjs"
|
||
if [ ! -f "$PROBE" ]; then
|
||
echo "::error::server probe not found at ${PROBE} — refusing to start the canvas server without the instrument this lane's failures are diagnosed from."
|
||
exit 1
|
||
fi
|
||
cd .next/standalone
|
||
PORT="${CANVAS_PORT}" HOSTNAME=127.0.0.1 NODE_OPTIONS="--require ${PROBE}" \
|
||
node server.js > ../../canvas.log 2>&1 &
|
||
echo $! > ../../canvas.pid
|
||
cd ../..
|
||
# Readiness is the real signal, not a sleep: poll the exact route
|
||
# the specs navigate to (`/?m=chat`) until it answers 2xx. A
|
||
# production server has nothing to compile, so this settles in
|
||
# ~1s; the budget stays 120s only as an upper bound (unchanged
|
||
# from the dev lane — it is a deadline, not a wait).
|
||
READY=""
|
||
for i in $(seq 1 60); do
|
||
# Tempfile-routed -w + set +e/-e prevents curl-exit-code
|
||
# pollution of the captured status (lint-curl-status-capture.yml).
|
||
set +e
|
||
curl -s -o /dev/null -w '%{http_code}' "http://localhost:${CANVAS_PORT}/?m=chat" > /tmp/canvas-ready.code
|
||
set -e
|
||
CODE=$(cat /tmp/canvas-ready.code 2>/dev/null || echo "000")
|
||
if [ "$CODE" -ge 200 ] && [ "$CODE" -lt 400 ]; then
|
||
echo "Canvas (production server) up after ~$((i*2))s (HTTP ${CODE})"
|
||
READY=1
|
||
break
|
||
fi
|
||
sleep 2
|
||
done
|
||
if [ -z "$READY" ]; then
|
||
echo "::error::Canvas production server did not answer /?m=chat in 120s (last HTTP ${CODE})"
|
||
cat canvas.log || true
|
||
exit 1
|
||
fi
|
||
|
||
# The probe is only useful if it actually loaded, and a --require that
|
||
# silently no-ops (wrong path, NODE_OPTIONS stripped, a syntax error
|
||
# in the file) leaves a log that looks exactly like a healthy quiet
|
||
# run. That is the same shape as the failure being investigated, so
|
||
# assert the instrument is live rather than assuming it.
|
||
if ! grep -q '\[probe .*\] attached' canvas.log; then
|
||
echo "::error::the core#5106 server probe did not attach — canvas.log has no '[probe] attached' line, so a silent canvas.log would prove nothing. NODE_OPTIONS/--require did not take effect."
|
||
cat canvas.log || true
|
||
exit 1
|
||
fi
|
||
echo "Server probe attached (event-loop + static-asset instrumentation live)."
|
||
|
||
# --- React Flow stylesheet: RENDER-BLOCKING, and SERVED ----------
|
||
#
|
||
# This used to assert only that the document NAMED the asset. That
|
||
# was satisfied by a `<link rel="preload" as="style">`, and a
|
||
# preload is not a stylesheet: Next puts PAGE css behind a preload
|
||
# plus an RSC-payload entry that REACT inserts during hydration, so
|
||
# the rule was reliably absent for the first few hundred ms — and
|
||
# for ~5s under CPU contention, measured at 20x throttling on this
|
||
# exact build. That window is what produced "workspace node never
|
||
# became hittable" (runs 628513, 630140) and
|
||
# "<rect> from <svg data-testid=rf__background> subtree intercepts
|
||
# pointer events" (run 629224): a guard that passes while the page
|
||
# can still render unstyled is not a guard.
|
||
#
|
||
# So assert the property that actually removes the window: the
|
||
# initial document must carry a PLAIN `<link rel="stylesheet">`
|
||
# for the sheet that holds the rule. That only happens when the
|
||
# import lives in a LAYOUT stylesheet (canvas/src/app/globals.css),
|
||
# and it makes the browser block rendering on it — there is then no
|
||
# state in which the canvas exists and the rule does not.
|
||
#
|
||
# Non-empty first: grepping for the empty string matches every
|
||
# file, so an empty RF_CSS_PATH would make the document check a
|
||
# no-op that reports success.
|
||
if [ -z "${RF_CSS_PATH:-}" ]; then
|
||
echo "::error::RF_CSS_PATH is empty — the build step did not export it. The document check below would search for the empty string, which matches every file. Refusing to assert vacuously."
|
||
exit 1
|
||
fi
|
||
set +e
|
||
curl -s -o /tmp/canvas-doc.html -w '%{http_code}' "http://localhost:${CANVAS_PORT}/?m=chat" > /tmp/canvas-doc.code
|
||
curl -s -o /tmp/canvas-rf.css -w '%{http_code}' "http://localhost:${CANVAS_PORT}${RF_CSS_PATH}" > /tmp/canvas-rf.code
|
||
set -e
|
||
RF_CODE=$(cat /tmp/canvas-rf.code 2>/dev/null || echo "000")
|
||
# Split the document into one <link ...> tag per line and require a
|
||
# SINGLE tag that is both rel="stylesheet" and points at this exact
|
||
# href. Attribute-order independent, and — unlike a bare
|
||
# `grep -F href="..."` — a preload of the same href cannot satisfy
|
||
# it. Anchoring on the full href (not the basename) keeps an
|
||
# incidental occurrence of the hash from matching.
|
||
grep -oE '<link[^>]*>' /tmp/canvas-doc.html > /tmp/canvas-links.txt || true
|
||
if ! grep -F 'rel="stylesheet"' /tmp/canvas-links.txt | grep -qF 'href="'"${RF_CSS_PATH}"'"'; then
|
||
echo "::error::The served /?m=chat document has no render-blocking <link rel=\"stylesheet\" href=\"${RF_CSS_PATH}\">. Found these link tags:"
|
||
cat /tmp/canvas-links.txt
|
||
echo "::error::@xyflow/react/dist/style.css must be imported from a LAYOUT stylesheet (canvas/src/app/globals.css), not from a client component such as Canvas.tsx. Imported from a component it lands in the PAGE css chunk, which Next ships as a preload that React only turns into a stylesheet during hydration — leaving a window in which the canvas renders unstyled, the background svg swallows every click, and node hit-testing is meaningless."
|
||
exit 1
|
||
fi
|
||
if [ "$RF_CODE" != "200" ]; then
|
||
echo "::error::${RF_CSS_PATH} is not served (HTTP ${RF_CODE}) — the standalone static assets were not assembled."
|
||
exit 1
|
||
fi
|
||
if ! grep -qF 'react-flow__background' /tmp/canvas-rf.css; then
|
||
echo "::error::${RF_CSS_PATH} served but does not contain the .react-flow__background rule."
|
||
exit 1
|
||
fi
|
||
echo "React Flow stylesheet named by the document and served OK (${RF_CSS_PATH})"
|
||
|
||
- name: Run Playwright E2E tests
|
||
working-directory: canvas
|
||
env:
|
||
# CI=1 activates forbidOnly in playwright.config.ts (a stray
|
||
# `test.only` would otherwise green the suite while skipping the
|
||
# rest). passWithNoTests:false (also in the config) already makes
|
||
# a zero-match selection exit non-zero.
|
||
CI: "1"
|
||
run: |
|
||
set -euo pipefail
|
||
export E2E_PLATFORM_URL="http://127.0.0.1:${PLATFORM_PORT}"
|
||
export E2E_DATABASE_URL="${DATABASE_URL}"
|
||
export PLAYWRIGHT_BASE_URL="http://localhost:${CANVAS_PORT}"
|
||
|
||
# REQUIRE-LIVE guard (mirrors CP serving-e2e SERVING_E2E_REQUIRE_LIVE):
|
||
# this lane always runs now, so it MUST actually execute the round-trip
|
||
# specs. `pipefail` makes a real test failure (playwright non-zero) abort
|
||
# here under `set -e`; passWithNoTests:false makes a zero-match selection
|
||
# non-zero too. The assertions below are belt-and-braces.
|
||
npx playwright test e2e/chat-desktop.spec.ts e2e/chat-mobile.spec.ts e2e/chat-separation.spec.ts \
|
||
e2e/local-entry.spec.ts e2e/concierge-chat-rendering.spec.ts \
|
||
e2e/schedule-tab.spec.ts \
|
||
e2e/boot-regression.spec.ts \
|
||
e2e/css-diagnostic.spec.ts \
|
||
e2e/net-evidence-guard.spec.ts \
|
||
--reporter=list 2>&1 | tee /tmp/pw-chat.out
|
||
# net-evidence-guard.spec.ts is listed because a guard that is not
|
||
# SELECTED does not run, and this one is the thing that stops
|
||
# core#5106 recurring silently. It needs no platform and no canvas —
|
||
# it serves its own `/_next/static/**` on an ephemeral port — so it
|
||
# cannot add a dependency to this step.
|
||
#
|
||
# It contains ONE `test.fail()` (an expected-failure over a body that
|
||
# asserts nothing, so the only thing that can produce the failure is
|
||
# the guard firing in teardown — that is the mutation-proof). An
|
||
# expected-failure counts under `passed` and prints no `skipped`, so
|
||
# the two REQUIRE-LIVE assertions below are unaffected. Measured on
|
||
# @playwright/test 1.59.1: `11 passed`, zero skipped.
|
||
# The guard used to be `grep -qE '[0-9]+ (passed|failed|skipped)'` — which
|
||
# ACCEPTED `skipped` as proof of execution, while its own comment claimed
|
||
# "a silent all-skip can never report green". It could: an all-skipped run
|
||
# prints "27 skipped", satisfies the alternation, and posts SUCCESS having
|
||
# asserted nothing. Same green-by-no-op class this lane was un-vacuumed to
|
||
# kill, hiding inside the anti-vacuity check itself.
|
||
#
|
||
# Now: require a POSITIVE pass count, and refuse any skip. The chat +
|
||
# schedule-tab specs declare ZERO test.skip/test.fixme, so a `skipped` in
|
||
# the summary means someone added a conditional skip — that must be loud,
|
||
# not silently subtracted from this lane's coverage.
|
||
if ! grep -qE '[0-9]+ passed' /tmp/pw-chat.out; then
|
||
echo "::error::E2E Chat REQUIRE-LIVE: Playwright reported no PASSED tests — specs missing, all-skipped, or a zero-match selection. A gate that executes nothing must not report success."
|
||
exit 1
|
||
fi
|
||
if grep -qE '[0-9]+ skipped' /tmp/pw-chat.out; then
|
||
echo "::error::E2E Chat REQUIRE-LIVE: $(grep -oE '[0-9]+ skipped' /tmp/pw-chat.out) — the chat specs declare no test.skip/test.fixme, so a skip here is silently removed coverage on the only per-PR chat gate. Remove the skip, or delete the test outright so its loss is visible in the diff."
|
||
exit 1
|
||
fi
|
||
|
||
- name: Dump platform log on failure
|
||
if: failure()
|
||
run: cat workspace-server/platform.log || true
|
||
|
||
- name: Dump canvas log on failure
|
||
if: failure()
|
||
run: cat canvas/canvas.log || true
|
||
|
||
- name: Dump browser log on failure
|
||
if: failure()
|
||
run: docker logs "$PLAYWRIGHT_CONTAINER" || true
|
||
|
||
# `--reporter=list` writes NO playwright-report/ directory — only the HTML
|
||
# reporter does — so this step has been finding nothing and warning
|
||
# "No files were found with the provided path" on every failure. It cost
|
||
# us the failure screenshot and error-context.md on run 632484, the one
|
||
# captured recurrence of core#5106, leaving the job log as the sole
|
||
# evidence. `test-results/` is where `screenshot: "only-on-failure"`
|
||
# actually puts them; keep the report path too so adding an HTML reporter
|
||
# later still uploads.
|
||
- name: Upload Playwright failure artifacts
|
||
if: failure()
|
||
uses: actions/upload-artifact@c6a366c94c3e0affe28c06c8df20a878f24da3cf # v3.2.2
|
||
with:
|
||
name: playwright-report-chat
|
||
path: |
|
||
canvas/test-results/
|
||
canvas/playwright-report/
|
||
canvas/canvas.log
|
||
|
||
- name: Stop canvas
|
||
if: always()
|
||
run: |
|
||
if [ -f canvas/canvas.pid ]; then
|
||
kill "$(cat canvas/canvas.pid)" 2>/dev/null || true
|
||
fi
|
||
|
||
- name: Stop platform
|
||
if: always()
|
||
run: |
|
||
if [ -f workspace-server/platform.pid ]; then
|
||
kill "$(cat workspace-server/platform.pid)" 2>/dev/null || true
|
||
fi
|
||
|
||
- name: Stop service containers
|
||
if: always()
|
||
run: |
|
||
# timeout-wrap so a wedged docker daemon can't hang this always() step
|
||
# (a hung rm here is one way containers leak in the first place).
|
||
timeout 30 docker rm -fv "$PG_CONTAINER" 2>/dev/null || true
|
||
timeout 30 docker rm -fv "$REDIS_CONTAINER" 2>/dev/null || true
|
||
timeout 30 docker rm -fv "$PLAYWRIGHT_CONTAINER" 2>/dev/null || true
|