diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 306fc1ccf5b5..b006e5ff80e1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -251,7 +251,6 @@ jobs: ], }; }); - const createMatrix = (include) => ({ include }); const outputPath = process.env.GITHUB_OUTPUT; const isCanonicalRepository = process.env.OPENCLAW_CI_REPOSITORY === "openclaw/openclaw"; @@ -285,6 +284,7 @@ jobs: if (runNodeFull) { checksFastCoreTasks.push( { check_name: "checks-fast-bundled-protocol", runtime: "node", task: "bundled-protocol" }, + { check_name: "QA Smoke CI", runtime: "node", task: "qa-smoke-ci" }, { check_name: "checks-fast-bun-launcher", runtime: "bun", task: "bun-launcher" }, ); } else { @@ -922,6 +922,26 @@ jobs: pnpm test:bundled pnpm protocol:check ;; + qa-smoke-ci) + output_dir=".artifacts/qa-e2e/smoke-ci-profile" + export OPENCLAW_BUILD_PRIVATE_QA=1 + export OPENCLAW_ENABLE_PRIVATE_QA_CLI=1 + export OPENCLAW_DISABLE_BUNDLED_PLUGINS=0 + export OPENCLAW_QA_REDACT_PUBLIC_METADATA=1 + export OPENCLAW_QA_TRANSPORT_READY_TIMEOUT_MS=180000 + NODE_OPTIONS=--max-old-space-size=8192 node scripts/build-all.mjs qaRuntime + qa_exit_code=0 + pnpm openclaw qa run \ + --repo-root . \ + --qa-profile smoke-ci \ + --concurrency 8 \ + --output-dir "$output_dir" || qa_exit_code=$? + echo "QA smoke profile evidence: \`${output_dir}\`" >> "$GITHUB_STEP_SUMMARY" + if [ "$qa_exit_code" -ne 0 ]; then + echo "::error title=QA smoke profile failed::smoke-ci exited ${qa_exit_code}; evidence upload will still run" + exit "$qa_exit_code" + fi + ;; contracts-plugins-ci-routing) pnpm test:contracts:plugins pnpm test src/commands/status.scan-result.test.ts src/scripts/ci-changed-scope.test.ts test/scripts/changed-lanes.test.ts test/scripts/ci-workflow-guards.test.ts test/scripts/run-vitest.test.ts test/scripts/test-projects.test.ts @@ -938,6 +958,15 @@ jobs: ;; esac + - name: Upload QA smoke profile evidence + if: always() && matrix.task == 'qa-smoke-ci' + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: qa-smoke-profile-${{ github.run_id }}-${{ github.run_attempt }} + path: .artifacts/qa-e2e/smoke-ci-profile/ + if-no-files-found: warn + retention-days: 7 + checks-fast-plugin-contracts-shard: permissions: contents: read diff --git a/docs/ci.md b/docs/ci.md index 2fb79bddde4d..6543af64f93b 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -30,7 +30,7 @@ or an explicit manual dispatch. | `security-fast` | Private key detection, changed-workflow audit via `zizmor`, and production lockfile audit | Always on non-draft pushes and PRs | | `check-dependencies` | Production Knip dependency-only pass plus the unused-file allowlist guard | Node-relevant changes | | `build-artifacts` | Build `dist/`, Control UI, built-CLI smoke checks, embedded built-artifact checks, and reusable artifacts | Node-relevant changes | -| `checks-fast-core` | Fast Linux correctness lanes such as bundled, protocol, and CI-routing checks | Node-relevant changes | +| `checks-fast-core` | Fast Linux correctness lanes such as bundled, protocol, QA Smoke CI, and CI-routing checks | Node-relevant changes | | `checks-fast-contracts-plugins-*` | Two sharded plugin contract checks | Node-relevant changes | | `checks-fast-contracts-channels-*` | Two sharded channel contract checks | Node-relevant changes | | `checks-node-core-*` | Core Node test shards, excluding channel, bundled, contract, and extension lanes | Node-relevant changes | diff --git a/test/scripts/ci-workflow-guards.test.ts b/test/scripts/ci-workflow-guards.test.ts index 0d53bb9790cb..f21f0cac9ca2 100644 --- a/test/scripts/ci-workflow-guards.test.ts +++ b/test/scripts/ci-workflow-guards.test.ts @@ -795,13 +795,55 @@ describe("ci workflow guards", () => { it("keeps workflow guards in fast CI-routing checks", () => { const workflow = readCiWorkflow(); + const preflightStep = workflow.jobs.preflight.steps.find( + (step) => step.name === "Build CI manifest", + ); + const taxonomy = parse(readFileSync("taxonomy.yaml", "utf8")) as { + profiles: Array<{ id: string; categoryIds: string[] }>; + }; + const smokeProfile = taxonomy.profiles.find((profile) => profile.id === "smoke-ci"); + if (!smokeProfile) { + throw new Error("taxonomy.yaml is missing the smoke-ci profile"); + } const fastCoreJob = workflow.jobs["checks-fast-core"]; const runStep = fastCoreJob.steps.find( (step) => step.name === "Run ${{ matrix.task }} (${{ matrix.runtime }})", ); + const uploadStep = fastCoreJob.steps.find( + (step) => step.name === "Upload QA smoke profile evidence", + ); + const ciWorkflowText = readFileSync(".github/workflows/ci.yml", "utf8"); + + expect(preflightStep.run).not.toContain("qa-smoke-profile"); + expect(preflightStep.run).not.toContain("qa_category"); + expect(smokeProfile.categoryIds).toHaveLength(30); + for (const categoryId of smokeProfile.categoryIds) { + expect(ciWorkflowText).not.toContain(`"${categoryId}"`); + } + expect(runStep.run).toContain("bundled-protocol)"); + expect(runStep.run).toContain("qa-smoke-ci)"); expect(runStep.run).toContain("contracts-plugins-ci-routing)"); expect(runStep.run).toContain("ci-routing)"); + expect(ciWorkflowText).toContain( + '{ check_name: "QA Smoke CI", runtime: "node", task: "qa-smoke-ci" }', + ); + expect(runStep.run).toContain("--qa-profile smoke-ci"); + expect(runStep.run).toContain("--concurrency 8"); + expect(runStep.run).not.toContain("--category"); + expect(runStep.run).not.toContain("--allow-failures"); + expect(runStep.run).toContain("qa_exit_code=0"); + expect(runStep.run).toContain('exit "$qa_exit_code"'); + expect(runStep.run).toContain("scripts/build-all.mjs qaRuntime"); + expect(runStep.run).not.toContain("OPENAI_API_KEY"); + expect(runStep.run).toMatch( + /bundled-protocol\)\s+pnpm test:bundled\s+pnpm protocol:check\s+;;\s+qa-smoke-ci\)/, + ); + expect(uploadStep.if).toBe("always() && matrix.task == 'qa-smoke-ci'"); + expect(uploadStep.with).toMatchObject({ + path: ".artifacts/qa-e2e/smoke-ci-profile/", + "if-no-files-found": "warn", + }); expect(runStep.run.match(/test\/scripts\/ci-workflow-guards\.test\.ts/g)?.length).toBe(2); });