arch(sprint1): enforce boundaries and cycle checks in ci
This commit is contained in:
30
.github/workflows/architecture-governance.yml
vendored
Normal file
30
.github/workflows/architecture-governance.yml
vendored
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
name: Architecture Governance
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- '**'
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
architecture:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
cache: npm
|
||||||
|
|
||||||
|
- name: Install Dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Enforce Boundaries
|
||||||
|
run: npm run arch:check
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: npm run build
|
||||||
@@ -14,7 +14,10 @@
|
|||||||
"watch": "ng build --watch --configuration development",
|
"watch": "ng build --watch --configuration development",
|
||||||
"lavero": "ng serve --configuration=lavero --port 4202 --proxy-config proxy.conf.lavero.json",
|
"lavero": "ng serve --configuration=lavero --port 4202 --proxy-config proxy.conf.lavero.json",
|
||||||
"start:lavero": "ng serve --configuration=lavero --port 4202",
|
"start:lavero": "ng serve --configuration=lavero --port 4202",
|
||||||
"build:lavero": "ng build --configuration=lavero-production"
|
"build:lavero": "ng build --configuration=lavero-production",
|
||||||
|
"arch:check:boundaries": "node tools/architecture/check-boundaries.mjs",
|
||||||
|
"arch:check:cycles": "npx --yes madge --circular --extensions ts src/app --ts-config tsconfig.app.json",
|
||||||
|
"arch:check": "npm run arch:check:boundaries ; npm run arch:check:cycles"
|
||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
91
tools/architecture/check-boundaries.mjs
Normal file
91
tools/architecture/check-boundaries.mjs
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import fs from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
|
const ROOT = process.cwd();
|
||||||
|
const APP_ROOT = path.join(ROOT, 'src', 'app');
|
||||||
|
|
||||||
|
function listTsFiles(dir) {
|
||||||
|
const files = [];
|
||||||
|
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
const fullPath = path.join(dir, entry.name);
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
files.push(...listTsFiles(fullPath));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.isFile() && fullPath.endsWith('.ts') && !fullPath.endsWith('.spec.ts')) {
|
||||||
|
files.push(fullPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalize(filePath) {
|
||||||
|
return filePath.split(path.sep).join('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkFile(filePath, content) {
|
||||||
|
const violations = [];
|
||||||
|
const normalized = normalize(filePath);
|
||||||
|
|
||||||
|
const importRegex = /^\s*import\s+[^'"\n]+\s+from\s+['"]([^'"]+)['"];?/gm;
|
||||||
|
const imports = [];
|
||||||
|
let match;
|
||||||
|
|
||||||
|
while ((match = importRegex.exec(content)) !== null) {
|
||||||
|
imports.push(match[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isWidgetUi = normalized.includes('/src/app/widgets/ui/');
|
||||||
|
const isFacade = normalized.includes('/src/app/facades/');
|
||||||
|
const isRoutesFile = normalized.endsWith('/src/app/app.routes.ts');
|
||||||
|
|
||||||
|
for (const specifier of imports) {
|
||||||
|
const isEnvironmentImport = specifier.includes('environments/environment');
|
||||||
|
if (isWidgetUi && isEnvironmentImport) {
|
||||||
|
violations.push(`${normalized}: widget UI must not import environment (${specifier})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isWidgetUi && (
|
||||||
|
specifier.includes('/services/')
|
||||||
|
|| specifier.includes('/facades/')
|
||||||
|
|| specifier.includes('/core/')
|
||||||
|
|| specifier.includes('/interceptors/')
|
||||||
|
|| specifier === '@angular/router'
|
||||||
|
|| specifier === '@angular/common/http'
|
||||||
|
)) {
|
||||||
|
violations.push(`${normalized}: widget UI import boundary violation (${specifier})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isFacade && specifier === '@angular/common/http') {
|
||||||
|
violations.push(`${normalized}: facade must not import HttpClient directly`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isRoutesFile && specifier.includes('/brands/brand-routes')) {
|
||||||
|
violations.push(`${normalized}: routes must not import tenant brand-routes`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return violations;
|
||||||
|
}
|
||||||
|
|
||||||
|
const files = listTsFiles(APP_ROOT);
|
||||||
|
let violations = [];
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const content = fs.readFileSync(file, 'utf8');
|
||||||
|
violations = violations.concat(checkFile(file, content));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (violations.length > 0) {
|
||||||
|
console.error('Architecture boundary violations found:');
|
||||||
|
for (const violation of violations) {
|
||||||
|
console.error(`- ${violation}`);
|
||||||
|
}
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Architecture boundary checks passed.');
|
||||||
Reference in New Issue
Block a user