fix(maintainability): make gate parallel-pr friendly

This commit is contained in:
Tam Nhu Tran
2026-02-26 14:46:56 +07:00
parent 6d752724ec
commit fdb32e2c53
7 changed files with 202 additions and 7 deletions
+1 -1
View File
@@ -30,5 +30,5 @@ jobs:
- name: Build package
run: bun run build:all
- name: Validate (typecheck + lint + tests)
- name: Validate (typecheck + lint + format + maintainability [warn on PR] + tests)
run: bun run validate
+1 -1
View File
@@ -42,7 +42,7 @@ jobs:
- name: Build
run: bun run build:all
- name: Validate (typecheck + lint + tests)
- name: Validate (typecheck + lint + format + maintainability [strict] + tests)
run: bun run validate
- name: Release
+1 -1
View File
@@ -41,7 +41,7 @@ jobs:
- name: Build package
run: bun run build:all
- name: Validate (typecheck + lint + tests)
- name: Validate (typecheck + lint + format + maintainability [strict] + tests)
run: bun run validate
- name: Release
+27 -3
View File
@@ -43,6 +43,7 @@ CLI wrapper for instant switching between multiple provider accounts and alterna
| Mistake | Consequence | Correct Action |
|---------|-------------|----------------|
| Running `validate` without `format` first | format:check fails | Run `bun run format` BEFORE validate |
| Assuming maintainability check is always strict | PR/feature branches run warning mode by default | Use `bun run maintainability:check:strict` before merge when touching debt-sensitive code |
| Using `chore:` for dev→main PR | No npm release triggered | Use `feat:` or `fix:` prefix |
| Committing directly to `main` or `dev` | Bypasses CI/review | Always use PRs |
| Manual version bump or git tag | Conflicts with semantic-release | Let CI handle versioning |
@@ -59,7 +60,7 @@ Quality gates MUST pass before pushing. **Both projects have identical workflow.
# Main project (from repo root)
bun run format # Step 1: Fix formatting
bun run lint:fix # Step 2: Fix lint issues
bun run validate # Step 3: Full test gate (must pass)
bun run validate # Step 3: Full gate (typecheck + lint + format + maintainability + tests)
bun run validate:ci-parity # Step 4: CI parity gate (build + validate + base branch check)
# UI project (if UI changed)
@@ -78,7 +79,7 @@ bun run validate # Step 3: Final check (must pass)
| Project | Command | Runs |
|---------|---------|------|
| Main | `bun run validate` | typecheck + lint:fix + format:check + test:all |
| Main | `bun run validate` | typecheck + lint:fix + format:check + maintainability:check + test:all |
| UI | `bun run validate` | typecheck + lint:fix + format:check |
### ESLint Rules (ALL errors)
@@ -105,10 +106,31 @@ bun run validate # Step 3: Final check (must pass)
### Automatic Enforcement
- `prepublishOnly` / `prepack` runs `build:all` + `validate` + `sync-version.js`
- CI/CD runs `bun run validate` on every PR
- CI/CD runs `bun run validate` on every PR (maintainability is warning mode on PR events)
- husky `pre-commit` runs quick lint/type/format checks
- husky `pre-push` runs `bun run validate:ci-parity` to block CI drift before push
### Maintainability Baseline Gate
- Baseline file: `docs/metrics/maintainability-baseline.json`
- Metric collector/check script: `scripts/maintainability-baseline.js`
- Branch-aware gate wrapper: `scripts/maintainability-check.js`
- Enforcement path: `bun run maintainability:check` (included in `bun run validate`)
- Gate modes:
- `strict`: protected branches (`main`, `dev`, `hotfix/*`, `kai/hotfix-*`) and equivalent CI refs
- `warn`: pull request CI and non-protected local branches (non-blocking for parallel PR workflow)
- override commands:
- `bun run maintainability:check:strict`
- `bun run maintainability:check:warn`
- Gated metrics (must not increase vs baseline):
- `processExitReferenceCount`
- `synchronousFsApiReferenceCount`
- `largeFileCountOver350Loc`
- Baseline update policy:
1. Prefer reducing the metric and keeping the baseline unchanged.
2. On protected-branch integration (strict mode), if increase is intentional and accepted, run `bun run maintainability:baseline`.
3. Commit both the code change and `docs/metrics/maintainability-baseline.json`, and state reason in PR description.
## Critical Constraints (NEVER VIOLATE)
1. **NO EMOJIS in CLI output** - Terminal output uses ASCII only: [OK], [!], [X], [i]
@@ -359,6 +381,8 @@ rm -rf ~/.ccs # Clean environment
- [ ] `bun run validate` — all checks pass
- [ ] `bun run validate:ci-parity` — CI parity passed (also enforced by pre-push hook)
- [ ] `cd ui && bun run format && bun run validate` — if UI changed
- [ ] If touching debt-sensitive code, run `bun run maintainability:check:strict` before opening/merging PR
- [ ] If strict mode fails and increase is intentional: `bun run maintainability:baseline` and commit `docs/metrics/maintainability-baseline.json`
**Code:**
- [ ] Conventional commit format (`feat:`, `fix:`, etc.)
+6
View File
@@ -203,15 +203,21 @@ All criteria achieved:
## Maintainability Gate (Issue #539 Foundation)
- Baseline metrics artifact: `docs/metrics/maintainability-baseline.json`
- Branch-aware gate wrapper: `scripts/maintainability-check.js`
- Generate or refresh baseline:
- `bun run maintainability:baseline`
- `npm run maintainability:baseline`
- Run regression check gate:
- `bun run maintainability:check`
- `npm run maintainability:check`
- `bun run maintainability:check:strict` (force strict locally)
The baseline/check scripts enumerate git-tracked files under `src` for deterministic results and fail fast if git file listing is unavailable.
Default gate behavior:
- strict mode on protected branches (`main`, `dev`, `hotfix/*`, `kai/hotfix-*`)
- warning mode on PR CI and non-protected branches (parallel PR friendly)
The check mode supports a maintainability regression gate that blocks increases in:
- `process.exit` references
- synchronous fs API references
+3 -1
View File
@@ -69,7 +69,9 @@
"validate:ci-parity": "bash scripts/ci-parity-gate.sh",
"verify:bundle": "node scripts/verify-bundle.js",
"maintainability:baseline": "node scripts/maintainability-baseline.js --out docs/metrics/maintainability-baseline.json",
"maintainability:check": "node scripts/maintainability-baseline.js --check docs/metrics/maintainability-baseline.json",
"maintainability:check": "node scripts/maintainability-check.js",
"maintainability:check:strict": "node scripts/maintainability-check.js --strict",
"maintainability:check:warn": "node scripts/maintainability-check.js --warn",
"test": "bun run build && bun run test:all",
"test:ci": "bun run test:all",
"test:all": "bun test tests/unit tests/integration tests/npm",
+163
View File
@@ -0,0 +1,163 @@
#!/usr/bin/env node
const { execFileSync, spawnSync } = require('child_process');
const path = require('path');
const PROJECT_ROOT = path.resolve(__dirname, '..');
const BASELINE_FILE = path.join('docs', 'metrics', 'maintainability-baseline.json');
const BASELINE_SCRIPT = path.join(PROJECT_ROOT, 'scripts', 'maintainability-baseline.js');
const PROTECTED_BRANCHES = new Set(['main', 'dev']);
const HOTFIX_PREFIXES = ['hotfix/', 'kai/hotfix-'];
function hasFlag(name) {
return process.argv.slice(2).includes(name);
}
function detectBranchName() {
try {
return execFileSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
cwd: PROJECT_ROOT,
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'ignore'],
}).trim();
} catch {
return '';
}
}
function isProtectedBranch(branchName) {
if (!branchName) {
return false;
}
if (PROTECTED_BRANCHES.has(branchName)) {
return true;
}
return HOTFIX_PREFIXES.some(prefix => branchName.startsWith(prefix));
}
function detectMode() {
if (hasFlag('--strict')) {
return 'strict';
}
if (hasFlag('--warn')) {
return 'warn';
}
if (hasFlag('--off')) {
return 'off';
}
const explicitMode = (process.env.CCS_MAINTAINABILITY_MODE || '').toLowerCase().trim();
if (explicitMode === 'strict' || explicitMode === 'warn' || explicitMode === 'off') {
return explicitMode;
}
const eventName = process.env.GITHUB_EVENT_NAME || '';
if (eventName === 'pull_request' || eventName === 'pull_request_target') {
return 'warn';
}
const gitHubRef = process.env.GITHUB_REF || '';
if (gitHubRef.startsWith('refs/heads/')) {
const branchFromRef = gitHubRef.slice('refs/heads/'.length);
if (isProtectedBranch(branchFromRef)) {
return 'strict';
}
}
return isProtectedBranch(detectBranchName()) ? 'strict' : 'warn';
}
function runBaselineCheck() {
return spawnSync('node', [BASELINE_SCRIPT, '--check', BASELINE_FILE], {
cwd: PROJECT_ROOT,
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe'],
});
}
function writeStreams(result) {
if (result.stdout) {
process.stdout.write(result.stdout);
}
if (result.stderr) {
process.stderr.write(result.stderr);
}
}
function tryParseJson(stdout) {
if (!stdout) {
return null;
}
try {
return JSON.parse(stdout);
} catch {
return null;
}
}
function formatViolations(violations) {
if (!Array.isArray(violations) || violations.length === 0) {
return [];
}
return violations.map(violation => {
if (!violation || typeof violation !== 'object') {
return '- unknown violation';
}
const metric = violation.metric || 'unknown';
const baseline = typeof violation.baseline === 'number' ? violation.baseline : 'n/a';
const current = typeof violation.current === 'number' ? violation.current : 'n/a';
return `- ${metric}: baseline=${baseline}, current=${current}`;
});
}
function main() {
const mode = detectMode();
if (mode === 'off') {
console.log('[i] Maintainability gate disabled (mode=off).');
process.exit(0);
}
const result = runBaselineCheck();
if (mode === 'strict') {
writeStreams(result);
process.exit(result.status === null ? 1 : result.status);
}
if (result.status === 0) {
writeStreams(result);
process.exit(0);
}
const parsed = tryParseJson(result.stdout);
const branchName = detectBranchName();
console.log('[!] Maintainability regression detected (warning-only mode).');
if (branchName) {
console.log(`[i] Branch: ${branchName}`);
}
if (parsed && Array.isArray(parsed.violations) && parsed.violations.length > 0) {
console.log('[i] Violations:');
for (const line of formatViolations(parsed.violations)) {
console.log(line);
}
} else {
writeStreams(result);
}
console.log('[i] This is non-blocking on PR/feature branches to support parallel workflow.');
console.log(
'[i] Use strict mode when needed: bun run maintainability:check:strict'
);
}
main();