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
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>
230 lines
12 KiB
YAML
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."
|