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: |
|
||||
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:
|
||||
|
||||
@@ -13,6 +13,20 @@ const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
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() {
|
||||
// ── 1. Start 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
|
||||
|
||||
// ── 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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user