Files
molecule-core/.gitea/workflows/e2e-ephemeral-happy-path.yml
T
hongming cb2b956909
Block internal-flavored paths / Block forbidden paths (push) Successful in 17s
Block integration-tester contamination artifacts / Block staging-trigger / invalid manifest contamination (push) Successful in 16s
CI / Python Lint & Test (push) Successful in 20s
Concierge Creates Workspace Hermetic / Concierge Creates Workspace Hermetic (push) Successful in 31s
E2E Peer Visibility (literal MCP list_peers) / detect-changes (push) Successful in 13s
ci-required-drift / drift (push) Successful in 35s
E2E Staging SaaS (full lifecycle) / pr-validate (push) Successful in 33s
CI / Detect changes (push) Successful in 2m7s
E2E Staging SaaS (full lifecycle) / E2E Staging Platform Boot (push) Successful in 1m42s
E2E Staging SaaS (full lifecycle) / E2E Staging Concierge user_tasks (push) Successful in 1m29s
E2E Staging SaaS (full lifecycle) / E2E Staging Concierge (compile+skip) (push) Successful in 22s
E2E API Smoke Test / detect-changes (push) Successful in 2m17s
E2E Chat / detect-changes (push) Successful in 2m17s
E2E Staging Canvas (Playwright) / detect-changes (push) Successful in 2m7s
E2E Staging SaaS (full lifecycle) / E2E Staging Workspace Requests (core#2606) (push) Successful in 1m36s
Handlers Postgres Integration / detect-changes (push) Successful in 14s
Lint curl status-code capture / Scan workflows for curl status-capture pollution (push) Successful in 12s
Lint forbidden tenant-env keys / Scan workspace_secrets writers for forbidden env keys (push) Successful in 11s
Lint forbidden tenant-env keys / Scan for repo-host token write into tenant workspace surface (push) Successful in 12s
lint-bp-context-emit-match / lint-bp-context-emit-match (push) Successful in 25s
Lint forbidden hand-written mcp__ tool-id literals / Scan for hand-written mcp__ tool-id literals (push) Successful in 13s
E2E Staging SaaS (full lifecycle) / E2E Staging SaaS (push) Failing after 2m30s
lint-continue-on-error-tracking / lint-continue-on-error-tracking (push) Successful in 38s
lint-no-coe-on-required / lint-no-coe-on-required (push) Successful in 25s
lint-required-workflows-docker-host-pinned / Lint docker-host pin on docker-touching workflows (push) Successful in 15s
lint-phantom-gate / lint-phantom-gate (push) Successful in 32s
Lint publish-runner timeout-minutes / Lint publish-runner timeout-minutes (push) Successful in 35s
lint-staging-tenant-cd-gate-chain / lint-staging-tenant-cd-gate-chain (push) Successful in 20s
lint-env-coupling-dismissal / lint-env-coupling-dismissal (push) Successful in 1m0s
Lint workflow YAML (Gitea-1.22.6-hostile shapes) / Lint workflow YAML for Gitea-1.22.6-hostile shapes (push) Successful in 24s
lint-setup-go-cache / lint-setup-go-cache (push) Successful in 33s
Lint schedule budget / Zero-cron ratchet (push) Successful in 44s
E2E Staging SaaS (full lifecycle) / E2E Staging Plugin Install Lifecycle (push) Successful in 1m27s
Secret scan / Scan diff for credential-shaped strings (push) Successful in 21s
E2E Staging SaaS (full lifecycle) / E2E Staging Concierge Creates Workspace (push) Successful in 2m50s
Local Provision Lifecycle E2E / Local Provision Lifecycle E2E (stub) (push) Successful in 38s
E2E Staging SaaS (full lifecycle) / E2E Staging Concierge Platform Agent (push) Successful in 2m16s
main-canary / canary (push) Successful in 1m45s
merge-commit-guard / Verify merged PR commits are on main (push) Successful in 2m24s
template-delivery-e2e / detect-changes (push) Successful in 2m39s
CI / Platform (Go) (push) Successful in 7s
CI / Canvas (Next.js) (push) Successful in 7s
CI / Shellcheck (E2E scripts) (push) Successful in 4s
E2E Peer Visibility (literal MCP list_peers) / E2E Peer Visibility (push) Successful in 13s
E2E API Smoke Test / E2E API Smoke Test (push) Successful in 7s
E2E Chat / E2E Chat (push) Successful in 8s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (push) Successful in 7s
E2E Staging SaaS (full lifecycle) / Prune stale e2e DNS records (push) Successful in 13s
Handlers Postgres Integration / Handlers Postgres Integration (push) Successful in 33s
Local Provision Lifecycle E2E / Local Provision Lifecycle E2E (real image + MiniMax LLM, advisory) (push) Successful in 1m18s
template-delivery-e2e / Template-asset delivery (fresh seo-agent — config+prompts via asset channel, seo-all via plugin reconcile) (push) Successful in 7s
CI / Canvas Deploy Status (push) Successful in 5s
CI / all-required (push) Successful in 11s
E2E Ephemeral CP Happy Path / E2E Ephemeral CP Happy Path (push) Successful in 35m39s
fix(ci): key the ephemeral-gate concurrency group per-PR — kills the phantom job (#4146)
Closes #4122.

The gate was the only heavy E2E lane with a global, unkeyed concurrency group; every sibling keys per-SHA/PR. That cross-PR queue is where the phantom jobs came from: entries that never get a runner (runner="-", started_at=1970-01-01), re-post "Waiting to run" onto a context an EXECUTED job already resolved green, then red it as "Has been cancelled".

Confirmed again while landing this: on #4151 a gate job was cancelled with no runner while NO other ephemeral run was active — a queued entry cancelled with nothing to queue behind it.

Safe to key per-PR because the dind (#4116) isolates each run: own nested daemon, DIND_NS-namespaced containers, ephemeral published ports. cancel-in-progress is likewise now safe — a superseded run is one atomic docker rm -fv, and Teardown is always()-gated.

Unblocks the mc#4081 soak (a ghost could red a genuine pass, so N consecutive greens could never accumulate) and removes the multi-hour PR backlog.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 01:27:55 +00:00

230 lines
12 KiB
YAML

name: E2E Ephemeral CP Happy Path
# RFC "one pre-merge ephemeral gate" §04 — the one-scenario PROOF (advisory).
#
# Runs the CORE happy-path (test_staging_full_saas.sh) against a THROWAWAY CP it
# spins up itself, with THIS PR's tenant image substituted in and the RELEASED
# controlplane as the baseline — NO shared staging, NO post-merge dependency.
# This validates the SDK-owned-happy-path model (the happy-path E2E is a
# cross-boundary contract both repos run against an ephemeral CP).
#
# ADVISORY during the proof soak. The migration end-state — move the contract
# into the SDK, wire both repos, flip to required, demote the E2E Staging jobs —
# is tracked in the RFC. Do NOT renew this mask silently.
#
# Reuses the CP repo's ephemeral spin-up plumbing (scripts/deploy/pr-ephemeral-cp.sh),
# the shared harness the RFC generalizes.
"on":
pull_request:
branches: [main]
paths:
- 'workspace-server/**'
- 'tests/e2e/test_staging_full_saas.sh'
- 'tests/e2e/ephemeral_cp_happy_path.sh'
- 'tests/e2e/lib/**'
- '.gitea/workflows/e2e-ephemeral-happy-path.yml'
push:
branches: [main]
paths:
- 'workspace-server/**'
- 'tests/e2e/test_staging_full_saas.sh'
- 'tests/e2e/ephemeral_cp_happy_path.sh'
- 'tests/e2e/lib/**'
- '.gitea/workflows/e2e-ephemeral-happy-path.yml'
concurrency:
# Keyed per-PR, like every sibling E2E lane. This was ONCE a global,
# unkeyed group ("e2e-ephemeral-happy-path-runner") because the pre-dind
# topology bound host resources and overlapping runs cross-wired. The dind
# (#4116) removed that constraint: each run gets its own nested daemon, its
# containers are namespaced by DIND_NS=<run_id>, and the dind's ports are
# published on EPHEMERAL host ports (-p 127.0.0.1::8080). Concurrent runs
# cannot collide, so the runner pool — not a workflow-level group — is the
# right place to bound parallelism.
#
# The global group was actively harmful (#4122): it was the only unkeyed
# group among the heavy E2E lanes, and its cross-PR queue is where the
# phantom jobs came from — a queue entry that never gets a runner, posts
# "Waiting to run" onto a context an EXECUTED job already resolved green,
# and then reds it as "Has been cancelled". That made the mc#4081 soak
# unconvergeable (a ghost can red a genuine pass) and, at volume, serialized
# every open PR into a multi-hour backlog on one slot.
#
# cancel-in-progress is now safe (it was not before): a superseded run's
# entire topology is one `docker rm -fv` of its dind, so a cancel cannot
# leak a half-torn-down namespace. A new push supersedes its own older run
# instead of queueing behind it (the RFC's finding-#2 shape).
group: e2e-ephemeral-happy-path-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
env:
GITHUB_SERVER_URL: https://git.moleculesai.app
# The controlplane BASELINE in the image-substitution matrix — a core PR tests
# its tenant against a stable CP. Built locally from the cloned CP main (below)
# using Dockerfile.dockerprov: the local-docker provisioner shells out to the
# `docker` CLI, which the distroless/static ROOT Dockerfile lacks. main == the
# released baseline, and building is self-contained (no dependency on a published
# tag — there is no controlplane:staging-latest).
CP_BASELINE_IMAGE: controlplane:baseline-dockerprov
jobs:
# bp-exempt: advisory proof gate (RFC §04). CI / all-required is the merge aggregate.
happy-path:
name: E2E Ephemeral CP Happy Path
# docker-host: needs a real docker.sock + molecule-core-net to spin the CP,
# its Postgres, and the provisioned tenant (same runner class as harness-replays).
runs-on: docker-host
# mc#4081 — advisory soak; flip to REQUIRED per the RFC §04 end-state.
continue-on-error: true
timeout-minutes: 40
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
# Per-job DISPOSABLE dind (tests/harness/dind.sh — the harness-replays
# pattern, task#78): the WHOLE topology (PG, CP, tenants, workspaces,
# image builds) lives inside a nested daemon and dies with one atomic
# `docker rm -fv`, even on cancel. This is the structural fix for the
# shared docker-host interference that killed the gate's first CI runs
# (host-loopback docker-proxy dying mid-run with every container healthy;
# mc#4081) AND for cross-run tenant leaks (pr-ephemeral-cp.sh `down` only
# sweeps the leg network — per-org-net tenants leaked). dind.sh exports
# DOCKER_HOST (all subsequent docker/build steps target the dind) and
# BASE = the dind's pre-forwarded :8080, which becomes the caller-visible
# CP URL (the runner passes it via CP_HOST_BASE_OVERRIDE, CP#1549).
# Trade-off: no shared layer cache — each run rebuilds/pulls fresh into
# the dind (hermetic > fast during the soak; a cache volume can come later).
- name: Start disposable dind
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false
env:
DIND_NS: ${{ github.run_id }}-ephemeral
run: bash tests/harness/dind.sh up
# e2e creds from the Infisical SSOT: the MiniMax LLM key (the SAME key the
# post-merge gate uses; RFC finding D moves it to a low-value e2e path later)
# and AUTO_SYNC_TOKEN (devops-engineer PAT) to clone the CP spin-up plumbing.
# FORK SAFETY: same-repo PRs only — a fork must never harvest these.
- name: Fetch e2e creds from Infisical SSOT
id: creds
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false
env:
INFISICAL_CI_CLIENT_ID: ${{ secrets.INFISICAL_CI_CLIENT_ID }}
INFISICAL_CI_CLIENT_SECRET: ${{ secrets.INFISICAL_CI_CLIENT_SECRET }}
INFISICAL_PROJECT_ID: ${{ secrets.INFISICAL_CI_PROJECT_ID }}
run: |
set -uo pipefail
BASE="https://key.moleculesai.app"
TOK=$(curl -fsS -X POST "$BASE/api/v1/auth/universal-auth/login" \
-H 'Content-Type: application/json' \
-d "{\"clientId\":\"$INFISICAL_CI_CLIENT_ID\",\"clientSecret\":\"$INFISICAL_CI_CLIENT_SECRET\"}" \
| python3 -c 'import sys,json; d=json.load(sys.stdin); v=d.get("accessToken"); sys.stdout.write(v if isinstance(v,str) and v else "")')
[ -n "$TOK" ] || { echo "::error::Infisical login returned empty accessToken"; exit 1; }
read_secret() {
curl -fsS "$BASE/api/v3/secrets/raw/$1?workspaceId=$INFISICAL_PROJECT_ID&environment=prod&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 "")'
}
MINIMAX_API_KEY=$(read_secret MINIMAX_API_KEY %2Fshared%2Fcontrolplane)
AUTO_SYNC_TOKEN=$(read_secret AUTO_SYNC_TOKEN %2Fshared%2Fdev-utils)
echo "::add-mask::$MINIMAX_API_KEY"
echo "::add-mask::$AUTO_SYNC_TOKEN"
echo "MINIMAX_API_KEY=$MINIMAX_API_KEY" >> "$GITHUB_ENV"
echo "AUTO_SYNC_TOKEN=$AUTO_SYNC_TOKEN" >> "$GITHUB_ENV"
echo "creds loaded (minimax len=${#MINIMAX_API_KEY}, sync len=${#AUTO_SYNC_TOKEN})"
# The shared spin-up plumbing (RFC §04): pr-ephemeral-cp.sh lives in the CP
# repo today; clone it read-only. (When the harness is generalized this
# becomes a vendored/shared action, not a clone.)
- name: Clone controlplane for the ephemeral spin-up plumbing
if: env.AUTO_SYNC_TOKEN != ''
run: |
set -euo pipefail
rm -rf .cp-ephemeral
git clone --depth 1 \
"https://oauth2:${AUTO_SYNC_TOKEN}@git.moleculesai.app/molecule-ai/molecule-controlplane.git" \
.cp-ephemeral
chmod +x .cp-ephemeral/scripts/deploy/pr-ephemeral-cp.sh
echo "cloned pr-ephemeral-cp.sh"
# Pre-clone manifest deps so Dockerfile.tenant's COPY of .tenant-bundle-deps
# resolves (identical to harness-replays' pre-clone).
- name: Pre-clone manifest deps
if: env.AUTO_SYNC_TOKEN != ''
env:
MOLECULE_GITEA_TOKEN: ${{ env.AUTO_SYNC_TOKEN }}
run: |
set -euo pipefail
mkdir -p .tenant-bundle-deps
sed '/^[[:space:]]*\/\//d' manifest.json > .manifest-stripped.json
bash scripts/clone-manifest.sh \
.manifest-stripped.json \
.tenant-bundle-deps/workspace-configs-templates \
.tenant-bundle-deps/org-templates \
.tenant-bundle-deps/plugins
echo "manifest deps cloned"
# Build THIS PR's tenant image (the UNDER-TEST side of the substitution).
- name: Build PR tenant image
if: env.AUTO_SYNC_TOKEN != ''
env:
HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
run: |
set -euo pipefail
docker build \
-f workspace-server/Dockerfile.tenant \
--build-arg GIT_SHA="${HEAD_SHA}" \
-t "molecule-tenant:pr-${HEAD_SHA:0:8}" \
.
echo "TENANT_IMAGE=molecule-tenant:pr-${HEAD_SHA:0:8}" >> "$GITHUB_ENV"
# Build the CP baseline (the OTHER side of the substitution) from the cloned
# CP main using Dockerfile.dockerprov — the multi-stage image that ships the
# `docker` CLI the local-docker tenant provisioner shells out to. The root
# Dockerfile is distroless/static with NO docker CLI, so a CP built from it
# cannot provision tenants. main == the released baseline; self-contained (no
# published :staging-latest tag to depend on).
- name: Build baseline controlplane image (Dockerfile.dockerprov)
if: env.AUTO_SYNC_TOKEN != ''
run: |
set -euo pipefail
docker build \
-f .cp-ephemeral/Dockerfile.dockerprov \
-t "${CP_BASELINE_IMAGE}" \
.cp-ephemeral
echo "built ${CP_BASELINE_IMAGE} from cloned CP main (Dockerfile.dockerprov)"
# The proof: full-saas against the throwaway CP, zero staging creds.
- name: Run core happy-path against an ephemeral CP
if: env.AUTO_SYNC_TOKEN != ''
env:
CP_IMAGE: ${{ env.CP_BASELINE_IMAGE }}
TENANT_IMAGE: ${{ env.TENANT_IMAGE }}
CP_EPHEMERAL_SCRIPT: ${{ github.workspace }}/.cp-ephemeral/scripts/deploy/pr-ephemeral-cp.sh
MINIMAX_API_KEY: ${{ env.MINIMAX_API_KEY }}
PR_NUMBER: ${{ github.event.pull_request.number || 0 }}
HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
E2E_RUNTIME: hermes
# DIND mode — everything inside the disposable daemon; $BASE/$DOCKER_HOST
# come from the "Start disposable dind" step via GITHUB_ENV.
EPHEMERAL_DIND: "1"
run: bash tests/e2e/ephemeral_cp_happy_path.sh
# Atomic teardown: one rm destroys the entire nested topology (CP, PG,
# tenants, workspaces, volumes, networks, built images) — even after a
# cancel that skipped every other cleanup. Leaks structurally impossible.
- name: Teardown dind
# always() so a cancelled/failed run still reaps its dind — but gated on
# the same non-fork condition as the start step (an ungated always()
# step on the fork no-op path was exactly the template-delivery chdir
# failure, core#4057).
if: always() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
env:
DIND_NS: ${{ github.run_id }}-ephemeral
run: bash tests/harness/dind.sh down
# Fork-PR fallback: no creds → loud skip, never a silent green.
- name: Fork PR — advisory skip (no e2e creds)
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true
run: |
echo "::notice::Fork PR — ephemeral-CP happy-path proof skipped (no e2e creds by design). The post-merge run covers it."