mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 10:16:49 +00:00
fix(maintainability): make gate parallel-pr friendly
This commit is contained in:
@@ -30,5 +30,5 @@ jobs:
|
|||||||
- name: Build package
|
- name: Build package
|
||||||
run: bun run build:all
|
run: bun run build:all
|
||||||
|
|
||||||
- name: Validate (typecheck + lint + tests)
|
- name: Validate (typecheck + lint + format + maintainability [warn on PR] + tests)
|
||||||
run: bun run validate
|
run: bun run validate
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ jobs:
|
|||||||
- name: Build
|
- name: Build
|
||||||
run: bun run build:all
|
run: bun run build:all
|
||||||
|
|
||||||
- name: Validate (typecheck + lint + tests)
|
- name: Validate (typecheck + lint + format + maintainability [strict] + tests)
|
||||||
run: bun run validate
|
run: bun run validate
|
||||||
|
|
||||||
- name: Release
|
- name: Release
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ jobs:
|
|||||||
- name: Build package
|
- name: Build package
|
||||||
run: bun run build:all
|
run: bun run build:all
|
||||||
|
|
||||||
- name: Validate (typecheck + lint + tests)
|
- name: Validate (typecheck + lint + format + maintainability [strict] + tests)
|
||||||
run: bun run validate
|
run: bun run validate
|
||||||
|
|
||||||
- name: Release
|
- name: Release
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ CLI wrapper for instant switching between multiple provider accounts and alterna
|
|||||||
| Mistake | Consequence | Correct Action |
|
| Mistake | Consequence | Correct Action |
|
||||||
|---------|-------------|----------------|
|
|---------|-------------|----------------|
|
||||||
| Running `validate` without `format` first | format:check fails | Run `bun run format` BEFORE validate |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 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)
|
# Main project (from repo root)
|
||||||
bun run format # Step 1: Fix formatting
|
bun run format # Step 1: Fix formatting
|
||||||
bun run lint:fix # Step 2: Fix lint issues
|
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)
|
bun run validate:ci-parity # Step 4: CI parity gate (build + validate + base branch check)
|
||||||
|
|
||||||
# UI project (if UI changed)
|
# UI project (if UI changed)
|
||||||
@@ -78,7 +79,7 @@ bun run validate # Step 3: Final check (must pass)
|
|||||||
|
|
||||||
| Project | Command | Runs |
|
| 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 |
|
| UI | `bun run validate` | typecheck + lint:fix + format:check |
|
||||||
|
|
||||||
### ESLint Rules (ALL errors)
|
### ESLint Rules (ALL errors)
|
||||||
@@ -105,10 +106,31 @@ bun run validate # Step 3: Final check (must pass)
|
|||||||
### Automatic Enforcement
|
### Automatic Enforcement
|
||||||
|
|
||||||
- `prepublishOnly` / `prepack` runs `build:all` + `validate` + `sync-version.js`
|
- `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-commit` runs quick lint/type/format checks
|
||||||
- husky `pre-push` runs `bun run validate:ci-parity` to block CI drift before push
|
- 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)
|
## Critical Constraints (NEVER VIOLATE)
|
||||||
|
|
||||||
1. **NO EMOJIS in CLI output** - Terminal output uses ASCII only: [OK], [!], [X], [i]
|
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` — all checks pass
|
||||||
- [ ] `bun run validate:ci-parity` — CI parity passed (also enforced by pre-push hook)
|
- [ ] `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
|
- [ ] `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:**
|
**Code:**
|
||||||
- [ ] Conventional commit format (`feat:`, `fix:`, etc.)
|
- [ ] Conventional commit format (`feat:`, `fix:`, etc.)
|
||||||
|
|||||||
@@ -203,15 +203,21 @@ All criteria achieved:
|
|||||||
## Maintainability Gate (Issue #539 Foundation)
|
## Maintainability Gate (Issue #539 Foundation)
|
||||||
|
|
||||||
- Baseline metrics artifact: `docs/metrics/maintainability-baseline.json`
|
- Baseline metrics artifact: `docs/metrics/maintainability-baseline.json`
|
||||||
|
- Branch-aware gate wrapper: `scripts/maintainability-check.js`
|
||||||
- Generate or refresh baseline:
|
- Generate or refresh baseline:
|
||||||
- `bun run maintainability:baseline`
|
- `bun run maintainability:baseline`
|
||||||
- `npm run maintainability:baseline`
|
- `npm run maintainability:baseline`
|
||||||
- Run regression check gate:
|
- Run regression check gate:
|
||||||
- `bun run maintainability:check`
|
- `bun run maintainability:check`
|
||||||
- `npm 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.
|
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:
|
The check mode supports a maintainability regression gate that blocks increases in:
|
||||||
- `process.exit` references
|
- `process.exit` references
|
||||||
- synchronous fs API references
|
- synchronous fs API references
|
||||||
|
|||||||
+3
-1
@@ -69,7 +69,9 @@
|
|||||||
"validate:ci-parity": "bash scripts/ci-parity-gate.sh",
|
"validate:ci-parity": "bash scripts/ci-parity-gate.sh",
|
||||||
"verify:bundle": "node scripts/verify-bundle.js",
|
"verify:bundle": "node scripts/verify-bundle.js",
|
||||||
"maintainability:baseline": "node scripts/maintainability-baseline.js --out docs/metrics/maintainability-baseline.json",
|
"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": "bun run build && bun run test:all",
|
||||||
"test:ci": "bun run test:all",
|
"test:ci": "bun run test:all",
|
||||||
"test:all": "bun test tests/unit tests/integration tests/npm",
|
"test:all": "bun test tests/unit tests/integration tests/npm",
|
||||||
|
|||||||
@@ -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();
|
||||||
Reference in New Issue
Block a user