Files
molecule-core/.gitea/workflows/e2e-peer-visibility.yml
T
hongming-ceo-delegated caf86d1091
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 9s
Block integration-tester contamination artifacts / Block staging-trigger / invalid manifest contamination (pull_request) Successful in 9s
CI / Python Lint & Test (pull_request) Successful in 6s
sop-checklist / all-items-acked (pull_request) ack advisory until agent-team — would be failure: acked: 0/10 — missing: comprehensive-testing, local-postgres-e2e, staging-smoke, +7 — body
sop-checklist / na-declarations (pull_request) N/A: (none)
sop-checklist / all-items-acked (pull_request_target) Successful in 4s
CI / Detect changes (pull_request) Successful in 14s
ci-required-drift / drift (pull_request) Successful in 18s
E2E Peer Visibility (literal MCP list_peers) / detect-changes (pull_request) Successful in 8s
Concierge Creates Workspace Hermetic / Concierge Creates Workspace Hermetic (pull_request) Successful in 16s
E2E Staging SaaS (full lifecycle) / E2E Staging SaaS (pull_request) Has been skipped
E2E Staging SaaS (full lifecycle) / E2E Staging Platform Boot (pull_request) Has been skipped
E2E Staging SaaS (full lifecycle) / E2E Staging Concierge user_tasks (pull_request) Has been skipped
E2E Chat / detect-changes (pull_request) Successful in 18s
E2E API Smoke Test / detect-changes (pull_request) Successful in 20s
E2E Staging SaaS (full lifecycle) / E2E Staging Workspace Requests (core#2606) (pull_request) Has been skipped
E2E Staging SaaS (full lifecycle) / E2E Staging Concierge Platform Agent (pull_request) Has been skipped
E2E Staging SaaS (full lifecycle) / E2E Staging Plugin Install Lifecycle (pull_request) Has been skipped
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 19s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 9s
Lint curl status-code capture / Scan workflows for curl status-capture pollution (pull_request) Successful in 7s
Lint forbidden tenant-env keys / Scan for repo-host token write into tenant workspace surface (pull_request) Successful in 7s
Lint forbidden hand-written mcp__ tool-id literals / Scan for hand-written mcp__ tool-id literals (pull_request) Successful in 7s
Lint forbidden tenant-env keys / Scan workspace_secrets writers for forbidden env keys (pull_request) Successful in 8s
lint-bp-context-emit-match / lint-bp-context-emit-match (pull_request) Successful in 18s
lint-env-coupling-dismissal / lint-env-coupling-dismissal (pull_request) Successful in 25s
lint-continue-on-error-tracking / lint-continue-on-error-tracking (pull_request) Successful in 26s
lint-no-coe-on-required / lint-no-coe-on-required (pull_request) Successful in 19s
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 16s
lint-required-workflows-docker-host-pinned / Lint docker-host pin on docker-touching workflows (pull_request) Successful in 7s
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 19s
Lint pre-flip continue-on-error / Verify continue-on-error flips have run-log proof (pull_request) Successful in 26s
lint-required-context-exists-in-bp / lint-required-context-exists-in-bp (pull_request) Successful in 29s
Lint schedule budget / Zero-cron ratchet (pull_request) Successful in 19s
lint-staging-tenant-cd-gate-chain / lint-staging-tenant-cd-gate-chain (pull_request) Successful in 17s
PR Diff Guard / PR diff guard (pull_request) Successful in 12s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 9s
lint-setup-go-cache / lint-setup-go-cache (pull_request) Successful in 21s
Lint workflow YAML (Gitea-1.22.6-hostile shapes) / Lint workflow YAML for Gitea-1.22.6-hostile shapes (pull_request) Successful in 24s
sop-checklist / review-refire (pull_request_target) Successful in 4s
gate-check-v3 / gate-check (pull_request_target) Successful in 11s
template-delivery-e2e / detect-changes (pull_request) Successful in 14s
CI / Platform (Go) (pull_request) Successful in 4s
CI / Canvas (Next.js) (pull_request) Successful in 5s
security-review / approved (pull_request_target) Approved via pull_request_review trigger
qa-review / approved (pull_request_target) Approved via pull_request_review trigger
reserved-path-review / reserved-path-review (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_review) Successful in 8s
security-review / approved (pull_request_review) Successful in 8s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 4s
E2E Chat / E2E Chat (pull_request) Successful in 5s
Local Provision Lifecycle E2E / Local Provision Lifecycle E2E (stub) (pull_request) Successful in 39s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 4s
CI / Canvas Deploy Status (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 4s
Ops Scripts Tests / Ops scripts (unittest) (pull_request) Successful in 30s
E2E Peer Visibility (literal MCP list_peers) / E2E Peer Visibility (pull_request) Successful in 21s
gitea-merge-queue / queue (pull_request_review) Successful in 51s
Local Provision Lifecycle E2E / Local Provision Lifecycle E2E (real image + MiniMax LLM, advisory) (pull_request) Successful in 52s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Successful in 2m34s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 7m18s
CI / all-required (pull_request) Successful in 8s
audit-force-merge / audit (pull_request_target) Successful in 8s
E2E Staging SaaS (full lifecycle) / pr-validate (pull_request) Waiting to run
E2E Staging SaaS (full lifecycle) / E2E Staging Concierge (compile+skip) (pull_request) Waiting to run
E2E Staging SaaS (full lifecycle) / E2E Staging Concierge Creates Workspace (pull_request) Waiting to run
E2E Staging SaaS (full lifecycle) / Prune stale e2e DNS records (pull_request) Waiting to run
fix(dev): clean stale stack before dynamic ports
2026-07-09 03:14:33 -07:00

559 lines
31 KiB
YAML

name: E2E Peer Visibility (literal MCP list_peers)
# WHY A DEDICATED WORKFLOW (not folded into e2e-staging-saas.yml)
# --------------------------------------------------------------
# This is the systemic fix for a real trust failure. Hermes and OpenClaw
# were reported "fleet-verified / cascade-complete" because the *proxy*
# signals were green (registry registration + heartbeat for Hermes; model
# round-trip 200 for OpenClaw). A freshly-provisioned workspace asked on
# canvas "can you see your peers" actually FAILS:
# - Hermes: 401 on the molecule MCP `list_peers` call
# - OpenClaw: native `sessions_list` fallback, sees no platform peers
# Tasks #142/#159 were even marked "completed" under this proxy flaw.
#
# A dedicated workflow (vs extending e2e-staging-saas.yml) because:
# - It must provision MULTIPLE distinct runtimes (hermes, openclaw,
# claude-code) in ONE org and assert each sees the others. The
# full-saas script is single-runtime-per-run (E2E_RUNTIME) and folding
# a multi-runtime matrix into it would conflate concerns and bloat its
# already-45-min run.
# - It needs its own concurrency group so it doesn't fight full-saas /
# canvas for the staging org-creation quota.
# - It needs an independent, non-required status-context name so it can
# be RED today (the in-flight Hermes-401 / OpenClaw-MCP-wiring fixes
# have not landed) WITHOUT wedging unrelated merges — and flipped to
# REQUIRED in one branch-protection edit once it goes green
# (flip-to-required checklist: molecule-core#1296).
#
# THE ASSERTION IS NOT A PROXY. The driving script
# tests/e2e/test_peer_visibility_mcp_staging.sh issues the byte-for-byte
# JSON-RPC `tools/call name=list_peers` envelope to `POST
# /workspaces/:id/mcp` using each workspace's OWN bearer token, through
# the real WorkspaceAuth + MCPRateLimiter middleware chain — the exact
# call mcp_molecule_list_peers makes from a canvas agent. It does NOT
# read a registry row, /health, the heartbeat table, or
# GET /registry/:id/peers.
#
# HONEST GATE — NO continue-on-error. Per feedback_fix_root_not_symptom a
# fake-green mask would defeat the entire purpose. This workflow goes red
# on today's broken behavior and green only when the root-cause fixes
# actually land. It is intentionally NOT in branch_protections — see PR
# body for the required-vs-not decision + flip tracking issue.
#
# Gitea 1.22.6 / act_runner notes honored:
# - No cross-repo `uses:` (feedback_gitea_cross_repo_uses_blocked). The
# actions/checkout SHA is the one e2e-staging-canvas.yml already uses
# successfully (a mirrored SHA — see #1277/PR#1292 root-cause).
# - 2026-05-21 retrigger: verify fresh platform-tenant image after the
# publish Buildx DOCKER_CONFIG fix restored staging-latest image updates.
# - Per-SHA concurrency, not global (feedback_concurrency_group_per_sha).
# - Workflow-level GITHUB_SERVER_URL pinned
# (feedback_act_runner_github_server_url).
# - pr-validate posts a status under the same check name so a
# workflow-only PR is not silently statusless and the context is
# flip-to-required-ready (mirrors e2e-staging-saas.yml's proven shape;
# real EC2-provisioning E2E is push/dispatch/cron only — it is 30+ min
# and cannot run per-PR-update).
#
# LOCAL BACKEND (added 2026-05-15 — feedback_local_must_mimic_production,
# feedback_mandatory_local_e2e_before_ship, feedback_local_test_before_
# staging_e2e)
# --------------------------------------------------------------------
# The standing rule is that the local prod-mimic stack runs a MANDATORY
# local-Postgres E2E BEFORE staging E2E. A staging-only peer-visibility
# gate caught regressions late + expensively (cold EC2).
#
# UN-DORMANTED 2026-07-04: the local assertion is no longer a separate
# non-required `peer-visibility-local` job that was SKIPPED on every
# pull_request. It is now FOLDED INTO the single required `peer-visibility`
# job below and RUNS on ANY event (pull_request included) whenever
# peer-visibility-relevant paths changed (peervis). It runs the SAME
# byte-identical assertion (tests/e2e/lib/peer_visibility_assert.sh via
# tests/e2e/test_peer_visibility_mcp_local.sh) against the local
# docker-compose stack — built + booted exactly like e2e-api.yml's proven
# E2E API Smoke Test job (ephemeral pg/redis ports, go build, background
# platform-server). Local boot is minutes (not the 30+ min cold-EC2 path),
# so the REAL molecules-server/local-docker list_peers gate now fires
# per-PR — closing the dormancy where a peer-visibility-code PR merged
# with zero real coverage.
#
# The local backend uses external-mode workspaces by default so it tests
# the literal platform MCP list_peers path without depending on local
# template container boot/heartbeat (and needs NO LLM keys, so the per-PR
# gate is fully secret-free / fork-safe). Container-mode runtime boot
# remains available via PV_LOCAL_PROVISION_MODE=container for targeted
# debugging. The former `E2E Peer Visibility (local)` context is retired;
# the required `E2E Peer Visibility` context IS the local assertion now.
# NO `paths:` FILTER — REQUIRED-CHECK CONTRACT (#1296, 2026-06-13).
# The `E2E Peer Visibility` context is now a REQUIRED status check on
# branch_protections/main. A required-check workflow must NOT carry an
# `on: paths:` (or `paths-ignore:`) filter — lint-required-no-paths.py
# hard-fails it, because a docs-only PR that doesn't match the glob would
# never fire the workflow and the required context would sit `pending`
# forever, wedging the PR (feedback_path_filtered_workflow_cant_be_required).
#
# Path-scoping is NOT lost — it moved into the `changes` detect job below
# (detect-changes.py --profile peer-visibility → `peervis`) and is applied
# PER-STEP inside the single always-running `peer-visibility` job. That job
# emits exactly ONE check run under the required name `E2E Peer Visibility`
# regardless of which paths changed — branch-protection-clean, exactly like
# handlers-postgres-integration.yml (PR #2264 check_name_parity incident).
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
concurrency:
# Per-SHA (feedback_concurrency_group_per_sha). A single global group
# would let a queued staging/main push behind a PR run get cancelled,
# leaving any gate that reads "completed run at SHA" stuck.
group: e2e-peer-visibility-${{ github.event.pull_request.head.sha || github.sha }}
cancel-in-progress: false
env:
GITHUB_SERVER_URL: https://git.moleculesai.app
jobs:
# Path-scoping for the required `E2E Peer Visibility` gate. Replaces the
# old `on: paths:` filter (which a required check may not carry — see the
# `on:` header). Emits `peervis=true` when any peer-visibility-relevant
# file changed. On workflow_dispatch / schedule / a zero/unknown base SHA
# the detect helper fails OPEN (peervis=true) so manual + cron runs always
# exercise the real staging E2E. Mirrors ci.yml's `Detect changes` job.
changes:
name: detect-changes
runs-on: ubuntu-latest
continue-on-error: false
outputs:
peervis: ${{ steps.filter.outputs.peervis }}
debug: ${{ steps.filter.outputs.debug }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
# depth:2 covers the common push case; detect-changes.py fetches the
# exact PR base SHA itself when the shallow checkout lacks it.
fetch-depth: 2
- id: filter
env:
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
PR_BASE_REF: ${{ github.event.pull_request.base.ref }}
PUSH_BEFORE: ${{ github.event.before }}
run: |
BASE_SHA="${PR_BASE_SHA:-$PUSH_BEFORE}"
python3 .gitea/scripts/detect-changes.py \
--profile peer-visibility \
--event-name "${{ github.event_name }}" \
--pr-base-sha "$PR_BASE_SHA" \
--base-ref "$PR_BASE_REF" \
--push-before "${GITHUB_EVENT_BEFORE:-$PUSH_BEFORE}" || {
# Script crash / uncaught error: fail open so the E2E gate runs
# rather than silently no-oping a potentially-breaking PR.
echo "peervis=true" >> "$GITHUB_OUTPUT"
echo "debug=detect-script-error event=${{ github.event_name }} base=$BASE_SHA" >> "$GITHUB_OUTPUT"
exit 0
}
echo "debug=profile=peer-visibility event=${{ github.event_name }} base=$BASE_SHA" >> "$GITHUB_OUTPUT"
# THE required-check emitter. ONE always-running job (no job-level `if:`)
# named `E2E Peer Visibility`, so it posts EXACTLY ONE check run under the
# required context regardless of event/paths — branch-protection-clean
# (feedback_branch_protection_check_name_parity; a job-level `if:` that
# skips would post a SKIPPED check run that fails the required eval even
# beside a SUCCESS sibling — the PR #2264 / handlers-postgres lesson).
#
# UN-DORMANTED 2026-07-04 (#3204-sibling CI-red sweep). Previously the
# required lane only ran the REAL list_peers assertion on the STAGING path
# (push/dispatch/cron + peervis); every pull_request got a bash-`-n`
# SYNTAX check only, and the actual local assertion lived in a SEPARATE
# non-required job that was `if: event != pull_request`-SKIPPED on PRs.
# Net effect: a peer-visibility-CODE-changing PR merged with ZERO real
# list_peers coverage on the molecules-server/local-docker path (the same
# dormant-gate class that let plugin-502 reach prod). The required lane now
# RUNS the byte-identical external-mode local assertion
# (tests/e2e/test_peer_visibility_mcp_local.sh) on EVERY event — PR
# included — whenever peer-visibility-relevant paths changed (peervis),
# provisioning real workspaces on the local prod-mimic stack and asserting
# each sees its peers via the literal MCP `list_peers` call. Fail-before /
# pass-after is genuine: the script exits 10 on a real regression.
# Per-step paths:
# - peervis (ANY event, incl. pull_request) → the REAL local-docker
# external-mode fresh-provision list_peers assertion (the per-PR
# molecules-server gate; secret-free, so fork-safe).
# - push/dispatch/cron + peervis (ADDITIONALLY) → the REAL staging
# fresh-provision list_peers E2E (deeper cold-provision coverage).
# - !peervis → no-op pass (paths filter excluded this run).
# The former standalone `peer-visibility-local` job is folded in here so
# the required context IS the local assertion (single always-running
# emitter — no skipped sibling to wedge branch protection).
peer-visibility:
name: E2E Peer Visibility
needs: changes
# docker-host: the folded-in local assertion boots a real docker
# postgres/redis + go-built platform-server (same substrate as
# e2e-api.yml's required E2E API Smoke Test / the old
# peer-visibility-local job that ran green on push). ubuntu-latest lacks
# the docker socket + persistent GOCACHE bind-mount this needs.
runs-on: docker-host
timeout-minutes: 60
# SECURITY (RC #11317): still NO real secrets at job level. This job runs
# on pull_request (it is the always-running required-context emitter) and
# a job-level `${{ secrets.* }}` would be present in the env of the PR run
# — exfiltratable by an untrusted/fork PR. The staging secrets
# (CP_STAGING_ADMIN_API_TOKEN + the three MOLECULE_STAGING_*_API_KEY +
# INFISICAL_CI_*) are therefore bound PER-STEP on the push/dispatch/cron-
# only steps below, each guarded by `if: github.event_name !=
# 'pull_request'`. Only NON-secret env lives at job level: URLs, run id,
# runtime list, per-run container names, and the LOCAL stack's STATIC
# admin token literal `local-e2e-admin-token` (the local platform's
# dev-mode fixed admin token — NOT a secret; identical to what the folded
# peer-visibility-local job used on its green runs).
env:
MOLECULE_CP_URL: https://staging-api.moleculesai.app
E2E_RUN_ID: "${{ github.run_id }}-${{ github.run_attempt }}"
PV_RUNTIMES: "hermes openclaw claude-code"
# LOCAL external-mode assertion (folded from peer-visibility-local).
# external mode needs NO LLM keys — it exercises the literal platform
# MCP list_peers path on external-mode workspace rows (a runtime whose
# key is absent is skipped, not failed), so the per-PR gate is fully
# secret-free. Per-run container names + ephemeral ports avoid
# host-network act_runner collisions (feedback_act_runner_*).
PV_LOCAL_PROVISION_MODE: external
ADMIN_TOKEN: local-e2e-admin-token
MOLECULE_ADMIN_TOKEN: local-e2e-admin-token
PG_CONTAINER: pg-e2e-pv-${{ github.run_id }}-${{ github.run_attempt }}
REDIS_CONTAINER: redis-e2e-pv-${{ github.run_id }}-${{ github.run_attempt }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
# --- ALL events: cheap syntax preflight before the heavier boot ------
- name: Validate driving scripts + shared assertion lib
run: |
bash -n tests/e2e/lib/peer_visibility_assert.sh
echo "lib/peer_visibility_assert.sh — bash syntax OK"
bash -n tests/e2e/test_peer_visibility_mcp_staging.sh
echo "test_peer_visibility_mcp_staging.sh — bash syntax OK"
bash -n tests/e2e/test_peer_visibility_token_mint_staging.sh
echo "test_peer_visibility_token_mint_staging.sh — bash syntax OK"
bash -n tests/e2e/test_peer_visibility_mcp_local.sh
echo "test_peer_visibility_mcp_local.sh — bash syntax OK"
legacy_token_suffix="test""-token"
if rg -n "$legacy_token_suffix" tests/e2e/test_*staging*.sh; then
echo "::error::staging E2E must use production-safe admin token minting"
exit 1
fi
# === REAL local-docker external-mode list_peers assertion ============
# Runs on ANY event (pull_request INCLUDED) whenever peer-visibility-
# relevant paths changed. Secret-free (external mode needs no LLM
# keys), so it is fork-safe at job level. This is the per-PR
# molecules-server/local-docker gate that was previously dormant
# (PRs got bash -n only; the real assertion was skipped). Folded from
# the former peer-visibility-local job; bootstrap mirrors e2e-api.yml's
# proven E2E API Smoke Test (per-run container names + ephemeral ports;
# go build; background platform-server). HONEST — no continue-on-error.
- name: (local) Set up Go
if: needs.changes.outputs.peervis == 'true'
uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
with:
go-version: 'stable'
# cache:false — self-hosted runner bind-mounts a persistent
# GOCACHE/GOMODCACHE; actions/cache corrupts it (fleet-wide finding).
cache: false
cache-dependency-path: workspace-server/go.sum
- name: (local) Pre-pull alpine + ensure provisioner network
if: needs.changes.outputs.peervis == 'true'
run: |
docker pull alpine:latest >/dev/null
docker network create molecule-core-net >/dev/null 2>&1 || true
echo "alpine:latest pre-pulled; molecule-core-net ensured."
- name: (local) Start Postgres (docker, ephemeral port)
if: needs.changes.outputs.peervis == 'true'
run: |
docker rm -f "$PG_CONTAINER" 2>/dev/null || true
docker run -d --name "$PG_CONTAINER" \
-e POSTGRES_USER=dev -e POSTGRES_PASSWORD=dev -e POSTGRES_DB=molecule \
-p 127.0.0.1::5432 postgres:16 >/dev/null
PG_PORT=$(docker port "$PG_CONTAINER" 5432/tcp | awk -F: '/^(0\.0\.0\.0|127\.0\.0\.1):/ {print $2; exit}')
[ -n "$PG_PORT" ] || PG_PORT=$(docker port "$PG_CONTAINER" 5432/tcp | head -1 | awk -F: '{print $NF}')
if [ -z "$PG_PORT" ]; then
echo "::error::Could not resolve host port for $PG_CONTAINER"
docker logs "$PG_CONTAINER" || true; exit 1
fi
echo "DATABASE_URL=postgres://dev:dev@127.0.0.1:${PG_PORT}/molecule?sslmode=disable" >> "$GITHUB_ENV"
for i in $(seq 1 30); do
docker exec "$PG_CONTAINER" pg_isready -U dev >/dev/null 2>&1 && { echo "Postgres ready after ${i}s"; exit 0; }
sleep 1
done
echo "::error::Postgres did not become ready in 30s"; docker logs "$PG_CONTAINER" || true; exit 1
- name: (local) Start Redis (docker, ephemeral port)
if: needs.changes.outputs.peervis == 'true'
run: |
docker rm -f "$REDIS_CONTAINER" 2>/dev/null || true
docker run -d --name "$REDIS_CONTAINER" -p 127.0.0.1::6379 redis:7 >/dev/null
REDIS_PORT=$(docker port "$REDIS_CONTAINER" 6379/tcp | awk -F: '/^(0\.0\.0\.0|127\.0\.0\.1):/ {print $2; exit}')
[ -n "$REDIS_PORT" ] || REDIS_PORT=$(docker port "$REDIS_CONTAINER" 6379/tcp | head -1 | awk -F: '{print $NF}')
if [ -z "$REDIS_PORT" ]; then
echo "::error::Could not resolve host port for $REDIS_CONTAINER"
docker logs "$REDIS_CONTAINER" || true; exit 1
fi
echo "REDIS_URL=redis://127.0.0.1:${REDIS_PORT}" >> "$GITHUB_ENV"
for i in $(seq 1 15); do
docker exec "$REDIS_CONTAINER" redis-cli ping 2>/dev/null | grep -q PONG && { echo "Redis ready after ${i}s"; exit 0; }
sleep 1
done
echo "::error::Redis did not become ready in 15s"; docker logs "$REDIS_CONTAINER" || true; exit 1
- name: (local) Build platform
if: needs.changes.outputs.peervis == 'true'
working-directory: workspace-server
run: go build -o platform-server ./cmd/server
- name: (local) Pick platform port
if: needs.changes.outputs.peervis == 'true'
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 "PORT=${PLATFORM_PORT}" >> "$GITHUB_ENV"
echo "BASE=http://127.0.0.1:${PLATFORM_PORT}" >> "$GITHUB_ENV"
echo "Platform host port: ${PLATFORM_PORT}"
- name: (local) Kill stale platform-server before start
if: needs.changes.outputs.peervis == 'true'
run: |
killed=0
for pid in $(grep -l "platform-serve" /proc/[0-9]*/comm 2>/dev/null); do
kpid="${pid%/comm}"; kpid="${kpid##*/}"
cmdline=$(cat "/proc/${kpid}/cmdline" 2>/dev/null | tr '\0' ' ')
if echo "$cmdline" | grep -q "platform-server"; then
echo "Killing stale platform-server pid ${kpid}"
kill "$kpid" 2>/dev/null || true; killed=$((killed + 1))
fi
done
[ "$killed" -gt 0 ] && sleep 2 || true
echo "stale-kill done ($killed killed)"
- name: (local) Start platform (background)
if: needs.changes.outputs.peervis == 'true'
working-directory: workspace-server
run: |
./platform-server > platform.log 2>&1 &
echo $! > platform.pid
- name: (local) Wait for /health
if: needs.changes.outputs.peervis == 'true'
run: |
# /health binds only AFTER the full migration chain is applied on
# cold start, so a 200 is the real "migrations done + listening"
# signal. Generous wall-clock budget (chain grows each release);
# fail loud at once if the background process has exited.
DEADLINE_SECS=180
PLATFORM_PID="$(cat workspace-server/platform.pid 2>/dev/null || true)"
start=$(date +%s)
while :; do
if curl -sf "$BASE/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: (local) Run fresh-provision peer-visibility E2E (literal MCP list_peers)
# HONEST gate — NO continue-on-error. external-mode workspaces so the
# required context tests the literal MCP list_peers path without
# coupling to template container boot. Exit 10 = regression reproduced
# (the gate firing as designed) — fail-before / pass-after.
if: needs.changes.outputs.peervis == 'true'
run: bash tests/e2e/test_peer_visibility_mcp_local.sh
- name: (local) Dump platform log on failure
if: failure() && needs.changes.outputs.peervis == 'true'
run: cat workspace-server/platform.log || true
- name: (local) Stop platform
if: always() && needs.changes.outputs.peervis == 'true'
run: |
if [ -f workspace-server/platform.pid ]; then
kill "$(cat workspace-server/platform.pid)" 2>/dev/null || true
fi
- name: (local) Stop service containers
if: always() && needs.changes.outputs.peervis == 'true'
run: |
docker rm -f "$PG_CONTAINER" 2>/dev/null || true
docker rm -f "$REDIS_CONTAINER" 2>/dev/null || true
# --- no peer-visibility-relevant change: no-op pass (any event) ------
- name: No-op pass (paths filter excluded this run)
if: needs.changes.outputs.peervis != 'true'
run: |
echo "No peer-visibility-relevant changes — E2E Peer Visibility gate satisfied without running the E2E."
echo "::notice::E2E Peer Visibility no-op pass (detect-changes profile peer-visibility excluded this run)."
echo "::notice::detect-changes debug: ${{ needs.changes.outputs.debug }}"
# --- push/dispatch/cron + relevant change: the REAL staging E2E ------
# KMS wave-B (Gitea-Actions-secrets -> Infisical SSOT): source the staging
# CP admin bearer (CP_ADMIN_API_TOKEN @ staging /shared/controlplane-admin)
# and the staging MiniMax key (MINIMAX_API_KEY @ staging /shared/controlplane)
# from Infisical via the CI machine identity instead of the duplicated
# CP_STAGING_ADMIN_API_TOKEN / MOLECULE_STAGING_MINIMAX_API_KEY Gitea Actions
# secrets. Exported to $GITHUB_ENV under the same MOLECULE_ADMIN_TOKEN /
# E2E_MINIMAX_API_KEY names the downstream verify + run + teardown steps read.
#
# SECURITY / TRUSTED-REF GATING: this step carries the same
# `github.event_name != 'pull_request' && peervis == 'true'` guard as every
# other staging step in this job, so the INFISICAL_CI_* machine-identity
# secrets are NEVER present in a pull_request (fork-exfiltratable) run —
# mirroring the per-step secret-binding policy this job already enforces.
- name: Fetch staging secrets from Infisical KMS
id: kms_staging
if: github.event_name != 'pull_request' && needs.changes.outputs.peervis == 'true'
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
E2E_MINIMAX_API_KEY=$(read_secret MINIMAX_API_KEY %2Fshared%2Fcontrolplane)
echo "::add-mask::$E2E_MINIMAX_API_KEY"
if [ -z "$E2E_MINIMAX_API_KEY" ]; then
echo "::error::Infisical returned empty MINIMAX_API_KEY at staging /shared/controlplane — failing closed."
exit 1
fi
{
echo "MOLECULE_ADMIN_TOKEN=$MOLECULE_ADMIN_TOKEN"
echo "E2E_MINIMAX_API_KEY=$E2E_MINIMAX_API_KEY"
} >> "$GITHUB_ENV"
echo "Staging secrets loaded from Infisical (admin_len=${#MOLECULE_ADMIN_TOKEN}, minimax_len=${#E2E_MINIMAX_API_KEY})"
- name: Verify admin token present
if: github.event_name != 'pull_request' && needs.changes.outputs.peervis == 'true'
# MOLECULE_ADMIN_TOKEN sourced from Infisical via the "Fetch staging
# secrets from Infisical KMS" step above (exported to $GITHUB_ENV).
run: |
if [ -z "$MOLECULE_ADMIN_TOKEN" ]; then
echo "::error::CP_STAGING_ADMIN_API_TOKEN secret not set (Railway staging CP_ADMIN_API_TOKEN)"
exit 2
fi
echo "Admin token present"
- name: Verify an LLM key present
if: github.event_name != 'pull_request' && needs.changes.outputs.peervis == 'true'
env:
# E2E_MINIMAX_API_KEY sourced from Infisical via the "Fetch staging
# secrets from Infisical KMS" step above (exported to $GITHUB_ENV).
E2E_ANTHROPIC_API_KEY: ${{ secrets.MOLECULE_STAGING_ANTHROPIC_API_KEY }}
E2E_OPENAI_API_KEY: ${{ secrets.MOLECULE_STAGING_OPENAI_API_KEY }}
run: |
if [ -z "${E2E_MINIMAX_API_KEY:-}" ] && [ -z "${E2E_ANTHROPIC_API_KEY:-}" ] && [ -z "${E2E_OPENAI_API_KEY:-}" ]; then
echo "::error::No LLM provider key set — workspaces fail at boot with 'No provider API key found'. Set MOLECULE_STAGING_MINIMAX_API_KEY (or ANTHROPIC / OPENAI)."
exit 2
fi
echo "LLM key present"
- name: CP staging health preflight
if: github.event_name != 'pull_request' && needs.changes.outputs.peervis == 'true'
run: |
code=$(curl -sS -o /dev/null -w "%{http_code}" --max-time 10 "$MOLECULE_CP_URL/health")
if [ "$code" != "200" ]; then
echo "::error::Staging CP unhealthy (HTTP $code) — infra, not a workspace bug. Failing loud per feedback_fix_root_not_symptom."
exit 1
fi
echo "Staging CP healthy"
- name: Run fresh-provision peer-visibility E2E (literal MCP list_peers)
if: github.event_name != 'pull_request' && needs.changes.outputs.peervis == 'true'
env:
# MOLECULE_ADMIN_TOKEN + E2E_MINIMAX_API_KEY sourced from Infisical via
# the "Fetch staging secrets from Infisical KMS" step above (exported to
# $GITHUB_ENV).
E2E_ANTHROPIC_API_KEY: ${{ secrets.MOLECULE_STAGING_ANTHROPIC_API_KEY }}
E2E_OPENAI_API_KEY: ${{ secrets.MOLECULE_STAGING_OPENAI_API_KEY }}
run: bash tests/e2e/test_peer_visibility_mcp_staging.sh
# Belt-and-braces scoped teardown: the script installs an EXIT/INT/
# TERM trap, but if the runner itself is cancelled the trap may not
# fire. This always() step deletes ONLY the e2e-pv-<run_id> org this
# run created — never a cluster-wide sweep
# (feedback_never_run_cluster_cleanup_tests_on_live_platform). The
# admin DELETE is idempotent so double-invoking is safe;
# sweep-stale-e2e-orgs is the final net (slug starts with 'e2e-').
- name: Teardown safety net (runs on cancel/failure)
if: always() && github.event_name != 'pull_request' && needs.changes.outputs.peervis == 'true'
env:
# Sourced from Infisical (staging /shared/controlplane-admin) via the
# "Fetch staging secrets from Infisical KMS" job step above.
ADMIN_TOKEN: ${{ env.MOLECULE_ADMIN_TOKEN }}
run: |
set +e
orgs=$(curl -sS "$MOLECULE_CP_URL/cp/admin/orgs?limit=500" \
-H "Authorization: Bearer $ADMIN_TOKEN" 2>/dev/null \
| python3 -c "
import json, sys, os, datetime
run_id = os.environ.get('GITHUB_RUN_ID', '')
try:
d = json.load(sys.stdin)
except Exception:
print(''); sys.exit(0)
# ONLY sweep slugs from THIS run. e2e-pv-<YYYYMMDD>-<run_id>-...
# Sweep today AND yesterday's UTC date so a midnight-crossing run
# still matches its own slug (same bug class as the saas/canvas
# safety nets).
today = datetime.date.today()
yest = today - datetime.timedelta(days=1)
dates = (today.strftime('%Y%m%d'), yest.strftime('%Y%m%d'))
if run_id:
prefixes = tuple(f'e2e-pv-{dt}-{run_id}-' for dt in dates)
else:
prefixes = tuple(f'e2e-pv-{dt}-' for dt in dates)
orgs = d if isinstance(d, list) else d.get('orgs', [])
cands = [o['slug'] for o in orgs
if any(o.get('slug','').startswith(p) for p in prefixes)
and o.get('instance_status') not in ('purged',)]
print('\n'.join(cands))
" 2>/dev/null)
for slug in $orgs; do
echo "Safety-net teardown: $slug"
set +e
curl -sS -o /tmp/pv-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/pv-cleanup.code
set -e
code=$(cat /tmp/pv-cleanup.code 2>/dev/null || echo "000")
if [ "$code" = "200" ] || [ "$code" = "204" ]; then
echo "[teardown] deleted $slug (HTTP $code)"
else
echo "::warning::pv teardown for $slug returned HTTP $code — sweep-stale-e2e-orgs will catch it within MAX_AGE_MINUTES. Body: $(head -c 300 /tmp/pv-cleanup.out 2>/dev/null)"
fi
done
exit 0