Files
metabuilder/.github/workflows/gated-pipeline.yml
Claude 9c982a6b93 fix(e2e): use Testcontainers for smoke stack instead of docker compose in CI
Replace manual docker compose start/stop in the CI workflow with
Testcontainers in Playwright global setup/teardown. This gives:
- Automatic container lifecycle tied to the test run
- Health-check-based wait strategies per service
- Clean teardown even on test failures
- No CI workflow coupling to Docker orchestration

Changes:
- e2e/global.setup.ts: Start smoke stack via DockerComposeEnvironment
  (nginx, phpMyAdmin, Mongo Express, RedisInsight) with health check waits
- e2e/global.teardown.ts: New file — stops Testcontainers environment
- e2e/playwright.config.ts: Register globalSetup/globalTeardown, bind dev
  servers to 0.0.0.0 in CI so nginx can proxy via host.docker.internal
- gated-pipeline.yml: Remove docker compose start/stop/verify steps,
  add 10min timeout to Playwright step
- e2e/deployment-smoke.spec.ts: Update doc comment
- package.json: Add testcontainers@^11.12.0 devDependency

https://claude.ai/code/session_018rmhuicK7L7jV2YBJDXiQz
2026-03-11 18:31:06 +00:00

1815 lines
66 KiB
YAML

name: Enterprise Gated Pipeline
on:
push:
branches: [ main, master, develop ]
tags:
- 'v*.*.*'
pull_request:
branches: [ main, master, develop ]
types: [opened, synchronize, ready_for_review, edited, reopened]
release:
types: [published]
issues:
types: [opened, edited, reopened]
issue_comment:
types: [created]
workflow_dispatch:
inputs:
environment:
description: 'Target deployment environment'
required: true
type: choice
options:
- staging
- production
skip_tests:
description: 'Skip pre-deployment tests (emergency only)'
required: false
type: boolean
default: false
run_codeql:
description: 'Run CodeQL semantic analysis'
required: false
type: boolean
default: false
codeql_languages:
description: 'CodeQL languages to analyze'
required: false
type: choice
options:
- all
- javascript-typescript
- python
- cpp
- go
default: all
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
permissions:
contents: read
pull-requests: write
checks: write
statuses: write
issues: write
deployments: write
packages: write
id-token: write
attestations: write
security-events: write
# ════════════════════════════════════════════════════════════════════════════════
# Unified Enterprise Gated Pipeline — One YML to Rule Them All
# ════════════════════════════════════════════════════════════════════════════════
#
# Standalone (event-guarded, run independently of gates):
# - Triage: Auto-label issues and PRs (issues/PR events only)
# - CodeQL: Semantic code analysis (manual dispatch only)
#
# 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 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:
# ============================================================================
# TRIAGE: Auto-label Issues and PRs (standalone, event-guarded)
# ============================================================================
triage-issue:
name: "Triage: Auto-Label Issue"
if: github.event_name == 'issues'
runs-on: ubuntu-latest
steps:
- name: Categorize and label issue
uses: actions/github-script@v7
with:
script: |
const issue = context.payload.issue;
const title = (issue.title || '').toLowerCase();
const body = (issue.body || '').toLowerCase();
const text = `${title}\n${body}`;
const labels = new Set();
const missing = [];
const typeMatchers = [
{ regex: /bug|error|crash|broken|fail/, label: 'bug' },
{ regex: /feature|enhancement|add|new|implement/, label: 'enhancement' },
{ regex: /document|readme|docs|guide/, label: 'documentation' },
{ regex: /test|testing|spec|e2e/, label: 'testing' },
{ regex: /security|vulnerability|exploit|xss|sql/, label: 'security' },
{ regex: /performance|slow|optimize|speed/, label: 'performance' },
];
for (const match of typeMatchers) {
if (text.match(match.regex)) {
labels.add(match.label);
}
}
const areaMatchers = [
{ regex: /frontend|react|next|ui|component|browser/, label: 'area: frontend' },
{ regex: /api|backend|service|server/, label: 'area: backend' },
{ regex: /database|prisma|schema|sql/, label: 'area: database' },
{ regex: /workflow|github actions|ci|pipeline/, label: 'area: workflows' },
{ regex: /docs|readme|guide/, label: 'area: documentation' },
];
for (const match of areaMatchers) {
if (text.match(match.regex)) {
labels.add(match.label);
}
}
if (text.match(/critical|urgent|asap|blocker/)) {
labels.add('priority: high');
} else if (text.match(/minor|low|nice to have/)) {
labels.add('priority: low');
} else {
labels.add('priority: medium');
}
if (text.match(/beginner|easy|simple|starter/) || labels.size <= 2) {
labels.add('good first issue');
}
const reproductionHints = ['steps to reproduce', 'expected', 'actual'];
for (const hint of reproductionHints) {
if (!body.includes(hint)) {
missing.push(hint);
}
}
const supportInfo = body.includes('version') || body.match(/v\d+\.\d+/);
if (!supportInfo) {
missing.push('version information');
}
if (labels.size > 0) {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
labels: Array.from(labels),
}).catch(e => console.log('Some labels may not exist:', e.message));
}
const checklist = missing.map(item => `- [ ] Add ${item}`).join('\n') || '- [x] Description includes key details.';
const summary = Array.from(labels).map(l => `- ${l}`).join('\n') || '- No labels inferred yet.';
const comment = [
'Thanks for reporting an issue! Quick triage:',
'',
'**Proposed labels:**',
summary,
'',
'**Missing details:**',
checklist,
'',
'Adding the missing details will help reviewers respond faster.',
].join('\n');
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
body: comment,
});
triage-pr:
name: "Triage: Auto-Label PR"
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- name: Analyze PR files and label
uses: actions/github-script@v7
with:
script: |
const pr = context.payload.pull_request;
const { data: files } = await github.rest.pulls.listFiles({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: pr.number,
});
const labels = new Set();
const fileFlags = {
workflows: files.some(f => f.filename.includes('.github/workflows')),
docs: files.some(f => f.filename.match(/\.(md|mdx)$/) || f.filename.startsWith('docs/')),
frontend: files.some(f => f.filename.includes('frontends/nextjs')),
db: files.some(f => f.filename.includes('prisma/') || f.filename.includes('dbal/')),
tests: files.some(f => f.filename.match(/(test|spec)\.[jt]sx?/)),
};
if (fileFlags.workflows) labels.add('area: workflows');
if (fileFlags.docs) labels.add('area: documentation');
if (fileFlags.frontend) labels.add('area: frontend');
if (fileFlags.db) labels.add('area: database');
if (fileFlags.tests) labels.add('tests');
const totalChanges = files.reduce((sum, f) => sum + f.additions + f.deletions, 0);
const highRiskPaths = files.filter(f => f.filename.includes('.github/workflows') || f.filename.includes('prisma/'));
let riskLabel = 'risk: low';
if (highRiskPaths.length > 0 || totalChanges >= 400) {
riskLabel = 'risk: high';
} else if (totalChanges >= 150) {
riskLabel = 'risk: medium';
}
labels.add(riskLabel);
const missing = [];
const body = (pr.body || '').toLowerCase();
if (!body.includes('test')) missing.push('Test plan');
if (fileFlags.frontend && !body.includes('screenshot')) missing.push('Screenshots for UI changes');
if (!body.match(/#\d+|https:\/\/github\.com/)) missing.push('Linked issue reference');
if (labels.size > 0) {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
labels: Array.from(labels),
}).catch(e => console.log('Some labels may not exist:', e.message));
}
const labelSummary = Array.from(labels).map(l => `- ${l}`).join('\n');
const missingList = missing.length ? missing.map(item => `- [ ] ${item}`).join('\n') : '- [x] Description includes required context.';
const comment = [
'**Automated PR triage**',
'',
'**Proposed labels:**',
labelSummary,
'',
'**Description check:**',
missingList,
'',
'If any labels look incorrect, feel free to adjust them.',
].join('\n');
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
body: comment,
});
# ============================================================================
# GATE 1: Code Quality Gates
# ============================================================================
gate-1-start:
name: "Gate 1: Code Quality - Starting"
runs-on: ubuntu-latest
if: |
github.event_name != 'issue_comment' &&
(github.event_name != 'pull_request' || !github.event.pull_request.draft)
steps:
- name: Gate 1 checkpoint
run: |
echo "Gate 1: CODE QUALITY VALIDATION"
echo "================================================"
echo "Running validation steps..."
- name: Create gate artifacts directory
run: |
mkdir -p gate-artifacts/gate-1
echo "started" > gate-artifacts/gate-1/status.txt
echo "$(date -Iseconds)" > gate-artifacts/gate-1/start-time.txt
- name: Upload gate start marker
uses: actions/upload-artifact@v6
with:
name: gate-1-start
path: gate-artifacts/gate-1/
# Atomic Step 1.1: DBAL Entity Schema Validation
schema-check:
name: "Gate 1.1: Validate DBAL Entity Schemas"
runs-on: ubuntu-latest
needs: gate-1-start
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Validate DBAL entity JSON schemas
run: |
python3 -c "
import json, sys, os, glob
schema_dir = 'dbal/shared/api/schema/entities'
errors = []
total_entities = 0
# 1. Validate entities.json registry exists and refs resolve
registry = os.path.join(schema_dir, 'entities.json')
if not os.path.exists(registry):
errors.append('Missing entities.json registry')
else:
with open(registry) as f:
reg = json.load(f)
refs = [e['\$ref'] for e in reg.get('entities', [])]
for ref in refs:
path = os.path.join(schema_dir, ref.lstrip('./'))
if not os.path.exists(path):
errors.append(f'Registry ref not found: {ref}')
# 2. Validate each entity JSON file
jsons = glob.glob(os.path.join(schema_dir, '**/*.json'), recursive=True)
entity_files = [f for f in jsons if os.path.basename(f) != 'entities.json']
def validate_doc(doc, rel):
if not doc or not isinstance(doc, dict):
errors.append(f'{rel}: empty or non-mapping document')
return
has_name = any(k in doc for k in ('entity', 'name', 'displayName'))
if not has_name:
errors.append(f'{rel}: missing entity/name/displayName key')
if 'fields' not in doc:
errors.append(f'{rel}: missing fields key')
elif not isinstance(doc['fields'], dict):
errors.append(f'{rel}: fields must be a mapping')
else:
for fname, fdef in doc['fields'].items():
if not isinstance(fdef, dict) or 'type' not in fdef:
errors.append(f'{rel}: field {fname} missing type')
for filepath in sorted(entity_files):
rel = os.path.relpath(filepath, schema_dir)
try:
with open(filepath) as f:
doc = json.load(f)
if isinstance(doc, list):
for i, item in enumerate(doc):
validate_doc(item, f'{rel}[{i}]')
total_entities += 1
else:
validate_doc(doc, rel)
total_entities += 1
except json.JSONDecodeError as e:
errors.append(f'{rel}: JSON parse error: {e}')
print(f'Checked {len(entity_files)} files, {total_entities} entities')
if errors:
print(f'Found {len(errors)} error(s):')
for e in errors:
print(f' ERROR: {e}')
sys.exit(1)
else:
print('All DBAL entity schemas valid')
"
- name: Record validation result
if: always()
run: |
mkdir -p gate-artifacts/gate-1
echo "${{ job.status }}" > gate-artifacts/gate-1/schema-check.txt
echo "$(date -Iseconds)" > gate-artifacts/gate-1/schema-check-time.txt
- name: Upload validation result
if: always()
uses: actions/upload-artifact@v6
with:
name: gate-1-schema-result
path: gate-artifacts/gate-1/
# Atomic Step 1.2: TypeScript Check
typecheck:
name: "Gate 1.2: TypeScript Type Check"
runs-on: ubuntu-latest
needs: schema-check
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Setup npm with Nexus
uses: ./.github/actions/setup-npm
with:
node-version: '20'
- name: Build workspace packages
run: npm run build --workspaces --if-present 2>&1
- name: Run TypeScript type check
run: npm run typecheck -w frontends/nextjs
- name: Record validation result
if: always()
run: |
mkdir -p gate-artifacts/gate-1
echo "${{ job.status }}" > gate-artifacts/gate-1/typecheck.txt
echo "$(date -Iseconds)" > gate-artifacts/gate-1/typecheck-time.txt
- name: Upload validation result
if: always()
uses: actions/upload-artifact@v6
with:
name: gate-1-typecheck-result
path: gate-artifacts/gate-1/
# Atomic Step 1.3: ESLint
lint:
name: "Gate 1.3: Lint Code"
runs-on: ubuntu-latest
needs: schema-check
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Setup npm with Nexus
uses: ./.github/actions/setup-npm
with:
node-version: '20'
- name: Build workspace packages
run: npm run build --workspaces --if-present 2>&1
- name: Run ESLint
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)
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)"
exit 1
fi
echo "Lint: passed with $LOCAL_ERRORS issues (within threshold)"
- name: Record validation result
if: always()
run: |
mkdir -p gate-artifacts/gate-1
echo "${{ job.status }}" > gate-artifacts/gate-1/lint.txt
echo "$(date -Iseconds)" > gate-artifacts/gate-1/lint-time.txt
- name: Upload validation result
if: always()
uses: actions/upload-artifact@v6
with:
name: gate-1-lint-result
path: gate-artifacts/gate-1/
# Atomic Step 1.4: Security Scan
security-scan:
name: "Gate 1.4: Security Scan"
runs-on: ubuntu-latest
needs: schema-check
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Setup npm with Nexus
uses: ./.github/actions/setup-npm
with:
node-version: '20'
- name: Run dependency audit
run: |
mkdir -p gate-artifacts/gate-1
npm audit --json > gate-artifacts/gate-1/audit-results.json 2>&1
echo "Security audit completed"
continue-on-error: true
- name: Record validation result
if: always()
run: |
mkdir -p gate-artifacts/gate-1
echo "${{ job.status }}" > gate-artifacts/gate-1/security-scan.txt
echo "$(date -Iseconds)" > gate-artifacts/gate-1/security-scan-time.txt
- name: Upload validation result
if: always()
uses: actions/upload-artifact@v6
with:
name: gate-1-security-result
path: gate-artifacts/gate-1/
# Atomic Step 1.5: File Size Check
file-size-check:
name: "Gate 1.5: File Size Check"
runs-on: ubuntu-latest
needs: schema-check
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Check for oversized files
run: |
mkdir -p gate-artifacts/gate-1
LARGE_FILES=$(find frontends/ components/ hooks/ redux/ -name "*.ts" -o -name "*.tsx" 2>/dev/null | xargs wc -l 2>/dev/null | awk '$1 > 500 {print $2}' | head -20)
if [ -n "$LARGE_FILES" ]; then
echo "Large files (>500 LOC):" | tee gate-artifacts/gate-1/file-sizes.txt
echo "$LARGE_FILES" | tee -a gate-artifacts/gate-1/file-sizes.txt
else
echo "No oversized files found" > gate-artifacts/gate-1/file-sizes.txt
fi
continue-on-error: true
- name: Record validation result
if: always()
run: |
mkdir -p gate-artifacts/gate-1
echo "${{ job.status }}" > gate-artifacts/gate-1/file-size-check.txt
echo "$(date -Iseconds)" > gate-artifacts/gate-1/file-size-check-time.txt
- name: Upload validation result
if: always()
uses: actions/upload-artifact@v6
with:
name: gate-1-filesize-result
path: gate-artifacts/gate-1/
# Atomic Step 1.6: Code Complexity Check
code-complexity-check:
name: "Gate 1.6: Code Complexity Check"
runs-on: ubuntu-latest
needs: schema-check
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Check code complexity
run: |
mkdir -p gate-artifacts/gate-1
# Count files by category
FRONTEND_FILES=$(find frontends/ -name "*.ts" -o -name "*.tsx" 2>/dev/null | wc -l)
COMPONENT_FILES=$(find components/ -name "*.ts" -o -name "*.tsx" 2>/dev/null | wc -l)
REDUX_FILES=$(find redux/ -name "*.ts" -o -name "*.tsx" 2>/dev/null | wc -l)
HOOK_FILES=$(find hooks/ -name "*.ts" -o -name "*.tsx" 2>/dev/null | wc -l)
echo "Frontends: $FRONTEND_FILES files" | tee gate-artifacts/gate-1/complexity.txt
echo "Components: $COMPONENT_FILES files" | tee -a gate-artifacts/gate-1/complexity.txt
echo "Redux: $REDUX_FILES files" | tee -a gate-artifacts/gate-1/complexity.txt
echo "Hooks: $HOOK_FILES files" | tee -a gate-artifacts/gate-1/complexity.txt
continue-on-error: true
- name: Record validation result
if: always()
run: |
mkdir -p gate-artifacts/gate-1
echo "${{ job.status }}" > gate-artifacts/gate-1/complexity-check.txt
echo "$(date -Iseconds)" > gate-artifacts/gate-1/complexity-check-time.txt
- name: Upload validation result
if: always()
uses: actions/upload-artifact@v6
with:
name: gate-1-complexity-result
path: gate-artifacts/gate-1/
# Atomic Step 1.7: Stub Detection
stub-detection:
name: "Gate 1.7: Detect Stub Implementations"
runs-on: ubuntu-latest
needs: schema-check
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Detect stubs and placeholder code
run: |
mkdir -p gate-artifacts/gate-1
# Check for common stub patterns
STUBS=$(grep -rn "TODO\|FIXME\|HACK\|XXX\|not implemented\|throw new Error.*not implemented" \
--include="*.ts" --include="*.tsx" \
frontends/ components/ hooks/ redux/ 2>/dev/null | head -50)
if [ -n "$STUBS" ]; then
echo "Potential stubs/TODOs found:" > gate-artifacts/gate-1/stubs.txt
echo "$STUBS" >> gate-artifacts/gate-1/stubs.txt
else
echo "No stub implementations detected" > gate-artifacts/gate-1/stubs.txt
fi
continue-on-error: true
- name: Record validation result
if: always()
run: |
mkdir -p gate-artifacts/gate-1
echo "${{ job.status }}" > gate-artifacts/gate-1/stub-detection.txt
echo "$(date -Iseconds)" > gate-artifacts/gate-1/stub-detection-time.txt
- name: Upload validation result
if: always()
uses: actions/upload-artifact@v6
with:
name: gate-1-stub-result
path: gate-artifacts/gate-1/
gate-1-complete:
name: "Gate 1: Code Quality - Passed"
runs-on: ubuntu-latest
needs: [schema-check, typecheck, lint, security-scan, file-size-check, code-complexity-check, stub-detection]
steps:
- name: Download all gate 1 artifacts
uses: actions/download-artifact@v6
with:
pattern: gate-1-*
path: gate-artifacts/
merge-multiple: true
- name: Generate Gate 1 summary
run: |
echo "GATE 1 PASSED: CODE QUALITY"
echo "================================================"
echo "1.1 DBAL entity schemas validated"
echo "1.2 TypeScript types checked"
echo "1.3 Code linted"
echo "1.4 Security scan completed"
echo "1.5 File sizes checked"
echo "1.6 Code complexity analyzed"
echo "1.7 Stub implementations detected"
echo ""
echo "Proceeding to Gate 2: Testing..."
- name: Create consolidated gate report
run: |
mkdir -p gate-artifacts/gate-1
echo "completed" > gate-artifacts/gate-1/status.txt
echo "$(date -Iseconds)" > gate-artifacts/gate-1/end-time.txt
ls -la gate-artifacts/gate-1/
- name: Upload consolidated gate 1 report
uses: actions/upload-artifact@v6
with:
name: gate-1-complete-report
path: gate-artifacts/
# ============================================================================
# GATE 2: Testing Gates
# ============================================================================
gate-2-start:
name: "Gate 2: Testing - Starting"
runs-on: ubuntu-latest
needs: gate-1-complete
steps:
- name: Gate 2 checkpoint
run: |
echo "Gate 2: TESTING VALIDATION"
echo "================================================"
- name: Create gate artifacts directory
run: |
mkdir -p gate-artifacts/gate-2
echo "started" > gate-artifacts/gate-2/status.txt
echo "$(date -Iseconds)" > gate-artifacts/gate-2/start-time.txt
- name: Upload gate start marker
uses: actions/upload-artifact@v6
with:
name: gate-2-start
path: gate-artifacts/gate-2/
# Atomic Step 2.1: Unit Tests
test-unit:
name: "Gate 2.1: Unit Tests"
runs-on: ubuntu-latest
needs: gate-2-start
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Setup npm with Nexus
uses: ./.github/actions/setup-npm
with:
node-version: '20'
- name: Run unit tests with coverage
run: |
set -o pipefail
cd frontends/nextjs
npx vitest run --coverage --coverage.reporter=text --coverage.reporter=json-summary 2>&1 | tee /tmp/vitest-out.txt
FAILED=$(grep -oP '\d+ failed' /tmp/vitest-out.txt | head -1 | grep -oP '\d+' || echo "0")
PASSED=$(grep -oP '\d+ passed' /tmp/vitest-out.txt | head -1 | grep -oP '\d+' || echo "0")
echo "Tests: $PASSED passed, $FAILED failed"
if [ "$FAILED" -gt 20 ]; then
echo "::error::Too many test failures ($FAILED > 20 threshold)"
exit 1
fi
- name: Check coverage thresholds
run: |
cd frontends/nextjs
if [ -f coverage/coverage-summary.json ]; then
python3 -c "
import json, sys
with open('coverage/coverage-summary.json') as f:
data = json.load(f)
total = data['total']
for metric in ['lines', 'functions', 'branches', 'statements']:
pct = total[metric]['pct']
print(f'{metric}: {pct}%')
lines_pct = total['lines']['pct']
if lines_pct < 10:
print(f'::error::Line coverage {lines_pct}% is below 10% minimum')
sys.exit(1)
print(f'Coverage gate passed: {lines_pct}% lines covered')
"
else
echo "::warning::No coverage report generated — coverage check skipped"
fi
- name: Upload coverage report
if: always()
uses: actions/upload-artifact@v6
with:
name: coverage-report
path: frontends/nextjs/coverage/
retention-days: 7
- name: Record validation result
if: always()
run: |
mkdir -p gate-artifacts/gate-2
echo "${{ job.status }}" > gate-artifacts/gate-2/test-unit.txt
echo "$(date -Iseconds)" > gate-artifacts/gate-2/test-unit-time.txt
- name: Upload validation result
if: always()
uses: actions/upload-artifact@v6
with:
name: gate-2-unit-result
path: gate-artifacts/gate-2/
# Atomic Step 2.2: E2E Tests
test-e2e:
name: "Gate 2.2: E2E Tests"
runs-on: ubuntu-latest
needs: gate-2-start
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Setup npm with Nexus
uses: ./.github/actions/setup-npm
with:
node-version: '20'
- name: Build workspace packages
run: npm run build --workspaces --if-present 2>&1
- name: Install Playwright Browsers
run: npx playwright install --with-deps chromium
- name: Run Playwright tests
run: |
if [ -f e2e/playwright.config.ts ]; then
npx playwright test --config=e2e/playwright.config.ts 2>&1
else
echo "::warning::No playwright.config.ts found — E2E tests not configured"
fi
timeout-minutes: 10
- name: Upload test results
if: always()
uses: actions/upload-artifact@v6
with:
name: playwright-report
path: playwright-report/
retention-days: 7
- name: Record validation result
if: always()
run: |
mkdir -p gate-artifacts/gate-2
echo "${{ job.status }}" > gate-artifacts/gate-2/test-e2e.txt
echo "$(date -Iseconds)" > gate-artifacts/gate-2/test-e2e-time.txt
- name: Upload validation result
if: always()
uses: actions/upload-artifact@v6
with:
name: gate-2-e2e-result
path: gate-artifacts/gate-2/
# Atomic Step 2.3: DBAL Daemon Tests
test-dbal-daemon:
name: "Gate 2.3: DBAL Daemon E2E"
runs-on: ubuntu-latest
needs: gate-2-start
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Setup npm with Nexus
uses: ./.github/actions/setup-npm
with:
node-version: '20'
- name: Install Playwright Browsers
run: npx playwright install --with-deps chromium
- name: Run DBAL daemon suite
run: |
if [ -f e2e/playwright.dbal-daemon.config.ts ]; then
npx playwright test --config=e2e/playwright.dbal-daemon.config.ts 2>&1
else
echo "::warning::No DBAL daemon playwright config found — tests not configured"
fi
- name: Upload daemon test report
if: always()
uses: actions/upload-artifact@v6
with:
name: playwright-report-dbal-daemon
path: frontends/nextjs/playwright-report/
retention-days: 7
- name: Record validation result
if: always()
run: |
mkdir -p gate-artifacts/gate-2
echo "${{ job.status }}" > gate-artifacts/gate-2/test-dbal-daemon.txt
echo "$(date -Iseconds)" > gate-artifacts/gate-2/test-dbal-daemon-time.txt
- name: Upload validation result
if: always()
uses: actions/upload-artifact@v6
with:
name: gate-2-dbal-result
path: gate-artifacts/gate-2/
gate-2-complete:
name: "Gate 2: Testing - Passed"
runs-on: ubuntu-latest
needs: [test-unit, test-e2e, test-dbal-daemon]
steps:
- name: Download all gate 2 artifacts
uses: actions/download-artifact@v6
with:
pattern: gate-2-*
path: gate-artifacts/
merge-multiple: true
- name: Generate Gate 2 summary
run: |
echo "GATE 2 PASSED: TESTING"
echo "================================================"
echo "2.1 Unit tests passed"
echo "2.2 E2E tests passed"
echo "2.3 DBAL daemon tests passed"
echo ""
echo "Proceeding to Gate 3: Build & Package..."
- name: Create consolidated gate report
run: |
mkdir -p gate-artifacts/gate-2
echo "completed" > gate-artifacts/gate-2/status.txt
echo "$(date -Iseconds)" > gate-artifacts/gate-2/end-time.txt
ls -la gate-artifacts/gate-2/
- name: Upload consolidated gate 2 report
uses: actions/upload-artifact@v6
with:
name: gate-2-complete-report
path: gate-artifacts/
# ============================================================================
# GATE 3: Build & Package Gates
# ============================================================================
gate-3-start:
name: "Gate 3: Build & Package - Starting"
runs-on: ubuntu-latest
needs: gate-2-complete
steps:
- name: Gate 3 checkpoint
run: |
echo "Gate 3: BUILD & PACKAGE VALIDATION"
echo "================================================"
- name: Create gate artifacts directory
run: |
mkdir -p gate-artifacts/gate-3
echo "started" > gate-artifacts/gate-3/status.txt
echo "$(date -Iseconds)" > gate-artifacts/gate-3/start-time.txt
- name: Upload gate start marker
uses: actions/upload-artifact@v6
with:
name: gate-3-start
path: gate-artifacts/gate-3/
# Atomic Step 3.1: Build Application
build:
name: "Gate 3.1: Build Application"
runs-on: ubuntu-latest
needs: gate-3-start
outputs:
build-success: ${{ steps.build-step.outcome }}
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Setup npm with Nexus
uses: ./.github/actions/setup-npm
with:
node-version: '20'
- name: Build workspace packages
run: npm run build --workspaces --if-present 2>&1
- name: Build
id: build-step
run: npm run build -w frontends/nextjs
- name: Upload build artifacts
uses: actions/upload-artifact@v6
with:
name: dist
path: frontends/nextjs/.next/
retention-days: 7
- name: Record validation result
if: always()
run: |
mkdir -p gate-artifacts/gate-3
echo "${{ job.status }}" > gate-artifacts/gate-3/build.txt
echo "$(date -Iseconds)" > gate-artifacts/gate-3/build-time.txt
- name: Upload validation result
if: always()
uses: actions/upload-artifact@v6
with:
name: gate-3-build-result
path: gate-artifacts/gate-3/
# Atomic Step 3.2: Quality Metrics
quality-check:
name: "Gate 3.2: Code Quality Metrics"
runs-on: ubuntu-latest
needs: gate-3-start
if: github.event_name == 'pull_request'
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Check for console.log statements
run: |
if git diff origin/${{ github.base_ref }}...HEAD -- '*.ts' '*.tsx' '*.js' '*.jsx' | grep -E '^\+.*console\.(log|debug|info)'; then
echo "Found console.log statements in the changes"
echo "Please remove console.log statements before merging"
exit 1
fi
continue-on-error: true
- name: Check for TODO comments
run: |
TODO_COUNT=$(git diff origin/${{ github.base_ref }}...HEAD -- '*.ts' '*.tsx' '*.js' '*.jsx' | grep -E '^\+.*TODO|FIXME' | wc -l)
if [ $TODO_COUNT -gt 0 ]; then
echo "Found $TODO_COUNT TODO/FIXME comments in the changes"
fi
continue-on-error: true
- name: Record validation result
if: always()
run: |
mkdir -p gate-artifacts/gate-3
echo "${{ job.status }}" > gate-artifacts/gate-3/quality-check.txt
echo "$(date -Iseconds)" > gate-artifacts/gate-3/quality-check-time.txt
- name: Upload validation result
if: always()
uses: actions/upload-artifact@v6
with:
name: gate-3-quality-result
path: gate-artifacts/gate-3/
gate-3-complete:
name: "Gate 3: Build & Package - Passed"
runs-on: ubuntu-latest
needs: [build, quality-check]
if: always() && needs.build.result == 'success' && (needs.quality-check.result == 'success' || needs.quality-check.result == 'skipped')
steps:
- name: Download all gate 3 artifacts
uses: actions/download-artifact@v6
with:
pattern: gate-3-*
path: gate-artifacts/
merge-multiple: true
- name: Generate Gate 3 summary
run: |
echo "GATE 3 PASSED: BUILD & PACKAGE"
echo "================================================"
echo "3.1 Application built successfully"
echo "3.2 Quality metrics validated"
- name: Create consolidated gate report
run: |
mkdir -p gate-artifacts/gate-3
echo "completed" > gate-artifacts/gate-3/status.txt
echo "$(date -Iseconds)" > gate-artifacts/gate-3/end-time.txt
ls -la gate-artifacts/gate-3/
- name: Upload consolidated gate 3 report
uses: actions/upload-artifact@v6
with:
name: gate-3-complete-report
path: gate-artifacts/
# ============================================================================
# GATE 4: Development Assistance (PR Only)
# ============================================================================
gate-4-dev-feedback:
name: "Gate 4: Development Assistance"
runs-on: ubuntu-latest
needs: gate-3-complete
if: github.event_name == 'pull_request' && !github.event.pull_request.draft
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Analyze code metrics
id: quality
run: |
TOTAL_TS_FILES=$(find frontends/ components/ hooks/ redux/ -name "*.ts" -o -name "*.tsx" 2>/dev/null | wc -l)
LARGE_FILES=$(find frontends/ components/ hooks/ redux/ -name "*.ts" -o -name "*.tsx" -exec wc -l {} \; 2>/dev/null | awk '$1 > 150 {print $2}' | wc -l)
JSON_FILES=$(find frontends/ packages/ -name "*.json" 2>/dev/null | wc -l)
echo "total_ts_files=$TOTAL_TS_FILES" >> $GITHUB_OUTPUT
echo "large_files=$LARGE_FILES" >> $GITHUB_OUTPUT
echo "json_files=$JSON_FILES" >> $GITHUB_OUTPUT
- name: Check architectural compliance
id: architecture
uses: actions/github-script@v7
with:
script: |
let issues = [];
let suggestions = [];
// Get changed files
let changedFiles = [];
if (context.eventName === 'pull_request') {
const { data: files } = await github.rest.pulls.listFiles({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.issue.number,
});
changedFiles = files.map(f => f.filename);
}
// Check for hardcoded components
const hardcodedComponents = changedFiles.filter(f =>
f.endsWith('.tsx') &&
f.includes('src/components/') &&
!f.includes('src/components/ui/') &&
!f.includes('src/components/shared/') &&
!['RenderComponent', 'FieldRenderer', 'GenericPage'].some(g => f.includes(g))
);
if (hardcodedComponents.length > 0) {
suggestions.push(`Consider if these components could be declarative: ${hardcodedComponents.join(', ')}`);
}
// Check for DBAL entity schema changes
const schemaChanged = changedFiles.some(f => f.includes('dbal/shared/api/schema/entities/'));
if (schemaChanged) {
suggestions.push('DBAL entity schemas changed — verify DBAL daemon compatibility.');
}
// Check for large files
const largeFiles = parseInt('${{ steps.quality.outputs.large_files }}');
if (largeFiles > 0) {
issues.push(`${largeFiles} TypeScript files exceed 150 lines.`);
}
return { issues, suggestions };
- name: Provide development feedback
uses: actions/github-script@v7
with:
script: |
const analysis = JSON.parse('${{ steps.architecture.outputs.result }}');
const totalFiles = parseInt('${{ steps.quality.outputs.total_ts_files }}');
const largeFiles = parseInt('${{ steps.quality.outputs.large_files }}');
const jsonFiles = parseInt('${{ steps.quality.outputs.json_files }}');
let comment = `## Gate 4: Development Feedback\n\n`;
comment += `### Code Metrics\n\n`;
comment += `- TypeScript files: ${totalFiles}\n`;
comment += `- Files >150 LOC: ${largeFiles} ${largeFiles > 0 ? '(warning)' : '(ok)'}\n`;
comment += `- JSON config files: ${jsonFiles}\n`;
comment += `- Declarative ratio: ${((jsonFiles) / Math.max(totalFiles, 1) * 100).toFixed(1)}%\n\n`;
if (analysis.issues.length > 0) {
comment += `### Issues\n\n`;
analysis.issues.forEach(issue => comment += `- ${issue}\n`);
comment += '\n';
}
if (analysis.suggestions.length > 0) {
comment += `### Suggestions\n\n`;
analysis.suggestions.forEach(suggestion => comment += `- ${suggestion}\n`);
comment += '\n';
}
comment += `Gate 4 Complete - Development feedback provided\n`;
// Check if we already commented
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});
const botComment = comments.find(c =>
c.user.type === 'Bot' && c.body.includes('Gate 4: Development Feedback')
);
if (botComment) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: botComment.id,
body: comment
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: comment
});
}
# Handle Copilot mentions in comments
gate-4-copilot-interaction:
name: "Gate 4: Copilot Interaction"
runs-on: ubuntu-latest
if: |
github.event_name == 'issue_comment' &&
contains(github.event.comment.body, '@copilot')
steps:
- name: Parse Copilot request
uses: actions/github-script@v7
with:
script: |
const comment = context.payload.comment.body.toLowerCase();
const issue = context.payload.issue;
let response = `## Copilot Assistance\n\n`;
if (comment.includes('help') || !comment.match(/(implement|review|architecture|test)/)) {
response += `Mention **@copilot** with:\n`;
response += `- \`@copilot implement this\` - Implementation guidance\n`;
response += `- \`@copilot review this\` - Code review\n`;
response += `- \`@copilot architecture\` - Architecture guidance\n`;
response += `- \`@copilot test this\` - Testing guidance\n\n`;
}
response += `*Use GitHub Copilot in your IDE with project context for best results.*`;
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
body: response
});
# ============================================================================
# GATE 5: Staging Deployment (Main Branch Only)
# ============================================================================
gate-5-staging-deploy:
name: "Gate 5: Staging Deployment"
runs-on: ubuntu-latest
needs: gate-3-complete
if: |
github.event_name == 'push' &&
(github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master')
environment:
name: staging
url: https://staging.metabuilder.example.com
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Setup npm with Nexus
uses: ./.github/actions/setup-npm
with:
node-version: '20'
- name: Build for staging
run: npm run build -w frontends/nextjs
env:
NEXT_PUBLIC_ENV: staging
- name: Deploy to staging
run: |
echo "Deploying to staging environment..."
echo "Build artifacts ready for deployment"
- name: Run smoke tests
run: |
echo "Running smoke tests on staging..."
echo "Basic health checks completed"
# ============================================================================
# GATE 6: Production Deployment (Release/Manual with Approval)
# ============================================================================
gate-6-production-gate:
name: "Gate 6: Production Approval Gate"
runs-on: ubuntu-latest
needs: gate-3-complete
if: |
github.event_name == 'release' ||
(github.event_name == 'workflow_dispatch' && inputs.environment == 'production')
steps:
- name: Pre-production checklist
uses: actions/github-script@v7
with:
script: |
console.log('Production Deployment Gate');
console.log('Requires manual approval in GitHub Actions UI');
gate-6-production-deploy:
name: "Gate 6: Production Deployment"
runs-on: ubuntu-latest
needs: gate-6-production-gate
environment:
name: production
url: https://metabuilder.example.com
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Setup npm with Nexus
uses: ./.github/actions/setup-npm
with:
node-version: '20'
- name: Build for production
run: npm run build -w frontends/nextjs
env:
NEXT_PUBLIC_ENV: production
NODE_ENV: production
- name: Deploy to production
run: |
echo "Deploying to production environment..."
echo "Build artifacts ready for deployment"
- name: Run smoke tests
run: |
echo "Running smoke tests on production..."
echo "Production health checks completed"
- name: Post deployment summary
uses: actions/github-script@v7
with:
script: |
console.log('Gate 6: Production Deployment Complete');
await github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: `Production Deployment - ${new Date().toISOString().split('T')[0]}`,
body: `Production deployed at ${new Date().toISOString()}`,
labels: ['deployment', 'production']
});
# ============================================================================
# GATE 7: Container Build & Push (push/tag/dispatch only, not PRs)
# ════════════════════════════════════════════════════════════════════════════
# Tiered base images respecting the dependency DAG:
# Tier 1 (independent): base-apt, base-node-deps, base-pip-deps
# Tier 2 (← base-apt): base-conan-deps, base-android-sdk
# Tier 3 (← all above): devcontainer
# App images build after T1, pulling base images from GHCR.
# ════════════════════════════════════════════════════════════════════════════
# ============================================================================
# ── Tier 1: Independent base images ─────────────────────────────────────────
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'
strategy:
fail-fast: false
matrix:
include:
- image: base-apt
dockerfile: ./deployment/base-images/Dockerfile.apt
platforms: linux/amd64,linux/arm64
- image: base-node-deps
dockerfile: ./deployment/base-images/Dockerfile.node-deps
platforms: linux/amd64,linux/arm64
- image: base-pip-deps
dockerfile: ./deployment/base-images/Dockerfile.pip-deps
platforms: linux/amd64,linux/arm64
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Log in to GitHub Container Registry
uses: docker/login-action@v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels)
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/${{ matrix.image }}
tags: |
type=ref,event=branch
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=sha,prefix={{branch}}-
type=raw,value=latest,enable={{is_default_branch}}
- name: Build and push Docker image
id: build
uses: docker/build-push-action@v6
with:
context: .
file: ${{ matrix.dockerfile }}
platforms: ${{ matrix.platforms }}
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha,scope=${{ matrix.image }}
cache-to: type=gha,mode=max,scope=${{ matrix.image }}
build-args: |
BUILD_DATE=${{ github.event.head_commit.timestamp || github.run_started_at }}
VCS_REF=${{ github.sha }}
- name: Generate artifact attestation
uses: actions/attest-build-provenance@v4
with:
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/${{ matrix.image }}
subject-digest: ${{ steps.build.outputs.digest }}
push-to-registry: true
# ── Tier 2: Images depending on base-apt ────────────────────────────────────
container-base-tier2:
name: "Gate 7 T2: ${{ matrix.image }}"
runs-on: ubuntu-latest
needs: container-base-tier1
strategy:
fail-fast: false
matrix:
include:
- image: base-conan-deps
dockerfile: ./deployment/base-images/Dockerfile.conan-deps
platforms: linux/amd64,linux/arm64
- image: base-android-sdk
dockerfile: ./deployment/base-images/Dockerfile.android-sdk
platforms: linux/amd64,linux/arm64
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Log in to GitHub Container Registry
uses: docker/login-action@v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels)
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/${{ matrix.image }}
tags: |
type=ref,event=branch
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=sha,prefix={{branch}}-
type=raw,value=latest,enable={{is_default_branch}}
- name: Build and push Docker image
id: build
uses: docker/build-push-action@v6
with:
context: .
file: ${{ matrix.dockerfile }}
platforms: ${{ matrix.platforms }}
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha,scope=${{ matrix.image }}
cache-to: type=gha,mode=max,scope=${{ matrix.image }}
build-args: |
BASE_REGISTRY=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
BUILD_DATE=${{ github.event.head_commit.timestamp || github.run_started_at }}
VCS_REF=${{ github.sha }}
- name: Generate artifact attestation
uses: actions/attest-build-provenance@v4
with:
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/${{ matrix.image }}
subject-digest: ${{ steps.build.outputs.digest }}
push-to-registry: true
# ── Tier 3: Devcontainer (depends on all base images) ──────────────────────
container-base-tier3:
name: "Gate 7 T3: devcontainer"
runs-on: ubuntu-latest
needs: container-base-tier2
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Log in to GitHub Container Registry
uses: docker/login-action@v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels)
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/devcontainer
tags: |
type=ref,event=branch
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=sha,prefix={{branch}}-
type=raw,value=latest,enable={{is_default_branch}}
- name: Build and push Docker image
id: build
uses: docker/build-push-action@v6
with:
context: .
file: ./deployment/base-images/Dockerfile.devcontainer
platforms: linux/amd64,linux/arm64
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha,scope=devcontainer
cache-to: type=gha,mode=max,scope=devcontainer
build-args: |
BASE_REGISTRY=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
BUILD_DATE=${{ github.event.head_commit.timestamp || github.run_started_at }}
VCS_REF=${{ github.sha }}
- name: Generate artifact attestation
uses: actions/attest-build-provenance@v4
with:
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/devcontainer
subject-digest: ${{ steps.build.outputs.digest }}
push-to-registry: true
# ── App images (pull base images from GHCR) ────────────────────────────────
container-build-apps:
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()
strategy:
fail-fast: false
matrix:
include:
- image: nextjs-app
context: .
dockerfile: ./frontends/nextjs/Dockerfile
- image: codegen
context: .
dockerfile: ./frontends/codegen/Dockerfile
- image: pastebin
context: .
dockerfile: ./frontends/pastebin/Dockerfile
- image: emailclient
context: .
dockerfile: ./frontends/emailclient/Dockerfile
- image: postgres-dashboard
context: .
dockerfile: ./frontends/postgres/Dockerfile
- image: workflowui
context: .
dockerfile: ./frontends/workflowui/Dockerfile
- image: exploded-diagrams
context: .
dockerfile: ./frontends/exploded-diagrams/Dockerfile
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Log in to GitHub Container Registry
uses: docker/login-action@v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels)
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/${{ matrix.image }}
tags: |
type=ref,event=branch
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=sha,prefix=sha-
type=raw,value=latest,enable={{is_default_branch}}
- name: Build and push Docker image
id: build
uses: docker/build-push-action@v6
with:
context: ${{ matrix.context }}
file: ${{ matrix.dockerfile }}
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha,scope=${{ matrix.image }}
cache-to: type=gha,mode=max,scope=${{ matrix.image }}
build-args: |
BASE_REGISTRY=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
BUILD_DATE=${{ github.event.head_commit.timestamp || github.run_started_at }}
VCS_REF=${{ github.sha }}
VERSION=${{ steps.meta.outputs.version }}
- name: Generate artifact attestation
uses: actions/attest-build-provenance@v4
with:
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/${{ matrix.image }}
subject-digest: ${{ steps.build.outputs.digest }}
push-to-registry: true
# ── Container security scanning ────────────────────────────────────────────
container-security-scan:
name: "Gate 7 Scan: ${{ matrix.image }}"
runs-on: ubuntu-latest
needs: [container-base-tier3, container-build-apps]
if: github.event_name != 'pull_request' && github.event_name != 'issues' && github.event_name != 'issue_comment' && !failure()
continue-on-error: true # heavy/not-yet-built images must not block the gate
strategy:
fail-fast: false
matrix:
image:
- base-apt
- base-node-deps
- base-pip-deps
- base-conan-deps
- base-android-sdk
- devcontainer
- nextjs-app
- codegen
- pastebin
- emailclient
- postgres-dashboard
- workflowui
- exploded-diagrams
steps:
- name: Log in to GitHub Container Registry
uses: docker/login-action@v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@0.35.0
timeout-minutes: 15
with:
image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/${{ matrix.image }}:${{ github.ref_name }}
format: 'sarif'
output: 'trivy-results-${{ matrix.image }}.sarif'
- name: Upload Trivy results to GitHub Security tab
uses: github/codeql-action/upload-sarif@v4
with:
sarif_file: 'trivy-results-${{ matrix.image }}.sarif'
category: container-${{ matrix.image }}
# ── Multi-arch manifests ────────────────────────────────────────────────────
container-publish-manifest:
name: "Gate 7: Multi-Arch Manifests"
runs-on: ubuntu-latest
needs: [container-base-tier3, container-build-apps]
if: github.event_name != 'pull_request' && github.event_name != 'issues' && github.event_name != 'issue_comment' && !failure()
env:
REF_NAME: ${{ github.ref_name }}
steps:
- name: Log in to GitHub Container Registry
uses: docker/login-action@v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Verify multi-arch manifests
# build-push-action with platforms: linux/amd64,linux/arm64 already pushes
# a combined manifest — this step confirms all images are reachable.
run: |
for image in base-apt base-node-deps base-pip-deps base-conan-deps base-android-sdk devcontainer; do
echo "Inspecting $REGISTRY/$IMAGE_NAME/$image:$REF_NAME"
docker manifest inspect "$REGISTRY/$IMAGE_NAME/$image:$REF_NAME" || \
echo "WARNING: manifest not found for $image (may not have been built on this run)"
done
# ============================================================================
# CodeQL Semantic Analysis (manual dispatch only)
# ============================================================================
codeql-analyze:
name: "CodeQL: ${{ matrix.language }}"
runs-on: ubuntu-latest
if: github.event_name == 'workflow_dispatch' && inputs.run_codeql == true
timeout-minutes: 360
strategy:
fail-fast: false
matrix:
language: ${{ inputs.codeql_languages == 'all' && fromJSON('["javascript-typescript","python","cpp","go"]') || fromJSON(format('["{0}"]', inputs.codeql_languages)) }}
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Initialize CodeQL
uses: github/codeql-action/init@v4
with:
languages: ${{ matrix.language }}
queries: security-and-quality
config-file: ./.github/codeql/codeql-config.yml
- name: Setup Node.js
if: matrix.language == 'javascript-typescript'
uses: actions/setup-node@v5
with:
node-version: '20'
cache: 'npm'
- name: Setup Python
if: matrix.language == 'python'
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Setup Go
if: matrix.language == 'go'
uses: actions/setup-go@v5
with:
go-version: '1.21'
- name: Autobuild
uses: github/codeql-action/autobuild@v4
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4
with:
category: "/language:${{ matrix.language }}"
upload: true
wait-for-processing: true
codeql-summary:
name: "CodeQL: Summary"
needs: codeql-analyze
runs-on: ubuntu-latest
if: always() && github.event_name == 'workflow_dispatch' && inputs.run_codeql == true
steps:
- name: Summary Report
run: |
echo "## CodeQL Analysis Complete" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Available Features" >> $GITHUB_STEP_SUMMARY
echo "- **Code Search**: Use GitHub Advanced Search with CodeQL queries" >> $GITHUB_STEP_SUMMARY
echo "- **Security Tab**: View findings in repository Security tab" >> $GITHUB_STEP_SUMMARY
echo "- **API Access**: Query databases via CodeQL CLI or VS Code extension" >> $GITHUB_STEP_SUMMARY
# ============================================================================
# Summary Report
# ============================================================================
gates-summary:
name: "All Gates Summary"
runs-on: ubuntu-latest
needs: [gate-1-complete, gate-2-complete, gate-3-complete]
if: always()
steps:
- name: Download all gate artifacts
uses: actions/download-artifact@v6
with:
pattern: gate-*-complete-report
path: all-gate-artifacts/
merge-multiple: true
- name: Generate comprehensive summary
uses: actions/github-script@v7
with:
script: |
const gates = [
{ name: 'Gate 1: Code Quality', status: '${{ needs.gate-1-complete.result }}', steps: 7 },
{ name: 'Gate 2: Testing', status: '${{ needs.gate-2-complete.result }}', steps: 3 },
{ name: 'Gate 3: Build & Package', status: '${{ needs.gate-3-complete.result }}', steps: 2 }
];
let summary = '## Gated Pipeline Summary\n\n';
summary += '### Gate Results\n\n';
for (const gate of gates) {
const status = gate.status === 'success' ? 'passed' :
gate.status === 'failure' ? 'FAILED' :
gate.status === 'skipped' ? 'skipped' : 'pending';
summary += `**${gate.name}**: ${status} (${gate.steps} steps)\n`;
}
summary += '\n### Pipeline Flow\n\n';
summary += '```\n';
summary += 'Standalone: Triage (issues/PRs) | CodeQL (dispatch)\n';
summary += ' |\n';
summary += 'Gate 1: Code Quality (7 steps)\n';
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 += ' 2.1 Unit Tests (+ coverage) 2.2 E2E 2.3 DBAL\n';
summary += ' |\n';
summary += 'Gate 3: Build (2 steps)\n';
summary += ' 3.1 Build 3.2 Quality\n';
summary += ' |\n';
summary += 'Gate 4: Dev Feedback (PR only)\n';
summary += ' |\n';
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);
if (context.eventName === 'pull_request') {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: summary
});
}
- name: Upload complete audit trail
uses: actions/upload-artifact@v6
with:
name: complete-gate-audit-trail
path: all-gate-artifacts/
retention-days: 30