Files
molecule-core/.gitea/workflows/e2e-staging-canvas.yml
T
hongming-personal 0bd595c5dd
ci-required-drift / drift (pull_request) Has been cancelled
Block integration-tester contamination artifacts / Block staging-trigger / invalid manifest contamination (pull_request) Successful in 11s
CI / Python Lint & Test (pull_request) Successful in 10s
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 12s
E2E Peer Visibility (literal MCP list_peers) / detect-changes (pull_request) Successful in 14s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 16s
Harness Replays / detect-changes (pull_request) Successful in 22s
Lint curl status-code capture / Scan workflows for curl status-capture pollution (pull_request) Successful in 29s
lint-bp-context-emit-match / lint-bp-context-emit-match (pull_request) Failing after 37s
Lint forbidden tenant-env keys / Scan workspace_secrets writers for forbidden env keys (pull_request) Successful in 32s
lint-continue-on-error-tracking / lint-continue-on-error-tracking (pull_request) Successful in 44s
Lint forbidden tenant-env keys / Scan for repo-host token write into tenant workspace surface (pull_request) Successful in 30s
Lint forbidden hand-written mcp__ tool-id literals / Scan for hand-written mcp__ tool-id literals (pull_request) Successful in 13s
lint-no-coe-on-required / lint-no-coe-on-required (pull_request) Successful in 22s
Lint publish-runner timeout-minutes / Lint publish-runner timeout-minutes (pull_request) Successful in 16s
lint-required-workflows-docker-host-pinned / Lint docker-host pin on docker-touching workflows (pull_request) Successful in 11s
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 22s
Lint workflow YAML (Gitea-1.22.6-hostile shapes) / Lint workflow YAML for Gitea-1.22.6-hostile shapes (pull_request) Successful in 26s
E2E Chat / detect-changes (pull_request) Successful in 1m36s
CI / Detect changes (pull_request) Successful in 1m40s
Lint schedule budget / Zero-cron ratchet (pull_request) Successful in 27s
E2E API Smoke Test / detect-changes (pull_request) Successful in 1m39s
lint-staging-tenant-cd-gate-chain / lint-staging-tenant-cd-gate-chain (pull_request) Successful in 29s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 1m36s
lint-setup-go-cache / lint-setup-go-cache (pull_request) Successful in 30s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 12s
SECRET_PATTERNS drift lint / Detect SECRET_PATTERNS drift (pull_request) Successful in 19s
gate-check-v3 / gate-check (pull_request_target) Successful in 16s
sop-checklist / review-refire (pull_request_target) Has been skipped
sop-checklist / all-items-acked (pull_request) acked: 0/9 — missing: comprehensive-testing, local-postgres-e2e, staging-smoke, +6 — body-unfilled: comprehensive-testing, local-postgres-e2
sop-checklist / na-declarations (pull_request) N/A: (none)
lint-mask-pr-atomicity / lint-mask-pr-atomicity (pull_request) Successful in 1m27s
sop-checklist / all-items-acked (pull_request_target) Successful in 7s
Lint pre-flip continue-on-error / Verify continue-on-error flips have run-log proof (pull_request) Successful in 1m11s
Local Provision Lifecycle E2E / Local Provision Lifecycle E2E (stub) (pull_request) Successful in 44s
lint-required-context-exists-in-bp / lint-required-context-exists-in-bp (pull_request) Successful in 1m15s
template-delivery-e2e / detect-changes (pull_request) Successful in 34s
Ops Scripts Tests / Ops scripts (unittest) (pull_request) Successful in 34s
PR Diff Guard / PR diff guard (pull_request) Successful in 35s
E2E Peer Visibility (literal MCP list_peers) / E2E Peer Visibility (local) (pull_request) Has been skipped
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 3s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 2s
CI / Platform (Go) (pull_request) Successful in 3s
CI / Canvas (Next.js) (pull_request) Successful in 3s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Successful in 4s
E2E Chat / E2E Chat (pull_request) Successful in 4s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 4s
template-delivery-e2e / Template-asset delivery (fresh seo-agent — config+prompts via asset channel, seo-all via plugin reconcile) (pull_request) Successful in 2s
E2E Peer Visibility (literal MCP list_peers) / E2E Peer Visibility (pull_request) Successful in 12s
CI / Canvas Deploy Status (pull_request) Successful in 2s
CI / all-required (pull_request) Successful in 5s
Local Provision Lifecycle E2E / Local Provision Lifecycle E2E (real image + MiniMax LLM, advisory) (pull_request) Successful in 32s
Harness Replays / Harness Replays (pull_request) Successful in 1m17s
qa-review / approved (pull_request_target) Approved via pull_request_review trigger
security-review / approved (pull_request_target) Approved via pull_request_review trigger
qa-review / approved (pull_request_review) Successful in 9s
reserved-path-review / reserved-path-review (pull_request_target) Approved via pull_request_review trigger
security-review / approved (pull_request_review) Successful in 8s
reserved-path-review / reserved-path-review (pull_request_review) Successful in 10s
gitea-merge-queue / queue (pull_request_review) Has been cancelled
fix(ci): lint-continue-on-error-tracking — renew stale mask tracker (mc#2654 -> mc#3436)
Root cause: `lint-continue-on-error-tracking` (Tier 2e) requires every
job-level `continue-on-error: true` to reference an OPEN tracker <=14
days old (forced 14-day renewal cadence). The shared tracker mc#2654
(created 2026-06-12) is now CLOSED and 22 days old, and 20 masks across
16 workflows still resolved to it -> every one violated the lint ->
the whole check went RED.

Verified NOT the operator's hypothesised leak: no BP-required status
context carries continue-on-error (none of the 7 required emitters
—CI/all-required, E2E API Smoke Test, Handlers Postgres Integration,
Secret scan, qa-review/approved, security-review/approved,
reserved-path-review— is in the continue-on-error set). And the lint's
own API read works. The sole cause is the stale/closed shared tracker.

Fix (the renewal mechanism the lint is designed to enforce, done
NON-silently): filed a fresh, itemised umbrella tracker mc#3436 (open,
today) that enumerates every remaining mask + the burn-down plan, and
surgically repointed each mc#2654-resolved tracker comment to mc#3436
using the lint's exact +/-2-line first-match resolution (prose mentions
of mc#2654 left intact as history; ci.yml untouched to avoid tripping
lint-mask-pr-atomicity; per-job trackers already migrated —mc#3230
lifecycle-real, mc#3232 design-token-drift-gate, mc#3244
prune-stale-e2e-dns— left as-is).

Verification (vs live Gitea): all 23 continue-on-error:true directives
now resolve to an open, <=14d tracker -> 0 violations -> genuine green.
Comment-only changes; all edited YAML still parses; lint script + its
unit tests unchanged.

Tracking / burn-down: mc#3436 (renews 2026-07-18).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 17:06:27 -07:00

391 lines
20 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
name: E2E Staging Canvas (Playwright)
# Ported from .github/workflows/e2e-staging-canvas.yml on 2026-05-11 per RFC
# internal#219 §1 sweep. Differences from the GitHub version:
# - Dropped `workflow_dispatch.inputs` (Gitea 1.22.6 parser rejects them
# per feedback_gitea_workflow_dispatch_inputs_unsupported).
# - Dropped `merge_group:` (no Gitea merge queue).
# - Dropped `environment:` blocks (Gitea has no environments).
# - Workflow-level env.GITHUB_SERVER_URL pinned per
# feedback_act_runner_github_server_url.
# - `continue-on-error: true` on each job (RFC §1 contract).
#
# Playwright test suite that provisions a fresh staging org per run and
# verifies every workspace-panel tab renders REAL content (not just an
# empty/errored container). Complements e2e-staging-saas.yml (which tests
# the API shape) by exercising the actual browser + canvas bundle against
# live staging.
#
# PROMOTION-READINESS (toward making this a HARD merge-gate):
# NOW RELIABLE (spec hardened — staging-tabs.spec.ts):
# - All waits condition-based (toBeVisible/toHaveAttribute/expect.poll);
# no fixed waitForTimeout in the spec.
# - Tabs asserted on settled REAL content, not "container visible".
# - ErrorBoundary + visible error alerts fail non-degraded tabs.
# - Tab-list parity-checked vs live DOM; fail-closed on missing tenant.
# STILL BLOCKS PROMOTION-TO-REQUIRED (do NOT remove continue-on-error —
# CTO-owned, RFC internal#219 §1):
# - Infra dependency: real staging EC2 per run (12-20 min cold boot);
# AWS/Cloudflare/CP availability would become merge-blockers.
# - Shared-zone TLS/DNS/ACME propagation flake surface is upstream of
# this repo and outside its control.
# - Required-gate correctness needs CP_STAGING_ADMIN_API_TOKEN GUARANTEED
# present; today's skip-if-absent (core#2225) is right for non-gating
# but would skip-green a required check.
# - Single hermes/platform_managed workspace; agent-dependent content
# (live chat/traces round-trip) not exercised on staging (#2162).
# The full checklist lives at the foot of canvas/e2e/staging-tabs.spec.ts.
#
# Triggers: push to main, PR touching canvas sources + this workflow only
# after the PR enters `merge-queue`, manual dispatch, and scheduled cron to
# catch browser/runtime drift even when canvas is quiet.
# Added staging to push/pull_request branches so the auto-promote gate
# check (--event push --branch staging) can see a completed run for this
# workflow — mirrors what PR #1891 does for e2e-api.yml.
on:
# Trigger model (revised 2026-04-29):
#
# Always fires on push/pull_request; real work is gated per-step on
# `needs.detect-changes.outputs.canvas`. When canvas/ paths haven't
# changed, the no-op step alone runs and emits SUCCESS for the
# `Canvas tabs E2E` check, satisfying branch protection without
# spending CI cycles. See e2e-api.yml for the rationale on why this
# is a single job rather than two-jobs-sharing-name.
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
concurrency:
# Per-SHA grouping (changed 2026-04-28 from a single global group). The
# global group made auto-promote-staging brittle: when a staging push
# queued behind an in-flight run and a third entrant (a PR run, a
# follow-on push) entered the group, the staging push got cancelled —
# leaving auto-promote-staging looking at `completed/cancelled` for a
# required gate and refusing to advance main. Observed 2026-04-28
# 23:51-23:53 on staging tip 3f99fede.
#
# The original intent of the global group was to throttle parallel
# E2E provisions (each spins a fresh EC2). At our scale that throttle
# isn't worth the correctness cost — fresh-org-per-run isolates the
# state, and the cost of two parallel runs (~$0.001/min × 10min × 2)
# is rounding error vs. the cost of a stuck pipeline.
#
# Per-SHA still dedupes accidental double-triggers for the SAME SHA.
# It does NOT cancel obsolete-PR-version runs on force-push; that
# wasted CI is acceptable given the alternative is losing staging-tip
# data that auto-promote-staging needs.
group: e2e-staging-canvas-${{ github.event.pull_request.head.sha || github.sha }}
cancel-in-progress: false
env:
GITHUB_SERVER_URL: https://git.moleculesai.app
jobs:
detect-changes:
runs-on: ubuntu-latest
# Phase 3 (RFC #219 §1): surface broken workflows without blocking.
# mc#3436: pre-existing continue-on-error mask; root-fix and remove, do not renew silently.
continue-on-error: true
outputs:
canvas: ${{ steps.decide.outputs.canvas }}
debug: ${{ steps.decide.outputs.debug }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- id: decide
env:
GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}
QUEUE_LABEL: merge-queue
# Inline replacement for dorny/paths-filter — see e2e-api.yml.
# Cron and manual triggers always run real work (no diff context).
run: |
if [ "${{ github.event_name }}" = "schedule" ] || [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "canvas=true" >> "$GITHUB_OUTPUT"
echo "debug=manual-trigger event=${{ github.event_name }}" >> "$GITHUB_OUTPUT"
exit 0
fi
BASE="${GITHUB_BASE_REF:-${{ github.event.before }}}"
if [ "${{ github.event_name }}" = "pull_request" ] && [ -n "${{ github.event.pull_request.base.sha }}" ]; then
BASE="${{ github.event.pull_request.base.sha }}"
fi
if [ -z "$BASE" ] || echo "$BASE" | grep -qE '^0+$'; then
echo "canvas=true" >> "$GITHUB_OUTPUT"
echo "debug=new-branch-fallback base=$BASE" >> "$GITHUB_OUTPUT"
exit 0
fi
if ! git cat-file -e "$BASE" 2>/dev/null; then
git fetch --depth=1 origin "$BASE" 2>/dev/null || true
fi
if ! git cat-file -e "$BASE" 2>/dev/null; then
echo "canvas=true" >> "$GITHUB_OUTPUT"
echo "debug=base-missing-fallback base=$BASE" >> "$GITHUB_OUTPUT"
exit 0
fi
CHANGED=$(git diff --name-only "$BASE" HEAD)
CHANGED_FLAT=$(echo "$CHANGED" | tr '\n' ',')
if ! echo "$CHANGED" | grep -qE '^(canvas/|\.gitea/workflows/e2e-staging-canvas\.yml$)'; then
echo "canvas=false" >> "$GITHUB_OUTPUT"
echo "debug=no-matching-paths base=$BASE changed=$CHANGED_FLAT" >> "$GITHUB_OUTPUT"
exit 0
fi
if [ "${{ github.event_name }}" != "pull_request" ]; then
echo "canvas=true" >> "$GITHUB_OUTPUT"
echo "debug=non-pr-run base=$BASE changed=$CHANGED_FLAT" >> "$GITHUB_OUTPUT"
exit 0
fi
authfile=$(mktemp)
chmod 600 "$authfile"
printf 'header = "Authorization: token %s"\n' "$GITEA_TOKEN" > "$authfile"
labels=$(curl -fsS -K "$authfile" \
"${{ github.server_url }}/api/v1/repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/labels" \
| python3 -c 'import json,sys; print("\n".join(label.get("name","") for label in json.load(sys.stdin)))') || {
# Labels API unavailable: fail open so the E2E gate runs rather
# than silently no-oping a potentially-breaking PR.
rm -f "$authfile"
echo "canvas=true" >> "$GITHUB_OUTPUT"
echo "debug=labels-api-unavailable pr=${{ github.event.pull_request.number }} base=$BASE changed=$CHANGED_FLAT" >> "$GITHUB_OUTPUT"
exit 0
}
rm -f "$authfile"
LABELS_FLAT=$(echo "$labels" | tr '\n' ',')
if printf '%s\n' "$labels" | grep -qx "$QUEUE_LABEL"; then
echo "canvas=true" >> "$GITHUB_OUTPUT"
echo "debug=merge-queue-labeled base=$BASE changed=$CHANGED_FLAT" >> "$GITHUB_OUTPUT"
else
echo "PR is not in merge-queue; skipping heavy E2E Staging Canvas for normal PR path."
echo "canvas=false" >> "$GITHUB_OUTPUT"
echo "debug=no-merge-queue-label base=$BASE changed=$CHANGED_FLAT labels=$LABELS_FLAT" >> "$GITHUB_OUTPUT"
fi
# ONE job (no job-level `if:`) that always runs and reports under the
# required-check name `Canvas tabs E2E`. Real work is gated per-step on
# `needs.detect-changes.outputs.canvas`. See e2e-api.yml for the full
# rationale — same path-filter check-name parity issue blocked PR #2264
# (staging→main) on 2026-04-29 because branch protection treats matching-
# name check runs as a SET, and any SKIPPED member fails the eval.
playwright:
needs: detect-changes
name: Canvas tabs E2E
runs-on: ubuntu-latest
# Phase 3 (RFC #219 §1): surface broken workflows without blocking.
# mc#3436: pre-existing continue-on-error mask; root-fix and remove, do not renew silently.
continue-on-error: true
timeout-minutes: 40
env:
CANVAS_E2E_STAGING: '1'
MOLECULE_CP_URL: https://staging-api.moleculesai.app
# 2026-05-11: secret canonicalised from MOLECULE_STAGING_ADMIN_TOKEN
# (dead in org secret store) to CP_STAGING_ADMIN_API_TOKEN per
# internal#322 — see this PR for the cross-workflow sweep.
# MOLECULE_ADMIN_TOKEN now arrives via $GITHUB_ENV from the "Fetch staging
# CP admin token from Infisical KMS" step below (wave-B staging consumers
# migration). It is sourced from Infisical CP_ADMIN_API_TOKEN at
# staging /shared/controlplane-admin. The old CP_STAGING_ADMIN_API_TOKEN
# Gitea Actions secret is left in place until a green run validates this
# path (validate-before-delete). The skip-if-absent contract below is
# unchanged: an empty token still SKIPS green (operator config gap), and an
# untrusted-fork PR (KMS fetch gated off) lands on that same skip path.
defaults:
run:
working-directory: canvas
steps:
- name: No-op pass (paths filter excluded this commit)
if: needs.detect-changes.outputs.canvas != 'true'
working-directory: .
run: |
echo "No canvas / workflow changes — E2E Staging Canvas gate satisfied without running tests."
echo "::notice::E2E Staging Canvas no-op pass (paths filter excluded this commit)."
echo "::notice::detect-changes debug: ${{ needs.detect-changes.outputs.debug }}"
- if: needs.detect-changes.outputs.canvas == 'true'
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
# KMS wave-B (Gitea-Actions-secrets -> Infisical SSOT): source the staging
# CP admin bearer (CP_ADMIN_API_TOKEN @ staging /shared/controlplane-admin)
# from Infisical via the CI machine identity instead of the duplicated
# CP_STAGING_ADMIN_API_TOKEN Gitea Actions secret. Exported to $GITHUB_ENV
# under the same MOLECULE_ADMIN_TOKEN name the skip-check + test + teardown
# steps already read.
#
# TRUSTED-REF GATING: molecule-core is a PUBLIC repo; this step consumes the
# INFISICAL_CI_* machine-identity secrets, so it runs only on protected-
# branch push / operator workflow_dispatch / schedule, and on same-repo
# (non-fork) PRs. On an untrusted-fork PR it SKIPS cleanly — MOLECULE_ADMIN_
# TOKEN stays empty and the skip-if-absent contract below SKIPS the suite
# GREEN (operator config gap, not a code failure), identical to today's
# fork-PR behaviour. The job's working-directory default is `canvas`, but
# this step only calls absolute Infisical URLs, so cwd is irrelevant.
- name: Fetch staging CP admin token from Infisical KMS
id: kms_admin
working-directory: .
if: >-
needs.detect-changes.outputs.canvas == 'true' &&
(github.event_name == 'push' ||
github.event_name == 'workflow_dispatch' ||
github.event_name == 'schedule' ||
(github.event_name == 'pull_request' &&
github.event.pull_request.head.repo.fork == false))
env:
INFISICAL_CI_CLIENT_ID: ${{ secrets.INFISICAL_CI_CLIENT_ID }}
INFISICAL_CI_CLIENT_SECRET: ${{ secrets.INFISICAL_CI_CLIENT_SECRET }}
INFISICAL_PROJECT_ID: ${{ secrets.INFISICAL_CI_PROJECT_ID }}
run: |
set -uo pipefail
BASE="https://key.moleculesai.app"
TOK=$(curl -fsS -X POST "$BASE/api/v1/auth/universal-auth/login" \
-H 'Content-Type: application/json' \
-d "{\"clientId\":\"$INFISICAL_CI_CLIENT_ID\",\"clientSecret\":\"$INFISICAL_CI_CLIENT_SECRET\"}" \
| python3 -c 'import sys,json; d=json.load(sys.stdin); v=d.get("accessToken"); sys.stdout.write(v if isinstance(v,str) and v else "")')
if [ -z "$TOK" ]; then echo "::error::Infisical accessToken empty - failing closed"; exit 1; fi
read_secret() {
curl -fsS "$BASE/api/v3/secrets/raw/$1?workspaceId=$INFISICAL_PROJECT_ID&environment=staging&secretPath=$2" \
-H "Authorization: Bearer $TOK" \
| python3 -c 'import sys,json; d=json.load(sys.stdin); v=(d.get("secret") or {}).get("secretValue"); sys.stdout.write(v if isinstance(v,str) and v else "")'
}
MOLECULE_ADMIN_TOKEN=$(read_secret CP_ADMIN_API_TOKEN %2Fshared%2Fcontrolplane-admin)
echo "::add-mask::$MOLECULE_ADMIN_TOKEN"
if [ -z "$MOLECULE_ADMIN_TOKEN" ]; then
echo "::error::Infisical returned empty CP_ADMIN_API_TOKEN at staging /shared/controlplane-admin — failing closed."
exit 1
fi
echo "MOLECULE_ADMIN_TOKEN=$MOLECULE_ADMIN_TOKEN" >> "$GITHUB_ENV"
echo "MOLECULE_ADMIN_TOKEN loaded from Infisical staging SSOT (len=${#MOLECULE_ADMIN_TOKEN})"
# Skip-if-absent (core#2225), mirroring the serving-e2e gate's
# skip-if-secret-unset contract: a MISSING CI secret is an operator
# CONFIG gap, not a code regression, so it must not paint this E2E
# red. When CP_STAGING_ADMIN_API_TOKEN is unset we emit a LOUD
# ::warning:: + ::notice:: and skip the real provision/test steps (the
# job still completes green). When the secret IS present we run the
# full suite exactly as before. Operators: set
# CP_STAGING_ADMIN_API_TOKEN as a repo/org Actions secret on
# molecule-core to actually exercise this E2E.
- name: Check admin token (skip-if-absent)
id: token_check
if: needs.detect-changes.outputs.canvas == 'true'
run: |
if [ -z "$MOLECULE_ADMIN_TOKEN" ]; then
echo "::warning::CP_STAGING_ADMIN_API_TOKEN is not set on this runner — SKIPPING the staging canvas E2E (cannot auth to staging CP). This is an operator config gap, not a code failure; set the secret on molecule-core (repo or org Actions secrets) to run it. See core#2225."
echo "::notice::E2E Staging Canvas skipped: CP_STAGING_ADMIN_API_TOKEN absent."
echo "present=false" >> "$GITHUB_OUTPUT"
else
echo "CP_STAGING_ADMIN_API_TOKEN present ✓ — running staging canvas E2E."
echo "present=true" >> "$GITHUB_OUTPUT"
fi
- name: Set up Node
if: needs.detect-changes.outputs.canvas == 'true' && steps.token_check.outputs.present == 'true'
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: canvas/package-lock.json
- name: Install canvas deps
if: needs.detect-changes.outputs.canvas == 'true' && steps.token_check.outputs.present == 'true'
run: npm ci
- name: Install Playwright browsers
if: needs.detect-changes.outputs.canvas == 'true' && steps.token_check.outputs.present == 'true'
timeout-minutes: 10
run: |
PREBAKED_PLAYWRIGHT=/ms-playwright
if [ -d "${PREBAKED_PLAYWRIGHT}" ] && find "${PREBAKED_PLAYWRIGHT}" -maxdepth 3 -type f -name 'chrome' | grep -q .; then
echo "Using prebaked Playwright Chromium from ${PREBAKED_PLAYWRIGHT}"
echo "PLAYWRIGHT_BROWSERS_PATH=${PREBAKED_PLAYWRIGHT}" >> "$GITHUB_ENV"
exit 0
fi
npx playwright install --with-deps chromium
- name: Run staging canvas E2E
if: needs.detect-changes.outputs.canvas == 'true' && steps.token_check.outputs.present == 'true'
run: npx playwright test --config=playwright.staging.config.ts
- name: Upload Playwright report on failure
if: failure() && needs.detect-changes.outputs.canvas == 'true'
# Pinned to v3 for Gitea act_runner v0.6 compatibility — v4+ uses
# the GHES 3.10+ artifact protocol that Gitea 1.22.x does NOT
# implement (see ci.yml upload step for the canonical error
# cite). Drop this pin when Gitea ships the v4 protocol.
uses: actions/upload-artifact@c6a366c94c3e0affe28c06c8df20a878f24da3cf # v3.2.2
with:
name: playwright-report-staging
path: canvas/playwright-report-staging/
retention-days: 14
- name: Upload screenshots on failure
if: failure() && needs.detect-changes.outputs.canvas == 'true'
# Pinned to v3 for Gitea act_runner v0.6 compatibility (see above).
uses: actions/upload-artifact@c6a366c94c3e0affe28c06c8df20a878f24da3cf # v3.2.2
with:
name: playwright-screenshots
path: canvas/test-results/
retention-days: 14
# Safety-net teardown — fires only when Playwright's globalTeardown
# didn't (worker crash, runner cancel). Reads the slug from
# canvas/.playwright-staging-state.json (written by staging-setup
# as its first action, before any CP call) and deletes only that
# slug.
#
# Earlier versions of this step pattern-swept `e2e-canvas-<today>-*`
# orgs to compensate for setup-crash-before-state-file-write. That
# over-aggressive cleanup raced concurrent canvas-E2E runs and
# poisoned each other's tenants — observed 2026-04-30 when three
# real-test runs killed each other mid-test, surfacing as
# `getaddrinfo ENOTFOUND` once CP had cleaned up the just-deleted
# DNS record. Pattern-sweep removed; setup now writes the state
# file before any CP work, so the slug is always recoverable.
- name: Teardown safety net
if: always() && needs.detect-changes.outputs.canvas == 'true'
env:
# Sourced from Infisical (staging /shared/controlplane-admin) via the
# "Fetch staging CP admin token from Infisical KMS" job step above.
ADMIN_TOKEN: ${{ env.MOLECULE_ADMIN_TOKEN }}
run: |
set +e
STATE_FILE=".playwright-staging-state.json"
if [ ! -f "$STATE_FILE" ]; then
echo "::notice::No state file at canvas/$STATE_FILE — Playwright globalTeardown handled it (or setup never ran)."
exit 0
fi
slug=$(python3 -c "import json; print(json.load(open('$STATE_FILE')).get('slug',''))")
if [ -z "$slug" ]; then
echo "::warning::State file present but slug missing; nothing to clean up."
exit 0
fi
echo "Deleting orphan tenant: $slug"
# Verify HTTP 2xx instead of `>/dev/null || true` swallowing
# failures. A 5xx or timeout previously looked identical to
# success, leaving the tenant alive for up to ~45 min until
# sweep-stale-e2e-orgs caught it. Surface failures as
# workflow warnings naming the slug. Don't `exit 1` — a single
# cleanup miss shouldn't fail-flag the canvas test when the
# actual smoke check passed; the sweeper is the safety net.
# See molecule-controlplane#420.
# Tempfile-routed -w + set +e/-e prevents curl-exit-code
# pollution of the captured status (lint-curl-status-capture.yml).
set +e
curl -sS -o /tmp/canvas-cleanup.out -w "%{http_code}" \
-X DELETE "$MOLECULE_CP_URL/cp/admin/tenants/$slug" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"confirm\":\"$slug\"}" >/tmp/canvas-cleanup.code
set -e
code=$(cat /tmp/canvas-cleanup.code 2>/dev/null || echo "000")
if [ "$code" = "200" ] || [ "$code" = "204" ]; then
echo "[teardown] deleted $slug (HTTP $code)"
else
echo "::warning::canvas teardown for $slug returned HTTP $code — sweep-stale-e2e-orgs will catch it within ~45 min. Body: $(head -c 300 /tmp/canvas-cleanup.out 2>/dev/null)"
fi
exit 0