mirror of
https://github.com/glittercowboy/get-shit-done
synced 2026-05-13 10:36:38 +02:00
* feat(#2995): post-install path audit for workflow-invoked scripts Catches the gap class surfaced by #2994: a workflow references a script via ${GSD_HOME}/<path> that ships in the npm tarball but is not copied to the user's config dir at install time. Unit tests don't catch it because they resolve the script via path.join(__dirname, '..', 'scripts', …) — the source layout, not the deployed layout. Implementation built TDD per #2995, vertical slices with structured-IR assertions: scripts/audit-workflow-script-paths.cjs - Pure auditWorkflowScriptPaths({ workflowsDir, repoRoot, installedPrefixes }) returns { ok, findings: [{ workflow, path, kind }] } via the AUDIT_FINDING enum. - Two finding kinds: MISSING_FROM_REPO (typo / file deleted) and NOT_INSTALLED (#2994 class — first segment outside installed prefixes). - Tolerates ${GSD_HOME:-...} default-fallback syntax. tests/bug-2995-post-install-script-paths.test.cjs - 9 tests across 3 suites: • Pure-function pass and per-finding-kind detection (5 tests on synthetic fixtures). • Real workflow audit (2 tests asserting the actual repo's get-shit-done/workflows/ has no NEW gaps and KNOWN_GAPS stays consistent with audit findings). • Enum shape lock + extractReferences edge cases. - All assertions on typed AUDIT_FINDING enum / structured records; zero raw text matching. - KNOWN_GAPS is a Set keyed on `workflow|path|kind` strings; currently contains the #2994 entry. The companion test fails if a KNOWN_GAPS entry no longer matches a real finding (forces the allow-list to shrink as gaps fix). The audit immediately catches #2994's gap on `reapply-patches.md`. The allow-list contains exactly that entry; new gaps fail CI; #2994's fix will remove the entry as part of the same PR. Closes #2995 Refs #2994 * chore(#2995): add changeset fragment for PR #2996 * chore(#2995): add changeset fragment for PR #2996 * fix(#2995): emit both NOT_INSTALLED + MISSING_FROM_REPO; clean up fixture leak (CR) CodeRabbit on PR #2996 found two issues: 1. (Low value) auditWorkflowScriptPaths short-circuited on NOT_INSTALLED, masking MISSING_FROM_REPO for the same ref. Removed the `continue` so both findings emit in one run; added a regression test. 2. (Low value) bug-2995 test created tmpRoot in before() but never wrote into it; per-fixture mkdtempSync dirs leaked. Rooted fixture repos under tmpRoot so the after() cleanup actually frees them.
74 lines
2.6 KiB
JavaScript
74 lines
2.6 KiB
JavaScript
'use strict';
|
|
|
|
/**
|
|
* Post-install path audit for workflow-invoked scripts (#2995).
|
|
*
|
|
* Walks workflowsDir, extracts every `${GSD_HOME[...]}/<path>.<cjs|js|sh>`
|
|
* token, and asserts:
|
|
* 1. the file exists in the repo at that <path> (catches typos)
|
|
* 2. <path>'s first segment is in installedPrefixes (catches the
|
|
* #2994 class: source-vs-deployed-path mismatches)
|
|
*
|
|
* Pure function over (workflowsDir, repoRoot, installedPrefixes); no
|
|
* filesystem mutation. Tests assert on the typed AUDIT_FINDING enum.
|
|
*/
|
|
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
|
|
const AUDIT_FINDING = Object.freeze({
|
|
MISSING_FROM_REPO: 'missing_from_repo',
|
|
NOT_INSTALLED: 'not_installed',
|
|
});
|
|
|
|
// Match `${GSD_HOME}` or `${GSD_HOME:-...}` followed by a /-rooted path
|
|
// ending in .cjs/.js/.sh. The path is captured verbatim (relative to
|
|
// the install root).
|
|
const REF_RE = /\$\{GSD_HOME(?::-[^}]*)?\}\/([A-Za-z0-9_./-]+\.(?:cjs|js|sh))/g;
|
|
|
|
function listWorkflowFiles(dir) {
|
|
if (!fs.existsSync(dir)) return [];
|
|
return fs
|
|
.readdirSync(dir, { withFileTypes: true })
|
|
.filter((e) => e.isFile() && e.name.endsWith('.md'))
|
|
.map((e) => path.join(dir, e.name));
|
|
}
|
|
|
|
function extractReferences(content) {
|
|
const out = [];
|
|
let m;
|
|
// RegExp objects with /g state must be reset per call.
|
|
const re = new RegExp(REF_RE.source, 'g');
|
|
while ((m = re.exec(content)) !== null) {
|
|
out.push(m[1]);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function auditWorkflowScriptPaths({ workflowsDir, repoRoot, installedPrefixes }) {
|
|
const findings = [];
|
|
const installedSet = new Set(installedPrefixes);
|
|
for (const file of listWorkflowFiles(workflowsDir)) {
|
|
const content = fs.readFileSync(file, 'utf8');
|
|
const workflow = path.basename(file);
|
|
for (const ref of extractReferences(content)) {
|
|
const firstSegment = ref.split('/')[0];
|
|
// #2996 CR: emit BOTH findings simultaneously when a reference is
|
|
// both outside an installed prefix AND missing from the repo. The
|
|
// earlier `continue` short-circuited MISSING_FROM_REPO, so a
|
|
// developer who moved a missing reference to an installed prefix
|
|
// would only discover the second issue on a subsequent CI run.
|
|
if (!installedSet.has(firstSegment)) {
|
|
findings.push({ workflow, path: ref, kind: AUDIT_FINDING.NOT_INSTALLED });
|
|
}
|
|
const sourceFile = path.join(repoRoot, ref);
|
|
if (!fs.existsSync(sourceFile)) {
|
|
findings.push({ workflow, path: ref, kind: AUDIT_FINDING.MISSING_FROM_REPO });
|
|
}
|
|
}
|
|
}
|
|
return { ok: findings.length === 0, findings };
|
|
}
|
|
|
|
module.exports = { auditWorkflowScriptPaths, AUDIT_FINDING, extractReferences };
|