@freshworks/shiftleft-tools 1.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +351 -0
- package/bin/shiftleft.js +95 -0
- package/package.json +57 -0
- package/src/commands/doctor.js +208 -0
- package/src/commands/init-postman.js +298 -0
- package/src/commands/init-rules.js +78 -0
- package/src/commands/link.js +172 -0
- package/src/commands/protect.js +61 -0
- package/src/commands/run-tests.js +182 -0
- package/src/commands/setup-pipeline.js +209 -0
- package/src/commands/update.js +203 -0
- package/src/index.js +4 -0
- package/src/utils/copy-tree.js +98 -0
- package/src/utils/gitignore.js +26 -0
- package/src/utils/logger.js +9 -0
- package/src/utils/manifest.js +145 -0
- package/src/utils/stack.js +80 -0
- package/src/utils/template.js +135 -0
- package/templates/AGENTS.md +109 -0
- package/templates/CLAUDE.md +3 -0
- package/templates/jenkins/Jenkinsfile-java.groovy +432 -0
- package/templates/jenkins/Jenkinsfile-node.groovy +450 -0
- package/templates/postman/.husky/pre-commit +19 -0
- package/templates/postman/.prettierrc.json +5 -0
- package/templates/postman/README.md.ejs +147 -0
- package/templates/postman/collections/01-core.json.ejs +91 -0
- package/templates/postman/config/local.json.ejs +12 -0
- package/templates/postman/config/staging.json.ejs +26 -0
- package/templates/postman/environments/local.postman_environment.json.ejs +31 -0
- package/templates/postman/environments/staging.postman_environment.json.ejs +31 -0
- package/templates/postman/gitignore +16 -0
- package/templates/postman/npmrc +31 -0
- package/templates/postman/package.json.ejs +66 -0
- package/templates/postman/run-all-shim.sh +16 -0
- package/templates/postman/scripts/auth/generate-jwt.sh +113 -0
- package/templates/postman/scripts/auth/get-issuer-secret.sh +140 -0
- package/templates/postman/scripts/infra/start-mocks.sh +138 -0
- package/templates/postman/scripts/infra/stop-mocks.sh +43 -0
- package/templates/postman/scripts/lib/api_coverage.py +1122 -0
- package/templates/postman/scripts/lib/cleanup-reports.sh +101 -0
- package/templates/postman/scripts/lib/cleanup-stryker.sh +44 -0
- package/templates/postman/scripts/lib/report_combined.py +527 -0
- package/templates/postman/scripts/lib/report_consolidated.py +363 -0
- package/templates/postman/scripts/lib/report_generator.py +121 -0
- package/templates/postman/scripts/lib/report_migration.py +156 -0
- package/templates/postman/scripts/lib/report_mutation.py +110 -0
- package/templates/postman/scripts/lib/report_unit.py +353 -0
- package/templates/postman/scripts/lib/report_utils.py +973 -0
- package/templates/postman/scripts/report-generators/generate-consolidated-report.sh +445 -0
- package/templates/postman/scripts/report-generators/java-api-coverage-matrix.sh +257 -0
- package/templates/postman/scripts/report-generators/mutation-report.sh +672 -0
- package/templates/postman/scripts/report-generators/node-api-coverage-matrix.sh +167 -0
- package/templates/postman/scripts/report-generators/stage-report-artifacts.sh +27 -0
- package/templates/postman/scripts/run-all.sh +452 -0
- package/templates/postman/scripts/runners/run-mutation-tests.sh +113 -0
- package/templates/postman/scripts/runners/run-tests-local.sh +936 -0
- package/templates/postman/scripts/runners/run-tests-staging.sh +741 -0
- package/templates/postman-node/README.md.ejs +26 -0
- package/templates/postman-node/collections/crud/01-bootstrap.json.ejs +34 -0
- package/templates/postman-node/config/local.json.ejs +46 -0
- package/templates/postman-node/config/staging.json.ejs +31 -0
- package/templates/postman-node/local.test.env.ejs +3 -0
- package/templates/postman-node/mocks/external.js +14 -0
- package/templates/postman-node/package.json.ejs +39 -0
- package/templates/postman-node/requirements.txt +1 -0
- package/templates/postman-node/scripts/database/cleanup-mysql.sh +12 -0
- package/templates/postman-node/scripts/database/run-migrations.js +29 -0
- package/templates/postman-node/scripts/database/start-mysql.sh +34 -0
- package/templates/postman-node/scripts/database/wait-for-mysql.sh +36 -0
- package/templates/postman-node/scripts/lib/api_coverage_node.py +1137 -0
- package/templates/postman-node/scripts/lib/fetch-jwt.sh +86 -0
- package/templates/postman-node/scripts/lib/run-newman.sh +104 -0
- package/templates/postman-node/scripts/lib/setup-database.sh +55 -0
- package/templates/postman-node/scripts/lib/start-app.sh +48 -0
- package/templates/postman-node/scripts/lib/utils.sh +114 -0
- package/templates/postman-node/scripts/report-generators/stage-report-artifacts.sh +26 -0
- package/templates/postman-node/scripts/run-all.sh +303 -0
- package/templates/postman-node/scripts/runners/run-tests.sh +123 -0
- package/templates/postman-node/scripts/setup-mocks.js.ejs +29 -0
- package/templates/postman-node/stryker.config.js.ejs +51 -0
- package/templates/rules/local-test-setup.mdc +420 -0
- package/templates/rules/testing-node.mdc +66 -0
- package/templates/rules/testing.mdc +248 -0
- package/templates/skills/_shared/postman-standards.md +380 -0
- package/templates/skills/enhance-test-pipeline/SKILL-java.md +483 -0
- package/templates/skills/enhance-test-pipeline/SKILL-node.md +431 -0
- package/templates/skills/enhance-test-pipeline/SKILL.md +9 -0
- package/templates/skills/review-test-suite/SKILL-java.md +137 -0
- package/templates/skills/review-test-suite/SKILL-node.md +78 -0
- package/templates/skills/review-test-suite/SKILL.md +9 -0
- package/templates/skills/run-test-suite/SKILL-java.md +186 -0
- package/templates/skills/run-test-suite/SKILL-node.md +191 -0
- package/templates/skills/run-test-suite/SKILL.md +9 -0
- package/templates/skills/setup-api-tests/SKILL-java.md +1094 -0
- package/templates/skills/setup-api-tests/SKILL-node.md +141 -0
- package/templates/skills/setup-api-tests/SKILL.md +9 -0
- package/templates/skills/setup-mutation-tests/SKILL-java.md +303 -0
- package/templates/skills/setup-mutation-tests/SKILL-node.md +408 -0
- package/templates/skills/setup-mutation-tests/SKILL.md +9 -0
- package/templates/skills/setup-test-pipeline/SKILL-java.md +454 -0
- package/templates/skills/setup-test-pipeline/SKILL-node.md +318 -0
- package/templates/skills/setup-test-pipeline/SKILL.md +9 -0
- package/templates/skills/write-api-tests/SKILL-java.md +115 -0
- package/templates/skills/write-api-tests/SKILL-node.md +83 -0
- package/templates/skills/write-api-tests/SKILL.md +9 -0
- package/templates/stryker.config.js +50 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
lstatSync,
|
|
4
|
+
rmSync,
|
|
5
|
+
symlinkSync,
|
|
6
|
+
statSync,
|
|
7
|
+
readdirSync,
|
|
8
|
+
mkdirSync,
|
|
9
|
+
} from 'fs';
|
|
10
|
+
import { dirname, join } from 'path';
|
|
11
|
+
import { ensureGitignoreEntries } from '../utils/gitignore.js';
|
|
12
|
+
import ora from 'ora';
|
|
13
|
+
import { TEMPLATES_DIR, copyTemplate } from '../utils/template.js';
|
|
14
|
+
import { copyTemplateTree } from '../utils/copy-tree.js';
|
|
15
|
+
import { detectStack } from '../utils/stack.js';
|
|
16
|
+
import {
|
|
17
|
+
loadManifest,
|
|
18
|
+
createManifest,
|
|
19
|
+
saveManifest,
|
|
20
|
+
recordFile,
|
|
21
|
+
manifestKey,
|
|
22
|
+
} from '../utils/manifest.js';
|
|
23
|
+
import { logger } from '../utils/logger.js';
|
|
24
|
+
|
|
25
|
+
const GITIGNORE_HEADER = '# shiftleft managed symlinks (not committed)';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Cursor rule files to expose for a stack, mapped template -> dest filename.
|
|
29
|
+
* Mirrors the stack selection init-rules used when it copied rules.
|
|
30
|
+
*/
|
|
31
|
+
function cursorRuleLinks(stack) {
|
|
32
|
+
if (stack === 'node') {
|
|
33
|
+
return [{ template: 'rules/testing-node.mdc', dest: 'testing.mdc' }];
|
|
34
|
+
}
|
|
35
|
+
if (stack === 'java') {
|
|
36
|
+
return [
|
|
37
|
+
{ template: 'rules/testing.mdc', dest: 'testing.mdc' },
|
|
38
|
+
{ template: 'rules/local-test-setup.mdc', dest: 'local-test-setup.mdc' },
|
|
39
|
+
];
|
|
40
|
+
}
|
|
41
|
+
// Unknown stack: expose every rule template as-is.
|
|
42
|
+
return readdirSync(join(TEMPLATES_DIR, 'rules')).map((f) => ({ template: `rules/${f}`, dest: f }));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function removeExisting(p) {
|
|
46
|
+
try {
|
|
47
|
+
lstatSync(p); // throws if absent
|
|
48
|
+
rmSync(p, { recursive: true, force: true });
|
|
49
|
+
} catch {
|
|
50
|
+
/* nothing there */
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function ensureParent(p) {
|
|
55
|
+
const dir = dirname(p);
|
|
56
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Create a symlink, replacing whatever is at linkPath. Returns true on success. */
|
|
60
|
+
function trySymlink(target, linkPath) {
|
|
61
|
+
try {
|
|
62
|
+
ensureParent(linkPath);
|
|
63
|
+
removeExisting(linkPath);
|
|
64
|
+
const type = statSync(target).isDirectory() ? 'dir' : 'file';
|
|
65
|
+
symlinkSync(target, linkPath, type);
|
|
66
|
+
return true;
|
|
67
|
+
} catch {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function addToList(list, value) {
|
|
73
|
+
if (!list.includes(value)) list.push(value);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Point a repo's Cursor assets at the installed package's templates via
|
|
78
|
+
* symlinks, so `npm i -g shiftleft-tools@latest` updates every repo at
|
|
79
|
+
* once. Claude Code skills come from the plugin, not from here.
|
|
80
|
+
*
|
|
81
|
+
* In locked-down environments (no symlink permission, or `--copy`), falls back
|
|
82
|
+
* to copying the content and recording it as managed files instead.
|
|
83
|
+
*
|
|
84
|
+
* Pass `manifest` to operate on a caller-owned manifest (caller saves);
|
|
85
|
+
* otherwise this loads and saves `.shiftleft.json` itself.
|
|
86
|
+
*
|
|
87
|
+
* @returns {{ linked: string[], copied: string[], symlinkMode: boolean }}
|
|
88
|
+
*/
|
|
89
|
+
export function linkAssets(cwd, opts = {}) {
|
|
90
|
+
const { skills = true, rules = true, copy = false } = opts;
|
|
91
|
+
const ownManifest = !opts.manifest;
|
|
92
|
+
const stack = opts.stack || opts.manifest?.stack || detectStack(cwd);
|
|
93
|
+
const manifest = opts.manifest || loadManifest(cwd) || createManifest(stack);
|
|
94
|
+
manifest.symlinks = manifest.symlinks || [];
|
|
95
|
+
|
|
96
|
+
const linked = [];
|
|
97
|
+
const copied = [];
|
|
98
|
+
let symlinkMode = true;
|
|
99
|
+
|
|
100
|
+
const targets = [];
|
|
101
|
+
if (skills) {
|
|
102
|
+
targets.push({
|
|
103
|
+
rel: '.cursor/skills',
|
|
104
|
+
target: join(TEMPLATES_DIR, 'skills'),
|
|
105
|
+
fallback: () => copyTemplateTree('skills', join(cwd, '.cursor/skills'), {}, {
|
|
106
|
+
force: true, manifest, cwd,
|
|
107
|
+
}),
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
if (rules) {
|
|
111
|
+
for (const { template, dest } of cursorRuleLinks(stack)) {
|
|
112
|
+
const rel = `.cursor/rules/${dest}`;
|
|
113
|
+
targets.push({
|
|
114
|
+
rel,
|
|
115
|
+
target: join(TEMPLATES_DIR, template),
|
|
116
|
+
fallback: () => {
|
|
117
|
+
const res = copyTemplate(template, join(cwd, rel), {}, { force: true });
|
|
118
|
+
recordFile(manifest, cwd, rel, res.content, { template });
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
for (const t of targets) {
|
|
125
|
+
const linkPath = join(cwd, t.rel);
|
|
126
|
+
const key = manifestKey(cwd, t.rel);
|
|
127
|
+
|
|
128
|
+
if (!copy && trySymlink(t.target, linkPath)) {
|
|
129
|
+
addToList(manifest.symlinks, t.rel);
|
|
130
|
+
delete manifest.managed[key]; // no longer a content-managed file
|
|
131
|
+
linked.push(t.rel);
|
|
132
|
+
} else {
|
|
133
|
+
symlinkMode = false;
|
|
134
|
+
removeExisting(linkPath); // drop a stale symlink before copying
|
|
135
|
+
t.fallback();
|
|
136
|
+
manifest.symlinks = manifest.symlinks.filter((s) => s !== t.rel);
|
|
137
|
+
copied.push(t.rel);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Phase C drops the per-repo Claude skills copy (plugin replaces it). Prune
|
|
142
|
+
// any leftover .claude/skills entries from earlier (copy-based) versions.
|
|
143
|
+
for (const k of Object.keys(manifest.managed)) {
|
|
144
|
+
if (k.startsWith('.claude/skills/')) delete manifest.managed[k];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (linked.length) ensureGitignoreEntries(join(cwd, '.gitignore'), linked, GITIGNORE_HEADER);
|
|
148
|
+
if (ownManifest) saveManifest(cwd, manifest);
|
|
149
|
+
|
|
150
|
+
return { linked, copied, symlinkMode };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* CLI: shiftleft link
|
|
155
|
+
*/
|
|
156
|
+
export function link(options = {}) {
|
|
157
|
+
const cwd = process.cwd();
|
|
158
|
+
const spinner = ora(options.copy ? 'Copying Cursor assets...' : 'Linking Cursor assets...').start();
|
|
159
|
+
|
|
160
|
+
const { linked, copied, symlinkMode } = linkAssets(cwd, {
|
|
161
|
+
stack: options.stack,
|
|
162
|
+
copy: options.copy,
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
if (symlinkMode) {
|
|
166
|
+
spinner.succeed(`Linked ${linked.length} Cursor asset(s) to the installed shiftleft-tools package`);
|
|
167
|
+
logger.info('These are symlinks (gitignored). Run "shiftleft link" after a fresh clone or "npm i -g shiftleft-tools".');
|
|
168
|
+
} else {
|
|
169
|
+
spinner.succeed(`Copied ${copied.length} Cursor asset(s) (symlinks unavailable — using copies)`);
|
|
170
|
+
}
|
|
171
|
+
logger.info('Claude Code skills are provided by the shiftleft-tools plugin — install it once, not per repo.');
|
|
172
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { existsSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { loadManifest, saveManifest, getProtectedPaths, normalizeScriptPath } from '../utils/manifest.js';
|
|
4
|
+
import { logger } from '../utils/logger.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* CLI: shiftleft protect [paths...] [--remove] [--list]
|
|
8
|
+
*
|
|
9
|
+
* Marks library scripts (relative to postman/scripts/) as repo-owned so
|
|
10
|
+
* `shiftleft test` / `stage-scripts` won't overwrite them from the package.
|
|
11
|
+
* Use for a customized library script, e.g.
|
|
12
|
+
* shiftleft protect runners/run-tests-local.sh runners/run-tests-staging.sh
|
|
13
|
+
*/
|
|
14
|
+
export function protect(paths = [], options = {}) {
|
|
15
|
+
const cwd = process.cwd();
|
|
16
|
+
const manifest = loadManifest(cwd);
|
|
17
|
+
|
|
18
|
+
if (!manifest) {
|
|
19
|
+
logger.error('This project is not managed by shiftleft-tools (no .shiftleft.json). Run "shiftleft init-postman" first.');
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const current = getProtectedPaths(manifest);
|
|
24
|
+
|
|
25
|
+
if (options.list || (paths.length === 0 && !options.remove)) {
|
|
26
|
+
if (current.length === 0) {
|
|
27
|
+
logger.info('No protected paths. Add one with: shiftleft protect runners/run-tests-local.sh');
|
|
28
|
+
} else {
|
|
29
|
+
logger.info('Protected library scripts (not overwritten by staging):');
|
|
30
|
+
for (const p of current) console.log(` ${p}`);
|
|
31
|
+
}
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const normalized = paths.map(normalizeScriptPath).filter(Boolean);
|
|
36
|
+
|
|
37
|
+
if (options.remove) {
|
|
38
|
+
const set = new Set(current);
|
|
39
|
+
const removed = normalized.filter((p) => set.has(p));
|
|
40
|
+
manifest.protected = current.filter((p) => !normalized.includes(p));
|
|
41
|
+
if (manifest.protected.length === 0) delete manifest.protected;
|
|
42
|
+
saveManifest(cwd, manifest);
|
|
43
|
+
logger.success(removed.length ? `Unprotected: ${removed.join(', ')}` : 'Nothing to unprotect (paths were not in the list).');
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Add. Warn (don't block) if the path isn't present under postman/scripts.
|
|
48
|
+
for (const p of normalized) {
|
|
49
|
+
if (!existsSync(join(cwd, 'postman/scripts', p))) {
|
|
50
|
+
logger.warn(`${p} does not exist under postman/scripts/ yet — protect it once it's committed, or it will be missing on a fresh clone.`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const merged = Array.from(new Set([...current, ...normalized])).sort();
|
|
55
|
+
manifest.protected = merged;
|
|
56
|
+
saveManifest(cwd, manifest);
|
|
57
|
+
|
|
58
|
+
logger.success(`Protected ${normalized.length} path(s). Staging will no longer overwrite:`);
|
|
59
|
+
for (const p of merged) console.log(` ${p}`);
|
|
60
|
+
logger.info('Keep these files committed (or gitignore-excepted) so they survive a fresh clone.');
|
|
61
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { existsSync, readdirSync, statSync, chmodSync } from 'fs';
|
|
2
|
+
import { join, dirname, parse } from 'path';
|
|
3
|
+
import { spawnSync } from 'child_process';
|
|
4
|
+
import { copyTemplate } from '../utils/template.js';
|
|
5
|
+
import { copyTemplateTree } from '../utils/copy-tree.js';
|
|
6
|
+
import { detectStack, inferServiceName, buildContext, NODE_BASE_SCRIPT_EXCLUDES } from '../utils/stack.js';
|
|
7
|
+
import { loadManifest, getProtectedPaths } from '../utils/manifest.js';
|
|
8
|
+
import { logger } from '../utils/logger.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Entries (relative to postman/.gitignore) covering the staged script cache.
|
|
12
|
+
* Kept in sync with what stageScripts materializes.
|
|
13
|
+
*/
|
|
14
|
+
export const STAGED_GITIGNORE_ENTRIES = [
|
|
15
|
+
'scripts/.run-all-impl.sh',
|
|
16
|
+
'scripts/auth/',
|
|
17
|
+
'scripts/database/',
|
|
18
|
+
'scripts/infra/',
|
|
19
|
+
'scripts/lib/',
|
|
20
|
+
'scripts/report-generators/',
|
|
21
|
+
'scripts/runners/',
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
export const STAGED_GITIGNORE_HEADER =
|
|
25
|
+
'# shiftleft staged scripts (machine-local cache; refreshed by `shiftleft test`)';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Walk up from `start` to the repo root: the first directory containing a
|
|
29
|
+
* postman/ dir alongside a pom.xml or package.json. Lets `shiftleft test`
|
|
30
|
+
* work both from the repo root and from inside postman/scripts (where
|
|
31
|
+
* Jenkins and humans traditionally invoke run-all.sh).
|
|
32
|
+
*/
|
|
33
|
+
export function findRepoRoot(start) {
|
|
34
|
+
let dir = start;
|
|
35
|
+
const { root } = parse(start);
|
|
36
|
+
while (true) {
|
|
37
|
+
if (
|
|
38
|
+
existsSync(join(dir, 'postman')) &&
|
|
39
|
+
(existsSync(join(dir, 'pom.xml')) || existsSync(join(dir, 'package.json')))
|
|
40
|
+
) {
|
|
41
|
+
return dir;
|
|
42
|
+
}
|
|
43
|
+
if (dir === root) return null;
|
|
44
|
+
dir = dirname(dir);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function chmodShellScripts(dir) {
|
|
49
|
+
for (const entry of readdirSync(dir)) {
|
|
50
|
+
const p = join(dir, entry);
|
|
51
|
+
if (statSync(p).isDirectory()) chmodShellScripts(p);
|
|
52
|
+
else if (entry.endsWith('.sh')) chmodSync(p, 0o755);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Materialize the packaged library scripts into <repo>/postman/scripts.
|
|
58
|
+
*
|
|
59
|
+
* The scripts derive POSTMAN_DIR / PROJECT_ROOT from their own location
|
|
60
|
+
* (BASH_SOURCE), so they must physically live at postman/scripts — they are
|
|
61
|
+
* staged there as a gitignored, machine-local cache rather than vendored.
|
|
62
|
+
* The orchestrator stages as `.run-all-impl.sh` so the committed run-all.sh
|
|
63
|
+
* shim keeps its name. Repo-owned files (setup-mocks.js, wiremock/) are
|
|
64
|
+
* never touched, and any library scripts listed under `protected` in
|
|
65
|
+
* .shiftleft.json (paths relative to postman/scripts/, e.g. a customized
|
|
66
|
+
* runners/run-tests-local.sh) are left in place rather than overwritten.
|
|
67
|
+
*
|
|
68
|
+
* @returns {{ staged: number, stack: string, protected: string[] }}
|
|
69
|
+
*/
|
|
70
|
+
export function stageScripts(cwd, opts = {}) {
|
|
71
|
+
const stack = opts.stack || detectStack(cwd);
|
|
72
|
+
if (!stack) {
|
|
73
|
+
throw new Error('Could not detect project stack (no pom.xml or package.json).');
|
|
74
|
+
}
|
|
75
|
+
const context = buildContext(inferServiceName(cwd), stack);
|
|
76
|
+
const scriptsDir = join(cwd, 'postman/scripts');
|
|
77
|
+
|
|
78
|
+
// Repo-owned library scripts the package must not clobber on stage.
|
|
79
|
+
const protectedPaths = getProtectedPaths(loadManifest(cwd));
|
|
80
|
+
const baseExcludes = (stack === 'node' ? NODE_BASE_SCRIPT_EXCLUDES : ['run-all.sh']).concat(protectedPaths);
|
|
81
|
+
|
|
82
|
+
let staged = copyTemplateTree('postman/scripts', scriptsDir, context, {
|
|
83
|
+
force: true,
|
|
84
|
+
excludeDirs: ['wiremock'],
|
|
85
|
+
excludePaths: baseExcludes,
|
|
86
|
+
}).copied;
|
|
87
|
+
|
|
88
|
+
if (stack === 'node') {
|
|
89
|
+
staged += copyTemplateTree('postman-node/scripts', scriptsDir, context, {
|
|
90
|
+
force: true,
|
|
91
|
+
excludeFiles: ['run-all.sh', 'setup-mocks.js.ejs'],
|
|
92
|
+
excludePaths: protectedPaths,
|
|
93
|
+
}).copied;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const implTemplate = stack === 'node' ? 'postman-node/scripts/run-all.sh' : 'postman/scripts/run-all.sh';
|
|
97
|
+
copyTemplate(implTemplate, join(scriptsDir, '.run-all-impl.sh'), context, { force: true });
|
|
98
|
+
staged++;
|
|
99
|
+
|
|
100
|
+
chmodShellScripts(scriptsDir);
|
|
101
|
+
chmodSync(join(scriptsDir, '.run-all-impl.sh'), 0o755);
|
|
102
|
+
|
|
103
|
+
return { staged, stack, protected: protectedPaths };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* CLI: shiftleft stage-scripts
|
|
108
|
+
*/
|
|
109
|
+
export function stageScriptsCommand(options = {}) {
|
|
110
|
+
const root = findRepoRoot(process.cwd());
|
|
111
|
+
if (!root) {
|
|
112
|
+
logger.error('No postman/ directory found here or above. Run "shiftleft init-postman" first.');
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
const { staged, stack } = stageScripts(root, { stack: options.stack });
|
|
116
|
+
logger.success(`Staged ${staged} script files into postman/scripts (${stack}).`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Find a bash >= 4 on the system and return its absolute path, or null.
|
|
121
|
+
* The scripts use bash-4 features (associative arrays, `${x^^}`, mapfile);
|
|
122
|
+
* macOS ships bash 3.2, so we locate a newer one rather than rely on the
|
|
123
|
+
* `#!/usr/bin/env bash` shebang resolving to the wrong version.
|
|
124
|
+
*/
|
|
125
|
+
export function resolveBash() {
|
|
126
|
+
// macOS Homebrew paths, then the PATH bash, then common Linux/CI locations.
|
|
127
|
+
for (const cand of ['/opt/homebrew/bin/bash', '/usr/local/bin/bash', 'bash', '/usr/bin/bash', '/bin/bash']) {
|
|
128
|
+
try {
|
|
129
|
+
const r = spawnSync(cand, ['-c', 'echo "${BASH_VERSINFO[0]:-0} ${BASH}"'], { encoding: 'utf8' });
|
|
130
|
+
if (r.status === 0) {
|
|
131
|
+
const [major, path] = r.stdout.trim().split(/\s+/);
|
|
132
|
+
if (parseInt(major, 10) >= 4 && path) return path;
|
|
133
|
+
}
|
|
134
|
+
} catch {
|
|
135
|
+
/* try next candidate */
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* CLI: shiftleft test [run-all flags...]
|
|
143
|
+
* Stages the latest scripts, then runs the packaged orchestrator with all
|
|
144
|
+
* arguments passed through.
|
|
145
|
+
*/
|
|
146
|
+
export function runTests(args = []) {
|
|
147
|
+
const root = findRepoRoot(process.cwd());
|
|
148
|
+
if (!root) {
|
|
149
|
+
logger.error('No postman/ directory found here or above. Run "shiftleft init-postman" first.');
|
|
150
|
+
process.exit(1);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const bash = resolveBash();
|
|
154
|
+
if (!bash) {
|
|
155
|
+
logger.error('shiftleft test requires bash >= 4, but only an older bash was found.');
|
|
156
|
+
logger.error('macOS ships bash 3.2; install a newer one: brew install bash');
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
let impl;
|
|
161
|
+
try {
|
|
162
|
+
stageScripts(root);
|
|
163
|
+
impl = join(root, 'postman/scripts/.run-all-impl.sh');
|
|
164
|
+
} catch (error) {
|
|
165
|
+
logger.error(`Failed to stage scripts: ${error.message}`);
|
|
166
|
+
process.exit(1);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Run via the resolved bash, and put it first on PATH so the sub-scripts
|
|
170
|
+
// the orchestrator calls (their own `#!/usr/bin/env bash`) also use it.
|
|
171
|
+
const result = spawnSync(bash, [impl, ...args], {
|
|
172
|
+
cwd: join(root, 'postman/scripts'),
|
|
173
|
+
stdio: 'inherit',
|
|
174
|
+
env: { ...process.env, PATH: `${dirname(bash)}:${process.env.PATH}` },
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
if (result.error) {
|
|
178
|
+
logger.error(`Failed to run tests: ${result.error.message}`);
|
|
179
|
+
process.exit(1);
|
|
180
|
+
}
|
|
181
|
+
process.exit(result.status ?? 1);
|
|
182
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import inquirer from 'inquirer';
|
|
5
|
+
import { ensureDir, syncTemplate } from '../utils/template.js';
|
|
6
|
+
import { detectStack, inferServiceName, buildContext } from '../utils/stack.js';
|
|
7
|
+
import { loadManifest, createManifest, saveManifest } from '../utils/manifest.js';
|
|
8
|
+
import { ensureGitignoreEntries } from '../utils/gitignore.js';
|
|
9
|
+
import { stageScripts, STAGED_GITIGNORE_ENTRIES, STAGED_GITIGNORE_HEADER } from './run-tests.js';
|
|
10
|
+
import { logger } from '../utils/logger.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Audit what pipeline components exist in the current project.
|
|
14
|
+
*/
|
|
15
|
+
function auditPipeline(cwd) {
|
|
16
|
+
const stack = detectStack(cwd);
|
|
17
|
+
const isJava = stack === 'java';
|
|
18
|
+
const isNode = stack === 'node';
|
|
19
|
+
|
|
20
|
+
const audit = {
|
|
21
|
+
type: stack,
|
|
22
|
+
unitTests: false,
|
|
23
|
+
coverage: false,
|
|
24
|
+
postman: false,
|
|
25
|
+
postmanScripts: false,
|
|
26
|
+
runAllModern: false,
|
|
27
|
+
mutation: false,
|
|
28
|
+
apiCoverage: false,
|
|
29
|
+
qualityReport: false,
|
|
30
|
+
jenkinsfile: false,
|
|
31
|
+
jenkinsfileMutationStage: false,
|
|
32
|
+
jenkinsfileQualityReport: false,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
if (!stack) return audit;
|
|
36
|
+
|
|
37
|
+
if (isJava) {
|
|
38
|
+
audit.unitTests = existsSync(join(cwd, 'src/test/java'));
|
|
39
|
+
const pomContent = readFileSync(join(cwd, 'pom.xml'), 'utf8');
|
|
40
|
+
audit.coverage = pomContent.includes('jacoco');
|
|
41
|
+
audit.mutation = pomContent.includes('pitest-maven');
|
|
42
|
+
} else {
|
|
43
|
+
const pkgContent = JSON.parse(readFileSync(join(cwd, 'package.json'), 'utf8'));
|
|
44
|
+
const scripts = pkgContent.scripts || {};
|
|
45
|
+
audit.unitTests = !!(scripts.test || scripts['unit-tests']);
|
|
46
|
+
audit.coverage = !!(scripts['test:coverage'] || pkgContent.nyc);
|
|
47
|
+
audit.mutation = !!(scripts['mutation-tests'] || scripts['mutation-tests:full']);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const postmanDir = join(cwd, 'postman');
|
|
51
|
+
const scriptsDir = join(postmanDir, 'scripts');
|
|
52
|
+
audit.postman = existsSync(postmanDir);
|
|
53
|
+
audit.postmanScripts = existsSync(scriptsDir);
|
|
54
|
+
|
|
55
|
+
const runAllPath = join(scriptsDir, 'run-all.sh');
|
|
56
|
+
if (existsSync(runAllPath)) {
|
|
57
|
+
const runAllContent = readFileSync(runAllPath, 'utf8');
|
|
58
|
+
// The shiftleft shim delegates to the packaged orchestrator, which is
|
|
59
|
+
// always current — treat it as modern alongside legacy full copies.
|
|
60
|
+
audit.runAllModern =
|
|
61
|
+
runAllContent.includes('shiftleft test') ||
|
|
62
|
+
(runAllContent.includes('--skip-report') &&
|
|
63
|
+
runAllContent.includes('--skip-unit') &&
|
|
64
|
+
runAllContent.includes('--no-delay'));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const coverageScript = isJava
|
|
68
|
+
? join(scriptsDir, 'report-generators/java-api-coverage-matrix.sh')
|
|
69
|
+
: join(scriptsDir, 'report-generators/node-api-coverage-matrix.sh');
|
|
70
|
+
audit.apiCoverage = existsSync(coverageScript);
|
|
71
|
+
|
|
72
|
+
audit.qualityReport =
|
|
73
|
+
existsSync(join(scriptsDir, 'report-generators/stage-report-artifacts.sh')) &&
|
|
74
|
+
existsSync(join(scriptsDir, 'lib', 'report_generator.py'));
|
|
75
|
+
|
|
76
|
+
const jenkinsfilePath = join(cwd, 'Jenkinsfile');
|
|
77
|
+
if (existsSync(jenkinsfilePath)) {
|
|
78
|
+
audit.jenkinsfile = true;
|
|
79
|
+
const jfContent = readFileSync(jenkinsfilePath, 'utf8');
|
|
80
|
+
audit.jenkinsfileMutationStage =
|
|
81
|
+
jfContent.includes('Mutation') || jfContent.includes('mutation');
|
|
82
|
+
audit.jenkinsfileQualityReport =
|
|
83
|
+
jfContent.includes('QualityReport') || jfContent.includes('quality-report');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return audit;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function printAuditReport(audit) {
|
|
90
|
+
const status = (ok, label) => {
|
|
91
|
+
if (ok === 'stale') return ` \x1b[33mSTALE\x1b[0m ${label}`;
|
|
92
|
+
return ok
|
|
93
|
+
? ` \x1b[32mPRESENT\x1b[0m ${label}`
|
|
94
|
+
: ` \x1b[31mMISSING\x1b[0m ${label}`;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
console.log('\n\x1b[1mPipeline Audit Results\x1b[0m');
|
|
98
|
+
console.log('======================');
|
|
99
|
+
if (!audit.type) {
|
|
100
|
+
console.log(' Could not detect project type (no pom.xml or package.json found).');
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
console.log(` Project type: \x1b[1m${audit.type === 'java' ? 'Java / Spring Boot' : 'Node.js / Express'}\x1b[0m\n`);
|
|
104
|
+
console.log(status(audit.unitTests, 'Unit tests'));
|
|
105
|
+
console.log(status(audit.coverage, audit.type === 'java' ? 'JaCoCo coverage' : 'nyc/Istanbul coverage'));
|
|
106
|
+
console.log(status(audit.postman, 'Postman infrastructure (postman/ directory)'));
|
|
107
|
+
if (audit.postman) {
|
|
108
|
+
console.log(status(audit.runAllModern ? true : audit.postmanScripts ? 'stale' : false,
|
|
109
|
+
'run-all.sh (modern — with --skip-*, --env, --no-delay flags)'));
|
|
110
|
+
}
|
|
111
|
+
console.log(status(audit.mutation, audit.type === 'java' ? 'PIT mutation testing' : 'Stryker mutation testing'));
|
|
112
|
+
console.log(status(
|
|
113
|
+
audit.apiCoverage,
|
|
114
|
+
audit.type === 'java'
|
|
115
|
+
? 'report-generators/java-api-coverage-matrix.sh'
|
|
116
|
+
: 'report-generators/node-api-coverage-matrix.sh',
|
|
117
|
+
));
|
|
118
|
+
console.log(status(audit.qualityReport, 'Quality report (stage-report-artifacts.sh + report_generator.py)'));
|
|
119
|
+
console.log(status(audit.jenkinsfile, 'Jenkinsfile'));
|
|
120
|
+
if (audit.jenkinsfile) {
|
|
121
|
+
console.log(status(audit.jenkinsfileMutationStage, ' └─ Mutation Tests stage'));
|
|
122
|
+
console.log(status(audit.jenkinsfileQualityReport, ' └─ Quality report generation'));
|
|
123
|
+
}
|
|
124
|
+
console.log('');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Main setup-pipeline command.
|
|
129
|
+
*/
|
|
130
|
+
export async function setupPipeline(options) {
|
|
131
|
+
const cwd = process.cwd();
|
|
132
|
+
|
|
133
|
+
console.log('\n\x1b[1m\x1b[34mDev-Tools: Test Pipeline Setup\x1b[0m\n');
|
|
134
|
+
|
|
135
|
+
const auditSpinner = ora('Auditing pipeline components...').start();
|
|
136
|
+
const audit = auditPipeline(cwd);
|
|
137
|
+
auditSpinner.succeed('Audit complete');
|
|
138
|
+
|
|
139
|
+
printAuditReport(audit);
|
|
140
|
+
|
|
141
|
+
if (!audit.type) {
|
|
142
|
+
logger.error('Could not detect project type. Run this from your project root (pom.xml or package.json).');
|
|
143
|
+
process.exit(1);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const hasGaps = !audit.unitTests || !audit.coverage || !audit.mutation ||
|
|
147
|
+
!audit.apiCoverage || !audit.qualityReport || !audit.runAllModern ||
|
|
148
|
+
!audit.jenkinsfileMutationStage || !audit.jenkinsfileQualityReport;
|
|
149
|
+
|
|
150
|
+
if (!hasGaps && !options.force) {
|
|
151
|
+
logger.success('Pipeline looks complete! Use --force to re-copy scripts anyway.');
|
|
152
|
+
console.log('\nTo finish setup, invoke: /enhance-test-pipeline\n');
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (!options.yes) {
|
|
157
|
+
const { proceed } = await inquirer.prompt([
|
|
158
|
+
{
|
|
159
|
+
type: 'confirm',
|
|
160
|
+
name: 'proceed',
|
|
161
|
+
message: 'Copy missing/stale scripts from templates? (AI skill still needed for mutation config and Jenkinsfile)',
|
|
162
|
+
default: true,
|
|
163
|
+
},
|
|
164
|
+
]);
|
|
165
|
+
if (!proceed) {
|
|
166
|
+
logger.info('Aborted. No changes made.');
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
ensureDir(join(cwd, 'postman/scripts'));
|
|
172
|
+
|
|
173
|
+
const copySpinner = ora('Setting up pipeline scripts...').start();
|
|
174
|
+
const force = options.force || false;
|
|
175
|
+
const context = buildContext(inferServiceName(cwd), audit.type);
|
|
176
|
+
|
|
177
|
+
const manifest = loadManifest(cwd) || createManifest(audit.type);
|
|
178
|
+
manifest.stack = audit.type;
|
|
179
|
+
|
|
180
|
+
// Committed entrypoint is a thin shim; library scripts are staged from the
|
|
181
|
+
// package (gitignored cache) rather than vendored.
|
|
182
|
+
const shim = syncTemplate('postman/run-all-shim.sh', join(cwd, 'postman/scripts/run-all.sh'), context, {
|
|
183
|
+
manifest, cwd, force, adopt: true,
|
|
184
|
+
});
|
|
185
|
+
ensureGitignoreEntries(join(cwd, 'postman/.gitignore'), STAGED_GITIGNORE_ENTRIES, STAGED_GITIGNORE_HEADER);
|
|
186
|
+
const { staged } = stageScripts(cwd, { stack: audit.type });
|
|
187
|
+
|
|
188
|
+
saveManifest(cwd, manifest);
|
|
189
|
+
|
|
190
|
+
copySpinner.succeed(
|
|
191
|
+
`run-all.sh shim ${shim.status === 'conflict' ? 'CONFLICT (see .shiftleft-new)' : 'in place'}, `
|
|
192
|
+
+ `${staged} library scripts staged from package`
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
console.log('\n\x1b[1mNext Steps\x1b[0m');
|
|
196
|
+
console.log('==========');
|
|
197
|
+
console.log('\nScripts are copied. Use AI skills for config that requires repo inspection:');
|
|
198
|
+
|
|
199
|
+
if (!audit.mutation) {
|
|
200
|
+
console.log(`\n 1. \x1b[33mMutation testing\x1b[0m → /setup-mutation-tests`);
|
|
201
|
+
}
|
|
202
|
+
if (!audit.jenkinsfileMutationStage || !audit.jenkinsfileQualityReport) {
|
|
203
|
+
console.log(`\n 2. \x1b[33mJenkinsfile wiring\x1b[0m → /enhance-test-pipeline`);
|
|
204
|
+
}
|
|
205
|
+
if (audit.type === 'node' && !audit.postman) {
|
|
206
|
+
console.log(`\n 3. \x1b[33mPostman infrastructure\x1b[0m → shiftleft init-postman --stack node`);
|
|
207
|
+
}
|
|
208
|
+
console.log('\n Full audit: /enhance-test-pipeline\n');
|
|
209
|
+
}
|