mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-24 13:54:57 +00:00
- 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>
2123 lines
78 KiB
YAML
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
|