#!/usr/bin/env node import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; const CODE_EXTENSION = String.raw`\.[cm]?[jt]sx?$`; const noRuntimeImpact = [ /^\.codex\//, /^\.tmp-tests\//, /^docs\//, /^test\//, /^workpack\//, /^(?:AGENTS|PRODUCT|README)\.md$/, /^context\.md$/, /^\.env\.example$/, /^\.gitignore$/, /^Dockerfile\.client$/, /^docker-compose\.client(?:\.local)?\.yml$/, /^entrypoint\.client\.sh$/, /^install\.sh$/, /^scripts\/(?:check-import-boundaries\.mjs|clean-test-dist\.mjs|harbor-network-monitor\.sh|harbor-version\.mjs|install-macos-client\.sh)$/, /^tools\/test-singbox-(?:client-rc|gateway-native-traffic|native-traffic)\.sh$/, ]; const foundation = [ /^\.node-version$/, /^scripts\/check-sqlite-runtime\.mjs$/, /^\.dockerignore$/, /^\.gitea\/workflows\//, /^Dockerfile(?:\.runtime-base)?$/, /^docker-compose\.gateway\.yml$/, /^entrypoint\.sh$/, /^package(?:-lock)?\.json$/, /^scripts\/(?:build-on-107-deploy-111|build-runtime-base|deploy-gateway)\.sh$/, /^scripts\/runtime-impact\.mjs$/, /^tsconfig(?:\.[^.]+)?\.json$/, ]; const controlAndDataplane = [ /^src\/server\/services\/(?:sqlite|trafficHistoryStore|trafficHistoryService|trafficHistoryWorker)\.ts$/, /^src\/shared\/trafficHistory\.ts$/, /^buf\.gen\.yaml$/, /^proto\//, new RegExp(`^src/server/main${CODE_EXTENSION}`), new RegExp(`^src/server/(?:config|gatewayNativeRuntime|gatewayRouting|singbox|singboxRuntime|version)${CODE_EXTENSION}`), /^src\/server\/generated\//, new RegExp(`^src/server/adapters/neighbors${CODE_EXTENSION}`), new RegExp(`^src/server/services/(?:connectivityDiagnosticsService|deviceInventoryService|devicePolicyService|liveTrafficService|singboxSelectorService)${CODE_EXTENSION}`), new RegExp(`^src/shared/(?:connectivityDiagnostics|errors|liveTraffic)${CODE_EXTENSION}`), /^src\/server\/infrastructure\/dataplane\//, ]; const dataplane = [ new RegExp(`^src/server/dataplane${CODE_EXTENSION}`), new RegExp(`^src/server/services/(?:deviceTrafficService|domainTrafficService)${CODE_EXTENSION}`), ]; const control = [ /^index\.html$/, /^monitoring\//, /^public\//, /^src\/server\//, /^src\/shared\//, /^src\/web\//, /^vite\.config\.[cm]?[jt]s$/, ]; function matchesAny(file, patterns) { return patterns.some((pattern) => pattern.test(file)); } function normalizeFile(file) { return file.trim().replaceAll('\\', '/').replace(/^\.\//, ''); } export function classifyRuntimeImpact(files) { const affected = new Set(); for (const value of files) { const file = normalizeFile(value); if (!file || matchesAny(file, noRuntimeImpact)) continue; if (matchesAny(file, foundation) || matchesAny(file, controlAndDataplane)) { affected.add('control'); affected.add('dataplane'); } else if (matchesAny(file, dataplane)) { affected.add('dataplane'); } else if (matchesAny(file, control)) { affected.add('control'); } else { throw new Error(`Unclassified path: ${file}`); } } const affectedComponents = ['control', 'dataplane'].filter((component) => affected.has(component)); const restartScope = affected.has('dataplane') ? 'both' : affected.has('control') ? 'control' : 'none'; return { affectedComponents, restartScope }; } function formatImpact(impact) { return [ `affected-components=${impact.affectedComponents.join('+') || 'none'}`, `restart-scope=${impact.restartScope}`, ].join('\n'); } if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { try { const args = process.argv.slice(2); const files = args.includes('--stdin') ? fs.readFileSync(0, 'utf8').split(/\r?\n/) : args; console.log(formatImpact(classifyRuntimeImpact(files))); } catch (error) { console.error(`[runtime-impact] ${error.message}`); process.exitCode = 1; } }