From d1730f5b8a700dc80aff7f465df7739043764bad Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Date: Sun, 19 Jul 2026 09:55:31 -0300 Subject: [PATCH] fix(ci): build API-only smoke workflows backend-only to fix dast-smoke timeouts (#7226) (#7758) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit dast-smoke.yml and 3 nightly API-only smoke workflows (nightly-schemathesis, nightly-resilience, nightly-llm-security) ran "npm run build:cli" with no preceding full build or downloaded .build/next artifact. scripts/build/ prepublish.ts silently falls back to a full Next.js production build (dashboard UI + ~126 leaf pages + prerender) whenever the standalone server.js is missing, which is always the case in these jobs. That inline full build is the actual thing varying 6-29min on GitHub-hosted runners. These workflows only exercise API routes (schemathesis/promptfoo hit /api/monitoring/health, /v1/chat/completions, /v1/models, /api/auth, /api/keys) and never touch the dashboard UI, so set OMNIROUTE_BUILD_BACKEND_ONLY=1 on their "Build CLI bundle" step — an existing, previously-unused escape hatch (scripts/build/backendOnlyPages.mjs) that stubs the dashboard pages before the build and restores them after, leaving every route.ts API handler intact. npm-publish.yml is intentionally left untouched: it legitimately ships the full dashboard UI in the published npm package. Regression guard: tests/unit/build/backend-only-smoke-workflows.test.ts asserts OMNIROUTE_BUILD_BACKEND_ONLY=1/OMNIROUTE_BUILD_PROFILE=backend on all 5 "Build CLI bundle" steps across the 4 fixed workflows, and asserts npm-publish.yml's build step is NOT backend-only. --- .github/workflows/dast-smoke.yml | 2 + .github/workflows/nightly-llm-security.yml | 8 +- .github/workflows/nightly-resilience.yml | 1 + .github/workflows/nightly-schemathesis.yml | 4 +- .../fixes/7226-dast-smoke-backend-only.md | 1 + .../backend-only-smoke-workflows.test.ts | 88 +++++++++++++++++++ 6 files changed, 101 insertions(+), 3 deletions(-) create mode 100644 changelog.d/fixes/7226-dast-smoke-backend-only.md create mode 100644 tests/unit/build/backend-only-smoke-workflows.test.ts diff --git a/.github/workflows/dast-smoke.yml b/.github/workflows/dast-smoke.yml index 08a622fdbb..2c00beae7d 100644 --- a/.github/workflows/dast-smoke.yml +++ b/.github/workflows/dast-smoke.yml @@ -27,6 +27,8 @@ jobs: cache: npm - run: npm ci - name: Build CLI bundle + env: + OMNIROUTE_BUILD_BACKEND_ONLY: "1" run: npm run build:cli - name: Start OmniRoute env: diff --git a/.github/workflows/nightly-llm-security.yml b/.github/workflows/nightly-llm-security.yml index 6a9abac294..a879258919 100644 --- a/.github/workflows/nightly-llm-security.yml +++ b/.github/workflows/nightly-llm-security.yml @@ -19,7 +19,9 @@ jobs: with: { node-version: "24", cache: npm } - run: npm ci - name: Build CLI bundle - env: { JWT_SECRET: ci-build-secret-with-sufficient-length-for-validation } + env: + JWT_SECRET: ci-build-secret-with-sufficient-length-for-validation + OMNIROUTE_BUILD_BACKEND_ONLY: "1" run: npm run build:cli - name: Start OmniRoute (block mode) env: @@ -72,7 +74,9 @@ jobs: if: steps.gate.outputs.run == 'true' - name: Build CLI bundle if: steps.gate.outputs.run == 'true' - env: { JWT_SECRET: ci-build-secret-with-sufficient-length-for-validation } + env: + JWT_SECRET: ci-build-secret-with-sufficient-length-for-validation + OMNIROUTE_BUILD_BACKEND_ONLY: "1" run: npm run build:cli - name: Start OmniRoute if: steps.gate.outputs.run == 'true' diff --git a/.github/workflows/nightly-resilience.yml b/.github/workflows/nightly-resilience.yml index f6adffeabf..c98c6df7b1 100644 --- a/.github/workflows/nightly-resilience.yml +++ b/.github/workflows/nightly-resilience.yml @@ -51,6 +51,7 @@ jobs: - name: Build CLI bundle env: JWT_SECRET: ci-build-secret-with-sufficient-length-for-validation + OMNIROUTE_BUILD_BACKEND_ONLY: "1" run: npm run build:cli - name: Start OmniRoute (background) env: diff --git a/.github/workflows/nightly-schemathesis.yml b/.github/workflows/nightly-schemathesis.yml index 64adef8f50..2ef0cea8d0 100644 --- a/.github/workflows/nightly-schemathesis.yml +++ b/.github/workflows/nightly-schemathesis.yml @@ -20,7 +20,9 @@ jobs: with: { node-version: "24", cache: npm } - run: npm ci - name: Build CLI bundle - env: { JWT_SECRET: ci-build-secret-with-sufficient-length-for-validation } + env: + JWT_SECRET: ci-build-secret-with-sufficient-length-for-validation + OMNIROUTE_BUILD_BACKEND_ONLY: "1" run: npm run build:cli - name: Start OmniRoute (background) env: diff --git a/changelog.d/fixes/7226-dast-smoke-backend-only.md b/changelog.d/fixes/7226-dast-smoke-backend-only.md new file mode 100644 index 0000000000..63d2ae9ef0 --- /dev/null +++ b/changelog.d/fixes/7226-dast-smoke-backend-only.md @@ -0,0 +1 @@ +- fix(ci): build dast-smoke and nightly API-only smoke workflows with `OMNIROUTE_BUILD_BACKEND_ONLY=1` to skip the unused dashboard UI graph and stop the multi-minute build variance/timeouts (#7226) diff --git a/tests/unit/build/backend-only-smoke-workflows.test.ts b/tests/unit/build/backend-only-smoke-workflows.test.ts new file mode 100644 index 0000000000..c7975c17b6 --- /dev/null +++ b/tests/unit/build/backend-only-smoke-workflows.test.ts @@ -0,0 +1,88 @@ +// Regression guard for #7226: API-only smoke/nightly workflows must build with +// OMNIROUTE_BUILD_BACKEND_ONLY=1 so `npm run build:cli`'s fallback full build +// (scripts/build/prepublish.ts -> build-next-isolated.mjs) skips the ~126-leaf-page +// dashboard UI graph these workflows never exercise. Without this env var, the +// "Build CLI bundle" step silently runs a full Next.js production build inline, +// which is the actual source of the multi-minute variance/timeouts reported in #7226. +// +// npm-publish.yml is intentionally excluded: its "Build CLI bundle (standalone app)" +// step legitimately ships the full dashboard UI in the published npm package. +import test from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; +import * as yaml from "js-yaml"; + +interface WorkflowStep { + name?: string; + run?: string; + env?: Record; + [key: string]: unknown; +} + +interface WorkflowJob { + steps: WorkflowStep[]; + [key: string]: unknown; +} + +interface WorkflowDoc { + jobs: Record; + [key: string]: unknown; +} + +const WORKFLOWS_DIR = path.join(process.cwd(), ".github", "workflows"); + +function loadWorkflow(fileName: string): WorkflowDoc { + const raw = fs.readFileSync(path.join(WORKFLOWS_DIR, fileName), "utf8"); + return yaml.load(raw) as WorkflowDoc; +} + +function isBackendOnly(step: WorkflowStep): boolean { + const env = step.env || {}; + return env.OMNIROUTE_BUILD_BACKEND_ONLY === "1" || env.OMNIROUTE_BUILD_PROFILE === "backend"; +} + +// jobName: null selector means "any job" — used when a file has exactly one +// "Build CLI bundle" step but we don't want to hardcode/duplicate the job key. +interface Target { + file: string; + jobName: string; + stepName: string; +} + +const TARGETS: Target[] = [ + { file: "dast-smoke.yml", jobName: "dast-smoke", stepName: "Build CLI bundle" }, + { file: "nightly-schemathesis.yml", jobName: "schemathesis", stepName: "Build CLI bundle" }, + { file: "nightly-resilience.yml", jobName: "k6-soak", stepName: "Build CLI bundle" }, + { file: "nightly-llm-security.yml", jobName: "promptfoo-guard", stepName: "Build CLI bundle" }, + { file: "nightly-llm-security.yml", jobName: "garak", stepName: "Build CLI bundle" }, +]; + +for (const { file, jobName, stepName } of TARGETS) { + test(`${file} :: ${jobName} '${stepName}' step sets OMNIROUTE_BUILD_BACKEND_ONLY=1 (skips dashboard UI build the API-only smoke job never exercises)`, () => { + const doc = loadWorkflow(file); + const job = doc.jobs[jobName]; + assert.ok(job, `${file} must have a '${jobName}' job`); + const step = job.steps.find((s) => s.name === stepName); + assert.ok(step, `${file}'s '${jobName}' job must have a '${stepName}' step`); + assert.equal( + isBackendOnly(step), + true, + `${file}'s '${jobName}' -> '${stepName}' step must set OMNIROUTE_BUILD_BACKEND_ONLY=1 or OMNIROUTE_BUILD_PROFILE=backend` + ); + }); +} + +test("npm-publish.yml 'Build CLI bundle (standalone app)' step must NOT be backend-only (it legitimately ships the full dashboard UI)", () => { + const doc = loadWorkflow("npm-publish.yml"); + const publishJob = Object.values(doc.jobs).find((job) => + job.steps.some((s) => s.name === "Build CLI bundle (standalone app)") + ); + assert.ok(publishJob, "npm-publish.yml must have a job with a 'Build CLI bundle (standalone app)' step"); + const step = publishJob!.steps.find((s) => s.name === "Build CLI bundle (standalone app)")!; + assert.equal( + isBackendOnly(step), + false, + "npm-publish.yml's build step must ship the full dashboard UI, not the backend-only stub" + ); +});