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
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>
1857 lines
110 KiB
YAML
1857 lines
110 KiB
YAML
# 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:04–08: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.
|