diff --git a/.github/workflows/gated-pipeline.yml b/.github/workflows/gated-pipeline.yml index 08a5fd7c9..3aae366eb 100644 --- a/.github/workflows/gated-pipeline.yml +++ b/.github/workflows/gated-pipeline.yml @@ -444,16 +444,15 @@ jobs: run: | set -o pipefail cd frontends/nextjs - npx eslint . 2>&1 | tee /tmp/lint-out.txt || true - # Count errors in local src/ only (skip workspace transitive errors) + npx eslint . 2>&1 | tee /tmp/lint-out.txt + # Count errors only (warnings are tolerated, errors are not) LOCAL_ERRORS=$(grep -cE " error " /tmp/lint-out.txt 2>/dev/null || echo "0") - echo "Total lint issues: $LOCAL_ERRORS" - # Allow up to 1500 issues (pre-existing workspace type-safety warnings) - if [ "$LOCAL_ERRORS" -gt 1500 ]; then - echo "::error::Too many lint errors ($LOCAL_ERRORS > 1500 threshold)" + echo "Total lint errors: $LOCAL_ERRORS" + if [ "$LOCAL_ERRORS" -gt 0 ]; then + echo "::error::Lint errors found ($LOCAL_ERRORS errors, threshold is 0)" exit 1 fi - echo "Lint: passed with $LOCAL_ERRORS issues (within threshold)" + echo "Lint: passed with 0 errors" - name: Record validation result if: always() @@ -661,6 +660,7 @@ jobs: name: "Gate 2: Testing - Starting" runs-on: ubuntu-latest needs: [gate-1-complete, container-build-apps] + if: ${{ !inputs.skip_tests }} steps: - name: Gate 2 checkpoint run: | @@ -684,6 +684,7 @@ jobs: name: "Gate 2.1: Unit Tests" runs-on: ubuntu-latest needs: gate-2-start + if: ${{ !inputs.skip_tests }} steps: - name: Checkout code uses: actions/checkout@v6 @@ -755,6 +756,7 @@ jobs: name: "Gate 2.2: E2E Tests" runs-on: ubuntu-latest needs: gate-2-start + if: ${{ !inputs.skip_tests }} steps: - name: Checkout code uses: actions/checkout@v6 @@ -813,6 +815,7 @@ jobs: name: "Gate 2.3: DBAL Daemon E2E" runs-on: ubuntu-latest needs: gate-2-start + if: ${{ !inputs.skip_tests }} steps: - name: Checkout code uses: actions/checkout@v6 @@ -1569,7 +1572,7 @@ jobs: name: "Gate 7 App: ${{ matrix.image }}" runs-on: ubuntu-latest needs: [container-base-tier1] - if: github.event_name != 'issues' && github.event_name != 'issue_comment' && !failure() + if: github.event_name != 'issues' && github.event_name != 'issue_comment' && needs.container-base-tier1.result == 'success' strategy: fail-fast: false matrix: diff --git a/e2e/global.setup.ts b/e2e/global.setup.ts index b32309d13..27773f363 100644 --- a/e2e/global.setup.ts +++ b/e2e/global.setup.ts @@ -13,6 +13,20 @@ const __dirname = dirname(fileURLToPath(import.meta.url)) let environment: Awaited> | undefined +async function waitForServer(url: string, timeoutMs = 60000): Promise { + const deadline = Date.now() + timeoutMs + while (Date.now() < deadline) { + try { + const res = await fetch(url, { method: 'GET' }) + if (res.ok || res.status === 401 || res.status === 405) return // server is up + } catch { + // not ready yet + } + await new Promise(r => setTimeout(r, 1000)) + } + throw new Error(`Server at ${url} did not become ready within ${timeoutMs}ms`) +} + async function globalSetup() { // ── 1. Start smoke stack via Testcontainers ────────────────────────────── console.log('[setup] Starting smoke stack via Testcontainers...') @@ -32,14 +46,14 @@ async function globalSetup() { ;(globalThis as Record).__TESTCONTAINERS_ENV__ = environment // ── 2. Wait for dev servers (started by Playwright webServer config) ───── - await new Promise(resolve => setTimeout(resolve, 2000)) - // ── 3. Seed database ──────────────────────────────────────────────────── // workflowui uses basePath: '/workflowui', so the setup route is at /workflowui/api/setup const setupUrl = process.env.PLAYWRIGHT_BASE_URL ? new URL('/workflowui/api/setup', process.env.PLAYWRIGHT_BASE_URL.replace(/\/workflowui\/?$/, '')).href : 'http://localhost:3000/workflowui/api/setup' + await waitForServer(setupUrl) + try { const response = await fetch(setupUrl, { method: 'POST' }) if (!response.ok) { diff --git a/frontends/nextjs/src/app/api/bootstrap/route.ts b/frontends/nextjs/src/app/api/bootstrap/route.ts index a1a46559c..cac371f6b 100644 --- a/frontends/nextjs/src/app/api/bootstrap/route.ts +++ b/frontends/nextjs/src/app/api/bootstrap/route.ts @@ -74,6 +74,12 @@ export async function POST(request: NextRequest) { return limitResponse } + const setupSecret = process.env.SETUP_SECRET + const authHeader = request.headers.get('Authorization') + if (!setupSecret || authHeader !== `Bearer ${setupSecret}`) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + const results = { packages: 0, pages: 0, skipped: 0, errors: 0 } // Seed InstalledPackage records diff --git a/frontends/nextjs/src/app/api/setup/route.ts b/frontends/nextjs/src/app/api/setup/route.ts index 377e34c7a..07c3a1173 100644 --- a/frontends/nextjs/src/app/api/setup/route.ts +++ b/frontends/nextjs/src/app/api/setup/route.ts @@ -4,8 +4,15 @@ */ import type { NextRequest } from 'next/server' +import { NextResponse } from 'next/server' import { POST as bootstrap } from '@/app/api/bootstrap/route' export async function POST(request: NextRequest) { + const setupSecret = process.env.SETUP_SECRET + const authHeader = request.headers.get('Authorization') + if (!setupSecret || authHeader !== `Bearer ${setupSecret}`) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + return bootstrap(request) }