Compare commits

...

7 Commits

Author SHA1 Message Date
Claude
eb457faa9b Review fixes: parameterize DBAL base image, report seed errors, update pipeline docs
- DBAL Dockerfile: Add ARG BASE_REGISTRY=metabuilder so CI can override
  the FROM image path to ghcr.io/... (was hardcoded metabuilder/base-apt)
- Setup route: Return HTTP 207 + success:false when seed errors occur
  instead of always returning 200/true
- Pipeline: Update comments/diagram to reflect Gate 7 running after
  Gate 1 (not after Gate 6), add dbal + dbal-init to Trivy scan matrix

https://claude.ai/code/session_01ChKf8wbKQLBcNbBCtqCwT6
2026-03-11 21:10:20 +00:00
Claude
659324c823 fix(ci): build all container images to GHCR before E2E tests
Move Gate 7 container builds (base images T1→T2→T3 + app images) to
run right after Gate 1 instead of after Gate 3. Gate 2 (E2E) now
depends on container-build-apps completing, so the smoke stack pulls
prod images from GHCR — no special E2E images, same images used
everywhere.

- container-base-tier1 needs gate-1-complete (was gate-3-complete)
- container-build-apps runs on all events including PRs
- All images push: true unconditionally (E2E needs them in GHCR)
- E2E just logs into GHCR, smoke compose pulls via image: directives
- Added dbal + dbal-init to Gate 7 app matrix

https://claude.ai/code/session_01ChKf8wbKQLBcNbBCtqCwT6
2026-03-11 21:03:24 +00:00
Claude
d7816b09be fix(e2e): add real DBAL + PostgreSQL to smoke stack
Replace the DBAL API stubs in the smoke stack with a real C++ DBAL
daemon backed by PostgreSQL so E2E tests have a functioning backend
to seed and query data against.

- Add postgres (tmpfs-backed) and dbal services to smoke compose
- Add dbal-init to seed schemas/templates into named volumes
- Support DBAL_IMAGE env var to pull pre-built image from GHCR
  instead of building from source (for a publish-before-e2e flow)
- Update nginx smoke config to proxy /api to the real DBAL daemon
  instead of returning hardcoded stub responses
- DBAL auto-seeds on startup via DBAL_SEED_ON_STARTUP=true

https://claude.ai/code/session_01ChKf8wbKQLBcNbBCtqCwT6
2026-03-11 20:58:42 +00:00
Claude
8b0924ed65 fix(e2e): add /api/setup route to workflowui and fail fast on seed error
The E2E global setup calls POST /api/setup on localhost:3000, but port
3000 is the workflowui dev server which had no such route — it only
existed in the nextjs workspace. This caused a 404, leaving the DB
empty and making all data-dependent tests (workflowui-auth,
workflowui-templates) time out waiting for content that was never seeded.

- Add /api/setup/route.ts to workflowui that seeds InstalledPackage and
  PageConfig records via the DBAL REST API
- Make global setup throw on seed failure instead of logging and
  continuing, so the suite fails fast rather than running 250 tests
  against an empty database

https://claude.ai/code/session_01ChKf8wbKQLBcNbBCtqCwT6
2026-03-11 20:55:17 +00:00
84f8122ef3 Merge pull request #1506 from johndoe6345789/claude/fix-dirname-e2e-setup-EBgh1
Fix __dirname ReferenceError in E2E global setup
2026-03-11 19:21:27 +00:00
Claude
a8b87e405e Fix __dirname ReferenceError in E2E global setup
The root package.json uses "type": "module" (ESM), so __dirname is
not available. Derive it from import.meta.url instead.

https://claude.ai/code/session_01JJckq16HxKozwoh3XDJcQ1
2026-03-11 19:20:30 +00:00
a65b95a068 Merge pull request #1505 from johndoe6345789/claude/fix-github-actions-SSaHp
fix(ci): resolve E2E test failures and upgrade GitHub Actions to Node.js 24
2026-03-11 18:32:00 +00:00
6 changed files with 240 additions and 44 deletions

View File

@@ -71,12 +71,12 @@ permissions:
#
# Sequential Gates (fan-out/fan-in):
# Gate 1: Code Quality (DBAL schemas, typecheck, lint, security)
# Gate 2: Testing (unit with coverage, E2E, DBAL daemon)
# Gate 7: Container Build & Push to GHCR (after Gate 1, before testing)
# Gate 2: Testing (unit with coverage, E2E with prod images, DBAL daemon)
# Gate 3: Build & Package
# Gate 4: Development Assistance (PR only)
# Gate 5: Staging Deployment (main branch push)
# Gate 6: Production Deployment (release or manual with approval)
# Gate 7: Container Build & Push (push/tag/dispatch, not PRs)
# ════════════════════════════════════════════════════════════════════════════════
jobs:
@@ -654,13 +654,13 @@ jobs:
path: gate-artifacts/
# ============================================================================
# GATE 2: Testing Gates
# GATE 2: Testing Gates (runs after container images are published to GHCR)
# ============================================================================
gate-2-start:
name: "Gate 2: Testing - Starting"
runs-on: ubuntu-latest
needs: gate-1-complete
needs: [gate-1-complete, container-build-apps]
steps:
- name: Gate 2 checkpoint
run: |
@@ -759,6 +759,13 @@ jobs:
- name: Checkout code
uses: actions/checkout@v6
- name: Log in to GitHub Container Registry
uses: docker/login-action@v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Setup npm with Nexus
uses: ./.github/actions/setup-npm
with:
@@ -777,7 +784,7 @@ jobs:
else
echo "::warning::No playwright.config.ts found — E2E tests not configured"
fi
timeout-minutes: 10
timeout-minutes: 15
- name: Upload test results
if: always()
@@ -1297,7 +1304,7 @@ jobs:
});
# ============================================================================
# GATE 7: Container Build & Push (push/tag/dispatch only, not PRs)
# GATE 7: Container Build & Push to GHCR (after Gate 1, before testing)
# ════════════════════════════════════════════════════════════════════════════
# Tiered base images respecting the dependency DAG:
# Tier 1 (independent): base-apt, base-node-deps, base-pip-deps
@@ -1311,8 +1318,8 @@ jobs:
container-base-tier1:
name: "Gate 7 T1: ${{ matrix.image }}"
runs-on: ubuntu-latest
needs: gate-3-complete
if: github.event_name != 'pull_request' && github.event_name != 'issues' && github.event_name != 'issue_comment'
needs: gate-1-complete
if: github.event_name != 'issues' && github.event_name != 'issue_comment'
strategy:
fail-fast: false
matrix:
@@ -1363,7 +1370,7 @@ jobs:
context: .
file: ${{ matrix.dockerfile }}
platforms: ${{ matrix.platforms }}
push: ${{ github.event_name != 'pull_request' }}
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha,scope=${{ matrix.image }}
@@ -1431,7 +1438,7 @@ jobs:
context: .
file: ${{ matrix.dockerfile }}
platforms: ${{ matrix.platforms }}
push: ${{ github.event_name != 'pull_request' }}
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha,scope=${{ matrix.image }}
@@ -1490,7 +1497,7 @@ jobs:
context: .
file: ./deployment/base-images/Dockerfile.devcontainer
platforms: linux/amd64,linux/arm64
push: ${{ github.event_name != 'pull_request' }}
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha,scope=devcontainer
@@ -1512,7 +1519,7 @@ jobs:
name: "Gate 7 App: ${{ matrix.image }}"
runs-on: ubuntu-latest
needs: [container-base-tier1]
if: github.event_name != 'pull_request' && github.event_name != 'issues' && github.event_name != 'issue_comment' && !failure()
if: github.event_name != 'issues' && github.event_name != 'issue_comment' && !failure()
strategy:
fail-fast: false
matrix:
@@ -1538,6 +1545,12 @@ jobs:
- image: exploded-diagrams
context: .
dockerfile: ./frontends/exploded-diagrams/Dockerfile
- image: dbal
context: ./dbal
dockerfile: ./dbal/production/build-config/Dockerfile
- image: dbal-init
context: .
dockerfile: ./deployment/config/dbal/Dockerfile.init
steps:
- name: Checkout repository
uses: actions/checkout@v6
@@ -1571,7 +1584,7 @@ jobs:
with:
context: ${{ matrix.context }}
file: ${{ matrix.dockerfile }}
push: ${{ github.event_name != 'pull_request' }}
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha,scope=${{ matrix.image }}
@@ -1613,6 +1626,8 @@ jobs:
- postgres-dashboard
- workflowui
- exploded-diagrams
- dbal
- dbal-init
steps:
- name: Log in to GitHub Container Registry
uses: docker/login-action@v4
@@ -1776,7 +1791,13 @@ jobs:
summary += ' 1.1 DBAL Schemas 1.2 TypeScript 1.3 Lint\n';
summary += ' 1.4 Security 1.5 File Size 1.6 Complexity 1.7 Stubs\n';
summary += ' |\n';
summary += 'Gate 2: Testing (3 steps)\n';
summary += 'Gate 7: Containers (after Gate 1)\n';
summary += ' T1: base-apt, node-deps, pip-deps\n';
summary += ' T2: conan-deps, android-sdk\n';
summary += ' T3: devcontainer\n';
summary += ' Apps: 9 images (incl. dbal, dbal-init) -> GHCR\n';
summary += ' |\n';
summary += 'Gate 2: Testing (3 steps, pulls prod images)\n';
summary += ' 2.1 Unit Tests (+ coverage) 2.2 E2E 2.3 DBAL\n';
summary += ' |\n';
summary += 'Gate 3: Build (2 steps)\n';
@@ -1787,12 +1808,6 @@ jobs:
summary += 'Gate 5: Staging (main push)\n';
summary += ' |\n';
summary += 'Gate 6: Production (release/manual)\n';
summary += ' |\n';
summary += 'Gate 7: Containers (push/tag/dispatch)\n';
summary += ' T1: base-apt, node-deps, pip-deps\n';
summary += ' T2: conan-deps, android-sdk\n';
summary += ' T3: devcontainer\n';
summary += ' Apps: 7 images -> Trivy scan -> Multi-arch manifests\n';
summary += '```\n\n';
console.log(summary);

View File

@@ -5,7 +5,8 @@
ARG BUILD_TYPE=Release
# ── Build stage ──────────────────────────────────────────────────────────────
FROM metabuilder/base-apt:latest AS builder
ARG BASE_REGISTRY=metabuilder
FROM ${BASE_REGISTRY}/base-apt:latest AS builder
ARG BUILD_TYPE
@@ -56,7 +57,8 @@ RUN cd /dbal/build \
&& strip dbal_daemon
# ── Runtime stage ────────────────────────────────────────────────────────────
FROM metabuilder/base-apt:latest
ARG BASE_REGISTRY=metabuilder
FROM ${BASE_REGISTRY}/base-apt:latest
WORKDIR /app

View File

@@ -27,16 +27,13 @@ server {
proxy_read_timeout 120s;
}
# ── DBAL API stubs ────────────────────────────────────────────────────
# ── DBAL API — proxied to real C++ daemon ─────────────────────────────
location = /api/health {
add_header Content-Type application/json;
return 200 '{"status":"ok"}';
}
location = /api/version {
add_header Content-Type application/json;
return 200 '{"version":"smoke-stub"}';
location /api {
proxy_pass http://dbal:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_read_timeout 30s;
}
# ── Portal — must contain "MetaBuilder" ───────────────────────────────

View File

@@ -1,12 +1,8 @@
# docker-compose.smoke.yml — Lightweight smoke test stack for CI.
# docker-compose.smoke.yml — Smoke test stack for CI.
#
# Starts real admin-tool containers (phpMyAdmin, Mongo Express, RedisInsight)
# and a stub nginx gateway that returns 200 for all app paths.
# Uses only stock Docker Hub images — no custom builds required.
#
# The stub gateway lets path-routing smoke tests pass in CI without needing
# the full built stack. End-to-end deployment correctness is tested in
# staging/production against the real images.
# Includes a real DBAL daemon backed by PostgreSQL so E2E tests can seed
# and query data. Admin tools (phpMyAdmin, Mongo Express, RedisInsight)
# and an nginx gateway round out the stack.
#
# Usage:
# docker compose -f deployment/docker-compose.smoke.yml up -d --wait
@@ -27,6 +23,9 @@ services:
# On Linux (GitHub Actions) this requires the host-gateway extra_host.
extra_hosts:
- "host.docker.internal:host-gateway"
depends_on:
dbal:
condition: service_healthy
healthcheck:
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://127.0.0.1/"]
interval: 5s
@@ -35,6 +34,75 @@ services:
networks:
- smoke
# ── DBAL + PostgreSQL ────────────────────────────────────────────────────
postgres:
image: postgres:15-alpine
environment:
POSTGRES_USER: metabuilder
POSTGRES_PASSWORD: metabuilder
POSTGRES_DB: metabuilder
tmpfs:
- /var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U metabuilder"]
interval: 5s
timeout: 3s
retries: 10
networks:
- smoke
dbal-init:
image: ${DBAL_INIT_IMAGE:-ghcr.io/johndoe6345789/metabuilder/dbal-init:latest}
build:
context: ..
dockerfile: deployment/config/dbal/Dockerfile.init
volumes:
- dbal-schemas:/target/schemas/entities
- dbal-templates:/target/templates/sql
networks:
- smoke
dbal:
image: ${DBAL_IMAGE:-ghcr.io/johndoe6345789/metabuilder/dbal:latest}
build:
context: ../dbal
dockerfile: production/build-config/Dockerfile
ports:
- "8080:8080"
environment:
DBAL_ADAPTER: postgres
DATABASE_URL: "postgresql://metabuilder:metabuilder@postgres:5432/metabuilder"
DBAL_SCHEMA_DIR: /app/schemas/entities
DBAL_TEMPLATE_DIR: /app/templates/sql
DBAL_SEED_DIR: /app/seeds/database
DBAL_SEED_ON_STARTUP: "true"
DBAL_BIND_ADDRESS: 0.0.0.0
DBAL_PORT: 8080
DBAL_MODE: production
DBAL_DAEMON: "true"
DBAL_LOG_LEVEL: info
DBAL_AUTO_CREATE_TABLES: "true"
DBAL_ENABLE_HEALTH_CHECK: "true"
DBAL_ADMIN_TOKEN: "smoke-test-admin-token"
DBAL_CORS_ORIGIN: "*"
JWT_SECRET_KEY: "test-secret"
volumes:
- dbal-schemas:/app/schemas/entities:ro
- dbal-templates:/app/templates/sql:ro
depends_on:
dbal-init:
condition: service_completed_successfully
postgres:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-f", "http://127.0.0.1:8080/health"]
interval: 5s
timeout: 3s
retries: 15
start_period: 10s
networks:
- smoke
# ── Infrastructure (stock images) ─────────────────────────────────────────
mysql:
image: mysql:8.0
@@ -142,6 +210,12 @@ services:
networks:
- smoke
volumes:
dbal-schemas:
driver: local
dbal-templates:
driver: local
networks:
smoke:
driver: bridge

View File

@@ -6,7 +6,10 @@
* 2. Seeds the database via the /api/setup endpoint
*/
import { DockerComposeEnvironment, Wait } from 'testcontainers'
import { resolve } from 'path'
import { resolve, dirname } from 'path'
import { fileURLToPath } from 'url'
const __dirname = dirname(fileURLToPath(import.meta.url))
let environment: Awaited<ReturnType<DockerComposeEnvironment['up']>> | undefined
@@ -39,12 +42,13 @@ async function globalSetup() {
try {
const response = await fetch(setupUrl, { method: 'POST' })
if (!response.ok) {
console.error('[setup] Failed to seed database:', response.status, response.statusText)
} else {
console.log('[setup] Database seeded successfully')
throw new Error(`Seed endpoint returned ${response.status} ${response.statusText}`)
}
console.log('[setup] Database seeded successfully')
} catch (error) {
console.warn('[setup] Setup endpoint not available (non-fatal):', (error as Error).message)
const message = error instanceof Error ? error.message : String(error)
console.error('[setup] Failed to seed database:', message)
throw new Error(`[setup] Seeding failed — aborting test suite. ${message}`)
}
}

View File

@@ -0,0 +1,104 @@
/**
* POST /api/setup
*
* Seeds the database via the C++ DBAL REST API with system packages
* and page configurations. Called by E2E global setup to bootstrap
* test data before the test suite runs.
*/
import { NextResponse } from 'next/server'
const DBAL_URL =
process.env.DBAL_DAEMON_URL ??
process.env.DBAL_ENDPOINT ??
process.env.NEXT_PUBLIC_API_URL ??
'http://localhost:8080'
const ENTITY_BASE = `${DBAL_URL}/system/core`
const SEED_PACKAGES = [
{ packageId: 'package_manager', version: '1.0.0', enabled: true, config: '{"autoUpdate":false,"systemPackage":true,"uninstallProtection":true}' },
{ packageId: 'ui_header', version: '1.0.0', enabled: true, config: '{"systemPackage":true}' },
{ packageId: 'ui_footer', version: '1.0.0', enabled: true, config: '{"systemPackage":true}' },
{ packageId: 'ui_home', version: '1.0.0', enabled: true, config: '{"systemPackage":true,"defaultRoute":"/","publicAccess":true}' },
{ packageId: 'ui_auth', version: '1.0.0', enabled: true, config: '{"systemPackage":true}' },
{ packageId: 'ui_login', version: '1.0.0', enabled: true, config: '{"systemPackage":true}' },
{ packageId: 'dashboard', version: '1.0.0', enabled: true, config: '{"systemPackage":true,"defaultRoute":"/"}' },
{ packageId: 'user_manager', version: '1.0.0', enabled: true, config: '{"systemPackage":true,"minLevel":4}' },
{ packageId: 'role_editor', version: '1.0.0', enabled: true, config: '{"systemPackage":false,"minLevel":4}' },
{ packageId: 'admin_dialog', version: '1.0.0', enabled: true, config: '{"systemPackage":false,"minLevel":4}' },
{ packageId: 'database_manager', version: '1.0.0', enabled: true, config: '{"systemPackage":false,"minLevel":5,"dangerousOperations":true}' },
{ packageId: 'schema_editor', version: '1.0.0', enabled: true, config: '{"systemPackage":false,"minLevel":5,"dangerousOperations":true}' },
]
const SEED_PAGE_CONFIGS = [
{ path: '/', title: 'MetaBuilder', description: 'Data-driven application platform', packageId: 'ui_home', component: 'home_page', componentTree: '{}', level: 0, requiresAuth: false, isPublished: true, sortOrder: 0 },
{ path: '/dashboard', title: 'Dashboard', packageId: 'dashboard', component: 'dashboard_home', componentTree: '{}', level: 1, requiresAuth: true, isPublished: true, sortOrder: 0 },
{ path: '/profile', title: 'User Profile', packageId: 'dashboard', component: 'user_profile', componentTree: '{}', level: 1, requiresAuth: true, isPublished: true, sortOrder: 50 },
{ path: '/login', title: 'Login', packageId: 'ui_login', component: 'login_page', componentTree: '{}', level: 0, requiresAuth: false, isPublished: true, sortOrder: 0 },
{ path: '/admin', title: 'Administration', packageId: 'admin_dialog', component: 'admin_panel', componentTree: '{}', level: 4, requiresAuth: true, isPublished: true, sortOrder: 0 },
{ path: '/admin/users', title: 'User Management', packageId: 'user_manager', component: 'user_list', componentTree: '{}', level: 4, requiresAuth: true, isPublished: true, sortOrder: 10 },
{ path: '/admin/roles', title: 'Role Editor', packageId: 'role_editor', component: 'role_editor', componentTree: '{}', level: 4, requiresAuth: true, isPublished: true, sortOrder: 20 },
{ path: '/admin/database', title: 'Database Manager', packageId: 'database_manager', component: 'database_manager', componentTree: '{}', level: 5, requiresAuth: true, isPublished: true, sortOrder: 30 },
{ path: '/admin/schema', title: 'Schema Editor', packageId: 'schema_editor', component: 'schema_editor', componentTree: '{}', level: 5, requiresAuth: true, isPublished: true, sortOrder: 40 },
{ path: '/admin/packages', title: 'Package Manager', packageId: 'package_manager', component: 'package_list', componentTree: '{}', level: 4, requiresAuth: true, isPublished: true, sortOrder: 50 },
]
async function dbalPost(entity: string, data: Record<string, unknown>): Promise<{ ok: boolean; status: number }> {
const res = await fetch(`${ENTITY_BASE}/${entity}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
})
return { ok: res.ok, status: res.status }
}
export async function POST() {
const results = { packages: 0, pages: 0, skipped: 0, errors: 0 }
for (const pkg of SEED_PACKAGES) {
try {
const res = await dbalPost('InstalledPackage', {
...pkg,
installedAt: Math.floor(Date.now() / 1000),
})
if (res.ok) {
results.packages++
} else if (res.status === 409) {
results.skipped++
} else {
results.errors++
console.warn(`[Seed] Failed to seed package ${pkg.packageId}: HTTP ${res.status}`)
}
} catch (error) {
results.errors++
console.warn(`[Seed] Failed to seed package ${pkg.packageId}:`, error instanceof Error ? error.message : error)
}
}
for (const page of SEED_PAGE_CONFIGS) {
try {
const res = await dbalPost('PageConfig', page)
if (res.ok) {
results.pages++
} else if (res.status === 409) {
results.skipped++
} else {
results.errors++
console.warn(`[Seed] Failed to seed page ${page.path}: HTTP ${res.status}`)
}
} catch (error) {
results.errors++
console.warn(`[Seed] Failed to seed page ${page.path}:`, error instanceof Error ? error.message : error)
}
}
console.warn(`[Seed] Complete: ${results.packages} packages, ${results.pages} pages, ${results.skipped} skipped, ${results.errors} errors`)
const status = results.errors > 0 ? 207 : 200
return NextResponse.json({
success: results.errors === 0,
message: results.errors > 0 ? `Seeded with ${results.errors} error(s)` : 'Database seeded successfully',
results,
}, { status })
}