From ed3d635044e1cc512116852840af6230ec6f5b7e Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Thu, 22 Oct 2026 04:43:39 -0700 Subject: [PATCH 1/3] ci: split required tests into path-filtered parallel jobs Co-authored-by: CI Doctor Agent-Model: codex/gpt-6-sol/medium --- .github/workflows/smoke-test.yml | 4 +- .github/workflows/tests.yml | 170 +++++++++++++++++++++++++-- scripts/ci/select_test_paths.py | 60 ++++++++++ scripts/ci/test_select_test_paths.py | 28 +++++ 4 files changed, 252 insertions(+), 10 deletions(-) create mode 100644 scripts/ci/select_test_paths.py create mode 100644 scripts/ci/test_select_test_paths.py diff --git a/.github/workflows/smoke-test.yml b/.github/workflows/smoke-test.yml index ea31b1d61d..7ca4df5e06 100644 --- a/.github/workflows/smoke-test.yml +++ b/.github/workflows/smoke-test.yml @@ -8,7 +8,7 @@ on: default: '300' required: false workflow_run: - workflows: ["Tests"] + workflows: ["Deploy Dev"] branches: [main] types: [completed] # ADR-009 Phase 2: gate Tier 2 on PRs that touch deployment surfaces. @@ -46,7 +46,7 @@ jobs: name: kind cluster smoke test runs-on: ubuntu-latest # Run on manual dispatch, on any PR that passed the path filter above, - # or on a successful Tests workflow_run on main. + # or after a successful Deploy Dev run on main. if: > github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request' || diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d8ce44b4d0..44fc9b6748 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -2,7 +2,7 @@ name: Tests on: push: - branches: [ main, v1.0.x ] + branches: [ v1.0.x ] # No branches filter on pull_request: stacked PRs (base = another feature # branch) previously got ZERO test runs — every number on them was # single-sourced to the author's local sweep, and the 2026-08-22 threading @@ -22,16 +22,15 @@ permissions: contents: read # The push fallback is github.sha, NOT github.ref. On a push event `github.ref` -# is the *branch*, so every commit merged to main shared one concurrency group -# and each merge cancelled the previous commit's still-running verification. -# Measured across 39 Tests runs on main (2026-08-04 → 08-06): 18 cancelled. +# is the branch, so concurrent commits would cancel each other. This caused +# 18 of 39 Tests runs on main to be cancelled (2026-08-04 → 08-06). # # The loss is ordered by job depth, not random, which is what makes it worth # fixing rather than tolerating: # # Chart Lint no needs, ~8s survived 12 of 14 -# Test & Coverage no needs, ~5.5min survived 3 of 14 -# Service Tests needs:test, starts ~5.5min in survived 0 of 14 +# old Test & Coverage no needs, ~5.5min survived 3 of 14 +# Service Tests formerly needs:test survived 0 of 14 # # Service Tests is the ADR-009 Tier 1 real-DB tier — the deepest job, and the # only one that never once completed in a cancelled run. So the tier main was @@ -44,8 +43,149 @@ concurrency: cancel-in-progress: true jobs: - test: + paths: + name: Select test paths + runs-on: ubuntu-latest + outputs: + backend: ${{ steps.select.outputs.backend }} + frontend: ${{ steps.select.outputs.frontend }} + tools: ${{ steps.select.outputs.tools }} + chart: ${{ steps.select.outputs.chart }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - id: select + name: Classify changed paths + env: + EVENT_NAME: ${{ github.event_name }} + BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.merge_group.base_sha }} + run: python3 scripts/ci/select_test_paths.py + - run: python3 -m unittest discover -s scripts/ci -p 'test_select_test_paths.py' + + backend-tests: + name: Backend tests (${{ matrix.shard }}/3) + needs: paths + if: needs.paths.outputs.backend == 'true' + strategy: + fail-fast: false + matrix: + shard: [1, 2, 3] + runs-on: ubuntu-latest + env: + JWT_SECRET: test-jwt-secret + SENDGRID_API_KEY: fake-api-key + SENDGRID_FROM_EMAIL: test@example.com + FRONTEND_URL: http://localhost:3000 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '22' + cache: npm + cache-dependency-path: backend/package-lock.json + - run: cd backend && npm ci --include=dev + - name: Run backend tests with coverage + run: cd backend && npx jest --coverage --forceExit --shard=${{ matrix.shard }}/3 + - uses: actions/upload-artifact@v4 + with: + name: backend-coverage-${{ matrix.shard }} + path: backend/coverage/ + + backend-checks: + name: Backend typecheck and lint + needs: paths + if: needs.paths.outputs.backend == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '22' + cache: npm + cache-dependency-path: backend/package-lock.json + - run: cd backend && npm ci --include=dev + - run: cd backend && npm run tsc:check + - run: cd packages/types && ../../backend/node_modules/.bin/tsc --noEmit -p tsconfig.json + - run: cd backend && npm run lint:ts + + frontend-tests: + name: Frontend tests + needs: paths + if: needs.paths.outputs.frontend == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '22' + cache: npm + cache-dependency-path: frontend/package-lock.json + - run: cd frontend && npm install --legacy-peer-deps + - run: cd frontend && npm run typecheck + - run: cd frontend && npx jest --coverage --watchAll=false --forceExit + - run: cd frontend && node --test scripts/generate-seo-pages.test.mjs scripts/nginx-routing.test.mjs + + tool-tests: + name: CLI, MCP and hosted runtime tests + needs: paths + if: needs.paths.outputs.tools == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + - uses: actions/setup-node@v4 + with: + node-version: '22' + cache: npm + cache-dependency-path: | + package-lock.json + commonly-mcp/package-lock.json + cli/package-lock.json + workers/agent-runtime/package-lock.json + - run: npm ci --include=dev + - run: cd commonly-mcp && npm ci --include=dev + - run: cd cli && npm ci --include=dev + - run: cd workers/agent-runtime && npm ci --include=dev + - run: npm run verify:moltbot-tools + - run: npm run verify:litellm-patch-runner + - run: cd commonly-mcp && npm test + - run: cd cli && npm run lint && npm test + - run: cd workers/agent-runtime && npm run typecheck && npm test + + test-gate: name: Test & Coverage + if: always() + needs: [paths, backend-tests, backend-checks, frontend-tests, tool-tests, service-test, chart-lint] + runs-on: ubuntu-latest + env: + NEEDS_JSON: ${{ toJSON(needs) }} + steps: + - name: Verify every selected check passed + run: | + echo "$NEEDS_JSON" | jq -e ' + . as $jobs | + $jobs.paths.result == "success" and + $jobs["service-test"].result == "success" and + ($jobs["chart-lint"].result == "success" or + ($jobs.paths.outputs.chart == "false" and $jobs["chart-lint"].result == "skipped")) and + (if $jobs.paths.outputs.backend == "true" then + $jobs["backend-tests"].result == "success" and $jobs["backend-checks"].result == "success" + else + $jobs["backend-tests"].result == "skipped" and $jobs["backend-checks"].result == "skipped" + end) and + (if $jobs.paths.outputs.frontend == "true" then + $jobs["frontend-tests"].result == "success" + else $jobs["frontend-tests"].result == "skipped" end) and + (if $jobs.paths.outputs.tools == "true" then + $jobs["tool-tests"].result == "success" + else $jobs["tool-tests"].result == "skipped" end) + ' + + legacy-test: + # Retire only after the new gate passes a real merge_group run. + name: Legacy test and coverage runs-on: ubuntu-latest env: @@ -172,7 +312,6 @@ jobs: service-test: name: Service Tests (Tier 1 — real DBs) runs-on: ubuntu-latest - needs: test services: mongo: @@ -214,8 +353,19 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v3 + with: + fetch-depth: 0 + + # This job has no needs, so it starts with the other suites. Classify here + # independently; a docs-only PR still starts the job but runs no tests. + - id: select + env: + EVENT_NAME: ${{ github.event_name }} + BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.merge_group.base_sha }} + run: python3 scripts/ci/select_test_paths.py - name: Set up Node.js + if: steps.select.outputs.backend == 'true' uses: actions/setup-node@v4 with: node-version: '22' @@ -223,15 +373,19 @@ jobs: cache-dependency-path: '**/package-lock.json' - name: Install backend dependencies + if: steps.select.outputs.backend == 'true' run: cd backend && npm ci --include=dev - name: Run service tests (Tier 1) + if: steps.select.outputs.backend == 'true' run: | cd backend npx jest --testPathPattern="__tests__/service" --forceExit --runInBand chart-lint: name: Chart Lint (Tier 1.5) + needs: paths + if: needs.paths.outputs.chart == 'true' runs-on: ubuntu-latest env: diff --git a/scripts/ci/select_test_paths.py b/scripts/ci/select_test_paths.py new file mode 100644 index 0000000000..cbf9c623f6 --- /dev/null +++ b/scripts/ci/select_test_paths.py @@ -0,0 +1,60 @@ +"""Select CI suites from the actual PR or merge-group diff. + +Unknown paths and diff failures run every suite. A missing check must never +turn into a green required gate. +""" + +import os +import subprocess +from pathlib import Path + + +SUITES = ("backend", "frontend", "tools", "chart") + + +def select(paths): + selected = {suite: False for suite in SUITES} + if not paths: + return {suite: True for suite in SUITES} + + for path in paths: + if path.startswith("docs/") or ("/" not in path and path.endswith(".md")): + continue + if path.startswith("backend/"): + selected["backend"] = True + selected["chart"] = True + elif path.startswith("frontend/"): + selected["frontend"] = True + elif path.startswith(("cli/", "commonly-mcp/", "workers/agent-runtime/")): + selected["tools"] = True + elif path.startswith("k8s/"): + selected["chart"] = True + else: + return {suite: True for suite in SUITES} + return selected + + +def main(): + event = os.environ.get("EVENT_NAME") + base = os.environ.get("BASE_SHA") + head = os.environ.get("GITHUB_SHA") + if event not in ("pull_request", "merge_group") or not base or not head: + result = select([]) + else: + try: + changed = subprocess.check_output( + ["git", "diff", "--name-only", "-z", base, head], + stderr=subprocess.DEVNULL, + ).decode().rstrip("\0").split("\0") + result = select(changed) + except (subprocess.CalledProcessError, UnicodeDecodeError): + result = select([]) + + output = Path(os.environ["GITHUB_OUTPUT"]) + with output.open("a") as stream: + for suite, enabled in result.items(): + stream.write(f"{suite}={str(enabled).lower()}\n") + + +if __name__ == "__main__": + main() diff --git a/scripts/ci/test_select_test_paths.py b/scripts/ci/test_select_test_paths.py new file mode 100644 index 0000000000..a22c189e2e --- /dev/null +++ b/scripts/ci/test_select_test_paths.py @@ -0,0 +1,28 @@ +import unittest + +from select_test_paths import select + + +class SelectTestPathsTest(unittest.TestCase): + def test_docs_only_selects_no_suite(self): + self.assertEqual(select(["docs/README.md", "README.md"]), { + "backend": False, "frontend": False, "tools": False, "chart": False, + }) + + def test_frontend_only_does_not_select_backend(self): + self.assertEqual(select(["frontend/src/App.tsx"]), { + "backend": False, "frontend": True, "tools": False, "chart": False, + }) + + def test_backend_change_also_checks_chart(self): + self.assertEqual(select(["backend/src/server.ts"]), { + "backend": True, "frontend": False, "tools": False, "chart": True, + }) + + def test_unknown_and_empty_diff_fail_open(self): + for paths in ([], ["packages/types/src/index.ts"]): + self.assertTrue(all(select(paths).values())) + + +if __name__ == "__main__": + unittest.main() From c49d66187aeeec00030f364605ea017440299d58 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Thu, 22 Oct 2026 04:44:43 -0700 Subject: [PATCH 2/3] test(ci): probe frontend-only path selection Co-authored-by: CI Doctor Agent-Model: codex/gpt-6-sol/medium --- frontend/ci-path-probe.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 frontend/ci-path-probe.md diff --git a/frontend/ci-path-probe.md b/frontend/ci-path-probe.md new file mode 100644 index 0000000000..e98948debc --- /dev/null +++ b/frontend/ci-path-probe.md @@ -0,0 +1 @@ +Temporary CI path-filter probe for TASK-001. From 44c0b5e8ef0e4095b93eae754060f4356468ffe3 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Thu, 22 Oct 2026 04:53:22 -0700 Subject: [PATCH 3/3] ci: install backend dependencies for frontend and CLI suites Co-authored-by: CI Doctor Agent-Model: codex/gpt-6-sol/medium --- .github/workflows/tests.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 44fc9b6748..6702fad77b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -120,7 +120,11 @@ jobs: with: node-version: '22' cache: npm - cache-dependency-path: frontend/package-lock.json + cache-dependency-path: | + backend/package-lock.json + frontend/package-lock.json + # Frontend typecheck follows shared imports into backend TypeScript. + - run: cd backend && npm ci --include=dev - run: cd frontend && npm install --legacy-peer-deps - run: cd frontend && npm run typecheck - run: cd frontend && npx jest --coverage --watchAll=false --forceExit @@ -141,10 +145,13 @@ jobs: cache: npm cache-dependency-path: | package-lock.json + backend/package-lock.json commonly-mcp/package-lock.json cli/package-lock.json workers/agent-runtime/package-lock.json - run: npm ci --include=dev + # CLI tests load backend TypeScript through ts-node. + - run: cd backend && npm ci --include=dev - run: cd commonly-mcp && npm ci --include=dev - run: cd cli && npm ci --include=dev - run: cd workers/agent-runtime && npm ci --include=dev