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
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>
391 lines
20 KiB
YAML
391 lines
20 KiB
YAML
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
|