classify() under-approximated breakable checks, so scoped PRs skipped the check their diff would break and regressions reached next (#484 docs-parity, #482 windows-22 EBUSY). Fail-safe widen: any tests/** change forces full_matrix (OS-specific test failures); any docs/**, commands/**, agents/** change marks code_changed and selects docs-parity-live-registry (its runtime inputs). Updated the docs-only test that asserted the old buggy contract. Fixes #494 Co-authored-by: CI Rebase Check <ci@gsd-redux>
303 lines
9.1 KiB
JavaScript
303 lines
9.1 KiB
JavaScript
#!/usr/bin/env node
|
|
'use strict';
|
|
|
|
const { execFileSync } = require('child_process');
|
|
const { existsSync, readdirSync, appendFileSync } = require('fs');
|
|
const { join } = require('path');
|
|
|
|
const RULES = [
|
|
{
|
|
name: 'workflow automation',
|
|
match: path => path.startsWith('.github/workflows/') || path.startsWith('.github/rulesets/'),
|
|
fullMatrix: true,
|
|
tests: [
|
|
'tests/workflow-shell-pinning.test.cjs',
|
|
'tests/release-tarball-smoke-workflow.test.cjs',
|
|
'tests/lint-pr-check-project-dir.test.cjs',
|
|
'tests/pr-template-policy.test.cjs',
|
|
],
|
|
},
|
|
{
|
|
name: 'test harness',
|
|
match: path => path === 'scripts/run-tests.cjs',
|
|
fullMatrix: true,
|
|
tests: [
|
|
'tests/run-tests-harness.test.cjs',
|
|
'tests/workflow-shell-pinning.test.cjs',
|
|
],
|
|
},
|
|
{
|
|
name: 'environment and dependency gates',
|
|
match: path => [
|
|
'scripts/check-env.cjs',
|
|
'scripts/check-npm-integrity.cjs',
|
|
'package.json',
|
|
'package-lock.json',
|
|
].includes(path),
|
|
fullMatrix: true,
|
|
tests: [
|
|
'tests/check-env.test.cjs',
|
|
'tests/npm-integrity-gate.test.cjs',
|
|
'tests/package-manifest.test.cjs',
|
|
'tests/bug-3588-npm-audit-clean.test.cjs',
|
|
],
|
|
},
|
|
{
|
|
name: 'installer and package layout',
|
|
match: path => path.startsWith('bin/') ||
|
|
path.startsWith('get-shit-done/bin/') ||
|
|
path.includes('install') ||
|
|
path.includes('release-tarball-smoke'),
|
|
fullMatrix: true,
|
|
tests: [
|
|
'tests/install.test.cjs',
|
|
'tests/install-regressions.test.cjs',
|
|
'tests/install-runtime-artifacts.test.cjs',
|
|
'tests/install-path-detection.test.cjs',
|
|
'tests/release-tarball-smoke.install.test.cjs',
|
|
'tests/runtime-artifact-layout.test.cjs',
|
|
],
|
|
},
|
|
{
|
|
name: 'hooks',
|
|
match: path => path.startsWith('hooks/'),
|
|
fullMatrix: true,
|
|
tests: [
|
|
'tests/hook-validation.test.cjs',
|
|
'tests/managed-hooks.test.cjs',
|
|
'tests/hooks-opt-in.test.cjs',
|
|
'tests/sh-hook-paths.test.cjs',
|
|
'tests/precommit-alias-drift-hook.test.cjs',
|
|
'tests/prepush-enterprise-email-hook.test.cjs',
|
|
],
|
|
},
|
|
{
|
|
name: 'changeset tooling',
|
|
match: path => path.startsWith('scripts/changeset/') || path.startsWith('.changeset/'),
|
|
tests: [
|
|
'tests/changeset-cli.test.cjs',
|
|
'tests/changeset-lint.test.cjs',
|
|
'tests/changeset-new.test.cjs',
|
|
'tests/changeset-parse.test.cjs',
|
|
'tests/changeset-render.test.cjs',
|
|
'tests/changeset-serialize.test.cjs',
|
|
'tests/changeset-github-release-notes.test.cjs',
|
|
],
|
|
},
|
|
{
|
|
name: 'security scanners',
|
|
match: path => path.includes('secret-scan') ||
|
|
path.includes('base64-scan') ||
|
|
path.includes('prompt-injection-scan') ||
|
|
path.startsWith('tests/fixtures/adversarial/security/'),
|
|
tests: [
|
|
'tests/secret-scan-lint.test.cjs',
|
|
'tests/prompt-injection-scan.test.cjs',
|
|
'tests/security-prompt-injection.test.cjs',
|
|
'tests/read-injection-scanner.test.cjs',
|
|
'tests/security-scan.test.cjs',
|
|
],
|
|
},
|
|
{
|
|
name: 'command definitions',
|
|
match: path => path.startsWith('commands/'),
|
|
tests: [
|
|
'tests/command-contract.test.cjs',
|
|
'tests/command-routing-hub.test.cjs',
|
|
'tests/commands.test.cjs',
|
|
'tests/docs-parity-live-registry.test.cjs',
|
|
'tests/phase-command-router.test.cjs',
|
|
'tests/roadmap-command-router.test.cjs',
|
|
],
|
|
},
|
|
{
|
|
name: 'workflow prompts',
|
|
match: path => path.startsWith('get-shit-done/workflows/'),
|
|
tests: [
|
|
'tests/workflow-compat.test.cjs',
|
|
'tests/workflow-size-budget.test.cjs',
|
|
'tests/workflow-guard-registration.test.cjs',
|
|
'tests/commands.test.cjs',
|
|
'tests/bug-3683-workflow-colon-namespace-leak.test.cjs',
|
|
],
|
|
},
|
|
{
|
|
name: 'agent prompts',
|
|
match: path => path.startsWith('agents/'),
|
|
tests: [
|
|
'tests/agent-frontmatter.test.cjs',
|
|
'tests/agent-size-budget.test.cjs',
|
|
'tests/agent-skills.test.cjs',
|
|
'tests/agent-skills-awareness.test.cjs',
|
|
'tests/agent-required-reading-consistency.test.cjs',
|
|
'tests/docs-parity-live-registry.test.cjs',
|
|
],
|
|
},
|
|
{
|
|
name: 'docs content',
|
|
match: path => path.startsWith('docs/'),
|
|
fullMatrix: false,
|
|
tests: [
|
|
'tests/docs-parity-live-registry.test.cjs',
|
|
],
|
|
},
|
|
{
|
|
name: 'configuration',
|
|
match: path => /config|configuration|model-catalog|model-profile/.test(path),
|
|
tests: [
|
|
'tests/config.test.cjs',
|
|
'tests/config-get-default.test.cjs',
|
|
'tests/configuration-migrate-config.test.cjs',
|
|
'tests/model-catalog-runtime-defaults.test.cjs',
|
|
'tests/model-profiles.test.cjs',
|
|
],
|
|
},
|
|
];
|
|
|
|
function usage() {
|
|
return [
|
|
'Usage:',
|
|
' node scripts/ci-test-scope.cjs --base <sha> --head <sha>',
|
|
' node scripts/ci-test-scope.cjs --files <path-list>',
|
|
'',
|
|
'Prints JSON by default. With GITHUB_OUTPUT set, also writes workflow outputs.',
|
|
].join('\n');
|
|
}
|
|
|
|
function parseArgs(argv) {
|
|
const out = { base: null, head: null, files: null };
|
|
for (let i = 0; i < argv.length; i++) {
|
|
const arg = argv[i];
|
|
if (arg === '--base') {
|
|
out.base = argv[++i];
|
|
if (!out.base || out.base.startsWith('--')) throw new Error('--base requires a value');
|
|
} else if (arg.startsWith('--base=')) {
|
|
out.base = arg.slice('--base='.length);
|
|
if (!out.base) throw new Error('--base requires a value');
|
|
} else if (arg === '--head') {
|
|
out.head = argv[++i];
|
|
if (!out.head || out.head.startsWith('--')) throw new Error('--head requires a value');
|
|
} else if (arg.startsWith('--head=')) {
|
|
out.head = arg.slice('--head='.length);
|
|
if (!out.head) throw new Error('--head requires a value');
|
|
} else if (arg === '--files') {
|
|
out.files = argv[++i];
|
|
if (!out.files || out.files.startsWith('--')) throw new Error('--files requires a value');
|
|
} else if (arg.startsWith('--files=')) {
|
|
out.files = arg.slice('--files='.length);
|
|
if (!out.files) throw new Error('--files requires a value');
|
|
} else if (arg === '--help' || arg === '-h') {
|
|
console.log(usage());
|
|
process.exit(0);
|
|
} else {
|
|
throw new Error(`unknown argument: ${arg}`);
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function splitFiles(value) {
|
|
if (!value) return [];
|
|
return value.split(/[,\s]+/).map(v => v.trim()).filter(Boolean);
|
|
}
|
|
|
|
function changedFiles(args) {
|
|
if (args.files) return splitFiles(args.files);
|
|
if (!args.base || !args.head) {
|
|
throw new Error('--base/--head or --files is required');
|
|
}
|
|
const stdout = execFileSync('git', ['diff', '--name-only', args.base, args.head], {
|
|
encoding: 'utf8',
|
|
});
|
|
return splitFiles(stdout);
|
|
}
|
|
|
|
function existingTests(files) {
|
|
const all = new Set(readdirSync('tests').filter(f => f.endsWith('.test.cjs')).map(f => `tests/${f}`));
|
|
return files.filter(file => all.has(file) && existsSync(file));
|
|
}
|
|
|
|
function addAll(set, values) {
|
|
for (const value of values) set.add(value);
|
|
}
|
|
|
|
function classify(files) {
|
|
const targeted = new Set();
|
|
const windows = new Set();
|
|
const reasons = [];
|
|
let codeChanged = false;
|
|
let fullMatrix = false;
|
|
|
|
for (const file of files) {
|
|
if (/^(bin|get-shit-done|agents|commands|docs|hooks|tests|scripts)\//.test(file) ||
|
|
/^package(-lock)?\.json$/.test(file) ||
|
|
/^tsconfig.*\.json$/.test(file) ||
|
|
file.startsWith('.github/workflows/') ||
|
|
file.startsWith('.github/rulesets/')) {
|
|
codeChanged = true;
|
|
}
|
|
|
|
if (file.startsWith('tests/') && file.endsWith('.test.cjs')) {
|
|
targeted.add(file);
|
|
fullMatrix = true;
|
|
if (/windows|path|shell|workflow|install|hook/i.test(file)) {
|
|
windows.add(file);
|
|
}
|
|
}
|
|
|
|
for (const rule of RULES) {
|
|
if (rule.match(file)) {
|
|
addAll(targeted, rule.tests);
|
|
reasons.push(`${file}: ${rule.name}`);
|
|
if (rule.fullMatrix) fullMatrix = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
const targetedTests = existingTests([...targeted].sort());
|
|
|
|
// When code changed but no rule matched any changed file, fall back to the
|
|
// unit suite so the targeted lane always runs something meaningful (#408).
|
|
if (codeChanged && targetedTests.length === 0) {
|
|
targetedTests.push('unit');
|
|
}
|
|
|
|
const windowsTests = existingTests([...new Set([...windows, ...targetedTests.filter(t => /windows|path|shell|workflow|install|hook/i.test(t))])].sort());
|
|
|
|
return {
|
|
code_changed: codeChanged,
|
|
full_matrix: fullMatrix,
|
|
targeted_tests: targetedTests,
|
|
windows_tests: windowsTests,
|
|
reasons: [...new Set(reasons)].sort(),
|
|
};
|
|
}
|
|
|
|
function writeOutputs(result) {
|
|
if (!process.env.GITHUB_OUTPUT) return;
|
|
const lines = [
|
|
`code_changed=${result.code_changed}`,
|
|
`full_matrix=${result.full_matrix}`,
|
|
`targeted_tests=${result.targeted_tests.join(' ')}`,
|
|
`windows_tests=${result.windows_tests.join(' ')}`,
|
|
];
|
|
appendFileSync(process.env.GITHUB_OUTPUT, `${lines.join('\n')}\n`);
|
|
}
|
|
|
|
function main() {
|
|
try {
|
|
const args = parseArgs(process.argv.slice(2));
|
|
const files = changedFiles(args);
|
|
const result = classify(files);
|
|
result.changed_files = files;
|
|
writeOutputs(result);
|
|
console.log(JSON.stringify(result, null, 2));
|
|
} catch (error) {
|
|
console.error(`ci-test-scope: ${error.message}`);
|
|
console.error(usage());
|
|
process.exit(2);
|
|
}
|
|
}
|
|
|
|
main();
|