Files
metabuilder/.github/workflows/gated-pipeline.yml
johndoe6345789 b6859ab57f chore(ci): downgrade artifact actions v6→v4 and add act local CI config
- Downgrade all actions/upload-artifact and actions/download-artifact from
  v6 to v4 for compatibility with act's local artifact server (v6 uses a
  new Azure blob backend that act doesn't support)
- Add .actrc with arm64 architecture, artifact server, sequential jobs,
  and secret/env file pointers for local act runs
- Add .act-env to .gitignore (contains local node PATH override)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 00:16:25 +00:00

2123 lines
78 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
skip_containers:
description: 'Skip container builds (use existing GHCR images)'
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 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)
# ════════════════════════════════════════════════════════════════════════════════
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@v4
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@v4
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@v4
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
# 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 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 0 errors"
- 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@v4
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@v4
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@v4
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@v4
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@v4
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@v4
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@v4
with:
name: gate-1-complete-report
path: gate-artifacts/
# ============================================================================
# GATE 2: Testing Gates (runs after container images are published to GHCR)
# ============================================================================
# Detect which test suites need to run based on changed paths
check-app-changes:
name: "Check: App source changes"
runs-on: ubuntu-latest
needs: gate-1-complete
if: github.event_name != 'issues' && github.event_name != 'issue_comment'
outputs:
e2e_changed: ${{ steps.diff.outputs.e2e_changed }}
unit_changed: ${{ steps.diff.outputs.unit_changed }}
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Detect changed paths
id: diff
shell: bash
run: |
BEFORE="${{ github.event.before }}"
if [ -z "$BEFORE" ] || [ "$BEFORE" = "0000000000000000000000000000000000000000" ]; then
echo "e2e_changed=true" >> "$GITHUB_OUTPUT"
echo "unit_changed=true" >> "$GITHUB_OUTPUT"
echo "No before SHA — marking all changed"
exit 0
fi
git fetch --depth=1 origin "$BEFORE" 2>/dev/null || true
E2E=$(git diff --name-only "$BEFORE" "${{ github.sha }}" -- \
frontends e2e packages components 2>/dev/null || echo "")
UNIT=$(git diff --name-only "$BEFORE" "${{ github.sha }}" -- \
frontends/nextjs/src 2>/dev/null || echo "")
[ -n "$E2E" ] && echo "e2e_changed=true" >> "$GITHUB_OUTPUT" || echo "e2e_changed=false" >> "$GITHUB_OUTPUT"
[ -n "$UNIT" ] && echo "unit_changed=true" >> "$GITHUB_OUTPUT" || echo "unit_changed=false" >> "$GITHUB_OUTPUT"
echo "E2E paths changed: ${E2E:-none}"
echo "Unit paths changed: ${UNIT:-none}"
gate-2-start:
name: "Gate 2: Testing - Starting"
runs-on: ubuntu-latest
needs: [gate-1-complete]
if: ${{ !inputs.skip_tests }}
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@v4
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, check-app-changes]
if: ${{ !inputs.skip_tests }}
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Restore cached coverage report
id: cache-restore
if: needs.check-app-changes.outputs.unit_changed == 'false'
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
LAST_RUN=$(gh run list \
--repo "${{ github.repository }}" \
--workflow gated-pipeline.yml \
--branch "${{ github.ref_name }}" \
--status success \
--limit 1 \
--json databaseId \
--jq '.[0].databaseId' 2>/dev/null || echo "")
if [ -n "$LAST_RUN" ] && [ "$LAST_RUN" != "null" ]; then
gh run download "$LAST_RUN" \
--repo "${{ github.repository }}" \
--name coverage-report \
--dir frontends/nextjs/coverage/ 2>/dev/null \
&& echo "hit=true" >> "$GITHUB_OUTPUT" \
|| echo "hit=false" >> "$GITHUB_OUTPUT"
else
echo "hit=false" >> "$GITHUB_OUTPUT"
fi
- name: Setup npm with Nexus
if: steps.cache-restore.outputs.hit != 'true'
uses: ./.github/actions/setup-npm
with:
node-version: '20'
- name: Run unit tests with coverage
if: steps.cache-restore.outputs.hit != 'true'
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@v4
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@v4
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, check-app-changes]
if: ${{ !inputs.skip_tests }}
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Restore cached test results
id: cache-restore
if: needs.check-app-changes.outputs.e2e_changed == 'false'
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
LAST_RUN=$(gh run list \
--repo "${{ github.repository }}" \
--workflow gated-pipeline.yml \
--branch "${{ github.ref_name }}" \
--status success \
--limit 1 \
--json databaseId \
--jq '.[0].databaseId' 2>/dev/null || echo "")
if [ -n "$LAST_RUN" ] && [ "$LAST_RUN" != "null" ]; then
gh run download "$LAST_RUN" \
--repo "${{ github.repository }}" \
--name playwright-report \
--dir playwright-report/ 2>/dev/null \
&& echo "hit=true" >> "$GITHUB_OUTPUT" \
|| echo "hit=false" >> "$GITHUB_OUTPUT"
echo "Using cached results from run $LAST_RUN"
else
echo "hit=false" >> "$GITHUB_OUTPUT"
fi
- name: Log in to GitHub Container Registry
if: steps.cache-restore.outputs.hit != 'true'
uses: docker/login-action@v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Setup npm with Nexus
if: steps.cache-restore.outputs.hit != 'true'
uses: ./.github/actions/setup-npm
with:
node-version: '20'
- name: Build workspace packages
if: steps.cache-restore.outputs.hit != 'true'
run: npm run build --workspaces --if-present 2>&1
- name: Install Playwright Browsers
if: steps.cache-restore.outputs.hit != 'true'
run: npx playwright install --with-deps chromium
- name: Run Playwright tests
if: steps.cache-restore.outputs.hit != 'true'
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: 15
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
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@v4
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
if: ${{ !inputs.skip_tests }}
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@v4
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@v4
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@v4
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@v4
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@v4
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@v4
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@v4
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@v4
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@v4
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@v4
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 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
# 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-1-complete
if: github.event_name != 'issues' && github.event_name != 'issue_comment' && !inputs.skip_containers
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
with:
# host networking lets BuildKit reach Verdaccio on localhost:4873
driver-opts: network=host
- name: Start Verdaccio and publish patched packages
if: matrix.image == 'base-node-deps'
shell: bash
run: |
npm install -g verdaccio@6 --silent
mkdir -p /tmp/verdaccio-storage
cat > /tmp/verdaccio-ci.yaml << 'VERDACCIO_EOF'
storage: /tmp/verdaccio-storage
uplinks:
npmjs:
url: https://registry.npmjs.org/
timeout: 60s
max_fails: 3
packages:
'@esbuild-kit/*':
access: $all
publish: $all
proxy: npmjs
'**':
access: $all
publish: $all
proxy: npmjs
server:
keepAliveTimeout: 60
log:
type: stdout
format: pretty
level: warn
listen: 0.0.0.0:4873
VERDACCIO_EOF
verdaccio --config /tmp/verdaccio-ci.yaml &
timeout 30 bash -c 'until curl -sf http://localhost:4873/-/ping >/dev/null 2>&1; do sleep 1; done'
echo "Verdaccio ready"
# Publish patched tarballs
for tarball in deployment/npm-patches/*.tgz; do
[ -f "$tarball" ] || continue
echo "Publishing $tarball..."
npm publish "$tarball" \
--registry http://localhost:4873 \
--tag patched \
2>&1 | grep -v "^npm notice" || true
done
echo "Patched packages published to Verdaccio"
- name: Log in to GitHub Container Registry
uses: docker/login-action@v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Check if image already exists in GHCR
id: check
shell: bash
run: |
IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/${{ matrix.image }}:${{ github.ref_name }}"
if docker manifest inspect "$IMAGE" > /dev/null 2>&1; then
echo "exists=true" >> "$GITHUB_OUTPUT"
echo "Image $IMAGE already exists — skipping build"
else
echo "exists=false" >> "$GITHUB_OUTPUT"
echo "Image $IMAGE not found — will build"
fi
- name: Pull existing image from GHCR
if: steps.check.outputs.exists == 'true'
shell: bash
run: |
docker pull "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/${{ matrix.image }}:${{ github.ref_name }}"
- name: Extract metadata (tags, labels)
id: meta
if: steps.check.outputs.exists != 'true'
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
if: steps.check.outputs.exists != 'true'
uses: docker/build-push-action@v6
with:
context: .
file: ${{ matrix.dockerfile }}
platforms: ${{ matrix.platforms }}
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: |
type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/${{ matrix.image }}:${{ github.ref_name }}
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
if: steps.check.outputs.exists != 'true'
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
if: ${{ !inputs.skip_containers }}
strategy:
fail-fast: false
matrix:
include:
- image: base-conan-deps
dockerfile: ./deployment/base-images/Dockerfile.conan-deps
platforms: linux/amd64,linux/arm64
require_prebuilt: true
- image: base-android-sdk
dockerfile: ./deployment/base-images/Dockerfile.android-sdk
platforms: linux/amd64,linux/arm64
require_prebuilt: false
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: Check if image already exists in GHCR
id: check
shell: bash
run: |
IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/${{ matrix.image }}:${{ github.ref_name }}"
if docker manifest inspect "$IMAGE" > /dev/null 2>&1; then
echo "exists=true" >> "$GITHUB_OUTPUT"
echo "Image $IMAGE already exists — skipping build"
else
echo "exists=false" >> "$GITHUB_OUTPUT"
echo "Image $IMAGE not found — will build"
fi
- name: Fail if pre-built image required but missing
if: steps.check.outputs.exists != 'true' && matrix.require_prebuilt == true
shell: bash
run: |
echo "::error::${{ matrix.image }} must be pre-built and pushed to GHCR before CI can proceed."
echo "::error::Run locally: docker build --platform linux/amd64,linux/arm64 -f ${{ matrix.dockerfile }} -t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/${{ matrix.image }}:${{ github.ref_name }} --push ."
exit 1
- name: Pull existing image from GHCR
if: steps.check.outputs.exists == 'true'
shell: bash
run: |
docker pull "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/${{ matrix.image }}:${{ github.ref_name }}"
- name: Extract metadata (tags, labels)
id: meta
if: steps.check.outputs.exists != 'true' && matrix.require_prebuilt != true
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
if: steps.check.outputs.exists != 'true' && matrix.require_prebuilt != true
uses: docker/build-push-action@v6
with:
context: .
file: ${{ matrix.dockerfile }}
platforms: ${{ matrix.platforms }}
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: |
type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/${{ matrix.image }}:${{ github.ref_name }}
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
if: steps.check.outputs.exists != 'true' && matrix.require_prebuilt != true
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
if: ${{ !inputs.skip_containers }}
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: Check if image already exists in GHCR
id: check
shell: bash
run: |
IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/devcontainer:${{ github.ref_name }}"
if docker manifest inspect "$IMAGE" > /dev/null 2>&1; then
echo "exists=true" >> "$GITHUB_OUTPUT"
echo "Image $IMAGE already exists — skipping build"
else
echo "exists=false" >> "$GITHUB_OUTPUT"
echo "Image $IMAGE not found — will build"
fi
- name: Pull existing image from GHCR
if: steps.check.outputs.exists == 'true'
shell: bash
run: |
docker pull "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/devcontainer:${{ github.ref_name }}"
- name: Extract metadata (tags, labels)
id: meta
if: steps.check.outputs.exists != 'true'
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
if: steps.check.outputs.exists != 'true'
uses: docker/build-push-action@v6
with:
context: .
file: ./deployment/base-images/Dockerfile.devcontainer
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: |
type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/devcontainer:${{ github.ref_name }}
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
if: steps.check.outputs.exists != 'true'
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 != 'issues' && github.event_name != 'issue_comment' && needs.container-base-tier1.result == 'success' && !inputs.skip_containers
strategy:
fail-fast: false
matrix:
include:
- image: nextjs-app
context: .
dockerfile: ./frontends/nextjs/Dockerfile
watch_paths: frontends/nextjs packages components
- image: codegen
context: .
dockerfile: ./frontends/codegen/Dockerfile
watch_paths: frontends/codegen packages components
- image: pastebin
context: .
dockerfile: ./frontends/pastebin/Dockerfile
watch_paths: frontends/pastebin packages components
- image: emailclient
context: .
dockerfile: ./frontends/emailclient/Dockerfile
watch_paths: frontends/emailclient packages components
- image: postgres-dashboard
context: .
dockerfile: ./frontends/postgres/Dockerfile
watch_paths: frontends/postgres packages
- image: workflowui
context: .
dockerfile: ./frontends/workflowui/Dockerfile
watch_paths: frontends/workflowui packages components
- image: exploded-diagrams
context: .
dockerfile: ./frontends/exploded-diagrams/Dockerfile
watch_paths: frontends/exploded-diagrams
- image: dbal
context: ./dbal
dockerfile: ./dbal/production/build-config/Dockerfile
watch_paths: dbal
- image: dbal-init
context: .
dockerfile: ./deployment/config/dbal/Dockerfile.init
watch_paths: deployment/config/dbal dbal/shared
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: Check if image needs rebuild
id: check
shell: bash
run: |
IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/${{ matrix.image }}:${{ github.ref_name }}"
# If image doesn't exist in GHCR — must build
if ! docker manifest inspect "$IMAGE" > /dev/null 2>&1; then
echo "rebuild=true" >> "$GITHUB_OUTPUT"
echo "Image not in GHCR — will build"
exit 0
fi
# Image exists — check if watched paths changed in this push
BEFORE="${{ github.event.before }}"
if [ -z "$BEFORE" ] || [ "$BEFORE" = "0000000000000000000000000000000000000000" ]; then
echo "rebuild=true" >> "$GITHUB_OUTPUT"
echo "No before SHA (new branch or dispatch) — rebuilding"
exit 0
fi
# Fetch the before commit (shallow checkout only has HEAD)
git fetch --depth=1 origin "$BEFORE" 2>/dev/null || true
read -ra watch <<< "${{ matrix.watch_paths }}"
CHANGED=$(git diff --name-only "$BEFORE" "${{ github.sha }}" -- "${watch[@]}" 2>/dev/null || echo "")
if [ -z "$CHANGED" ]; then
echo "rebuild=false" >> "$GITHUB_OUTPUT"
echo "No changes in watched paths — pulling from GHCR"
else
echo "rebuild=true" >> "$GITHUB_OUTPUT"
printf "Changes detected — rebuilding:\n%s\n" "$CHANGED"
fi
- name: Pull existing image from GHCR
if: steps.check.outputs.rebuild == 'false'
shell: bash
run: |
docker pull "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/${{ matrix.image }}:${{ github.ref_name }}"
- name: Extract metadata (tags, labels)
id: meta
if: steps.check.outputs.rebuild == 'true'
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
if: steps.check.outputs.rebuild == 'true'
uses: docker/build-push-action@v6
with:
context: ${{ matrix.context }}
file: ${{ matrix.dockerfile }}
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: |
type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/${{ matrix.image }}:${{ github.ref_name }}
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
if: steps.check.outputs.rebuild == 'true'
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
- dbal
- dbal-init
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@v4
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 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';
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\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@v4
with:
name: complete-gate-audit-trail
path: all-gate-artifacts/
retention-days: 30