Files
molecule-core/.gitea/workflows/e2e-staging-saas.yml
T
hongming-ceo-delegated 4a9c50ba4c
ci-required-drift / drift (pull_request) Has been cancelled
sop-checklist / all-items-acked (pull_request_target) Successful in 5s
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 27s
Block integration-tester contamination artifacts / Block staging-trigger / invalid manifest contamination (pull_request) Successful in 36s
CI / Python Lint & Test (pull_request) Successful in 40s
Concierge Creates Workspace Hermetic / Concierge Creates Workspace Hermetic (pull_request) Successful in 49s
E2E Peer Visibility (literal MCP list_peers) / detect-changes (pull_request) Successful in 37s
E2E Staging SaaS (full lifecycle) / E2E Staging Concierge user_tasks (pull_request) Successful in 32s
E2E Staging SaaS (full lifecycle) / pr-validate (pull_request) Successful in 45s
E2E Staging SaaS (full lifecycle) / E2E Staging Platform Boot (pull_request) Successful in 44s
E2E Staging SaaS (full lifecycle) / E2E Staging SaaS (pull_request) Successful in 44s
E2E Staging SaaS (full lifecycle) / E2E Staging Workspace Requests (core#2606) (pull_request) Successful in 24s
E2E Staging SaaS (full lifecycle) / E2E Staging Concierge Creates Workspace (pull_request) Successful in 24s
E2E Staging SaaS (full lifecycle) / E2E Staging Concierge Platform Agent (pull_request) Successful in 25s
E2E Staging SaaS (full lifecycle) / E2E Staging Concierge (compile+skip) (pull_request) Successful in 27s
E2E Staging SaaS (full lifecycle) / E2E Staging Plugin Install Lifecycle (pull_request) Successful in 24s
Lint curl status-code capture / Scan workflows for curl status-capture pollution (pull_request) Successful in 30s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 35s
Lint forbidden tenant-env keys / Scan for repo-host token write into tenant workspace surface (pull_request) Successful in 39s
Lint forbidden tenant-env keys / Scan workspace_secrets writers for forbidden env keys (pull_request) Successful in 41s
Lint forbidden hand-written mcp__ tool-id literals / Scan for hand-written mcp__ tool-id literals (pull_request) Successful in 37s
lint-bp-context-emit-match / lint-bp-context-emit-match (pull_request) Successful in 53s
lint-continue-on-error-tracking / lint-continue-on-error-tracking (pull_request) Successful in 1m0s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 2m26s
lint-no-coe-on-required / lint-no-coe-on-required (pull_request) Successful in 35s
lint-required-workflows-docker-host-pinned / Lint docker-host pin on docker-touching workflows (pull_request) Successful in 22s
lint-phantom-gate / lint-phantom-gate (pull_request) Failing after 51s
Lint schedule budget / Zero-cron ratchet (pull_request) Successful in 34s
Lint publish-runner timeout-minutes / Lint publish-runner timeout-minutes (pull_request) Successful in 53s
lint-staging-tenant-cd-gate-chain / lint-staging-tenant-cd-gate-chain (pull_request) Successful in 24s
lint-setup-go-cache / lint-setup-go-cache (pull_request) Successful in 25s
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 53s
CI / Detect changes (pull_request) Successful in 3m49s
Lint workflow YAML (Gitea-1.22.6-hostile shapes) / Lint workflow YAML for Gitea-1.22.6-hostile shapes (pull_request) Successful in 20s
E2E API Smoke Test / detect-changes (pull_request) Successful in 3m44s
E2E Chat / detect-changes (pull_request) Successful in 3m42s
sop-checklist / review-refire (pull_request_target) Successful in 3s
lint-env-coupling-dismissal / lint-env-coupling-dismissal (pull_request) Successful in 2m5s
gate-check-v3 / gate-check (pull_request_target) Successful in 21s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 31s
Local Provision Lifecycle E2E / Local Provision Lifecycle E2E (stub) (pull_request) Successful in 48s
audit-force-merge / audit (pull_request_target) Successful in 25s
qa-review / approved (pull_request_target) Successful in 39s
security-review / approved (pull_request_target) Successful in 34s
reserved-path-review / reserved-path-review (pull_request_target) Failing after 38s
lint-required-context-exists-in-bp / lint-required-context-exists-in-bp (pull_request) Successful in 1m53s
Lint pre-flip continue-on-error / Verify continue-on-error flips have run-log proof (pull_request) Successful in 3m1s
template-delivery-e2e / detect-changes (pull_request) Successful in 2m3s
PR Diff Guard / PR diff guard (pull_request_target) Successful in 2m2s
E2E Peer Visibility (literal MCP list_peers) / E2E Peer Visibility (pull_request) Successful in 18s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 3s
E2E Staging SaaS (full lifecycle) / Prune stale e2e DNS records (pull_request) Successful in 25s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 4s
CI / Platform (Go) (pull_request) Successful in 4s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 3s
CI / Canvas (Next.js) (pull_request) Successful in 4s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Successful in 3s
E2E Chat / E2E Chat (pull_request) Successful in 6s
template-delivery-e2e / Template-asset delivery (fresh seo-agent — config+prompts via asset channel, seo-all via plugin reconcile) (pull_request) Successful in 3s
CI / Canvas Deploy Status (pull_request) Successful in 2s
CI / all-required (pull_request) Successful in 8s
Local Provision Lifecycle E2E / Local Provision Lifecycle E2E (real image + MiniMax LLM, advisory) (pull_request) Successful in 35s
Revert premature retire of e2e-staging-saas push trigger
Restores on:push[main]. The retire (PR #3999) was wrong: these jobs only
bash -n on pull_request (E2E_REQUIRE_LIVE=0) — the LIVE staging validation is
push-only, so removing push removed live coverage, relying on the UNPROVEN
ephemeral gate. Restore coverage; retire only after the ephemeral gate is
soak-proven green AND the push-failure is confirmed dead-infra not regression.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 18:51:06 -07:00

1857 lines
110 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.
# RE-ENABLED 2026-07-03: the full-journey canvas gate runs on MOLECULES-SERVER
# (local-docker, provider='local'); EC2-only jobs stay dormant behind
# vars.E2E_EC2_ENABLED until a cloud backend returns. (Was DISABLED 2026-06-27.)
name: E2E Staging SaaS (full lifecycle)
# Ported from .github/workflows/e2e-staging-saas.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).
#
# Dedicated workflow that provisions a fresh staging org per run, exercises
# the full workspace lifecycle (register → heartbeat → A2A → delegation →
# HMA memory → activity → peers), then tears down and asserts leak-free.
#
# Why a separate workflow (not folded into ci.yml):
# - The run takes ~25-35 min (EC2 boot + cloudflared DNS + provision sweeps +
# agent bootstrap), way too slow for every PR.
# - Needs its own concurrency group so two pushes don't fight over the
# same staging org slug prefix.
# - Has its own required secrets (session cookie, admin token) that most
# PRs don't need to read.
#
# Triggers:
# - Push to main (regression guard — fires on merges to main, not on PR updates)
# - pull_request: pr-validate always posts success; real E2E step runs only
# when provisioning-critical files change (detect-changes gates the step).
# - workflow_dispatch (manual re-run from UI)
#
# NO schedule/cron: per the no-nightly-e2e rule, this gate runs per-PR +
# on merge (push) only — never on a timer. The single consolidated
# staging e2e gate is this workflow's `E2E Staging Concierge Creates
# Workspace` job (the #3347 full-journey: create-org → canvas-works →
# agent-appears → create-team → assign-work → zero-leftover-teardown,
# tests/e2e/test_staging_concierge_creates_workspace_e2e.sh), which also
# gates the prod promote. Nightly drift detection was deleted, not moved.
#
# NOTE: A separate pr-validate job handles the pull_request path so this
# workflow posts CI status for workflow-only PRs. Without it, a PR that
# only touches the workflow file has no status check (workflow only fires
# on push, not PR branches), which blocks merge under branch protection.
# The E2E step itself only runs when provisioning-critical files change —
# pr-validate always posts success, avoiding the double-fire that motivated
# the pull_request-trigger removal in PRs #516/#530.
on:
# RE-ENABLED 2026-07-03 on MOLECULES-SERVER: push/pull_request restored. The
# full-journey canvas gate (`E2E Staging Concierge Creates Workspace`) no longer
# provisions EC2 — staging tenants run on the molecules-server (local-docker,
# provider='local') backend the CP now defaults to (no-AWS directive), so that
# job needs no AWS creds and no EC2 leak-check. `E2E Staging Plugin Install
# Lifecycle` was likewise moved onto the molecules-server backend (core#182:
# plugin install now rides docker CopyToContainer into the mol-ws-* container,
# not AWS EIC) and runs on push/dispatch/cron. The remaining EC2-only jobs
# (platform-boot, concierge-user-tasks, workspace-requests, concierge-platform)
# stay DORMANT behind the `E2E_EC2_ENABLED` repo var (unset →
# skip) until a cloud EC2 backend is wired again — re-enable by setting
# vars.E2E_EC2_ENABLED='1'. (Was DISABLED 2026-06-27 when the AWS account closed.)
# The schedule/cron trigger was DELETED (no-nightly-e2e rule) — not commented.
# Trunk-based (Phase 3 of internal#81): main is the only branch.
#
# core#3081 / lint-required-no-paths: NO `paths:` filter on either push or
# pull_request. The `E2E Staging Concierge Creates Workspace` job is now in
# .gitea/required-contexts.txt (merge-blocking), and lint-required-no-paths
# rejects any required workflow that path-filters its `on:` block — a
# path-filtered required context degrades the merge gate to a silent
# indefinite pending (Gitea 1.22.6 reports it as pending, never success,
# for PRs whose diff doesn't match the glob — wedging docs-only PRs
# forever; see feedback_path_filtered_workflow_cant_be_required).
#
# Per-job gating is moved to the job level (`if:` guards below) so the
# slow provisioning jobs (e2e-staging-saas, e2e-staging-platform-boot)
# still skip on docs-only PRs, but the workflow ITSELF is unconditional.
#
# NOTE (2026-07-11): a premature RFC-Phase-4 retire of this push[main] trigger
# was REVERTED. The jobs here only `bash -n` on pull_request (E2E_REQUIRE_LIVE=0);
# the LIVE staging run is push-only. So this push trigger IS the live coverage —
# it cannot be retired until the per-PR ephemeral gate is PROVEN green (soak) and
# the current push-failure is confirmed to be dead-infra, not a real regression.
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
# Per-SHA grouping (mirrors e2e-api.yml / local-provision-e2e.yml). This job
# is a status-reporting deploy gate, NOT a shared-fleet mutator: every run
# provisions its OWN isolated org (slug `e2e-<date>-<run_id>`, see E2E_RUN_ID
# below), so two commits' runs never corrupt each other's data. A STATIC group
# would make a newer main push cancel the older commit's in-flight run — and on
# Gitea 1.22.6 a superseded same-group run is CANCELLED (not queued) even with
# cancel-in-progress:false (status-reaper.yml lines 69-72, DB-verified
# 2026-05-12), stamping a cancel-red that wedges the `[*]` merge queue. Per-SHA
# gives each commit its own group so neither cancels the other. cancel-in-
# progress:false keeps a teardown from being aborted mid-run (orphan-leak guard).
# The finite staging org-creation quota is a throttling cost, not a corruption
# risk (isolated orgs), so it does NOT justify the static-group cancel-red.
concurrency:
group: e2e-staging-saas-${{ github.event.pull_request.head.sha || github.sha }}
cancel-in-progress: false
env:
GITHUB_SERVER_URL: https://git.moleculesai.app
jobs:
# PR-validation path: always posts success so branch protection can merge
# workflow-only PRs. The actual E2E step only runs when provisioning-
# critical files change (git-paths filter + if: guard below).
# All steps use continue-on-error: true so runner issues do not block merge.
# bp-exempt: informational PR-shape validation job; not a required gate.
pr-validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 1
# mc#2654: pre-existing continue-on-error mask; root-fix and remove, do not renew silently.
continue-on-error: true
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.11"
# mc#2654: pre-existing continue-on-error mask; root-fix and remove, do not renew silently.
continue-on-error: true
- name: YAML validation (best-effort)
run: |
echo "e2e-staging-saas.yml — PR validation: workflow YAML is valid."
echo "E2E step runs only when provisioning-critical files change."
# mc#2654: pre-existing continue-on-error mask; root-fix and remove, do not renew silently.
continue-on-error: true
# Actual E2E: runs on trunk pushes and PRs that touch provisioning-critical
# paths. pr-validate remains as the lightweight workflow-shape check for PRs,
# but it is not a substitute for live staging proof when this workflow or the
# staging harness changes.
# bp-exempt: full-lifecycle lane on molecules-server (local-docker); EC2 is a one-line knob (vars.E2E_EC2_ENABLED). Not a required gate.
e2e-staging-saas:
name: E2E Staging SaaS
runs-on: ubuntu-latest
# UN-PINNED from EC2 onto MOLECULES-SERVER (local-docker, provider='local'):
# the job-level `if: vars.E2E_EC2_ENABLED == '1' && (...)` guard is REMOVED so
# this provider-agnostic scenario (test_staging_full_saas.sh drives the CP admin
# API; the backend is the CP's provider config) runs on push against the working
# local-docker backend instead of being EC2-gated-skipped. On pull_request it
# self-checks green (E2E_REQUIRE_LIVE=0 + per-step PR gating below). EC2 remains
# a one-line knob via the E2E_AWS_LEAK_CHECK provider knob in the env block.
# 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
# Raised 45→75: step 10b now exercises pause→resume→online +
# hibernate→wake→online, each of which RE-PROVISIONS the parent (CP
# re-provision + heartbeat recovery, not a fresh EC2 cold start, but still
# minutes). The base provision→online→A2A matrix fits in ~35 min; the two
# extra lifecycle reprovisions need headroom under WORKSPACE_ONLINE_TIMEOUT.
timeout-minutes: 75
permissions:
contents: read
env:
MOLECULE_CP_URL: https://staging-api.moleculesai.app
# Single admin-bearer secret drives provision + tenant-token
# retrieval + teardown. Configure in
# Settings → Secrets and variables → Actions → Repository secrets.
# 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
# secrets 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 Verify-admin-token gate still
# validates MOLECULE_ADMIN_TOKEN is non-empty.
# AWS creds stay wired (empty/unused when the secret is unset + leak=off) so
# enabling EC2 is one line — set repo var E2E_EC2_ENABLED=1 (+ AWS_* secrets).
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: us-east-2
# provider knob: default molecules-server (local-docker, leak-check no-op);
# set repo var E2E_EC2_ENABLED=1 (+ AWS_* secrets, already wired below) to run
# the SAME scenario against an EC2-backed staging CP with the leak-check
# enforced — one-line enable.
E2E_AWS_LEAK_CHECK: ${{ vars.E2E_EC2_ENABLED == '1' && 'required' || 'off' }}
E2E_AWS_TERMINATE_LEAKS: '1'
# MiniMax is the PRIMARY LLM auth path post-2026-05-04. Switched
# from hermes+OpenAI default after #2578 (the staging OpenAI key
# account went over quota and stayed dead for 36+ hours, taking
# the full-lifecycle E2E red on every provisioning-critical push).
# claude-code template's `minimax` provider routes
# ANTHROPIC_BASE_URL to api.minimax.io/anthropic and reads
# MINIMAX_API_KEY at boot — separate billing account so an
# OpenAI quota collapse no longer wedges the gate. Mirrors the
# staging-smoke.yml + continuous-synth-e2e.yml migrations.
# E2E_MINIMAX_API_KEY now arrives via $GITHUB_ENV from the "Fetch staging
# secrets from Infisical KMS" step below (wave-B staging consumers
# migration). It is sourced from Infisical MINIMAX_API_KEY at
# staging /shared/controlplane. The old MOLECULE_STAGING_MINIMAX_API_KEY
# Gitea Actions secret is left in place until a green run validates this
# path (validate-before-delete).
# Direct-Anthropic alternative for operators who don't want to
# set up a MiniMax account (priority below MiniMax — first
# non-empty wins in test_staging_full_saas.sh's secrets-injection
# block). See #2578 PR comment for the rationale.
E2E_ANTHROPIC_API_KEY: ${{ secrets.MOLECULE_STAGING_ANTHROPIC_API_KEY }}
# OpenAI fallback — kept wired so an operator-dispatched run with
# E2E_RUNTIME=hermes or =codex via workflow_dispatch can still
# exercise the OpenAI path.
E2E_OPENAI_API_KEY: ${{ secrets.MOLECULE_STAGING_OPENAI_API_KEY }}
# google-adk (operator-dispatched only) auths Gemini with an
# AI-Studio key. Org policy disallows API keys in PROD (Vertex+ADC
# there); CI uses the keyed AI-Studio path with config model
# google_genai:gemini-2.5-pro. Vertex remains the supported prod path.
E2E_GOOGLE_API_KEY: ${{ secrets.MOLECULE_STAGING_GOOGLE_API_KEY }}
E2E_RUNTIME: ${{ github.event.inputs.runtime || 'claude-code' }}
# Pin the model when running on the default claude-code path —
# the per-runtime default ("sonnet") routes to direct Anthropic
# and defeats the cost saving. Operators can override via the
# workflow_dispatch flow (no input wired here yet — runtime
# override is enough for ad-hoc).
#
# claude-code MiniMax slug must be the BARE registered id `MiniMax-M2.7`.
# It is the BYOK-minimax form: registry_gen.go:88 registers it on the
# `minimax` arm (resolves provider=minimax via MINIMAX_API_KEY), so the
# #1994 byok-not-platform guard still passes. The COLON form
# `minimax:MiniMax-M2.7` is UNREGISTERED on claude-code (internal#718;
# derive_provider_matrix_test.go:288) — the claude-code adapter can't
# strip the `minimax:` prefix, so workspace-create 422s
# UNREGISTERED_MODEL_FOR_RUNTIME (real failure: job 295233, main 4b3590e3).
# The slash form `minimax/MiniMax-M2.7` is the platform-billed arm and
# would trip the byok guard. #2311 fixed the same colon-vs-bare bug in the
# pick_model_slug lib (tests/e2e/lib/model_slug.sh), but this env var
# OVERRIDES that lib, so the bare fix has to live here too.
E2E_MODEL_SLUG: ${{ github.event.inputs.runtime == 'hermes' && 'openai/gpt-4o' || github.event.inputs.runtime == 'codex' && 'openai/gpt-4o' || github.event.inputs.runtime == 'google-adk' && 'google_genai:gemini-2.5-pro' || 'MiniMax-M2.7' }}
E2E_RUN_ID: "${{ github.run_id }}-${{ github.run_attempt }}"
E2E_KEEP_ORG: ${{ github.event.inputs.keep_org && '1' || '0' }}
# Lifecycle transitions (step 10b): pause→resume→online +
# hibernate→wake→online on the provisioned parent. `auto` runs them in
# full mode (this job). Set `off` to skip the ~2x-reprovision cost on an
# ad-hoc dispatch. The timeout-minutes above is sized for this being on.
E2E_LIFECYCLE: auto
# Fail-closed-on-skip, gated by event (mirrors e2e-staging-concierge-creates-workspace):
# pull_request: 0 → PRs carry no staging creds; the harness runs a bash -n
# self-check and exit-0s green (the KMS creds-fetch below is
# push-only, so the token stays empty on PR).
# push / dispatch / schedule: 1 → the harness MUST prove ≥1 full
# provision→online→A2A cycle live on molecules-server, else it
# exits 5 rather than reporting a false green. Mirrors CP
# serving-e2e SERVING_E2E_REQUIRE_LIVE.
E2E_REQUIRE_LIVE: ${{ github.event_name == 'pull_request' && '0' || '1' }}
steps:
- 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)
# 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 verify + run + teardown steps already read.
#
# SECURITY (public repo): the KMS live-creds fetch runs ONLY on push /
# workflow_dispatch / schedule — NEVER on pull_request, so the INFISICAL_CI_*
# machine-identity secrets can never be reached with forked PR code in the
# tree. On pull_request this step is SKIPPED: the token stays empty, the
# Verify-admin-token gate exits 0 (PR-mode) and the harness self-checks green.
- name: Fetch staging secrets from Infisical KMS
id: kms_staging
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' || github.event_name == 'schedule'
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
run: |
# PR-mode: the KMS creds-fetch above is push-only, so on pull_request the
# token is EXPECTED empty and green — the harness runs its bash -n
# self-check and exit-0s. A missing token on push/dispatch/schedule stays
# a hard error. Leak-check is a no-op on molecules-server (leak=off), so
# AWS creds are only required when E2E_EC2_ENABLED=1 pins leak=required.
if [ -z "$MOLECULE_ADMIN_TOKEN" ]; then
if [ "${{ github.event_name }}" = "pull_request" ]; then
echo "PR-mode (E2E_REQUIRE_LIVE=0): no MOLECULE_ADMIN_TOKEN — harness will self-check (bash -n) and exit 0 green ✓"
exit 0
fi
echo "::error::CP_STAGING_ADMIN_API_TOKEN secret not set (Railway staging CP_ADMIN_API_TOKEN)"
exit 2
fi
if [ "${E2E_AWS_LEAK_CHECK}" = "required" ]; then
for var in AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY; do
if [ -z "${!var:-}" ]; then
echo "::error::$var not set — EC2 leak verification (E2E_AWS_LEAK_CHECK=required) cannot run"
exit 2
fi
done
fi
echo "Admin token present ✓"
- name: Verify LLM key present
# PR-mode: the LLM keys arrive via the push-only KMS fetch (MiniMax) /
# secrets (Anthropic/OpenAI); on pull_request they are empty and this live
# preflight is skipped. The harness self-check (bash -n) is the PR gate.
if: github.event_name != 'pull_request'
run: |
# Per-runtime key check — claude-code uses MiniMax; hermes /
# codex (operator-dispatched only) use OpenAI. Hard-fail
# rather than soft-skip per #2578's lesson — empty key
# silently falls through to the wrong SECRETS_JSON branch and
# produces a confusing auth error 5 min later instead of the
# clean "secret missing" message at the top.
case "${E2E_RUNTIME}" in
claude-code)
# Either MiniMax OR direct-Anthropic works — first
# non-empty wins in the test script's secrets-injection
# priority chain.
if [ -n "${E2E_MINIMAX_API_KEY:-}" ]; then
required_secret_name="MOLECULE_STAGING_MINIMAX_API_KEY"
required_secret_value="${E2E_MINIMAX_API_KEY}"
elif [ -n "${E2E_ANTHROPIC_API_KEY:-}" ]; then
required_secret_name="MOLECULE_STAGING_ANTHROPIC_API_KEY"
required_secret_value="${E2E_ANTHROPIC_API_KEY}"
else
required_secret_name="MOLECULE_STAGING_MINIMAX_API_KEY or MOLECULE_STAGING_ANTHROPIC_API_KEY"
required_secret_value=""
fi
;;
codex|hermes)
required_secret_name="MOLECULE_STAGING_OPENAI_API_KEY"
required_secret_value="${E2E_OPENAI_API_KEY:-}"
;;
google-adk)
required_secret_name="MOLECULE_STAGING_GOOGLE_API_KEY"
required_secret_value="${E2E_GOOGLE_API_KEY:-}"
;;
*)
echo "::warning::Unknown E2E_RUNTIME='${E2E_RUNTIME}' — skipping LLM-key check"
required_secret_name=""
required_secret_value="present"
;;
esac
if [ -n "$required_secret_name" ] && [ -z "$required_secret_value" ]; then
echo "::error::${required_secret_name} secret not set for runtime=${E2E_RUNTIME} — workspaces will fail at boot with 'No provider API key found'"
exit 2
fi
echo "LLM key present ✓ (runtime=${E2E_RUNTIME}, key=${required_secret_name}, len=${#required_secret_value})"
- name: CP staging health preflight
# PR-mode: no live staging call on PRs (they self-check only). Runs on
# push/dispatch/schedule, where the real provision follows.
if: github.event_name != 'pull_request'
run: |
# Poll to a stable Nx200 streak; a cold-start / mid-recreate CP
# (000/5xx/non-200) is a RETRY, not a canary red (mc#3244).
source tests/e2e/lib/cp_health_preflight.sh
cp_health_preflight "$MOLECULE_CP_URL" "workspace"
# core#2675: completion-gated lanes must distinguish "staging LLM proxy
# down" from "real code bug" with a distinct, machine-readable status
# description prefix `DEP-DOWN:staging-llm` so the redgate-reporter
# can dedup N identical reds into ONE incident issue. Wired into the
# pr-validate job first; the same source line is replicated in the
# other 3 completion-gated lanes in a follow-up commit (the file's
# 5 nearly-identical job blocks are mechanically derivable).
- name: LLM proxy preflight (DEP-DOWN:staging-llm)
# PR-mode: live LLM-proxy probe skipped on PRs (self-check only).
if: github.event_name != 'pull_request'
run: |
# shellcheck source=lib/llm_proxy_preflight.sh
source tests/e2e/lib/llm_proxy_preflight.sh
llm_proxy_preflight
# No `if:` — on pull_request the harness runs its bash -n self-check
# (E2E_REQUIRE_LIVE=0 + empty token) and exit-0s green; on push/dispatch/
# schedule it runs the real live journey on molecules-server. Mirrors the
# exemplar e2e-staging-concierge-creates-workspace Run step.
- name: Run full-lifecycle E2E
id: e2e
# No step-level `if:` — this runs LIVE on push/dispatch/schedule against
# the molecules-server (local-docker) backend. The #3965 EC2-gate that
# skipped this step off molecules-server is REVERTED: the harness now
# EC2-gates only the EC2/EIC-infra sub-steps (7b terminal-diagnose, 7c
# EIC file-write) internally, so the provider-agnostic journey
# (provision → online → routable → A2A → tool-use) runs live again. On
# pull_request the harness still self-checks (E2E_REQUIRE_LIVE=0 +
# bash -n) → green. Mirrors the exemplar concierge-creates-workspace
# Run step.
run: bash tests/e2e/test_staging_full_saas.sh
# Belt-and-braces teardown: the test script itself installs a trap
# for EXIT/INT/TERM, but if the GH runner itself is cancelled (e.g.
# someone pushes a new commit and workflow concurrency is set to
# cancel), the trap may not fire. This `always()` step runs even on
# cancellation and attempts the delete a second time. The admin
# DELETE endpoint is idempotent so double-invoking is safe.
- name: Teardown safety net (runs on cancel/failure)
# PR-mode: never curl live staging from a PR (no token; public repo).
if: always() && github.event_name != 'pull_request'
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: |
# Best-effort: find any e2e-YYYYMMDD-* orgs matching this run and
# nuke them. Catches the case where the script died before
# exporting its slug.
set +e
orgs=$(curl -sS "$MOLECULE_CP_URL/cp/admin/orgs" \
-H "Authorization: Bearer $ADMIN_TOKEN" 2>/dev/null \
| python3 -c "
import json, sys, os, datetime
run_id = os.environ.get('GITHUB_RUN_ID', '')
d = json.load(sys.stdin)
# ONLY sweep slugs from *this* CI run. Previously the filter was
# f'e2e-{today}-' which stomped on parallel CI runs AND any manual
# E2E probes a dev was running against staging (incident 2026-04-21
# 15:02Z: this workflow's safety net deleted an unrelated manual
# run's tenant 1s after it hit 'running').
# Sweep both today AND yesterday's UTC dates so a run that crosses
# midnight still matches its own slug — see the 2026-04-26→27
# canvas-safety-net incident for the same bug class.
today = datetime.date.today()
yesterday = today - datetime.timedelta(days=1)
dates = (today.strftime('%Y%m%d'), yesterday.strftime('%Y%m%d'))
if run_id:
prefixes = tuple(f'e2e-{d}-{run_id}-' for d in dates)
else:
prefixes = tuple(f'e2e-{d}-' for d in dates)
candidates = [o['slug'] for o in d.get('orgs', [])
if any(o.get('slug','').startswith(p) for p in prefixes)
and o.get('instance_status') not in ('purged',)]
print('\n'.join(candidates))
" 2>/dev/null)
# Per-slug verified DELETE (was `>/dev/null || true` — see
# molecule-controlplane#420). Surface non-2xx as a workflow
# warning naming the leaked slug; don't exit 1 (sweeper is
# the safety net within ~45 min).
leaks=()
for slug in $orgs; do
echo "Safety-net teardown: $slug"
# 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/saas-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/saas-cleanup.code
set -e
code=$(cat /tmp/saas-cleanup.code 2>/dev/null || echo "000")
if [ "$code" = "200" ] || [ "$code" = "204" ]; then
echo "[teardown] deleted $slug (HTTP $code)"
else
echo "::warning::saas teardown for $slug returned HTTP $code — sweep-stale-e2e-orgs will catch it within ~45 min. Body: $(head -c 300 /tmp/saas-cleanup.out 2>/dev/null)"
leaks+=("$slug")
fi
done
if [ ${#leaks[@]} -gt 0 ]; then
echo "::warning::saas teardown left ${#leaks[@]} leak(s): ${leaks[*]}"
fi
exit 0
# ── POST-RUN DNS PRUNE (core#81045 recurrence fix) ───────────────────────────
#
# The full-lifecycle E2E harness creates e2e-smoke-* and e2e-tmpl-* DNS
# records under the staging zone. When teardown is skipped (runner cancel,
# CP transient error, trap miss) these records leak and eventually exhaust
# the Cloudflare DNS record quota (error 81045), blocking staging tenant
# provisioning. This job runs after every real E2E run and prunes records
# older than a conservative age threshold.
#
# Design notes:
# - needs: e2e-staging-saas so it only runs when the real E2E job fires
# (push/dispatch/cron), not on PRs.
# - if: always() so it runs even when the E2E job fails or is cancelled,
# which is exactly when records are most likely to leak.
# - continue-on-error: true — pruning is best-effort janitorial cleanup;
# a transient CF API blip here must not block the merge gate. The
# sweep-stale-e2e-orgs workflow is the backstop.
# - Token and zone id come from repository secrets ONLY; never hardcoded.
# - --min-age-hours is conservative (2h) so in-flight records from a long
# E2E run or a recently-started dispatch are never touched.
# (non-required / best-effort cleanup job. mc#3147 = decision issue;
# mc#3173 = current governance tracker.)
# bp-exempt: best-effort DNS cleanup (runs on always()); not a required gate.
prune-stale-e2e-dns:
name: Prune stale e2e DNS records
runs-on: ubuntu-latest
needs: e2e-staging-saas
if: always()
# mc#3760: governance tracker — best-effort cleanup, transient CF API
# failures must not block the merge gate. Per mc#3147 (the decision
# issue), the fail-soft mask is KEEP until a durable idempotent
# retry/backoff fix lands. Tracker mc#3760 expires 14 days after 2026-07-10.
continue-on-error: true
timeout-minutes: 10
permissions:
contents: read
env:
# CF_API_TOKEN / CF_ZONE_ID now arrive via $GITHUB_ENV from the "Fetch
# CF_API_TOKEN + CF_ZONE_ID from Infisical SSOT" step below (wave-B
# aliases migration). They come from STAGING /shared/cloudflare-tunnel-admin
# (CLOUDFLARE_API_TOKEN = zone:dns:edit token, CLOUDFLARE_ZONE_ID = the
# moleculesai.app apex zone that hosts the staging.moleculesai.app records).
# The old CF_STAGING_DNS_API_TOKEN + CF_STAGING_ZONE_ID Gitea Actions
# secrets are left in place until a green run validates this path
# (validate-before-delete). On an untrusted-fork PR the fetch step skips
# and these stay empty, so the existing dry-run-preview soft-skip below
# fires exactly as before — PR behavior is unchanged.
#
# Staging tenant DNS records live under staging.moleculesai.app, not the
# apex zone, so the prefix matcher must anchor to the staging subdomain.
PRUNE_ZONE_DOMAIN: staging.moleculesai.app
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
# KMS wave-B (Gitea-Actions-secrets -> Infisical SSOT): source the staging
# zone-scoped Cloudflare DNS token + zone id from Infisical (env=staging
# /shared/cloudflare-tunnel-admin) via the CI machine identity instead of
# the duplicated CF_STAGING_DNS_API_TOKEN / CF_STAGING_ZONE_ID Gitea
# Actions secrets. Exported to $GITHUB_ENV under the same CF_API_TOKEN /
# CF_ZONE_ID names the prune steps already read, so nothing downstream
# changes.
#
# SAFE-ENABLE (Guard B / regression-prevention-guards.md §Guard F): this
# KMS live-creds fetch runs ONLY on push / workflow_dispatch / schedule —
# NEVER on pull_request. It MIRRORS the #3464 PR-mode guard on the concierge
# job's "Fetch staging secrets from Infisical KMS" step. The prior
# `(pull_request && fork==false)` clause treated same-repo PRs as trusted
# and drove the live Infisical fetch + `--apply` DNS prune on every same-repo
# PR (and could exit 1 on any Infisical/CF blip), reddening this job and
# wedging the `[*]` all-green queue once this required workflow is
# re-enabled. The live prune is post-run janitorial cleanup that only
# follows a real E2E run (push/dispatch/schedule), so on pull_request this
# step is simply SKIPPED: CF_API_TOKEN/CF_ZONE_ID stay empty, the
# dry-run-preview soft-skip below fires (exit 0), the prune step is gated
# off, and the job is cheap + green. Because the step never runs with an
# untrusted-fork PR's code in the tree, the CI Infisical machine-identity
# secrets are also safe here.
- name: Fetch CF_API_TOKEN + CF_ZONE_ID from Infisical SSOT
id: cf_staging_dns_creds
# Flattened to a single physical line: Gitea act_runner may not treat
# embedded YAML newlines in a folded `if:` as whitespace (push-path eval risk).
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' || github.event_name == 'schedule'
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
# This step only runs on push / workflow_dispatch / schedule (see the
# step-level `if:` above), so it never executes with an untrusted-fork
# PR's code in the tree — the CI Infisical machine-identity is safe to
# read here. On pull_request the step is skipped entirely (cheap green).
# If the identity is somehow missing on a trusted ref, soft-skip
# (best-effort janitor never blocks the gate).
if [ -z "${INFISICAL_CI_CLIENT_ID:-}" ] || [ -z "${INFISICAL_CI_CLIENT_SECRET:-}" ] || [ -z "${INFISICAL_PROJECT_ID:-}" ]; then
echo "::warning::Infisical CI identity secrets missing on a trusted ref — prune will soft-skip (best-effort janitor, never blocks the gate)."
exit 0
fi
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; v=json.load(sys.stdin).get("accessToken"); sys.stdout.write(v if isinstance(v,str) 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; v=json.load(sys.stdin).get("secret",{}).get("secretValue"); sys.stdout.write(v if isinstance(v,str) else "")'
}
CF_API_TOKEN=$(read_secret CLOUDFLARE_API_TOKEN %2Fshared%2Fcloudflare-tunnel-admin)
echo "::add-mask::$CF_API_TOKEN"
if [ -z "$CF_API_TOKEN" ]; then
echo "::error::Infisical returned empty CLOUDFLARE_API_TOKEN at staging /shared/cloudflare-tunnel-admin — failing closed."
exit 1
fi
CF_ZONE_ID=$(read_secret CLOUDFLARE_ZONE_ID %2Fshared%2Fcloudflare-tunnel-admin)
echo "::add-mask::$CF_ZONE_ID"
if [ -z "$CF_ZONE_ID" ]; then
echo "::error::Infisical returned empty CLOUDFLARE_ZONE_ID at staging /shared/cloudflare-tunnel-admin — failing closed."
exit 1
fi
{
echo "CF_API_TOKEN=$CF_API_TOKEN"
echo "CF_ZONE_ID=$CF_ZONE_ID"
} >> "$GITHUB_ENV"
echo "CF_API_TOKEN + CF_ZONE_ID loaded from Infisical staging SSOT (token_len=${#CF_API_TOKEN}, zone_len=${#CF_ZONE_ID})"
- name: Dry-run preview (read-only)
if: env.CF_API_TOKEN == '' || env.CF_ZONE_ID == ''
run: |
echo "::warning::CF_STAGING_DNS_API_TOKEN or CF_STAGING_ZONE_ID not configured — skipping DNS prune."
exit 0
- name: Prune stale e2e DNS records
if: env.CF_API_TOKEN != '' && env.CF_ZONE_ID != ''
run: |
set -euo pipefail
set +e
./scripts/ops/prune_cf_e2e_dns.sh --apply --min-age-hours 2
rc=$?
set -e
if [ "$rc" -ne 0 ]; then
echo "::warning::best-effort Cloudflare DNS prune failed with rc=$rc; leaving cleanup to sweep-stale-e2e-orgs/manual janitor and keeping this non-gating job green."
fi
exit 0
# ── PLATFORM-MANAGED BOOT REGRESSION (moonshot/kimi NOT_CONFIGURED) ──────────
#
# The REAL-boot complement to the deterministic unit suite
# (workspace_provision_platform_boot_test.go). Provisions a REAL staging
# claude-code workspace on the PLATFORM-managed path — provider=platform,
# model=moonshot/kimi-k2.6, NO tenant LLM key — and asserts it reaches
# status=online (NOT not_configured) and a completion returns 200, via the same
# online-wait + completion-assert the BYOK job uses.
#
# Why a SEPARATE job (not a matrix leg of e2e-staging-saas): the platform path
# injects NO secret and pins a different model, so its env block diverges from
# the BYOK job's. A dedicated job keeps each path's "verify key present" preflight
# honest (BYOK requires a key; platform requires its ABSENCE not to matter) and
# gives the regression its own named commit-status for branch protection.
#
# GATING (no continue-on-error), FALSE-GREEN-PROOF via E2E_REQUIRE_LIVE
# (0 on pull_request → PR-mode self-check; 1 on push/dispatch/cron → real
# staging boot, HARD FAILs on missing infra). Promoted to merge-blocking
# per #48 — RCA: molecule-controlplane PR #878 rendered the tenant
# `docker run` env block with a blank line that broke shell `\`-continuation,
# orphaning the image arg → `docker run exit=127` → no tenant container →
# prod onboarding outage 06:0408:09 UTC 2026-06-21 (fixed by CP #885). The
# real-boot e2e being advisory + PR-skipped is why that class escaped
# pre-merge. This job now exercises a real platform-managed boot on every
# push-to-main and is the merge gate.
#
# (formerly bp-required; now EC2-dormant behind vars.E2E_EC2_ENABLED — the
# authoritative directive is the bp-exempt line directly above the job key.)
#
# core#3081 / #48: NO `if:` guard on this job (mirrors
# e2e-staging-concierge-creates-workspace). The job IS a required status
# context (see .gitea/required-contexts.txt); a required context that never
# fires on pull_request degrades the merge gate to a silent indefinite
# pending (the exact failure mode lint-required-no-paths exists to prevent;
# see feedback_path_filtered_workflow_cant_be_required). The job runs on
# every PR with E2E_REQUIRE_LIVE=0 (the harness detects the missing-creds
# case and exit 0s after a bash -n self-check), and on push/dispatch/cron
# with E2E_REQUIRE_LIVE=1 (the real staging boot runs and HARD FAILs on
# missing infra).
# bp-exempt: platform-managed boot lane on molecules-server (local-docker); EC2 is a one-line knob (vars.E2E_EC2_ENABLED). Not a required gate.
e2e-staging-platform-boot:
name: E2E Staging Platform Boot
runs-on: ubuntu-latest
# UN-PINNED from EC2 onto MOLECULES-SERVER (local-docker, provider='local'):
# the job-level `if: vars.E2E_EC2_ENABLED == '1'` guard is REMOVED so this
# provider-agnostic platform-managed-boot scenario runs on push against the
# working local-docker backend instead of being EC2-gated-skipped. On
# pull_request it self-checks green (E2E_REQUIRE_LIVE=0 + per-step PR gating).
# EC2 remains a one-line knob via the E2E_AWS_LEAK_CHECK provider knob below.
timeout-minutes: 45
permissions:
contents: read
env:
MOLECULE_CP_URL: https://staging-api.moleculesai.app
# MOLECULE_ADMIN_TOKEN now arrives via $GITHUB_ENV from the "Fetch platform
# model+runtime from KMS SSOT" step below (wave-B staging consumers
# migration extends that same fetch). 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). On untrusted-fork
# PRs the KMS fetch is skipped and MOLECULE_ADMIN_TOKEN stays empty; the
# PR-mode (E2E_REQUIRE_LIVE=0) Verify-admin-token gate exits 0 as before.
# AWS creds stay wired (empty/unused when the secret is unset + leak=off) so
# enabling EC2 is one line — set repo var E2E_EC2_ENABLED=1 (+ AWS_* secrets).
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: us-east-2
# provider knob: default molecules-server (local-docker, leak-check no-op);
# set repo var E2E_EC2_ENABLED=1 (+ AWS_* secrets, already wired below) to run
# the SAME scenario against an EC2-backed staging CP with the leak-check
# enforced — one-line enable.
E2E_AWS_LEAK_CHECK: ${{ vars.E2E_EC2_ENABLED == '1' && 'required' || 'off' }}
E2E_AWS_TERMINATE_LEAKS: '1'
# The regression combo: claude-code + platform-managed + moonshot/kimi-k2.6.
# NO E2E_*_API_KEY is set — platform-managed billing is owned by Molecule via
# the CP LLM proxy. The harness's E2E_LLM_PATH=platform branch sends empty
# secrets and pin-selects the platform model.
#
# DE-HARDCODE (KMS SSOT, behavior-neutral): the platform model+runtime are no
# longer literals here. On a TRUSTED ref the "Fetch platform model+runtime
# from KMS SSOT" step below reads MOLECULE_LLM_DEFAULT_MODEL -> $E2E_MODEL ->
# $E2E_DEFAULT_PLATFORM_MODEL and MOLECULE_DEFAULT_RUNTIME -> $E2E_RUNTIME
# from Infisical (env=staging, path /shared/controlplane/llm) into
# $GITHUB_ENV, OVERRIDING the literals below at step-runtime. The staging KMS
# values are IDENTICAL to the old literals (model=moonshot/kimi-k2.6,
# runtime=claude-code), so this changes no behavior — it only moves the
# source of truth. These two literals remain as the FALLBACK for the paths
# the KMS step intentionally skips (untrusted-fork PR / local; those do NOT
# boot live — E2E_REQUIRE_LIVE=0), where the harness falls back to the shared
# platform default that MIRRORS the SSOT (minimax/MiniMax-M2.7) via
# pick_model_slug's ${E2E_DEFAULT_PLATFORM_MODEL:-minimax/MiniMax-M2.7}.
E2E_RUNTIME: claude-code
E2E_LLM_PATH: platform
# Smoke mode: a single parent workspace is enough to prove online +
# completion for the platform path (the A2A/delegation matrix is the BYOK
# job's job). Override E2E_DEFAULT_PLATFORM_MODEL via workflow_dispatch to
# exercise another platform model id.
E2E_MODE: smoke
E2E_RUN_ID: "platform-${{ github.run_id }}-${{ github.run_attempt }}"
E2E_KEEP_ORG: ${{ github.event.inputs.keep_org && '1' || '0' }}
# Fail-closed-on-skip (see BYOK job + e2e-staging-concierge-creates-workspace).
# smoke mode still runs steps 2/4/7/8b, so all four required milestones
# (provisioned/tenant_online/workspace_online/a2a_roundtrip) fire — the guard
# is valid for this lane too.
# pull_request: 0 → PRs have no staging creds; the harness's PR-mode
# self-check (bash -n) is the gate, then exit 0.
# push / dispatch / schedule: 1 → the real staging boot runs and HARD
# FAILs (exit 5) on a run that proves no live lifecycle.
E2E_REQUIRE_LIVE: ${{ github.event_name == 'pull_request' && '0' || '1' }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
# KMS SSOT (de-hardcode, behavior-neutral): source the platform model and
# runtime from Infisical (env=staging, path /shared/controlplane/llm) instead
# of the literals in this job's env block, mirroring the
# molecule-controlplane bench-provision-time.yml bootstrap pattern. Reads
# BOTH keys in one step:
# MOLECULE_LLM_DEFAULT_MODEL -> $E2E_MODEL (and $E2E_DEFAULT_PLATFORM_MODEL,
# which pick_model_slug consumes on the
# E2E_LLM_PATH=platform arm)
# MOLECULE_DEFAULT_RUNTIME -> $E2E_RUNTIME
# Both are masked and exported to $GITHUB_ENV so later steps (incl. the
# harness) read them as ordinary masked env vars, OVERRIDING the job-env
# literals. The staging KMS values equal the old literals
# (model=moonshot/kimi-k2.6, runtime=claude-code), so behavior is unchanged.
#
# TRUSTED-REF GATE (molecule-core is a PUBLIC repo): this step reads the CI
# Infisical machine-identity secrets, so it must NEVER run with an
# untrusted-fork PR's code in the tree (the checked-out code could exfiltrate
# the fetched values). 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 — the job then uses the in-file literal
# fallback (== the same KMS values), so the de-hardcode is invisible there.
# Mirrors the trusted-ref gate in mcp-plugin-delivery-contract-drift.yml.
- name: Fetch platform model+runtime from KMS SSOT
id: kms_llm_defaults
# SECURITY (public repo): the KMS live-creds fetch runs ONLY on push /
# workflow_dispatch / schedule — NEVER on pull_request (not even same-repo
# PRs), so the INFISICAL_CI_* machine-identity can never be reached with a
# PR's code in the tree. On pull_request this step is SKIPPED; the job then
# runs the harness PR-mode self-check (E2E_REQUIRE_LIVE=0) and exit-0s green.
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' || github.event_name == 'schedule'
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
# Trusted-ref gate: only protected-branch push / dispatch / schedule, or
# a same-repo (non-fork) PR, may read the CI KMS identity. Untrusted-fork
# PRs skip and fall back to the in-file literals (behavior-identical).
# NOTE: the step-level `if:` above already excludes ALL pull_request
# events; this inner case is retained as defense-in-depth.
case "${{ github.event_name }}" in
push|workflow_dispatch|schedule)
is_trusted=true
;;
pull_request)
if [ "${{ github.event.pull_request.head.repo.fork }}" = "false" ]; then
is_trusted=true
else
is_trusted=false
fi
;;
*)
is_trusted=false
;;
esac
if [ "$is_trusted" != "true" ]; then
echo "Untrusted ref (${{ github.event_name }}, fork PR) — skipping KMS fetch; using in-file literal fallback (== KMS values)."
exit 0
fi
if [ -z "${INFISICAL_CI_CLIENT_ID:-}" ] || [ -z "${INFISICAL_CI_CLIENT_SECRET:-}" ] || [ -z "${INFISICAL_PROJECT_ID:-}" ]; then
echo "::error::Infisical CI identity secrets missing on a trusted ref — cannot source the platform model+runtime SSOT."
exit 1
fi
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; v=json.load(sys.stdin).get("accessToken"); sys.stdout.write(v if isinstance(v,str) else "")')
if [ -z "$TOK" ]; then echo "::error::Infisical accessToken empty - failing closed"; exit 1; fi
read_secret() {
# $1=key $2=env $3=url-encoded secretPath
curl -fsS "$BASE/api/v3/secrets/raw/$1?workspaceId=$INFISICAL_PROJECT_ID&environment=$2&secretPath=$3" \
-H "Authorization: Bearer $TOK" \
| python3 -c 'import sys,json; v=json.load(sys.stdin).get("secret",{}).get("secretValue"); sys.stdout.write(v if isinstance(v,str) else "")'
}
E2E_MODEL=$(read_secret MOLECULE_LLM_DEFAULT_MODEL staging %2Fshared%2Fcontrolplane%2Fllm)
E2E_RUNTIME=$(read_secret MOLECULE_DEFAULT_RUNTIME staging %2Fshared%2Fcontrolplane%2Fllm)
if [ -z "$E2E_MODEL" ] || [ -z "$E2E_RUNTIME" ]; then
echo "::error::KMS returned an empty platform model or runtime (model_len=${#E2E_MODEL}, runtime_len=${#E2E_RUNTIME}) — fail closed."
exit 1
fi
# Wave-B: staging CP admin bearer (CP_ADMIN_API_TOKEN @ staging
# /shared/controlplane-admin) -> MOLECULE_ADMIN_TOKEN, replacing the
# duplicated CP_STAGING_ADMIN_API_TOKEN Gitea Actions secret. NOTE: this
# job is platform-managed; it must NOT carry any E2E_*_API_KEY (the
# "Assert NO BYOK key leaks" step enforces that), so only the admin
# token is sourced here.
MOLECULE_ADMIN_TOKEN=$(read_secret CP_ADMIN_API_TOKEN staging %2Fshared%2Fcontrolplane-admin)
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 "::add-mask::$E2E_MODEL"
echo "::add-mask::$E2E_RUNTIME"
echo "::add-mask::$MOLECULE_ADMIN_TOKEN"
# $E2E_DEFAULT_PLATFORM_MODEL is the var pick_model_slug reads on the
# E2E_LLM_PATH=platform arm; mirror the model into it so the platform
# slug resolves to the KMS value (not the lib's literal fallback).
{
echo "E2E_MODEL=$E2E_MODEL"
echo "E2E_DEFAULT_PLATFORM_MODEL=$E2E_MODEL"
echo "E2E_RUNTIME=$E2E_RUNTIME"
echo "MOLECULE_ADMIN_TOKEN=$MOLECULE_ADMIN_TOKEN"
} >> "$GITHUB_ENV"
echo "Platform model+runtime + admin token loaded from KMS SSOT (model_len=${#E2E_MODEL}, runtime_len=${#E2E_RUNTIME}, admin_len=${#MOLECULE_ADMIN_TOKEN})."
- name: Verify admin token present
env:
E2E_REQUIRE_LIVE: ${{ github.event_name == 'pull_request' && '0' || '1' }}
run: |
# PR-mode (#48): on pull_request the job runs with E2E_REQUIRE_LIVE=0
# and PRs carry no staging creds. Don't hard-fail here — the harness
# detects the missing-creds case, runs a bash -n self-check, and
# exit 0s. On push/dispatch/cron (E2E_REQUIRE_LIVE=1) the creds MUST
# be present and a missing token/AWS-cred is a hard error.
if [ -z "$MOLECULE_ADMIN_TOKEN" ]; then
if [ "${E2E_REQUIRE_LIVE}" = "0" ]; then
echo "PR-mode: no MOLECULE_ADMIN_TOKEN (E2E_REQUIRE_LIVE=0) — harness will self-check and skip the live boot ✓"
exit 0
fi
echo "::error::CP_STAGING_ADMIN_API_TOKEN secret not set (Railway staging CP_ADMIN_API_TOKEN)"
exit 2
fi
# Leak-check is a no-op on molecules-server (leak=off), so AWS creds are
# only required when E2E_EC2_ENABLED=1 pins E2E_AWS_LEAK_CHECK=required.
if [ "${E2E_AWS_LEAK_CHECK}" = "required" ]; then
for var in AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY; do
if [ -z "${!var:-}" ]; then
echo "::error::$var not set — EC2 leak verification (E2E_AWS_LEAK_CHECK=required) cannot run"
exit 2
fi
done
fi
echo "Admin token present ✓"
- name: Assert NO BYOK key leaks into the platform run
run: |
# The whole point of this job is the platform-managed path. A stray
# E2E_*_API_KEY in the runner env would (via the harness) still be
# skipped by the E2E_LLM_PATH=platform branch — but assert their
# absence loudly here so a future env edit can't silently convert this
# into a masked BYOK run that no longer exercises the regression.
for var in E2E_MINIMAX_API_KEY E2E_ANTHROPIC_API_KEY E2E_OPENAI_API_KEY; do
if [ -n "${!var:-}" ]; then
echo "::warning::$var is set in this platform-boot job's env — the harness ignores it on E2E_LLM_PATH=platform, but it should not be wired here."
fi
done
echo "Platform-managed path: no tenant LLM key required ✓"
- name: CP staging health preflight
# PR-mode: no live staging call on PRs (they self-check only). Runs on
# push/dispatch/schedule, where the real provision follows.
if: github.event_name != 'pull_request'
run: |
# Poll to a stable Nx200 streak; a cold-start / mid-recreate CP
# (000/5xx/non-200) is a RETRY, not a canary red (mc#3244).
source tests/e2e/lib/cp_health_preflight.sh
cp_health_preflight "$MOLECULE_CP_URL" "workspace"
# No `if:` — on pull_request the harness runs its bash -n self-check
# (E2E_REQUIRE_LIVE=0 + empty token) and exit-0s green; on push/dispatch/
# schedule it runs the real platform-managed boot on molecules-server.
- name: Run platform-managed boot E2E (online + completion)
id: e2e
# No step-level `if:` — see the e2e-staging-saas Run step. This runs LIVE
# on push/dispatch/schedule against molecules-server. The #3965 EC2-gate
# is REVERTED; the harness now EC2-gates only the EC2/EIC-infra sub-steps
# (7b/7c) internally, so the platform-managed-boot journey runs live on
# molecules-server again. On pull_request the harness self-checks green.
run: bash tests/e2e/test_staging_full_saas.sh
- name: Teardown safety net (runs on cancel/failure)
# PR-mode: never curl live staging from a PR (no token; public repo).
if: always() && github.event_name != 'pull_request'
env:
# Sourced from Infisical (staging /shared/controlplane-admin) via the
# "Fetch platform model+runtime from KMS SSOT" job step above.
ADMIN_TOKEN: ${{ env.MOLECULE_ADMIN_TOKEN }}
run: |
set +e
orgs=$(curl -sS "$MOLECULE_CP_URL/cp/admin/orgs" \
-H "Authorization: Bearer $ADMIN_TOKEN" 2>/dev/null \
| python3 -c "
import json, sys, os, datetime
run_id = os.environ.get('GITHUB_RUN_ID', '')
d = json.load(sys.stdin)
today = datetime.date.today()
yesterday = today - datetime.timedelta(days=1)
dates = (today.strftime('%Y%m%d'), yesterday.strftime('%Y%m%d'))
# smoke mode slugs are e2e-smoke-YYYYMMDD-platform-<run_id>-...
if run_id:
prefixes = tuple(f'e2e-smoke-{d}-platform-{run_id}-' for d in dates)
else:
prefixes = tuple(f'e2e-smoke-{d}-platform-' for d in dates)
candidates = [o['slug'] for o in d.get('orgs', [])
if any(o.get('slug','').startswith(p) for p in prefixes)
and o.get('instance_status') not in ('purged',)]
print('\n'.join(candidates))
" 2>/dev/null)
leaks=()
for slug in $orgs; do
echo "Safety-net teardown: $slug"
set +e
curl -sS -o /tmp/plat-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/plat-cleanup.code
set -e
code=$(cat /tmp/plat-cleanup.code 2>/dev/null || echo "000")
if [ "$code" = "200" ] || [ "$code" = "204" ]; then
echo "[teardown] deleted $slug (HTTP $code)"
else
echo "::warning::platform-boot teardown for $slug returned HTTP $code — sweep-stale-e2e-orgs will catch it within ~45 min. Body: $(head -c 300 /tmp/plat-cleanup.out 2>/dev/null)"
leaks+=("$slug")
fi
done
if [ ${#leaks[@]} -gt 0 ]; then
echo "::warning::platform-boot teardown left ${#leaks[@]} leak(s): ${leaks[*]}"
fi
exit 0
# ── CONCIERGE user_tasks PRIMITIVE (Feature 3) — real-staging REST+MCP+authz ──
#
# Drives tests/e2e/test_staging_concierge_e2e.sh against a fresh throwaway
# tenant: the full agent→user "ask" contract over BOTH surfaces (REST +
# the MCP tools/call envelope a canvas concierge agent uses) PLUS the
# cross-workspace authz scoping (ws-B can't touch ws-A's task). Reuses the
# same CP-admin org-provision/teardown scaffolding + _lib.sh + AWS-leak-check
# lib as the full-SaaS harness (the script SOURCEs them — no duplication).
#
# GATING (no continue-on-error): user_tasks is a pure DB/handler primitive
# with NO LLM container dependency (workspaces are created 'external' — row
# only, no EC2), so this is fast (~provision + TLS, no 10-min cold boot) and
# NOT subject to the cp#245 boot-timeout flake the full-SaaS job carries. It
# therefore has no honest reason to be masked. Runs on push-to-main /
# workflow_dispatch / cron only (needs live staging infra — never on PR, where
# the pr-validate job above already posts the workflow's PR status).
# bp-required: pending #2430
e2e-staging-concierge-user-tasks:
name: E2E Staging Concierge user_tasks
runs-on: ubuntu-latest
# UN-PINNED from EC2 onto MOLECULES-SERVER (local-docker, provider='local'):
# the job-level `if: vars.E2E_EC2_ENABLED == '1' && (...)` guard is REMOVED so
# this provider-agnostic scenario (test_staging_concierge_e2e.sh drives the CP
# admin API) runs on push against the working local-docker backend instead of
# being EC2-gated-skipped. NOTE: this harness has no PR-mode self-check, so on
# pull_request the live Run step is gated off and a bash -n self-check step
# (below) keeps the job terminal-green. EC2 stays a one-line knob via the
# E2E_AWS_LEAK_CHECK provider knob below.
timeout-minutes: 30
permissions:
contents: read
env:
MOLECULE_CP_URL: https://staging-api.moleculesai.app
# 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 Verify-admin-token gate still
# validates MOLECULE_ADMIN_TOKEN is non-empty.
# AWS creds stay wired (empty/unused when the secret is unset + leak=off) so
# enabling EC2 is one line — set repo var E2E_EC2_ENABLED=1 (+ AWS_* secrets).
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: us-east-2
# provider knob: default molecules-server (local-docker, leak-check no-op);
# set repo var E2E_EC2_ENABLED=1 (+ AWS_* secrets, already wired below) to run
# the SAME scenario against an EC2-backed staging CP with the leak-check
# enforced — one-line enable.
E2E_AWS_LEAK_CHECK: ${{ vars.E2E_EC2_ENABLED == '1' && 'required' || 'off' }}
E2E_AWS_TERMINATE_LEAKS: '1'
E2E_RUN_ID: "${{ github.run_id }}-${{ github.run_attempt }}"
E2E_KEEP_ORG: ${{ github.event.inputs.keep_org && '1' || '0' }}
# PR-aware fail-closed guard (mirrors exemplar). On pull_request the live Run
# step is gated off and a bash -n self-check keeps the job green; on push it
# runs live. (The concierge_e2e harness itself has no REQUIRE_LIVE handling —
# this var documents the invariant and is harmless/unused by the script.)
E2E_REQUIRE_LIVE: ${{ github.event_name == 'pull_request' && '0' || '1' }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.11"
# SECURITY (public repo): the KMS live-creds fetch runs ONLY on push /
# workflow_dispatch / schedule — NEVER on pull_request, so the INFISICAL_CI_*
# machine-identity can never be reached with forked PR code in the tree. On
# pull_request this step is SKIPPED: the token stays empty, the live Run step
# is gated off, and the bash -n self-check step keeps the job terminal-green.
- name: Fetch staging CP admin token from Infisical KMS
id: kms_admin
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' || github.event_name == 'schedule'
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})"
- name: Verify admin token + AWS creds present
run: |
# PR-mode: the KMS creds-fetch above is push-only, so on pull_request the
# token is EXPECTED empty and green — the live Run step is gated off and
# the bash -n self-check step is the PR gate. A missing token on
# push/dispatch/schedule stays a hard error.
if [ -z "$MOLECULE_ADMIN_TOKEN" ]; then
if [ "${{ github.event_name }}" = "pull_request" ]; then
echo "PR-mode: no MOLECULE_ADMIN_TOKEN — live boot gated off; bash -n self-check is the PR gate ✓"
exit 0
fi
echo "::error::CP_STAGING_ADMIN_API_TOKEN secret not set (Railway staging CP_ADMIN_API_TOKEN)"
exit 2
fi
# Leak-check is a no-op on molecules-server (leak=off), so AWS creds are
# only required when E2E_EC2_ENABLED=1 pins E2E_AWS_LEAK_CHECK=required.
if [ "${E2E_AWS_LEAK_CHECK}" = "required" ]; then
for var in AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY; do
if [ -z "${!var:-}" ]; then
echo "::error::$var not set — EC2 leak verification (E2E_AWS_LEAK_CHECK=required) cannot run"
exit 2
fi
done
fi
echo "Admin token present ✓"
# PR-mode self-check: the concierge_e2e harness hard-fails on an empty
# MOLECULE_ADMIN_TOKEN (no built-in PR-mode), so on pull_request we do NOT run
# it live. This bash -n step is the PR gate — it proves the harness is
# syntactically clean and exit-0s green. The real live journey runs on
# push/dispatch/schedule via the gated Run step below.
- name: PR-mode self-check (bash -n)
if: github.event_name == 'pull_request'
run: |
if ! bash -n tests/e2e/test_staging_concierge_e2e.sh; then
echo "::error::PR-mode self-check FAILED: bash -n on test_staging_concierge_e2e.sh returned non-zero — script has a syntax error"
exit 1
fi
echo "PR-mode self-check PASSED: test_staging_concierge_e2e.sh is bash-clean (live journey runs on push-to-main) ✓"
- name: CP staging health preflight
# PR-mode: no live staging call on PRs (self-check only).
if: github.event_name != 'pull_request'
run: |
# Poll to a stable Nx200 streak; a cold-start / mid-recreate CP
# (000/5xx/non-200) is a RETRY, not a canary red (mc#3244).
source tests/e2e/lib/cp_health_preflight.sh
cp_health_preflight "$MOLECULE_CP_URL" "workspace"
- name: Run concierge user_tasks E2E
# PR-mode: gated off (harness hard-fails on empty token); the bash -n
# self-check above is the PR gate. Runs live on push/dispatch/schedule.
if: github.event_name != 'pull_request'
run: bash tests/e2e/test_staging_concierge_e2e.sh
- name: Teardown safety net (runs on cancel/failure)
# PR-mode: never curl live staging from a PR (no token; public repo).
if: always() && github.event_name != 'pull_request'
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: |
# Sweep any e2e-cncrg-YYYYMMDD-<run_id>-* org this run created if the
# script died before its EXIT trap fired. Run-id scoped so it never
# stomps a concurrent run's fresh tenant (see the saas job's note).
set +e
orgs=$(curl -sS "$MOLECULE_CP_URL/cp/admin/orgs" \
-H "Authorization: Bearer $ADMIN_TOKEN" 2>/dev/null \
| python3 -c "
import json, sys, os, datetime
run_id = os.environ.get('GITHUB_RUN_ID', '')
d = json.load(sys.stdin)
today = datetime.date.today()
yesterday = today - datetime.timedelta(days=1)
dates = (today.strftime('%Y%m%d'), yesterday.strftime('%Y%m%d'))
if run_id:
prefixes = tuple(f'e2e-cncrg-{d}-{run_id}-' for d in dates)
else:
prefixes = tuple(f'e2e-cncrg-{d}-' for d in dates)
candidates = [o['slug'] for o in d.get('orgs', [])
if any(o.get('slug','').startswith(p) for p in prefixes)
and o.get('instance_status') not in ('purged',)]
print('\n'.join(candidates))
" 2>/dev/null)
leaks=()
for slug in $orgs; do
echo "Safety-net teardown: $slug"
set +e
curl -sS -o /tmp/cncrg-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/cncrg-cleanup.code
set -e
code=$(cat /tmp/cncrg-cleanup.code 2>/dev/null || echo "000")
if [ "$code" = "200" ] || [ "$code" = "204" ]; then
echo "[teardown] deleted $slug (HTTP $code)"
else
echo "::warning::concierge teardown for $slug returned HTTP $code — sweep-stale-e2e-orgs will catch it within ~45 min. Body: $(head -c 300 /tmp/cncrg-cleanup.out 2>/dev/null)"
leaks+=("$slug")
fi
done
if [ ${#leaks[@]} -gt 0 ]; then
echo "::warning::concierge teardown left ${#leaks[@]} leak(s): ${leaks[*]}"
fi
exit 0
# ── CONCIERGE FUNCTIONAL: it ACTUALLY CREATES A WORKSPACE (real-LLM) ─────────
#
# core#2606: a NORMAL (non-platform) workspace raises a task + approval to the
# user via the wsAuth POST /workspaces/:id/requests endpoint, and both surface
# in the org's unified /requests/pending view. Its OWN job (not a step in the
# concierge job) so a concierge failure can never mask this proof (CR2 +
# Researcher review on #2660). Own throwaway org (e2e-req-*) with t.Cleanup
# teardown; skips LOUD without staging creds.
# bp-required: pending #2619
e2e-staging-workspace-requests:
name: E2E Staging Workspace Requests (core#2606)
runs-on: ubuntu-latest
# UN-PINNED from EC2 onto MOLECULES-SERVER (local-docker, provider='local'):
# the job-level `if: vars.E2E_EC2_ENABLED == '1' && (...)` guard is REMOVED so
# this provider-agnostic Go staginge2e (drives the CP admin API) runs on push
# against the working local-docker backend instead of being EC2-gated-skipped.
# This job has NO AWS creds and no leak-check (nothing to un-pin there). On
# pull_request the KMS fetch is skipped → CP_ADMIN_API_TOKEN empty →
# requireStagingEnv t.Skips LOUD (go test exit 0, green) — a compile+skip that
# keeps the job terminal-green; the live run happens on push/dispatch/schedule.
timeout-minutes: 35
permissions:
contents: read
env:
CP_BASE_URL: https://staging-api.moleculesai.app
# CP_ADMIN_API_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 Verify-admin-token gate still
# validates CP_ADMIN_API_TOKEN is non-empty.
STAGING_E2E: '1'
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
with:
go-version: 'stable'
cache: false
cache-dependency-path: workspace-server/go.sum
# SECURITY (public repo): the KMS live-creds fetch runs ONLY on push /
# workflow_dispatch / schedule — NEVER on pull_request, so the INFISICAL_CI_*
# machine-identity can never be reached with forked PR code in the tree. On
# pull_request this step is SKIPPED: CP_ADMIN_API_TOKEN stays empty and
# requireStagingEnv t.Skips the go test LOUD (exit 0, green compile+skip).
- name: Fetch staging CP admin token from Infisical KMS
id: kms_admin
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' || github.event_name == 'schedule'
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 "")'
}
CP_ADMIN_API_TOKEN=$(read_secret CP_ADMIN_API_TOKEN %2Fshared%2Fcontrolplane-admin)
echo "::add-mask::$CP_ADMIN_API_TOKEN"
if [ -z "$CP_ADMIN_API_TOKEN" ]; then
echo "::error::Infisical returned empty CP_ADMIN_API_TOKEN at staging /shared/controlplane-admin — failing closed."
exit 1
fi
echo "CP_ADMIN_API_TOKEN=$CP_ADMIN_API_TOKEN" >> "$GITHUB_ENV"
echo "CP_ADMIN_API_TOKEN loaded from Infisical staging SSOT (len=${#CP_ADMIN_API_TOKEN})"
- name: Verify admin token present
run: |
# PR-mode: the KMS creds-fetch above is push-only, so on pull_request the
# token is EXPECTED empty and green — requireStagingEnv t.Skips the go
# test LOUD (compile+skip). A missing token on push/dispatch/schedule
# stays a hard error.
if [ -z "$CP_ADMIN_API_TOKEN" ]; then
if [ "${{ github.event_name }}" = "pull_request" ]; then
echo "PR-mode: no CP_ADMIN_API_TOKEN — go test will requireStagingEnv-skip LOUD (compile+skip, exit 0 green) ✓"
exit 0
fi
echo "::error::CP_STAGING_ADMIN_API_TOKEN secret not set (Railway staging CP_ADMIN_API_TOKEN)"
exit 2
fi
echo "Admin token present"
- name: CP staging health preflight
# PR-mode: no live staging call on PRs (the go test self-skips green).
if: github.event_name != 'pull_request'
run: |
# Poll to a stable Nx200 streak; a cold-start / mid-recreate CP
# (000/5xx/non-200) is a RETRY, not a canary red (mc#3244).
source tests/e2e/lib/cp_health_preflight.sh
cp_health_preflight "$CP_BASE_URL" "workspace-requests"
# No `if:` — on pull_request CP_ADMIN_API_TOKEN is empty so requireStagingEnv
# t.Skips LOUD (go test compiles then exit-0s green); on push/dispatch/
# schedule it runs the real live journey on molecules-server.
- name: Run workspace-requests staginge2e (core#2606)
working-directory: workspace-server
run: go test -tags staging_e2e ./internal/staginge2e/ -run TestWorkspaceCanRaiseTaskAndApprovalToUser -count=1 -v -timeout 30m
# Drives tests/e2e/test_staging_concierge_creates_workspace_e2e.sh — the
# RFC docs/design/rfc-platform-agent.md §11.4 "Reach" check turned into a gate:
# send the org concierge a natural-language A2A message ("create a workspace
# named e2e-cncrg-worker-<runid> with role engineer") and assert the
# DETERMINISTIC SIDE EFFECT — that named workspace now EXISTS in GET /workspaces
# — which can only happen if the concierge's LLM really invoked the
# create_workspace platform-MCP tool (a real org mutation), NOT just that a REST
# API returned 200.
#
# GATING (no continue-on-error), but FALSE-GREEN-PROOF via E2E_REQUIRE_LIVE=1:
# this is a REAL-LLM, REAL-tool test, so it depends on the concierge being
# provisioned on the DEDICATED platform-agent image (Dockerfile.platform-agent,
# ships /opt/molecule-mcp-server — the ONLY image where create_workspace lights
# up; see platform_agent.go's SELF-HOST CAVEAT). A parallel agent is wiring that
# image into the staging provision path. The script SKIPs LOUD when the
# concierge is absent / not online / not on the platform-agent image — but with
# E2E_REQUIRE_LIVE=1 the harness converts that skip into a HARD FAIL (exit 5) so
# a silently-missing platform-agent image can NEVER false-green this gate. Runs
# on push-to-main / workflow_dispatch / cron only (needs live staging infra +
# a model — never on PR, where pr-validate posts the workflow's PR status).
# (enforcement sequenced on molecules-server — the authoritative directive is
# the `bp-required: pending #3411` line directly above the job key; core#3081
# also adds the A2A-probe step in the test script: it reads the
# concierge's /configs/mcp_servers.yaml via GET /workspaces/:id/files/mcp_servers.yaml
# and asserts the molecule-platform MCP server is declared with create_workspace.
# The previous false-green slipped because the proxies were healthy, the
# concierge was online, and the platform-agent image was baked — but the
# mcp_servers.yaml overlay on the concierge's /configs did not name the
# platform server, so the LLM could not call the tool. Probing the overlay
# directly is the only way to fail fast before burning LLM budget on a
# 7-minute cold-concierge tool call that will never succeed.)
#
# core#3081 / CR2 #12653: NO `if:` guard on this job. The job IS the
# required status context (see .gitea/required-contexts.txt) — a required
# context that never fires on pull_request degrades the merge gate to a
# silent indefinite pending (the exact failure mode lint-required-no-paths
# exists to prevent; see feedback_path_filtered_workflow_cant_be_required).
# The job runs on every PR; E2E_REQUIRE_LIVE is 0 on PR (the script
# detects the missing-creds case and exit 0s with a self-check), 1 on
# push-to-main / dispatch / cron (the real staging test runs and HARD
# FAILs on missing infra).
# bp-required: pending #3411 (molecules-server enforcement sequenced — kept below
# the ENFORCED marker in .gitea/required-contexts.txt until a green main run +
# creds-less PR/fork path, then BP-PATCH + promote).
e2e-staging-concierge-creates-workspace:
name: E2E Staging Concierge Creates Workspace
runs-on: ubuntu-latest
# 45 -> 75 (matches the sibling E2E Staging SaaS job): the full-journey STEP 5
# (assign-team-work) adds TEAM_ONLINE + a second AGENT_ACT (~22 min) once
# runtime#181 lands and STEP 5 is reachable. Worst-case step budget then runs
# PROVISION 900 + CONCIERGE_ONLINE 900 + AGENT_ACT 420 + TEAM_ONLINE 900 +
# work AGENT_ACT 420 ~= 59 min, which overruns the old 45-min cap.
timeout-minutes: 75
permissions:
contents: read
env:
MOLECULE_CP_URL: https://staging-api.moleculesai.app
# MOLECULE_ADMIN_TOKEN now arrives via $GITHUB_ENV from the "Fetch staging
# secrets 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). On untrusted-fork PRs the KMS fetch is
# skipped and MOLECULE_ADMIN_TOKEN stays empty (verify exits 2, same as
# today); same-repo PRs and push/dispatch/cron source it from Infisical.
# MOLECULES-SERVER (local-docker, provider='local'): the CP provisions the
# staging tenant as a local-docker container on the molecules-server host, NOT
# an EC2 VM — so this gate needs NO AWS creds and there is NO EC2 to leak.
# E2E_AWS_LEAK_CHECK=off makes tests/e2e/lib/aws_leak_check.sh a clean no-op
# (the org-level zero-leftover teardown assertion still runs). Re-add the AWS
# env + set E2E_AWS_LEAK_CHECK=required only if a cloud EC2 backend returns.
E2E_AWS_LEAK_CHECK: 'off'
# The concierge is platform_managed on SaaS (the CP-exported LLM proxy
# supplies its model — no BYOK key needed for the concierge itself). The
# MiniMax key is wired anyway so a staging image that boots the concierge
# BYOK-MiniMax (parallel-agent image work) still has a model; harmless when
# the concierge is platform-managed.
# E2E_MINIMAX_API_KEY now arrives via $GITHUB_ENV from the "Fetch staging
# secrets from Infisical KMS" step below (wave-B staging consumers
# migration). It is sourced from Infisical MINIMAX_API_KEY at
# staging /shared/controlplane. The old MOLECULE_STAGING_MINIMAX_API_KEY
# Gitea Actions secret is left in place until a green run validates this
# path (validate-before-delete).
# False-green guard, gated by event:
# pull_request: 0 → PR has no staging creds, the script's PR-mode
# self-check (bash -n) is the gate; a no-creds real
# test would just exit 2 at the ADMIN_TOKEN check.
# push / dispatch / schedule: 1 → the real staging test runs and
# HARD FAILs (exit 5) on a missing platform-agent
# image / never-online concierge / no creds.
E2E_REQUIRE_LIVE: ${{ github.event_name == 'pull_request' && '0' || '1' }}
E2E_RUN_ID: "${{ github.run_id }}-${{ github.run_attempt }}"
E2E_KEEP_ORG: ${{ github.event.inputs.keep_org && '1' || '0' }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.11"
# core#3081: PyYAML is NO LONGER a dependency of the test script — the
# A2A-probe (step 4.5/6) now goes through the A2A channel (live
# message/send + JSON + regex parse), not a yaml.read of mcp_servers.yaml.
# The earlier PyYAML install was for the now-removed config-text probe.
# 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 verify + run + teardown steps already read.
#
# SAFE-ENABLE (Guard B / regression-prevention-guards.md §Guard F): the
# KMS live-staging creds fetch runs ONLY on push / workflow_dispatch /
# schedule — NEVER on pull_request. The prior `(pull_request &&
# fork==false)` clause loaded live creds on every same-repo PR, which drove
# the job PAST the script's PR-mode self-check into a LIVE staging
# provision on each PR update — a per-PR staging flood that would wedge the
# `[*]` all-green queue once this required workflow is re-enabled. Dropping
# the PR clause makes ALL PRs reach the cheap `bash -n` self-check (green,
# no live load); the live gate runs on push-to-main + manual dispatch only.
# On a PR the token stays empty → Verify-admin-token exits 0 (PR-mode) →
# the harness self-checks (bash -n) and exit-0s green.
- name: Fetch staging secrets from Infisical KMS
id: kms_staging
# Flattened to a single physical line: Gitea act_runner may not treat
# embedded YAML newlines in a folded `if:` as whitespace (push-path eval risk).
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' || github.event_name == 'schedule'
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
run: |
# MOLECULES-SERVER: only the CP admin bearer is required (drives provision
# + tenant-token + teardown). No AWS creds — the tenant is a local-docker
# container on the molecules-server host, not an EC2 VM, so there is no
# EC2 leak verification to run.
if [ -z "$MOLECULE_ADMIN_TOKEN" ]; then
# SAFE-ENABLE PR-mode: on pull_request the KMS fetch is intentionally
# skipped (no live creds on PRs), so an empty token here is EXPECTED
# and green — the harness runs its bash -n self-check and exit-0s.
# A missing token on push/dispatch/schedule is still a hard error.
if [ "${{ github.event_name }}" = "pull_request" ]; then
echo "PR-mode (E2E_REQUIRE_LIVE=0): no MOLECULE_ADMIN_TOKEN — harness will self-check (bash -n) and exit 0 green ✓"
exit 0
fi
echo "::error::MOLECULE_ADMIN_TOKEN empty (Infisical staging CP_ADMIN_API_TOKEN @ /shared/controlplane-admin)"
exit 2
fi
echo "Admin token present ✓ (molecules-server: no AWS creds needed)"
- name: CP staging health preflight
# SAFE-ENABLE: no live staging call on PRs (they self-check only). Runs
# on push/dispatch/schedule, where the real provision follows.
if: github.event_name != 'pull_request'
run: |
# Poll to a stable Nx200 streak; a cold-start / mid-recreate CP
# (000/5xx/non-200) is a RETRY, not a canary red (mc#3244).
source tests/e2e/lib/cp_health_preflight.sh
cp_health_preflight "$MOLECULE_CP_URL" "workspace"
# core#3081: the A2A-probe (asserting the live concierge's tool list
# actually contains mcp__molecule-platform__create_workspace, not just
# that the mcp_servers.yaml text declared it) lives INSIDE the test
# script as step 4.5/6 — it is the GATE. A separate "advisory" step
# here would mask failure (Researcher finding #2 from PR #3085 review).
# The script-internal probe fails HARD on a missing tool via
# E2E_REQUIRE_LIVE=1 (exit 5), so a missing overlay produces a clear
# ::error:: line and a red job status — not a green mask.
- name: Run concierge-creates-workspace functional E2E
run: bash tests/e2e/test_staging_concierge_creates_workspace_e2e.sh
- name: Teardown safety net (runs on cancel/failure)
# PR-mode: never curl live staging from a PR (the KMS fetch is push-only, so
# ADMIN_TOKEN is empty on PR and this would otherwise send an unauthenticated
# request to the live staging CP). Zero live-staging traffic from a PR.
if: always() && github.event_name != 'pull_request'
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: |
# Sweep any e2e-cncrg-mk-YYYYMMDD-<run_id>-* org this run created if the
# script died before its EXIT trap fired. Run-id scoped so it never
# stomps a concurrent run's fresh tenant.
set +e
orgs=$(curl -sS "$MOLECULE_CP_URL/cp/admin/orgs" \
-H "Authorization: Bearer $ADMIN_TOKEN" 2>/dev/null \
| python3 -c "
import json, sys, os, datetime
run_id = os.environ.get('GITHUB_RUN_ID', '')
d = json.load(sys.stdin)
today = datetime.date.today()
yesterday = today - datetime.timedelta(days=1)
dates = (today.strftime('%Y%m%d'), yesterday.strftime('%Y%m%d'))
if run_id:
prefixes = tuple(f'e2e-cncrg-mk-{d}-{run_id}-' for d in dates)
else:
prefixes = tuple(f'e2e-cncrg-mk-{d}-' for d in dates)
candidates = [o['slug'] for o in d.get('orgs', [])
if any(o.get('slug','').startswith(p) for p in prefixes)
and o.get('instance_status') not in ('purged',)]
print('\n'.join(candidates))
" 2>/dev/null)
leaks=()
for slug in $orgs; do
echo "Safety-net teardown: $slug"
set +e
curl -sS -o /tmp/cncrg-mk-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/cncrg-mk-cleanup.code
set -e
code=$(cat /tmp/cncrg-mk-cleanup.code 2>/dev/null || echo "000")
if [ "$code" = "200" ] || [ "$code" = "204" ]; then
echo "[teardown] deleted $slug (HTTP $code)"
else
echo "::warning::concierge-mk teardown for $slug returned HTTP $code — sweep-stale-e2e-orgs will catch it within ~45 min. Body: $(head -c 300 /tmp/cncrg-mk-cleanup.out 2>/dev/null)"
leaks+=("$slug")
fi
done
if [ ${#leaks[@]} -gt 0 ]; then
echo "::warning::concierge-mk teardown left ${#leaks[@]} leak(s): ${leaks[*]}"
fi
exit 0
# ── CONCIERGE / PLATFORM-AGENT Go staginge2e (Features 1,2,4,5,6) ────────────
#
# Drives TestConciergePlatformAgent_Staging (workspace-server/internal/
# staginge2e/concierge_platform_test.go), which REUSES the lifecycle suite's
# harness (requireStagingEnv / adminCreateOrg / tenantAdminToken /
# tenantCreateWorkspace / doTenantJSON / jsonField) to assert, against a real
# tenant: platform-agent install + /org/identity (1), kind on the workspace
# API (2), discovery peers admin-auth regression guard (4), BYOK billing-mode
# round-trip (5), and the concierge config-tab auth sweep (6). It asserts
# OBSERVABLE state (sole root re-parenting, kind discriminator, resolved_mode,
# non-401 tabs) — not just HTTP 200.
#
# Two jobs, mirroring e2e-workspace-lifecycle.yml's honest pattern:
# • concierge-compile-skip (every push/PR/dispatch): proves the staginge2e
# suite still COMPILES under -tags=staging_e2e and SKIPs LOUD without
# creds. GATING (no mask) — a broken test file fails at PR time.
# • concierge-staging (push-to-main/dispatch/cron): the real live run with
# staging creds + t.Cleanup teardown.
# bp-exempt: PR-time compile-only check (build the concierge e2e test, then
# skip execution — no staging creds on PR). pr-validate posts the workflow's
# PR status; this job is not itself a branch-protection gate.
e2e-staging-concierge-compile-skip:
name: E2E Staging Concierge (compile+skip)
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
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
- name: go vet (staging_e2e tag)
working-directory: workspace-server
run: go vet -tags staging_e2e ./internal/staginge2e/...
- name: Compile + skip-run (must SKIP LOUD without STAGING_E2E)
working-directory: workspace-server
run: |
# No STAGING_E2E / creds → the suite MUST skip (not pass-with-zero-
# assertions). go test exit 0 with a SKIP line is the contract.
out=$(go test -tags staging_e2e ./internal/staginge2e/ -run 'TestConciergePlatformAgent|TestPlatformAgentMgmtMCP' -count=1 -v 2>&1)
echo "$out"
echo "$out" | grep -q "SKIP: TestConciergePlatformAgent_Staging" \
|| { echo "::error::expected a LOUD skip of TestConciergePlatformAgent_Staging without creds"; exit 1; }
echo "$out" | grep -q "SKIP: TestPlatformAgentMgmtMCP_Staging" \
|| { echo "::error::expected a LOUD skip of TestPlatformAgentMgmtMCP_Staging without creds"; exit 1; }
# bp-required: pending #2430
e2e-staging-concierge-platform:
name: E2E Staging Concierge Platform Agent
runs-on: ubuntu-latest
# GUARD B (regression-prevention-guards.md §Guard B): DE-GATED from
# E2E_EC2_ENABLED. This job runs the fresh-org platform-agent mgmt-MCP
# present+callable gate (TestPlatformAgentMgmtMCP_Staging), which provisions
# against staging = MOLECULES-SERVER (local-docker, provider='local') — NOT
# an EC2 VM. It never needed AWS creds; the blanket E2E_EC2_ENABLED gate
# applied at the 2026-06-27 AWS toggle-off left the merged Guard B test
# DORMANT (has never once run). Scoped to push / workflow_dispatch / schedule
# (mirrors e2e-staging-plugin-lifecycle) so it runs LIVE against staging on
# merge-to-main + on-demand dispatch, WITHOUT adding a redundant per-PR Go
# compile (the PR-time compile+skip is already owned by the sibling
# e2e-staging-concierge-compile-skip job). PROMOTION to a required merge gate
# in required-contexts.txt is the sequenced follow-up (task step 4): add a
# pull_request cheap-green PR-mode leg + the enforced entry only AFTER a
# green, non-flaky live run is verified.
# UN-PINNED: the job-level push-only `if:` is REMOVED (was folded, now
# converted the same way as the un-pinned scenarios) so the job also runs on
# pull_request. On PR the KMS fetch is skipped → CP_ADMIN_API_TOKEN empty →
# requireStagingEnv t.Skips LOUD (go test compile+skip, exit 0 green); the live
# run stays on push/dispatch/schedule. The sibling e2e-staging-concierge-compile
# -skip already owns a redundant PR compile of the same tests, so this is a
# cheap green on PR.
timeout-minutes: 40
permissions:
contents: read
env:
CP_BASE_URL: https://staging-api.moleculesai.app
# CP_ADMIN_API_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 Verify-admin-token gate still
# validates CP_ADMIN_API_TOKEN is non-empty.
STAGING_E2E: '1'
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
# SECURITY (public repo): the KMS live-creds fetch runs ONLY on push /
# workflow_dispatch / schedule — NEVER on pull_request, so the INFISICAL_CI_*
# machine-identity can never be reached with forked PR code in the tree. On
# pull_request this step is SKIPPED: CP_ADMIN_API_TOKEN stays empty and
# requireStagingEnv t.Skips the go tests LOUD (exit 0, green compile+skip).
- name: Fetch staging CP admin token from Infisical KMS
id: kms_admin
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' || github.event_name == 'schedule'
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 "")'
}
CP_ADMIN_API_TOKEN=$(read_secret CP_ADMIN_API_TOKEN %2Fshared%2Fcontrolplane-admin)
echo "::add-mask::$CP_ADMIN_API_TOKEN"
if [ -z "$CP_ADMIN_API_TOKEN" ]; then
echo "::error::Infisical returned empty CP_ADMIN_API_TOKEN at staging /shared/controlplane-admin — failing closed."
exit 1
fi
echo "CP_ADMIN_API_TOKEN=$CP_ADMIN_API_TOKEN" >> "$GITHUB_ENV"
echo "CP_ADMIN_API_TOKEN loaded from Infisical staging SSOT (len=${#CP_ADMIN_API_TOKEN})"
- name: Verify admin token present
run: |
# PR-mode: the KMS creds-fetch above is push-only, so on pull_request the
# token is EXPECTED empty and green — requireStagingEnv t.Skips the go
# tests LOUD (compile+skip). A missing token on push/dispatch/schedule
# stays a hard error.
if [ -z "$CP_ADMIN_API_TOKEN" ]; then
if [ "${{ github.event_name }}" = "pull_request" ]; then
echo "PR-mode: no CP_ADMIN_API_TOKEN — go tests will requireStagingEnv-skip LOUD (compile+skip, exit 0 green) ✓"
exit 0
fi
echo "::error::CP_STAGING_ADMIN_API_TOKEN secret not set (Railway staging CP_ADMIN_API_TOKEN)"
exit 2
fi
echo "Admin token present"
- name: CP staging health preflight
# PR-mode: no live staging call on PRs (the go tests self-skip green).
if: github.event_name != 'pull_request'
run: |
# Poll to a stable Nx200 streak; a cold-start / mid-recreate CP
# (000/5xx/non-200) is a RETRY, not a canary red (mc#3244).
source tests/e2e/lib/cp_health_preflight.sh
cp_health_preflight "$CP_BASE_URL" "concierge"
# No `if:` — on pull_request CP_ADMIN_API_TOKEN is empty so requireStagingEnv
# t.Skips LOUD (go test compiles then exit-0s green); live on push/dispatch/
# schedule.
- name: Run concierge/platform-agent staginge2e
working-directory: workspace-server
run: go test -tags staging_e2e ./internal/staginge2e/ -run TestConciergePlatformAgent_Staging -count=1 -v -timeout 35m
# Fresh-org platform-agent MANAGEMENT-MCP present+callable gate. The step
# above (TestConciergePlatformAgent_Staging) asserts only DB/handler state
# and NEVER waits for the concierge to boot online, so a fresh platform
# agent that can't load its mgmt MCP still passes it. This one provisions a
# fresh org and requires the concierge to reach status=online (RCA #2970
# makes that unreachable without mcp_server_present=true) + the
# provision_workspace verb loaded — it goes RED on the operator "test14"
# fresh-org failure (mgmt-MCP plugin absent) and green once the fleet runs a
# runtime that can resolve the CP's presign:// declared plugin sources.
- name: Run fresh-org platform-agent mgmt-MCP staginge2e
working-directory: workspace-server
run: go test -tags staging_e2e ./internal/staginge2e/ -run TestPlatformAgentMgmtMCP_Staging -count=1 -v -timeout 30m
# Teardown: the test installs a t.Cleanup admin-DELETE of its own tenant
# (e2e-cncrg-* / e2e-mcp-* slug), running even on a t.Fatal. The age-guarded
# sweep-stale-e2e-orgs workflow (30-min floor, e2e- prefix) is the final
# net for a tenant orphaned by a hard runner cancel.
# Tenant plugin-install lifecycle guard (canvas Plugins tab regression class):
# boots a real staging workspace, asserts the registry is non-empty (the
# "no registry available" report), installs a registry plugin via
# POST /workspaces/:id/plugins, then asserts (a) GET /workspaces/:id/plugins
# returns it — guards the SaaS EIC readback (CP #3125), (b) the workspace
# comes back online+routable and serves A2A after the install-triggered
# restart — guards the #159 mgmt-MCP self-heal online-gate. Drives
# TestPluginInstallLifecycle_Staging (workspace-server/internal/staginge2e).
#
# GATING: fail-loud (no continue-on-error) on push/dispatch/cron — a real
# red here blocks the deploy path. NOT yet a branch-protection REQUIRED
# context: to make it merge-blocking, add
# "E2E Staging SaaS (full lifecycle) / E2E Staging Plugin Install Lifecycle"
# to .gitea/required-contexts.txt AND to branch_protections/main
# status_check_contexts (owner action — same two-step the platform-boot
# gate documents above). Until then it runs + fails loud but is advisory,
# exactly like e2e-staging-workspace-requests.
# bp-required: pending #3133
e2e-staging-plugin-lifecycle:
name: E2E Staging Plugin Install Lifecycle
runs-on: ubuntu-latest
# MOLECULES-SERVER (local-docker) live gate — NO EC2 dependency. The CP
# provisions the staging tenant as a local-docker container, so the plugin
# install rides docker CopyToContainer into the mol-ws-* container, NOT the
# AWS EIC path. This job was previously EC2-dormant (E2E_EC2_ENABLED) which
# is exactly why the local-docker plugin-install 502 (core#182) reached prod
# uncaught. UN-PINNED: the job-level push-only `if:` is REMOVED (converted the
# same way as the un-pinned scenarios) so the job also runs on pull_request. On
# PR the KMS token fetch is skipped → CP_ADMIN_API_TOKEN empty →
# requireStagingEnv skips LOUD (go test compile+skip, exit 0 green); the live
# run stays on push/dispatch/schedule. Mirrors the live molecules-server gate
# e2e-staging-concierge-creates-workspace.
timeout-minutes: 60
permissions:
contents: read
env:
CP_BASE_URL: https://staging-api.moleculesai.app
# CP_ADMIN_API_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 Verify-admin-token gate still
# validates CP_ADMIN_API_TOKEN is non-empty.
STAGING_E2E: '1'
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
with:
go-version: 'stable'
# cache:false — see the concierge-platform job; the self-hosted
# runner bind-mounts a persistent GOCACHE/GOMODCACHE and
# actions/cache corrupts it.
cache: false
cache-dependency-path: workspace-server/go.sum
# SECURITY (public repo): the KMS live-creds fetch runs ONLY on push /
# workflow_dispatch / schedule — NEVER on pull_request, so the INFISICAL_CI_*
# machine-identity can never be reached with forked PR code in the tree. On
# pull_request this step is SKIPPED: CP_ADMIN_API_TOKEN stays empty and
# requireStagingEnv t.Skips the go test LOUD (exit 0, green compile+skip).
- name: Fetch staging CP admin token from Infisical KMS
id: kms_admin
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' || github.event_name == 'schedule'
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 "")'
}
CP_ADMIN_API_TOKEN=$(read_secret CP_ADMIN_API_TOKEN %2Fshared%2Fcontrolplane-admin)
echo "::add-mask::$CP_ADMIN_API_TOKEN"
if [ -z "$CP_ADMIN_API_TOKEN" ]; then
echo "::error::Infisical returned empty CP_ADMIN_API_TOKEN at staging /shared/controlplane-admin — failing closed."
exit 1
fi
echo "CP_ADMIN_API_TOKEN=$CP_ADMIN_API_TOKEN" >> "$GITHUB_ENV"
echo "CP_ADMIN_API_TOKEN loaded from Infisical staging SSOT (len=${#CP_ADMIN_API_TOKEN})"
- name: Verify admin token present
run: |
# PR-mode: the KMS creds-fetch above is push-only, so on pull_request the
# token is EXPECTED empty and green — requireStagingEnv t.Skips the go
# test LOUD (compile+skip). A missing token on push/dispatch/schedule
# stays a hard error.
if [ -z "$CP_ADMIN_API_TOKEN" ]; then
if [ "${{ github.event_name }}" = "pull_request" ]; then
echo "PR-mode: no CP_ADMIN_API_TOKEN — go test will requireStagingEnv-skip LOUD (compile+skip, exit 0 green) ✓"
exit 0
fi
echo "::error::CP_STAGING_ADMIN_API_TOKEN secret not set (Railway staging CP_ADMIN_API_TOKEN)"
exit 2
fi
echo "Admin token present"
- name: CP staging health preflight
# PR-mode: no live staging call on PRs (the go test self-skips green).
if: github.event_name != 'pull_request'
run: |
# Poll to a stable Nx200 streak; a cold-start / mid-recreate CP
# (000/5xx/non-200) is a RETRY, not a canary red (mc#3244).
source tests/e2e/lib/cp_health_preflight.sh
cp_health_preflight "$CP_BASE_URL" "plugin-lifecycle"
# No `if:` — on pull_request CP_ADMIN_API_TOKEN is empty so requireStagingEnv
# t.Skips LOUD (go test compiles then exit-0s green); live on push/dispatch/
# schedule.
- name: Run plugin-install-lifecycle staginge2e
working-directory: workspace-server
run: go test -tags staging_e2e ./internal/staginge2e/ -run TestPluginInstallLifecycle_Staging -count=1 -v -timeout 55m
# Teardown: the test installs a t.Cleanup admin-DELETE of its own tenant
# (e2e-plgn-* slug), running even on a t.Fatal. The age-guarded
# sweep-stale-e2e-orgs workflow (30-min floor, e2e- prefix) is the final
# net for a tenant orphaned by a hard runner cancel.