mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-24 13:54:57 +00:00
fix(security+ci): address code review findings
Security: - /api/setup and /api/bootstrap now require Authorization: Bearer $SETUP_SECRET before executing any database seed operations E2E: - global.setup.ts: replace fixed 2s sleep with waitForServer() poll loop (60s timeout, 1s interval) so seed POST only fires when server is ready CI pipeline: - lint gate: remove || true so ESLint failures propagate; tighten error threshold from 1500 to 0 (errors are now a hard gate) - container-build-apps: replace !failure() with explicit needs.container-base-tier1.result == 'success' so a failed tier-1 build blocks Gate 2 instead of being silently skipped - skip_tests workflow_dispatch input now wired to gate-2-start, test-unit, test-e2e, and test-dbal-daemon jobs Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
19
.github/workflows/gated-pipeline.yml
vendored
19
.github/workflows/gated-pipeline.yml
vendored
@@ -444,16 +444,15 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
set -o pipefail
|
set -o pipefail
|
||||||
cd frontends/nextjs
|
cd frontends/nextjs
|
||||||
npx eslint . 2>&1 | tee /tmp/lint-out.txt || true
|
npx eslint . 2>&1 | tee /tmp/lint-out.txt
|
||||||
# Count errors in local src/ only (skip workspace transitive errors)
|
# Count errors only (warnings are tolerated, errors are not)
|
||||||
LOCAL_ERRORS=$(grep -cE " error " /tmp/lint-out.txt 2>/dev/null || echo "0")
|
LOCAL_ERRORS=$(grep -cE " error " /tmp/lint-out.txt 2>/dev/null || echo "0")
|
||||||
echo "Total lint issues: $LOCAL_ERRORS"
|
echo "Total lint errors: $LOCAL_ERRORS"
|
||||||
# Allow up to 1500 issues (pre-existing workspace type-safety warnings)
|
if [ "$LOCAL_ERRORS" -gt 0 ]; then
|
||||||
if [ "$LOCAL_ERRORS" -gt 1500 ]; then
|
echo "::error::Lint errors found ($LOCAL_ERRORS errors, threshold is 0)"
|
||||||
echo "::error::Too many lint errors ($LOCAL_ERRORS > 1500 threshold)"
|
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
echo "Lint: passed with $LOCAL_ERRORS issues (within threshold)"
|
echo "Lint: passed with 0 errors"
|
||||||
|
|
||||||
- name: Record validation result
|
- name: Record validation result
|
||||||
if: always()
|
if: always()
|
||||||
@@ -661,6 +660,7 @@ jobs:
|
|||||||
name: "Gate 2: Testing - Starting"
|
name: "Gate 2: Testing - Starting"
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [gate-1-complete, container-build-apps]
|
needs: [gate-1-complete, container-build-apps]
|
||||||
|
if: ${{ !inputs.skip_tests }}
|
||||||
steps:
|
steps:
|
||||||
- name: Gate 2 checkpoint
|
- name: Gate 2 checkpoint
|
||||||
run: |
|
run: |
|
||||||
@@ -684,6 +684,7 @@ jobs:
|
|||||||
name: "Gate 2.1: Unit Tests"
|
name: "Gate 2.1: Unit Tests"
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: gate-2-start
|
needs: gate-2-start
|
||||||
|
if: ${{ !inputs.skip_tests }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
@@ -755,6 +756,7 @@ jobs:
|
|||||||
name: "Gate 2.2: E2E Tests"
|
name: "Gate 2.2: E2E Tests"
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: gate-2-start
|
needs: gate-2-start
|
||||||
|
if: ${{ !inputs.skip_tests }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
@@ -813,6 +815,7 @@ jobs:
|
|||||||
name: "Gate 2.3: DBAL Daemon E2E"
|
name: "Gate 2.3: DBAL Daemon E2E"
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: gate-2-start
|
needs: gate-2-start
|
||||||
|
if: ${{ !inputs.skip_tests }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
@@ -1569,7 +1572,7 @@ jobs:
|
|||||||
name: "Gate 7 App: ${{ matrix.image }}"
|
name: "Gate 7 App: ${{ matrix.image }}"
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [container-base-tier1]
|
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:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
|
|||||||
@@ -13,6 +13,20 @@ const __dirname = dirname(fileURLToPath(import.meta.url))
|
|||||||
|
|
||||||
let environment: Awaited<ReturnType<DockerComposeEnvironment['up']>> | undefined
|
let environment: Awaited<ReturnType<DockerComposeEnvironment['up']>> | undefined
|
||||||
|
|
||||||
|
async function waitForServer(url: string, timeoutMs = 60000): Promise<void> {
|
||||||
|
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() {
|
async function globalSetup() {
|
||||||
// ── 1. Start smoke stack via Testcontainers ──────────────────────────────
|
// ── 1. Start smoke stack via Testcontainers ──────────────────────────────
|
||||||
console.log('[setup] Starting smoke stack via Testcontainers...')
|
console.log('[setup] Starting smoke stack via Testcontainers...')
|
||||||
@@ -32,14 +46,14 @@ async function globalSetup() {
|
|||||||
;(globalThis as Record<string, unknown>).__TESTCONTAINERS_ENV__ = environment
|
;(globalThis as Record<string, unknown>).__TESTCONTAINERS_ENV__ = environment
|
||||||
|
|
||||||
// ── 2. Wait for dev servers (started by Playwright webServer config) ─────
|
// ── 2. Wait for dev servers (started by Playwright webServer config) ─────
|
||||||
await new Promise(resolve => setTimeout(resolve, 2000))
|
|
||||||
|
|
||||||
// ── 3. Seed database ────────────────────────────────────────────────────
|
// ── 3. Seed database ────────────────────────────────────────────────────
|
||||||
// workflowui uses basePath: '/workflowui', so the setup route is at /workflowui/api/setup
|
// workflowui uses basePath: '/workflowui', so the setup route is at /workflowui/api/setup
|
||||||
const setupUrl = process.env.PLAYWRIGHT_BASE_URL
|
const setupUrl = process.env.PLAYWRIGHT_BASE_URL
|
||||||
? new URL('/workflowui/api/setup', process.env.PLAYWRIGHT_BASE_URL.replace(/\/workflowui\/?$/, '')).href
|
? new URL('/workflowui/api/setup', process.env.PLAYWRIGHT_BASE_URL.replace(/\/workflowui\/?$/, '')).href
|
||||||
: 'http://localhost:3000/workflowui/api/setup'
|
: 'http://localhost:3000/workflowui/api/setup'
|
||||||
|
|
||||||
|
await waitForServer(setupUrl)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(setupUrl, { method: 'POST' })
|
const response = await fetch(setupUrl, { method: 'POST' })
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
|||||||
@@ -74,6 +74,12 @@ export async function POST(request: NextRequest) {
|
|||||||
return limitResponse
|
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 }
|
const results = { packages: 0, pages: 0, skipped: 0, errors: 0 }
|
||||||
|
|
||||||
// Seed InstalledPackage records
|
// Seed InstalledPackage records
|
||||||
|
|||||||
@@ -4,8 +4,15 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { NextRequest } from 'next/server'
|
import type { NextRequest } from 'next/server'
|
||||||
|
import { NextResponse } from 'next/server'
|
||||||
import { POST as bootstrap } from '@/app/api/bootstrap/route'
|
import { POST as bootstrap } from '@/app/api/bootstrap/route'
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
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)
|
return bootstrap(request)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user