* docs(#457): rewrite ADR-457 to ground truth and accept build-at-publish The prior draft asserted a codebase state that never existed (13 tsc-generated files, src/ trees, a tests/cjs-ts-parity.test.cjs). Corrected to verified ground truth (84 bin/lib .cjs, 1 value-baked package-identity.cjs, no tsc pipeline), distinguished value-baking from transpilation so package-identity stops being miscited as precedent, made check-in-the-artifact vs build-at-publish the central decision, and flipped status to Accepted (build-at-publish). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * build(#537): pilot TS build-at-publish for bin/lib (semver-compare) First hand-written module collapsed to a TypeScript source of truth per ADR-457. src/semver-compare.cts compiles (tsc, strict, noEmitOnError) to a gitignored get-shit-done/bin/lib/semver-compare.cjs. build:lib is wired into build, pretest, pretest:coverage, and prepublishOnly so the artifact is built before test and shipped on publish. Type-aware ESLint on src/**/*.cts immediately caught the params were over-typed as `unknown` (no-base-to-string); narrowed to a honest VersionInput domain type. Behavior preserved: semver-compare.test.cjs (14) and bug-10 (4) pass against the generated output; runtime consumer changeset/cli.cjs unaffected. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#537): make build-at-publish robust across all CI paths (codex review) Adversarial review found the pilot's generated artifact would be missing on clean CI checkouts. `pretest`/`pretest:coverage` only fire for `npm test`, but CI runs `test:unit`/`test:integration`/`test:install` and `node run-tests.cjs` directly — none of which built the artifact, so any suite requiring semver-compare.cjs would hit module-not-found on a clean checkout, and install-smoke's `npm pack` could ship without it. - Add a `prepare` script (`npm run build:lib`). `npm ci` runs it automatically, so every CI test job and install-smoke's pack emit the artifact before use. This is the idiomatic npm mechanism for compiled-output-not-in-git and fixes both the test and pack paths in one place. - Add `src/` + `tsconfig.build.json` to ci-test-scope and the install-smoke / mutation path filters, so a source-only edit to a migrated module still triggers its tests and mutation coverage (prevents silent CI skips). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#537): map src/*.cts to built artifact in mutation changed-files detection Follow-up to the codex re-review. The prior commit added src/**/*.cts to the mutation workflow's path trigger but left its "compute changed core lib files" step diffing only get-shit-done/bin/lib/**/*.cjs — which are now gitignored and never appear in a diff. A source-only edit would trigger the workflow then early-exit ("no core lib files changed"), silently skipping mutation testing. Map each changed src/*.cts to its built get-shit-done/bin/lib/*.cjs path (the on-disk artifact Stryker mutates after prepare/build:lib), merge with the hand-written .cjs diff, and apply the test/excluded-module filters once. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#537): use 'src/' pathspec in mutation diff (git glob doesn't match top-level) Codex review caught that `git diff -- 'src/**/*.cts'` returns empty for a top-level file like src/semver-compare.cts — git's default pathspec glob does not match `**` across zero directories (verified on git 2.50.1). The prior commit's src-detection therefore never fired, so source-only changes still skipped mutation. Switch to the dir-scoped pathspec 'src/' + a `.cts` grep (robust for flat and nested layouts), and broaden the workflow path trigger to 'src/**' to match install-smoke. Verified end-to-end: a change to src/semver-compare.cts now resolves to get-shit-done/bin/lib/semver-compare.cjs in the --mutate list. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore(#537): add changeset fragment for build-at-publish pilot Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#537): replace prepare with prepack + build-if-missing; defer mutation wiring CI surfaced three real issues the local run and codex review missed: 1. lockfile-sync failed on every platform. Root cause: `npm ci --dry-run` (the repo's lockfile health check) RUNS the `prepare` script, but in dry-run the devDependencies aren't installed, so `tsc` is not found (exit 127) and the check reports a misleading "out of sync". `prepare` is the wrong hook for a build needing a devDep. Replace it with `prepack` (runs only on pack/publish, when node_modules exists) for the tarball path, and build the artifact inside scripts/run-tests.cjs (build-if-missing) for the test path — the universal chokepoint every CI test invocation funnels through, including the direct `node run-tests.cjs --files-from` step that bypasses npm lifecycle hooks. The guard is a no-op once built, so the run-tests harness test is unaffected. 2. The Stryker mutation gate ran only 1 test against semver-compare (~0% score, 71/71 mutants surviving) — a Stryker test-selection problem orthogonal to the build migration, and raising the score needs property tests (ADR-456). Revert the mutation.yml src wiring; mutation coverage for src-authored modules is a separate follow-up tracked in #537. (The deletion of the gitignored top-level .cjs does not match the workflow's `bin/lib/**/*.cjs` git pathspec, so the gate skips cleanly.) Verified: clean-room `npm ci --dry-run` exits 0; deleting the artifact then running a suite rebuilds it; run-tests harness 22/22 green; `npm pack` includes the built artifact via prepack. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
313 lines
9.6 KiB
JavaScript
313 lines
9.6 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: 'TS runtime sources (ADR-457 build-at-publish)',
|
|
// src/*.cts compiles into get-shit-done/bin/lib/*.cjs; a source-only edit must
|
|
// still trigger the migrated module's tests (otherwise CI silently skips them).
|
|
match: path => path.startsWith('src/') || path === 'tsconfig.build.json',
|
|
tests: [
|
|
'tests/semver-compare.test.cjs',
|
|
'tests/bug-10-semver-policy-consolidation.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();
|