Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
187a9bf
feat(e2e): staging full-SaaS workflow — per-run org provision + leak-…
Apr 21, 2026
f470085
feat(e2e): canary + canvas Playwright workflows; delegation mechanics
Apr 21, 2026
d7193df
feat(e2e): pivot to admin-bearer-only auth + add sanity self-check wo…
Apr 21, 2026
6bd674e
fix(e2e): CP DELETE /cp/admin/tenants body uses 'confirm', not 'confi…
Apr 21, 2026
7dd66c9
Merge pull request #1355 from Molecule-AI/staging
molecule-ai[bot] Apr 21, 2026
4675402
feat(workspace): pre-stop serialization for pause/resume (closes #1386)
molecule-ai[bot] Apr 21, 2026
a510573
fix(e2e): poll instance_status not status in staging harness
Apr 21, 2026
37a02d6
fix(e2e): derive tenant domain from CP URL (staging vs prod)
Apr 21, 2026
e9d111d
fix(e2e): send X-Molecule-Org-Id on tenant calls
Apr 21, 2026
81c4c02
fix(e2e): safety-net teardown only sweeps this run's orgs
Apr 21, 2026
f2583c2
chore: PM-triggered CI re-run
molecule-ai[bot] Apr 21, 2026
a49a7e0
chore: force Platform(Go) CI run on main — validate go vet clean
molecule-ai[bot] Apr 21, 2026
3fe90d1
fix(tenant-image): remove node user so canvas uid 1000 can be created
Apr 21, 2026
a14cf86
Merge pull request #1445 from Molecule-AI/fix/tenant-dockerfile-uid-c…
HongmingWang-Rabbit Apr 21, 2026
f376c83
research: add crewai-competitive-proof-points-brief.md
molecule-ai[bot] Apr 21, 2026
6f470d0
research: add enterprise-case-study-legal-clearance-brief.md
molecule-ai[bot] Apr 21, 2026
deeea0d
research: add enterprise-case-study-pipeline-targeting-brief.md
molecule-ai[bot] Apr 21, 2026
38e9eba
fix(P0): CWE-22 path traversal in copyFilesToContainer + ContextMenu …
molecule-ai[bot] Apr 21, 2026
8f8be17
fix(core): resolve main build — remove duplicate SSRF function declar…
Apr 21, 2026
d86b8fe
Merge pull request #1469 from Molecule-AI/fix/main-build-dedupe-ssrf
molecule-ai[bot] Apr 21, 2026
5be20ac
fix(e2e): inject OPENAI_API_KEY into workspace secrets
Apr 21, 2026
392282c
fix(e2e): set MODEL_PROVIDER=openai for Hermes runtime
Apr 21, 2026
b8b3d5c
fix(e2e): MODEL_PROVIDER is provider:model slug, not just provider
Apr 21, 2026
5e130b7
fix(e2e): delegation raw curl missing X-Molecule-Org-Id
Apr 21, 2026
859d676
fix(CI): correct BASE in detect-changes (PR/push race); catch Runtime…
molecule-ai[bot] Apr 21, 2026
ce52b67
fix(build): add missing fmt import to a2a_proxy.go
HongmingWang-Rabbit Apr 21, 2026
64ccf8e
fix: CWE-78 rm scope, go vet failures, delegation idempotency
molecule-ai[bot] Apr 21, 2026
bd020d8
ci(e2e): wire MOLECULE_STAGING_OPENAI_KEY into workflow env
Apr 21, 2026
2133e56
Merge pull request #1491 from Molecule-AI/feat/e2e-staging-saas-cicd
HongmingWang-Rabbit Apr 21, 2026
a01e4a1
fix(core): consolidate 3 security fixes — CWE-22/CWE-78/SSRF dedup
molecule-ai[bot] Apr 21, 2026
42a8c01
ci: trigger for PR #1497
molecule-ai[bot] Apr 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .ci-trigger/RERUN
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
PR_1497_trigger
153 changes: 153 additions & 0 deletions .github/workflows/canary-staging.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
name: Canary — staging SaaS smoke (every 30 min)

# Minimum viable health check: provisions one Hermes workspace on a fresh
# staging org, sends one A2A message, verifies PONG, tears down. ~8 min
# wall clock. Pages on failure by opening a GitHub issue; auto-closes the
# issue on the next green run.
#
# The full-SaaS workflow (e2e-staging-saas.yml) covers the broader surface
# but runs only on provisioning-critical pushes + nightly — this one
# catches drift in the 30-min window between those runs (AMI health, CF
# cert rotation, WorkOS session stability, etc.).
#
# Lean mode: E2E_MODE=canary skips the child workspace + HMA memory +
# peers/activity checks. One parent workspace + one A2A turn is enough
# to signal "SaaS stack end-to-end is alive."

on:
schedule:
# Every 30 min. Cron on GitHub-hosted runners has a known drift of
# a few minutes under load — that's fine for a canary.
- cron: '*/30 * * * *'
workflow_dispatch:

# Serialise with the full-SaaS workflow so they don't contend for the
# same org-create quota on staging. Different group key from
# e2e-staging-saas since we don't mind queueing canaries behind one
# full run, but two canaries SHOULD queue against each other.
concurrency:
group: canary-staging
cancel-in-progress: false

permissions:
# Needed to open / close the alerting issue.
issues: write
contents: read

jobs:
canary:
name: Canary smoke
runs-on: ubuntu-latest
timeout-minutes: 15

env:
MOLECULE_CP_URL: https://staging-api.moleculesai.app
MOLECULE_ADMIN_TOKEN: ${{ secrets.MOLECULE_STAGING_ADMIN_TOKEN }}
E2E_MODE: canary
E2E_RUNTIME: hermes
E2E_RUN_ID: "canary-${{ github.run_id }}"

steps:
- uses: actions/checkout@v4

- name: Verify admin token present
run: |
if [ -z "$MOLECULE_ADMIN_TOKEN" ]; then
echo "::error::MOLECULE_STAGING_ADMIN_TOKEN not set"
exit 2
fi

- name: Canary run
id: canary
run: bash tests/e2e/test_staging_full_saas.sh

# Alerting: open an issue on first failure, auto-close on recovery.
# Title includes a stable marker so multiple consecutive failures
# don't spam — they just add comments to the existing issue.
- name: Open issue on failure
if: failure()
uses: actions/github-script@v7
with:
script: |
const title = '🔴 Canary failing: staging SaaS smoke';
const runURL = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
const body =
`Canary run failed at ${new Date().toISOString()}.\n\n` +
`Run: ${runURL}\n\n` +
`This issue auto-closes on the next green canary run. ` +
`Consecutive failures add a comment here rather than a new issue.`;

// Find an existing open canary issue (stable title match).
const { data: existing } = await github.rest.issues.listForRepo({
owner: context.repo.owner, repo: context.repo.repo,
state: 'open', labels: 'canary-staging',
per_page: 10,
});
const match = existing.find(i => i.title === title);

if (match) {
await github.rest.issues.createComment({
owner: context.repo.owner, repo: context.repo.repo,
issue_number: match.number,
body: `Canary still failing. ${runURL}`,
});
core.info(`Commented on existing issue #${match.number}`);
} else {
await github.rest.issues.create({
owner: context.repo.owner, repo: context.repo.repo,
title, body,
labels: ['canary-staging', 'bug'],
});
core.info('Opened new canary failure issue');
}

- name: Auto-close canary issue on success
if: success()
uses: actions/github-script@v7
with:
script: |
const title = '🔴 Canary failing: staging SaaS smoke';
const { data: open } = await github.rest.issues.listForRepo({
owner: context.repo.owner, repo: context.repo.repo,
state: 'open', labels: 'canary-staging',
per_page: 10,
});
const match = open.find(i => i.title === title);
if (match) {
await github.rest.issues.createComment({
owner: context.repo.owner, repo: context.repo.repo,
issue_number: match.number,
body: `Canary recovered at ${new Date().toISOString()}. Closing.`,
});
await github.rest.issues.update({
owner: context.repo.owner, repo: context.repo.repo,
issue_number: match.number,
state: 'closed',
});
core.info(`Closed recovered canary issue #${match.number}`);
}

- name: Teardown safety net
if: always()
env:
ADMIN_TOKEN: ${{ secrets.MOLECULE_STAGING_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
d = json.load(sys.stdin)
today = __import__('datetime').date.today().strftime('%Y%m%d')
candidates = [o['slug'] for o in d.get('orgs', [])
if o.get('slug','').startswith(f'e2e-{today}-canary-')
and o.get('status') not in ('purged',)]
print('\n'.join(candidates))
" 2>/dev/null)
for slug in $orgs; do
curl -sS -X DELETE "$MOLECULE_CP_URL/cp/admin/tenants/$slug" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"confirm\":\"$slug\"}" >/dev/null || true
done
exit 0
17 changes: 12 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,17 @@ jobs:
fetch-depth: 0
- id: check
run: |
# For push events: diff against previous commit (handles merge commits)
# For PR events: diff against the base branch
if [ "${{ github.event_name }}" = "pull_request" ]; then
# For PR events: diff against the base branch (not HEAD~1 of the branch,
# which may be unrelated after force-pushes). When a push updates a PR,
# both pull_request and push events fire — prefer the PR base so that
# the diff is always computed against the actual merge base, not the
# previous SHA on the branch which may be on a different history line.
BASE="${GITHUB_BASE_REF:-${{ github.event.before }}}"
# GITHUB_BASE_REF is set by GitHub for PR events (the base branch name).
# For pull_request events we use the stored base.sha; for push events
# (or when base.sha is unavailable) fall back to github.event.before.
if [ "${{ github.event_name }}" = "pull_request" ] && [ -n "${{ github.event.pull_request.base.sha }}" ]; then
BASE="${{ github.event.pull_request.base.sha }}"
else
BASE="${{ github.event.before }}"
fi
# Fallback: if BASE is empty or all zeros (new branch), run everything
if [ -z "$BASE" ] || echo "$BASE" | grep -qE '^0+$'; then
Expand Down Expand Up @@ -182,6 +187,8 @@ jobs:
needs: changes
if: needs.changes.outputs.python == 'true'
runs-on: [self-hosted, macos, arm64]
env:
WORKSPACE_ID: test
defaults:
run:
working-directory: workspace
Expand Down
116 changes: 116 additions & 0 deletions .github/workflows/e2e-staging-canvas.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
name: E2E Staging Canvas (Playwright)

# Playwright test suite that provisions a fresh staging org per run and
# verifies every workspace-panel tab renders without crashing. Complements
# e2e-staging-saas.yml (which tests the API shape) by exercising the
# actual browser + canvas bundle against live staging.
#
# Triggers: push to main or PR touching canvas sources + this workflow,
# manual dispatch, and weekly cron to catch browser/runtime drift even
# when canvas is quiet.

on:
push:
branches: [main]
paths:
- 'canvas/**'
- '.github/workflows/e2e-staging-canvas.yml'
pull_request:
branches: [main]
paths:
- 'canvas/**'
- '.github/workflows/e2e-staging-canvas.yml'
workflow_dispatch:
schedule:
# Weekly on Sunday 08:00 UTC — catches Chrome / Playwright / Next.js
# release-note-shaped regressions that don't ride in with a PR.
- cron: '0 8 * * 0'

concurrency:
group: e2e-staging-canvas
cancel-in-progress: false

jobs:
playwright:
name: Canvas tabs E2E
runs-on: ubuntu-latest
timeout-minutes: 40

env:
CANVAS_E2E_STAGING: '1'
MOLECULE_CP_URL: https://staging-api.moleculesai.app
MOLECULE_ADMIN_TOKEN: ${{ secrets.MOLECULE_STAGING_ADMIN_TOKEN }}

defaults:
run:
working-directory: canvas

steps:
- uses: actions/checkout@v4

- name: Verify admin token present
run: |
if [ -z "$MOLECULE_ADMIN_TOKEN" ]; then
echo "::error::Missing MOLECULE_STAGING_ADMIN_TOKEN"
exit 2
fi

- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: canvas/package-lock.json

- name: Install canvas deps
run: npm ci

- name: Install Playwright browsers
run: npx playwright install --with-deps chromium

- name: Run staging canvas E2E
run: npx playwright test --config=playwright.staging.config.ts

- name: Upload Playwright report on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: playwright-report-staging
path: canvas/playwright-report-staging/
retention-days: 14

- name: Upload screenshots on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: playwright-screenshots
path: canvas/test-results/
retention-days: 14

# Safety-net teardown mirrors the bash-harness workflow — if
# globalTeardown didn't run (worker crash, runner cancel), this
# step sweeps any e2e-canvas-* org tagged with today's date.
- name: Teardown safety net
if: always()
env:
ADMIN_TOKEN: ${{ secrets.MOLECULE_STAGING_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
d = json.load(sys.stdin)
today = __import__('datetime').date.today().strftime('%Y%m%d')
candidates = [o['slug'] for o in d.get('orgs', [])
if o.get('slug','').startswith(f'e2e-canvas-{today}-')
and o.get('status') not in ('purged',)]
print('\n'.join(candidates))
" 2>/dev/null)
for slug in $orgs; do
curl -sS -X DELETE "$MOLECULE_CP_URL/cp/admin/tenants/$slug" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"confirm\":\"$slug\"}" >/dev/null || true
done
exit 0
Loading
Loading