From 03a0fb27ca8ed41326f306c5364e1228117f72b0 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Wed, 11 Mar 2026 10:51:05 +0700 Subject: [PATCH] fix(ci): remove maintainability baseline gate blocking releases The maintainability gate (process.exit count + sync FS API count) was a raw grep counter that provided no actionable signal for a CLI tool. It blocked the v7.53.0 stable release because new features naturally increased the counts beyond the baseline. Removed: - scripts/maintainability-baseline.js - scripts/maintainability-check.js - docs/metrics/maintainability-baseline.json - All maintainability:* npm scripts - Gate references from release, dev-release, and CI workflows Quality gates retained: typecheck, eslint, prettier, full test suite. --- .github/workflows/ci.yml | 2 +- .github/workflows/dev-release.yml | 5 +- .github/workflows/release.yml | 2 +- docs/metrics/maintainability-baseline.json | 9 - package.json | 6 +- scripts/maintainability-baseline.js | 308 --------------------- scripts/maintainability-check.js | 163 ----------- 7 files changed, 4 insertions(+), 491 deletions(-) delete mode 100644 docs/metrics/maintainability-baseline.json delete mode 100644 scripts/maintainability-baseline.js delete mode 100644 scripts/maintainability-check.js diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e4041af2..3cc7fc24 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,5 +30,5 @@ jobs: - name: Build package run: bun run build:all - - name: Validate (typecheck + lint + format + maintainability [warn on PR] + tests) + - name: Validate (typecheck + lint + format + tests) run: bun run validate diff --git a/.github/workflows/dev-release.yml b/.github/workflows/dev-release.yml index e61a5661..1c12c76e 100644 --- a/.github/workflows/dev-release.yml +++ b/.github/workflows/dev-release.yml @@ -48,10 +48,7 @@ jobs: - name: Build run: bun run build:all - - name: Validate (typecheck + lint + format + maintainability [warn] + tests) - env: - # Dev channel should surface maintainability regressions without blocking every merge. - CCS_MAINTAINABILITY_MODE: warn + - name: Validate (typecheck + lint + format + tests) run: bun run validate - name: Release diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index caaaed21..3bec763a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -43,7 +43,7 @@ jobs: - name: Build package run: bun run build:all - - name: Validate (typecheck + lint + format + maintainability [strict] + tests) + - name: Validate (typecheck + lint + format + tests) run: bun run validate - name: Release diff --git a/docs/metrics/maintainability-baseline.json b/docs/metrics/maintainability-baseline.json deleted file mode 100644 index 31a9b4e8..00000000 --- a/docs/metrics/maintainability-baseline.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "sourceDirectory": "src", - "largeFileThresholdLoc": 350, - "typeScriptFileCount": 390, - "locInSrc": 83786, - "processExitReferenceCount": 185, - "synchronousFsApiReferenceCount": 894, - "largeFileCountOver350Loc": 61 -} diff --git a/package.json b/package.json index a351e3b1..40c70904 100644 --- a/package.json +++ b/package.json @@ -65,13 +65,9 @@ "lint:fix": "eslint src/ --fix", "format": "prettier --write src/", "format:check": "prettier --check src/", - "validate": "bun run typecheck && bun run lint:fix && bun run format:check && bun run maintainability:check && bun run test:all", + "validate": "bun run typecheck && bun run lint:fix && bun run format:check && bun run test:all", "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-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", diff --git a/scripts/maintainability-baseline.js b/scripts/maintainability-baseline.js deleted file mode 100644 index e249ad8f..00000000 --- a/scripts/maintainability-baseline.js +++ /dev/null @@ -1,308 +0,0 @@ -#!/usr/bin/env node - -const { execFileSync } = require('child_process'); -const fs = require('fs'); -const path = require('path'); - -const PROJECT_ROOT = path.resolve(__dirname, '..'); -const SRC_DIR = path.join(PROJECT_ROOT, 'src'); -const DEFAULT_BASELINE_PATH = path.join( - PROJECT_ROOT, - 'docs', - 'metrics', - 'maintainability-baseline.json' -); - -const TYPESCRIPT_EXTENSIONS = new Set(['.ts', '.tsx', '.cts', '.mts']); -const LARGE_FILE_THRESHOLD_LOC = 350; - -const FS_SYNC_APIS = [ - 'accessSync', - 'appendFileSync', - 'chmodSync', - 'chownSync', - 'closeSync', - 'copyFileSync', - 'cpSync', - 'existsSync', - 'fchmodSync', - 'fchownSync', - 'fdatasyncSync', - 'fstatSync', - 'fsyncSync', - 'ftruncateSync', - 'futimesSync', - 'lchmodSync', - 'lchownSync', - 'linkSync', - 'lstatSync', - 'lutimesSync', - 'mkdirSync', - 'mkdtempSync', - 'openSync', - 'opendirSync', - 'readFileSync', - 'readdirSync', - 'readlinkSync', - 'readSync', - 'realpathSync', - 'renameSync', - 'rmSync', - 'rmdirSync', - 'statSync', - 'symlinkSync', - 'truncateSync', - 'unlinkSync', - 'utimesSync', - 'writeFileSync', - 'writeSync', - 'writevSync', -]; - -const PROCESS_EXIT_PATTERN = /\bprocess\s*\.\s*exit\b/g; -const FS_SYNC_PATTERN = new RegExp(`\\b(?:${FS_SYNC_APIS.join('|')})\\b`, 'g'); - -function printUsage() { - console.log( - [ - 'Usage:', - ' node scripts/maintainability-baseline.js', - ' node scripts/maintainability-baseline.js --out [path]', - ' node scripts/maintainability-baseline.js --check [path]', - '', - 'Defaults:', - ` baseline path: ${path.relative(PROJECT_ROOT, DEFAULT_BASELINE_PATH)}`, - ].join('\n') - ); -} - -function parseArgs(argv) { - const options = { - outPath: null, - checkPath: null, - }; - - for (let index = 0; index < argv.length; index += 1) { - const arg = argv[index]; - - if (arg === '--help' || arg === '-h') { - printUsage(); - process.exit(0); - } - - if (arg === '--out' || arg === '--write') { - const nextArg = argv[index + 1]; - if (nextArg && !nextArg.startsWith('--')) { - options.outPath = nextArg; - index += 1; - } else { - options.outPath = path.relative(process.cwd(), DEFAULT_BASELINE_PATH); - } - continue; - } - - if (arg === '--check') { - const nextArg = argv[index + 1]; - if (nextArg && !nextArg.startsWith('--')) { - options.checkPath = nextArg; - index += 1; - } else { - options.checkPath = path.relative(process.cwd(), DEFAULT_BASELINE_PATH); - } - continue; - } - - throw new Error(`Unknown argument: ${arg}`); - } - - return options; -} - -function collectTrackedFilesFromGit() { - let output; - try { - output = execFileSync('git', ['ls-files', '-z', '--', 'src'], { - cwd: PROJECT_ROOT, - encoding: 'utf8', - stdio: ['ignore', 'pipe', 'ignore'], - }); - } catch { - throw new Error( - 'Unable to enumerate tracked files via git. Run this command from a git checkout with git installed.' - ); - } - - if (!output) { - return []; - } - - return output - .split('\0') - .filter(Boolean) - .sort((left, right) => left.localeCompare(right)) - .map(relativePath => path.resolve(PROJECT_ROOT, relativePath)) - .filter(filePath => { - const relativeToSrc = path.relative(SRC_DIR, filePath); - if (relativeToSrc.startsWith('..') || path.isAbsolute(relativeToSrc)) { - return false; - } - - const stats = fs.statSync(filePath); - if (!stats.isFile()) { - throw new Error(`Tracked path is not a file: ${path.relative(PROJECT_ROOT, filePath)}`); - } - - return true; - }); -} - -function collectFilesInSrc() { - return collectTrackedFilesFromGit(); -} - -function countLines(content) { - if (content.length === 0) { - return 0; - } - - const lineBreakMatches = content.match(/\r\n|\n|\r/g); - const lineBreakCount = lineBreakMatches ? lineBreakMatches.length : 0; - const endsWithLineBreak = content.endsWith('\n') || content.endsWith('\r'); - - return endsWithLineBreak ? lineBreakCount : lineBreakCount + 1; -} - -function countMatches(content, pattern) { - const matches = content.match(pattern); - return matches ? matches.length : 0; -} - -function collectMetrics() { - if (!fs.existsSync(SRC_DIR)) { - throw new Error(`Directory not found: ${SRC_DIR}`); - } - - const files = collectFilesInSrc(); - - let typeScriptFileCount = 0; - let locInSrc = 0; - let processExitReferenceCount = 0; - let synchronousFsApiReferenceCount = 0; - let largeFileCountOver350Loc = 0; - - for (const filePath of files) { - const content = fs.readFileSync(filePath, 'utf8'); - const loc = countLines(content); - const extension = path.extname(filePath).toLowerCase(); - const isTypeScriptFile = TYPESCRIPT_EXTENSIONS.has(extension); - - locInSrc += loc; - processExitReferenceCount += countMatches(content, PROCESS_EXIT_PATTERN); - synchronousFsApiReferenceCount += countMatches(content, FS_SYNC_PATTERN); - - if (isTypeScriptFile) { - typeScriptFileCount += 1; - if (loc > LARGE_FILE_THRESHOLD_LOC) { - largeFileCountOver350Loc += 1; - } - } - } - - return { - sourceDirectory: 'src', - largeFileThresholdLoc: LARGE_FILE_THRESHOLD_LOC, - typeScriptFileCount, - locInSrc, - processExitReferenceCount, - synchronousFsApiReferenceCount, - largeFileCountOver350Loc, - }; -} - -function writeMetrics(outPath, metrics) { - const resolvedOutPath = path.resolve(process.cwd(), outPath); - fs.mkdirSync(path.dirname(resolvedOutPath), { recursive: true }); - fs.writeFileSync(resolvedOutPath, `${JSON.stringify(metrics, null, 2)}\n`, 'utf8'); -} - -function runCheck(checkPath, currentMetrics) { - const resolvedCheckPath = path.resolve(process.cwd(), checkPath); - const baselineContent = fs.readFileSync(resolvedCheckPath, 'utf8'); - const baselineMetrics = JSON.parse(baselineContent); - - if (baselineMetrics.sourceDirectory !== currentMetrics.sourceDirectory) { - throw new Error( - `Baseline sourceDirectory mismatch: expected "${currentMetrics.sourceDirectory}", got "${baselineMetrics.sourceDirectory}"` - ); - } - - if (baselineMetrics.largeFileThresholdLoc !== currentMetrics.largeFileThresholdLoc) { - throw new Error( - `Baseline largeFileThresholdLoc mismatch: expected ${currentMetrics.largeFileThresholdLoc}, got ${baselineMetrics.largeFileThresholdLoc}` - ); - } - - const gatedKeys = [ - 'processExitReferenceCount', - 'synchronousFsApiReferenceCount', - ]; - - const violations = []; - for (const key of gatedKeys) { - if (typeof baselineMetrics[key] !== 'number') { - throw new Error(`Baseline is missing numeric metric: ${key}`); - } - - if (currentMetrics[key] > baselineMetrics[key]) { - violations.push({ - metric: key, - baseline: baselineMetrics[key], - current: currentMetrics[key], - }); - } - } - - return { - gate: 'maintainability-baseline', - baselinePath: path.relative(PROJECT_ROOT, resolvedCheckPath), - passed: violations.length === 0, - comparedMetrics: gatedKeys, - baseline: { - typeScriptFileCount: baselineMetrics.typeScriptFileCount, - locInSrc: baselineMetrics.locInSrc, - processExitReferenceCount: baselineMetrics.processExitReferenceCount, - synchronousFsApiReferenceCount: baselineMetrics.synchronousFsApiReferenceCount, - largeFileCountOver350Loc: baselineMetrics.largeFileCountOver350Loc, - }, - current: { - typeScriptFileCount: currentMetrics.typeScriptFileCount, - locInSrc: currentMetrics.locInSrc, - processExitReferenceCount: currentMetrics.processExitReferenceCount, - synchronousFsApiReferenceCount: currentMetrics.synchronousFsApiReferenceCount, - largeFileCountOver350Loc: currentMetrics.largeFileCountOver350Loc, - }, - violations, - }; -} - -function main() { - const options = parseArgs(process.argv.slice(2)); - const metrics = collectMetrics(); - - if (options.outPath) { - writeMetrics(options.outPath, metrics); - } - - if (options.checkPath) { - const checkResult = runCheck(options.checkPath, metrics); - console.log(JSON.stringify(checkResult, null, 2)); - if (!checkResult.passed) { - process.exit(1); - } - return; - } - - console.log(JSON.stringify(metrics, null, 2)); -} - -main(); diff --git a/scripts/maintainability-check.js b/scripts/maintainability-check.js deleted file mode 100644 index 8971ef84..00000000 --- a/scripts/maintainability-check.js +++ /dev/null @@ -1,163 +0,0 @@ -#!/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();