pr-checkmate 1.23.23 → 2.0.0
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 +68 -15
- package/cspell.json +3 -8
- package/dist/cli.js +37 -62
- package/dist/config/constants.d.ts +21 -3
- package/dist/config/constants.js +33 -6
- package/dist/config/pr-checkmate-workflow.yml +33 -327
- package/dist/core/checks/dependencies/banned-imports.js +23 -24
- package/dist/core/checks/dependencies/circular-deps.js +1 -0
- package/dist/core/checks/dependencies/dependency-check.js +1 -0
- package/dist/core/checks/dependencies/grype-scan.js +1 -0
- package/dist/core/checks/dependencies/npm-audit.js +6 -1
- package/dist/core/checks/dependencies/vuln-scan.js +1 -0
- package/dist/core/checks/findings.d.ts +198 -19
- package/dist/core/checks/findings.js +474 -22
- package/dist/core/checks/git/custom-rules.js +21 -16
- package/dist/core/checks/git/diff-smell.d.ts +17 -0
- package/dist/core/checks/git/diff-smell.js +43 -24
- package/dist/core/checks/git/large-files.js +14 -2
- package/dist/core/checks/git/leftover-debug.js +23 -14
- package/dist/core/checks/git/lockfile-drift.js +2 -1
- package/dist/core/checks/git/merge-conflict.js +19 -8
- package/dist/core/checks/git/missing-tests.js +22 -6
- package/dist/core/checks/git/todo-fixme.js +10 -9
- package/dist/core/checks/languages/shellcheck.d.ts +8 -0
- package/dist/core/checks/languages/shellcheck.js +55 -0
- package/dist/core/checks/languages/typecheck.js +1 -0
- package/dist/core/checks/pr/pr-size-check.js +2 -1
- package/dist/core/checks/quality/config-validation.d.ts +14 -4
- package/dist/core/checks/quality/config-validation.js +119 -17
- package/dist/core/checks/quality/dead-code.js +1 -0
- package/dist/core/checks/quality/duplicate-check.js +1 -0
- package/dist/core/checks/quality/license-header.js +12 -2
- package/dist/core/checks/quality/spellcheck.d.ts +50 -0
- package/dist/core/checks/quality/spellcheck.js +141 -13
- package/dist/core/checks/quality/symlinks.d.ts +16 -0
- package/dist/core/checks/quality/symlinks.js +62 -0
- package/dist/core/checks/quality/yaml-lint.js +22 -9
- package/dist/core/checks/security/diff-security.js +111 -28
- package/dist/core/checks/security/dockerfile-security.js +23 -20
- package/dist/core/checks/security/migration-safety.js +19 -17
- package/dist/core/checks/security/sensitive-file-guard.js +37 -5
- package/dist/core/checks/security/workflow-security.d.ts +10 -0
- package/dist/core/checks/security/workflow-security.js +48 -26
- package/dist/core/commands.d.ts +27 -0
- package/dist/core/commands.js +91 -0
- package/dist/core/context.d.ts +12 -3
- package/dist/core/context.js +118 -16
- package/dist/core/install/cache.d.ts +39 -0
- package/dist/core/install/cache.js +99 -0
- package/dist/core/install/doctor.d.ts +35 -0
- package/dist/core/install/doctor.js +145 -0
- package/dist/core/install/ensure.d.ts +60 -0
- package/dist/core/install/ensure.js +169 -0
- package/dist/core/install/lock.d.ts +33 -0
- package/dist/core/install/lock.js +99 -0
- package/dist/core/install/profiles.d.ts +64 -0
- package/dist/core/install/profiles.js +170 -0
- package/dist/core/registry.js +4 -0
- package/dist/core/reporters.d.ts +13 -2
- package/dist/core/reporters.js +145 -104
- package/dist/core/run-checks.js +91 -14
- package/dist/core/targets.js +26 -4
- package/dist/core/types.d.ts +49 -1
- package/dist/init.d.ts +29 -14
- package/dist/init.js +204 -262
- package/dist/interfaces/index.d.ts +130 -0
- package/dist/scripts/generate-install-manifests.d.ts +39 -0
- package/dist/scripts/generate-install-manifests.js +121 -0
- package/dist/scripts/publish-github.js +18 -4
- package/dist/utils/detect-language.d.ts +37 -4
- package/dist/utils/detect-language.js +113 -19
- package/dist/utils/http.d.ts +59 -0
- package/dist/utils/http.js +119 -0
- package/dist/utils/logger.d.ts +52 -6
- package/dist/utils/logger.js +125 -33
- package/dist/utils/pool.d.ts +49 -0
- package/dist/utils/pool.js +124 -0
- package/dist/utils/update-notifier.d.ts +9 -0
- package/dist/utils/update-notifier.js +57 -3
- package/eslint.config.mjs +14 -2
- package/install-manifests/commitlint/package-lock.json +1028 -0
- package/install-manifests/commitlint/package.json +9 -0
- package/install-manifests/core/package-lock.json +1335 -0
- package/install-manifests/core/package.json +11 -0
- package/install-manifests/cpp/package-lock.json +29 -0
- package/install-manifests/cpp/package.json +8 -0
- package/install-manifests/duplicate/package-lock.json +113 -0
- package/install-manifests/duplicate/package.json +8 -0
- package/install-manifests/js/package-lock.json +5099 -0
- package/install-manifests/js/package.json +26 -0
- package/install-manifests/markdown/package-lock.json +1281 -0
- package/install-manifests/markdown/package.json +8 -0
- package/install-manifests/python/package-lock.json +21 -0
- package/install-manifests/python/package.json +8 -0
- package/install-manifests/spellcheck/package-lock.json +1047 -0
- package/install-manifests/spellcheck/package.json +8 -0
- package/package.json +13 -28
- package/pr-checkmate.schema.json +1070 -96
- package/src/config/cspell.json +3 -8
- package/src/config/pr-checkmate-workflow.yml +33 -327
- package/src/config/pr-checkmate.schema.json +1070 -96
|
@@ -22,10 +22,81 @@ export interface CspellConfig {
|
|
|
22
22
|
dictionaries?: string[];
|
|
23
23
|
enabled?: boolean;
|
|
24
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* Client `reporter` block: where the summary goes and what happens if it cannot
|
|
27
|
+
* get there.
|
|
28
|
+
*
|
|
29
|
+
* Lives here rather than beside the other run types in `core/types.ts` because
|
|
30
|
+
* it is part of the config file's shape, and `core/types.ts` already imports
|
|
31
|
+
* this module — declaring it in both is what let the two copies drift.
|
|
32
|
+
*/
|
|
33
|
+
/**
|
|
34
|
+
* Client `install` block: which tooling is fetched, when, and what a failure to
|
|
35
|
+
* fetch it means.
|
|
36
|
+
*
|
|
37
|
+
* The defaults are chosen so that an existing installation behaves exactly as it
|
|
38
|
+
* did before this block existed. `strategy: "eager"` is the documented way back
|
|
39
|
+
* to the old behaviour without waiting for a release from us.
|
|
40
|
+
*/
|
|
41
|
+
export interface InstallConfig {
|
|
42
|
+
strategy?: 'adaptive' | 'eager' | 'manual';
|
|
43
|
+
profiles?: string[];
|
|
44
|
+
excludeProfiles?: string[];
|
|
45
|
+
minFilesPerLanguage?: number;
|
|
46
|
+
reuseLocal?: 'compatible' | 'always' | 'off';
|
|
47
|
+
onMissingTool?: 'auto' | 'skip' | 'error';
|
|
48
|
+
offline?: boolean;
|
|
49
|
+
waitTimeoutMs?: number;
|
|
50
|
+
installTimeoutMs?: number;
|
|
51
|
+
cacheDir?: string;
|
|
52
|
+
prewarm?: boolean;
|
|
53
|
+
deny?: string[];
|
|
54
|
+
}
|
|
55
|
+
export interface ReporterConfig {
|
|
56
|
+
/**
|
|
57
|
+
* What a failed post means for the run.
|
|
58
|
+
* - `warn` (default) — log it, exit code unchanged.
|
|
59
|
+
* - `error` — fail the run, for teams whose review depends on the comment.
|
|
60
|
+
* - `off` — stay silent.
|
|
61
|
+
*/
|
|
62
|
+
onPostFailure?: 'warn' | 'error' | 'off';
|
|
63
|
+
/**
|
|
64
|
+
* `update` (default) edits the previous summary in place; `append` leaves one
|
|
65
|
+
* comment per run, giving a history in the thread.
|
|
66
|
+
*/
|
|
67
|
+
mode?: 'update' | 'append';
|
|
68
|
+
/** Per-request timeout in ms (default 10000). A hung API must not hold the run open. */
|
|
69
|
+
timeoutMs?: number;
|
|
70
|
+
/** Attempts per request for retryable failures — 5xx, 429, network (default 3). */
|
|
71
|
+
retries?: number;
|
|
72
|
+
/**
|
|
73
|
+
* Pages of existing comments searched for our marker before giving up
|
|
74
|
+
* (default 10, 100 comments per page). Raise it on very long threads.
|
|
75
|
+
*/
|
|
76
|
+
maxPages?: number;
|
|
77
|
+
/** Trim the comment: a table of forty green rows helps nobody. */
|
|
78
|
+
include?: {
|
|
79
|
+
passed?: boolean;
|
|
80
|
+
skipped?: boolean;
|
|
81
|
+
details?: boolean;
|
|
82
|
+
docsLink?: boolean;
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* Post nothing when the run produced no warnings and no failures, so a clean
|
|
86
|
+
* PR does not notify every subscriber.
|
|
87
|
+
*/
|
|
88
|
+
skipWhenClean?: boolean;
|
|
89
|
+
}
|
|
25
90
|
export interface PRCheckMateConfig {
|
|
26
91
|
sourcePath: string | string[];
|
|
92
|
+
excludePaths?: string[];
|
|
27
93
|
ignoreDirs?: string[];
|
|
28
94
|
languages?: Language[];
|
|
95
|
+
detect?: {
|
|
96
|
+
minFiles?: number;
|
|
97
|
+
excludePaths?: string[];
|
|
98
|
+
disable?: Language[];
|
|
99
|
+
};
|
|
29
100
|
customRules?: Array<{
|
|
30
101
|
name: string;
|
|
31
102
|
pattern: string;
|
|
@@ -36,6 +107,28 @@ export interface PRCheckMateConfig {
|
|
|
36
107
|
flags?: string;
|
|
37
108
|
}>;
|
|
38
109
|
severity?: Record<string, 'error' | 'warn' | 'off'>;
|
|
110
|
+
log?: {
|
|
111
|
+
level?: 'error' | 'warn' | 'info' | 'log' | 'debug';
|
|
112
|
+
format?: 'pretty' | 'ci' | 'json';
|
|
113
|
+
color?: 'auto' | 'always' | 'never';
|
|
114
|
+
timestamps?: boolean;
|
|
115
|
+
stderrLevels?: Array<'error' | 'warn' | 'info' | 'log' | 'debug'>;
|
|
116
|
+
sampleLimit?: number;
|
|
117
|
+
};
|
|
118
|
+
install?: InstallConfig;
|
|
119
|
+
reporter?: ReporterConfig;
|
|
120
|
+
run?: {
|
|
121
|
+
reportSkipped?: 'auto' | 'always' | 'never';
|
|
122
|
+
onDegradedScope?: 'warn' | 'error' | 'off';
|
|
123
|
+
concurrency?: number | 'auto' | 'unlimited';
|
|
124
|
+
heavyConcurrency?: number;
|
|
125
|
+
checkTimeoutMs?: number;
|
|
126
|
+
timings?: boolean;
|
|
127
|
+
fullScanBehaviour?: 'scan' | 'skip';
|
|
128
|
+
};
|
|
129
|
+
severityDefaults?: {
|
|
130
|
+
failOn?: 'error' | 'warn' | 'never';
|
|
131
|
+
};
|
|
39
132
|
lint?: {
|
|
40
133
|
rules?: Record<string, any>;
|
|
41
134
|
ignorePatterns?: string[];
|
|
@@ -89,6 +182,7 @@ export interface PRCheckMateConfig {
|
|
|
89
182
|
};
|
|
90
183
|
diffSmell?: {
|
|
91
184
|
enabled?: boolean;
|
|
185
|
+
ignorePaths?: string[];
|
|
92
186
|
};
|
|
93
187
|
prSize?: {
|
|
94
188
|
enabled?: boolean;
|
|
@@ -98,6 +192,7 @@ export interface PRCheckMateConfig {
|
|
|
98
192
|
leftoverDebug?: {
|
|
99
193
|
enabled?: boolean;
|
|
100
194
|
severity?: 'error' | 'warn';
|
|
195
|
+
commentHandling?: 'language' | 'legacy' | 'off';
|
|
101
196
|
};
|
|
102
197
|
todoFixme?: {
|
|
103
198
|
enabled?: boolean;
|
|
@@ -192,6 +287,18 @@ export interface PRCheckMateConfig {
|
|
|
192
287
|
diffSecurity?: {
|
|
193
288
|
enabled?: boolean;
|
|
194
289
|
ignore?: string[];
|
|
290
|
+
failOn?: 'error' | 'warn' | 'never';
|
|
291
|
+
severityOverrides?: Record<string, 'error' | 'warn' | 'off'>;
|
|
292
|
+
ignorePaths?: string[];
|
|
293
|
+
disablePatterns?: string[];
|
|
294
|
+
extraPatterns?: Array<{
|
|
295
|
+
label: string;
|
|
296
|
+
pattern: string;
|
|
297
|
+
severity?: 'error' | 'warn';
|
|
298
|
+
view?: 'raw' | 'code';
|
|
299
|
+
flags?: string;
|
|
300
|
+
}>;
|
|
301
|
+
commentHandling?: 'language' | 'legacy' | 'off';
|
|
195
302
|
};
|
|
196
303
|
mergeConflict?: {
|
|
197
304
|
enabled?: boolean;
|
|
@@ -200,16 +307,35 @@ export interface PRCheckMateConfig {
|
|
|
200
307
|
workflowSecurity?: {
|
|
201
308
|
enabled?: boolean;
|
|
202
309
|
ignore?: string[];
|
|
310
|
+
failOn?: 'error' | 'warn' | 'never';
|
|
311
|
+
severityOverrides?: Record<string, 'error' | 'warn' | 'off'>;
|
|
312
|
+
ignorePaths?: string[];
|
|
203
313
|
};
|
|
204
314
|
dockerfileSecurity?: {
|
|
205
315
|
enabled?: boolean;
|
|
206
316
|
ignore?: string[];
|
|
317
|
+
failOn?: 'error' | 'warn' | 'never';
|
|
318
|
+
severityOverrides?: Record<string, 'error' | 'warn' | 'off'>;
|
|
319
|
+
ignorePaths?: string[];
|
|
207
320
|
};
|
|
208
321
|
caseCollision?: {
|
|
209
322
|
enabled?: boolean;
|
|
210
323
|
};
|
|
211
324
|
configValidation?: {
|
|
212
325
|
enabled?: boolean;
|
|
326
|
+
severity?: 'error' | 'warn';
|
|
327
|
+
validateCheckNames?: boolean;
|
|
328
|
+
allowUnknownKeys?: string[];
|
|
329
|
+
};
|
|
330
|
+
symlinks?: {
|
|
331
|
+
enabled?: boolean;
|
|
332
|
+
severity?: 'error' | 'warn';
|
|
333
|
+
ignore?: string[];
|
|
334
|
+
};
|
|
335
|
+
shellcheck?: {
|
|
336
|
+
enabled?: boolean;
|
|
337
|
+
minSeverity?: 'error' | 'warning' | 'info' | 'style';
|
|
338
|
+
ignore?: string[];
|
|
213
339
|
};
|
|
214
340
|
yamlLint?: {
|
|
215
341
|
enabled?: boolean;
|
|
@@ -250,6 +376,10 @@ export interface PRCheckMateConfig {
|
|
|
250
376
|
};
|
|
251
377
|
sensitiveFileGuard?: {
|
|
252
378
|
enabled?: boolean;
|
|
379
|
+
ignorePaths?: string[];
|
|
380
|
+
ignore?: string[];
|
|
381
|
+
failOn?: 'error' | 'warn' | 'never';
|
|
382
|
+
severityOverrides?: Record<string, 'error' | 'warn' | 'off'>;
|
|
253
383
|
};
|
|
254
384
|
swift?: {
|
|
255
385
|
enabled?: boolean;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build step: turn our own lockfile into one installable manifest per profile.
|
|
3
|
+
*
|
|
4
|
+
* Why the manifests are cut out of `package-lock.json` rather than resolved
|
|
5
|
+
* fresh from the registry:
|
|
6
|
+
*
|
|
7
|
+
* - **What clients fetch is then exactly what we tested.** Resolving at build
|
|
8
|
+
* time would drift the moment a transitive dependency publishes a patch, and
|
|
9
|
+
* the tooling clients run would stop being the tooling that passed our suite.
|
|
10
|
+
* - **The build stays offline.** A release pipeline that needs the network to
|
|
11
|
+
* produce a manifest is a release pipeline that fails when the registry does.
|
|
12
|
+
* - **Integrity comes free.** Every entry carries the hash npm recorded, so the
|
|
13
|
+
* runtime install is a plain `npm ci`, and npm refuses a tampered tarball
|
|
14
|
+
* itself — over the whole transitive tree, not just the packages we name.
|
|
15
|
+
*/
|
|
16
|
+
interface LockEntry {
|
|
17
|
+
version?: string;
|
|
18
|
+
resolved?: string;
|
|
19
|
+
integrity?: string;
|
|
20
|
+
dependencies?: Record<string, string>;
|
|
21
|
+
optionalDependencies?: Record<string, string>;
|
|
22
|
+
peerDependencies?: Record<string, string>;
|
|
23
|
+
peerDependenciesMeta?: Record<string, {
|
|
24
|
+
optional?: boolean;
|
|
25
|
+
}>;
|
|
26
|
+
dev?: boolean;
|
|
27
|
+
link?: boolean;
|
|
28
|
+
[key: string]: unknown;
|
|
29
|
+
}
|
|
30
|
+
export interface Lock {
|
|
31
|
+
lockfileVersion: number;
|
|
32
|
+
packages: Record<string, LockEntry>;
|
|
33
|
+
}
|
|
34
|
+
/** Build the `package.json` + `package-lock.json` pair for one profile. */
|
|
35
|
+
export declare function buildManifest(lock: Lock, profile: string, packages: string[], overrides?: Record<string, unknown>): {
|
|
36
|
+
pkg: object;
|
|
37
|
+
lockfile: object;
|
|
38
|
+
};
|
|
39
|
+
export {};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.buildManifest = buildManifest;
|
|
7
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
const profiles_1 = require("../core/install/profiles");
|
|
10
|
+
/**
|
|
11
|
+
* Find the entry npm would resolve `name` to from inside `fromPath`.
|
|
12
|
+
*
|
|
13
|
+
* This reproduces node's own lookup: nearest `node_modules` first, then upwards
|
|
14
|
+
* to the root. Getting it wrong shows up as a package silently resolved to the
|
|
15
|
+
* wrong copy — `dpdm` bundles its own TypeScript, so "nearest wins" is not
|
|
16
|
+
* theoretical here.
|
|
17
|
+
*/
|
|
18
|
+
function resolveEntry(lock, name, fromPath) {
|
|
19
|
+
const segments = fromPath === '' ? [] : fromPath.split('/');
|
|
20
|
+
for (let depth = segments.length; depth >= 0; depth--) {
|
|
21
|
+
const prefix = segments.slice(0, depth).join('/');
|
|
22
|
+
const candidate = prefix ? `${prefix}/node_modules/${name}` : `node_modules/${name}`;
|
|
23
|
+
if (lock.packages[candidate])
|
|
24
|
+
return candidate;
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
/** Every dependency path reachable from the given top-level packages. */
|
|
29
|
+
function closureOf(lock, roots) {
|
|
30
|
+
const seen = new Set();
|
|
31
|
+
const queue = [];
|
|
32
|
+
for (const name of roots) {
|
|
33
|
+
const entryPath = resolveEntry(lock, name, '');
|
|
34
|
+
if (entryPath)
|
|
35
|
+
queue.push(entryPath);
|
|
36
|
+
}
|
|
37
|
+
while (queue.length > 0) {
|
|
38
|
+
const current = queue.shift();
|
|
39
|
+
if (seen.has(current))
|
|
40
|
+
continue;
|
|
41
|
+
seen.add(current);
|
|
42
|
+
const entry = lock.packages[current];
|
|
43
|
+
if (!entry)
|
|
44
|
+
continue;
|
|
45
|
+
// Optional AND peer dependencies are included, both for the same reason:
|
|
46
|
+
// npm 7+ installs peers automatically, so a peer absent from the manifest is
|
|
47
|
+
// not simply skipped — npm resolves it fresh against the registry, gets the
|
|
48
|
+
// latest version, and then rejects the manifest as out of sync. That is
|
|
49
|
+
// exactly how the commitlint profile failed: `Missing: typescript@7.0.2 from
|
|
50
|
+
// lock file`, a version we never shipped and never tested.
|
|
51
|
+
const names = [
|
|
52
|
+
...Object.keys(entry.dependencies ?? {}),
|
|
53
|
+
...Object.keys(entry.optionalDependencies ?? {}),
|
|
54
|
+
...Object.keys(entry.peerDependencies ?? {}),
|
|
55
|
+
];
|
|
56
|
+
for (const name of names) {
|
|
57
|
+
const resolved = resolveEntry(lock, name, current);
|
|
58
|
+
if (resolved && !seen.has(resolved))
|
|
59
|
+
queue.push(resolved);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return seen;
|
|
63
|
+
}
|
|
64
|
+
/** Build the `package.json` + `package-lock.json` pair for one profile. */
|
|
65
|
+
function buildManifest(lock, profile, packages, overrides) {
|
|
66
|
+
const closure = closureOf(lock, packages);
|
|
67
|
+
const dependencies = {};
|
|
68
|
+
for (const name of packages) {
|
|
69
|
+
const entryPath = resolveEntry(lock, name, '');
|
|
70
|
+
const version = entryPath ? lock.packages[entryPath]?.version : undefined;
|
|
71
|
+
if (!version)
|
|
72
|
+
throw new Error(`profile "${profile}": ${name} is not in package-lock.json`);
|
|
73
|
+
// Pinned, not ranged: a range would let the client resolve a version we
|
|
74
|
+
// never ran a single check against.
|
|
75
|
+
dependencies[name] = version;
|
|
76
|
+
}
|
|
77
|
+
const name = `pr-checkmate-profile-${profile}`;
|
|
78
|
+
// `overrides` are carried over — without them npm re-checks each declared
|
|
79
|
+
// range against the tree and rejects the manifest: `dpdm` asks for
|
|
80
|
+
// `typescript@^5.9.3`, our override points it at the copy we ship, and a
|
|
81
|
+
// manifest that dropped the override fails with `Missing: typescript@5.9.3`.
|
|
82
|
+
//
|
|
83
|
+
// Only the ones that apply, though. These manifests are the list of what a
|
|
84
|
+
// client's machine downloads at run time, so they are meant to be read; an
|
|
85
|
+
// override naming a package this profile never installs is noise in exactly
|
|
86
|
+
// the artefact that has to be scrutable.
|
|
87
|
+
const applicable = Object.fromEntries(Object.entries(overrides ?? {}).filter(([pkg]) => closure.has(resolveEntry(lock, pkg, '') ?? '')));
|
|
88
|
+
const scoped = Object.keys(applicable).length > 0 ? { overrides: applicable } : {};
|
|
89
|
+
const root = { name, version: '1.0.0', dependencies, ...scoped };
|
|
90
|
+
const pkg = { ...root, private: true };
|
|
91
|
+
const packagesOut = { '': root };
|
|
92
|
+
for (const entryPath of [...closure].sort()) {
|
|
93
|
+
const entry = { ...lock.packages[entryPath] };
|
|
94
|
+
// `dev` is about our repository, not about this manifest; leaving it set
|
|
95
|
+
// would make `npm ci --omit=dev` skip tooling the client needs.
|
|
96
|
+
delete entry.dev;
|
|
97
|
+
packagesOut[entryPath] = entry;
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
pkg,
|
|
101
|
+
lockfile: { name, version: '1.0.0', lockfileVersion: 3, requires: true, packages: packagesOut },
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
function main() {
|
|
105
|
+
const root = process.cwd();
|
|
106
|
+
const lock = JSON.parse(node_fs_1.default.readFileSync(node_path_1.default.join(root, 'package-lock.json'), 'utf-8'));
|
|
107
|
+
const manifest = JSON.parse(node_fs_1.default.readFileSync(node_path_1.default.join(root, 'package.json'), 'utf-8'));
|
|
108
|
+
const outDir = node_path_1.default.join(root, 'install-manifests');
|
|
109
|
+
node_fs_1.default.rmSync(outDir, { recursive: true, force: true });
|
|
110
|
+
for (const profile of profiles_1.PROFILES) {
|
|
111
|
+
const { pkg, lockfile } = buildManifest(lock, profile.name, profile.packages, manifest.overrides);
|
|
112
|
+
const dir = node_path_1.default.join(outDir, profile.name);
|
|
113
|
+
node_fs_1.default.mkdirSync(dir, { recursive: true });
|
|
114
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(dir, 'package.json'), `${JSON.stringify(pkg, null, 2)}\n`);
|
|
115
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(dir, 'package-lock.json'), `${JSON.stringify(lockfile, null, 2)}\n`);
|
|
116
|
+
const count = Object.keys(lockfile.packages).length - 1;
|
|
117
|
+
process.stdout.write(`[manifests]: ${profile.name} — ${count} package(s)\n`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (require.main === module)
|
|
121
|
+
main();
|
|
@@ -21,16 +21,30 @@ function publishToGithub() {
|
|
|
21
21
|
delete pkg.scripts?.prepublishOnly;
|
|
22
22
|
fs_1.default.writeFileSync(tempPath, JSON.stringify(pkg, null, 2));
|
|
23
23
|
try {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
// `fs.renameSync` rather than shelling out to `mv`: the paths are built here
|
|
25
|
+
// and are not attacker-controlled, but a template literal inside execSync is
|
|
26
|
+
// a shell invocation regardless, and one that runs holding npm credentials.
|
|
27
|
+
// Found by our own Diff Security once `--full` began reviewing whole files
|
|
28
|
+
// (ticket 067) — the delta mode could never see it, since the file is old.
|
|
29
|
+
fs_1.default.renameSync(pkgPath, backupPath);
|
|
30
|
+
fs_1.default.renameSync(tempPath, pkgPath);
|
|
31
|
+
(0, child_process_1.execSync)('npm publish --registry=https://npm.pkg.github.com', { stdio: 'inherit' });
|
|
27
32
|
utils_1.logger.log('[publishToGithub]: ✅ Published to GitHub Packages');
|
|
28
33
|
}
|
|
29
34
|
catch (err) {
|
|
30
35
|
utils_1.logger.error(`[publishToGithub]: ${err}`);
|
|
31
36
|
}
|
|
32
37
|
finally {
|
|
33
|
-
|
|
38
|
+
// The backup only exists if the first rename succeeded; restoring is
|
|
39
|
+
// best-effort so a failure here cannot mask the original error.
|
|
40
|
+
if (fs_1.default.existsSync(backupPath)) {
|
|
41
|
+
try {
|
|
42
|
+
fs_1.default.renameSync(backupPath, pkgPath);
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
utils_1.logger.error(`[publishToGithub]: could not restore package.json: ${err}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
34
48
|
}
|
|
35
49
|
}
|
|
36
50
|
publishToGithub();
|
|
@@ -1,15 +1,48 @@
|
|
|
1
|
+
/** Client `detect` block: how strictly a language is inferred from the tree. */
|
|
2
|
+
export interface DetectOptions {
|
|
3
|
+
/** Source files needed to count a language as present. A marker file bypasses it. */
|
|
4
|
+
minFiles?: number;
|
|
5
|
+
/** Extra paths excluded from the count, on top of the built-in list. */
|
|
6
|
+
excludePaths?: string[];
|
|
7
|
+
/** Languages never reported, whatever the tree looks like. */
|
|
8
|
+
disable?: Language[];
|
|
9
|
+
}
|
|
1
10
|
export type Language = 'typescript' | 'javascript' | 'python' | 'cpp' | 'swift' | 'kotlin' | 'go' | 'rust' | 'csharp' | 'ruby' | 'php';
|
|
11
|
+
/** One detected language, with the evidence that detected it. */
|
|
12
|
+
export interface LanguageUsage {
|
|
13
|
+
lang: Language;
|
|
14
|
+
/** Tracked source files, excluding generated, vendored and manifest files. */
|
|
15
|
+
files: number;
|
|
16
|
+
/** A marker file declares the language regardless of the file count. */
|
|
17
|
+
marker: boolean;
|
|
18
|
+
}
|
|
2
19
|
/**
|
|
3
|
-
* Detects
|
|
4
|
-
*
|
|
5
|
-
*
|
|
20
|
+
* Detects the languages a repository uses, and *why* each one was detected.
|
|
21
|
+
*
|
|
22
|
+
* Two different decisions read this, and they should not read it the same way:
|
|
23
|
+
*
|
|
24
|
+
* - **Which checks to run.** A marker is enough. A repo scaffolded from a
|
|
25
|
+
* template has `pyproject.toml` and no `.py` yet, and running Python checks on
|
|
26
|
+
* it costs nothing but a skip.
|
|
27
|
+
* - **Which toolchains to download** (ticket 064). A marker is *not* enough — a
|
|
28
|
+
* `Cargo.toml` beside zero `.rs` files would pull a Rust profile on every CI
|
|
29
|
+
* run for a language nobody has written yet. That decision wants `files > 0`.
|
|
30
|
+
*
|
|
31
|
+
* Returning the evidence rather than a bare list is what lets each caller pick.
|
|
6
32
|
*
|
|
7
33
|
* @param root Repository root.
|
|
8
34
|
* @param ignoreDirs Directory names whose contents do not count as the repo's own
|
|
9
35
|
* code. Defaults to the curated list; `buildContext` passes the client's
|
|
10
36
|
* `ignoreDirs` so a repo that vendors dependencies is described by its own
|
|
11
37
|
* sources. Marker files are unaffected — they are only ever read from `root`.
|
|
38
|
+
* @param detect Client overrides for the heuristics.
|
|
12
39
|
* @returns languages ordered by tracked file count (most-used first). `.h` headers
|
|
13
40
|
* are intentionally excluded from the C++ count since they are ambiguous with C.
|
|
14
41
|
*/
|
|
15
|
-
export declare function
|
|
42
|
+
export declare function detectLanguageUsage(root?: string, ignoreDirs?: string[], detect?: DetectOptions): Promise<LanguageUsage[]>;
|
|
43
|
+
/**
|
|
44
|
+
* The languages a repository uses, most-used first — the answer to "which checks
|
|
45
|
+
* should run". See {@link detectLanguageUsage} when the file counts matter, as
|
|
46
|
+
* they do for deciding what to download.
|
|
47
|
+
*/
|
|
48
|
+
export declare function detectLanguages(root?: string, ignoreDirs?: string[], detect?: DetectOptions): Promise<Language[]>;
|
|
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.detectLanguageUsage = detectLanguageUsage;
|
|
6
7
|
exports.detectLanguages = detectLanguages;
|
|
7
8
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
9
|
const node_path_1 = __importDefault(require("node:path"));
|
|
@@ -36,6 +37,35 @@ const MARKER_FILES = {
|
|
|
36
37
|
ruby: ['Gemfile', '.rubocop.yml'],
|
|
37
38
|
php: ['composer.json'],
|
|
38
39
|
};
|
|
40
|
+
/**
|
|
41
|
+
* Paths whose files never count towards a language, on top of `ignoreDirs`.
|
|
42
|
+
*
|
|
43
|
+
* Every entry here comes from a real client repository, not from imagination:
|
|
44
|
+
*
|
|
45
|
+
* - `**\/*UnitTest*\/**` — Gradle/Android writes HTML+JS test reports into
|
|
46
|
+
* `tests/testProdReleaseUnitTest_25.4.11/js/`. One generated `report.js` was
|
|
47
|
+
* enough to make a 302-file Kotlin project detect as JavaScript.
|
|
48
|
+
* - `get-pip.py` — vendored bootstrap script, not the project's Python.
|
|
49
|
+
* - `vendor` / `third_party` — dependencies someone committed; the repo did not
|
|
50
|
+
* write them and should not be judged by them.
|
|
51
|
+
*/
|
|
52
|
+
const DEFAULT_DETECT_EXCLUDES = [
|
|
53
|
+
'**/*UnitTest*/**',
|
|
54
|
+
'**/get-pip.py',
|
|
55
|
+
'**/vendor/**',
|
|
56
|
+
'**/third_party/**',
|
|
57
|
+
];
|
|
58
|
+
/**
|
|
59
|
+
* Extensions that mean a different language depending on where the file sits.
|
|
60
|
+
*
|
|
61
|
+
* `.rs` is Rust almost everywhere — but in an Android project `src/main/rs/`
|
|
62
|
+
* holds RenderScript, and `BrainEyeAndroid/src/main/rs/yuv2rgb.rs` made a mobile
|
|
63
|
+
* SDK detect as a Rust project. Real Rust (`build.rs`, `src/main.rs` in a Tauri
|
|
64
|
+
* app) is unaffected, which is why this is a path rule rather than a count threshold.
|
|
65
|
+
*/
|
|
66
|
+
const EXTENSION_COLLISIONS = [
|
|
67
|
+
{ ext: '.rs', notUnder: /(^|\/)src\/main\/rs\//, lang: 'rust' },
|
|
68
|
+
];
|
|
39
69
|
/**
|
|
40
70
|
* Source extensions per language, used to count tracked files via `git ls-files`.
|
|
41
71
|
*/
|
|
@@ -67,11 +97,39 @@ function hasMarker(root, lang) {
|
|
|
67
97
|
});
|
|
68
98
|
}
|
|
69
99
|
/**
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
100
|
+
* Every marker filename that is also a source file, so it is never counted as
|
|
101
|
+
* one. `Package.swift` is a Swift *manifest*: counting it made a C++ repository
|
|
102
|
+
* report one Swift source it does not have. Same shape for `setup.py` and
|
|
103
|
+
* `conanfile.py`.
|
|
104
|
+
*
|
|
105
|
+
* The marker still detects the language on its own — that path is untouched.
|
|
106
|
+
* What changes is only that a manifest no longer inflates the file count, which
|
|
107
|
+
* is what the install decision in ticket 064 will read.
|
|
73
108
|
*/
|
|
74
|
-
|
|
109
|
+
function markerFilenames() {
|
|
110
|
+
const names = new Set();
|
|
111
|
+
for (const list of Object.values(MARKER_FILES)) {
|
|
112
|
+
for (const name of list) {
|
|
113
|
+
if (!name.includes('*') && node_path_1.default.extname(name))
|
|
114
|
+
names.add(name);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return names;
|
|
118
|
+
}
|
|
119
|
+
/** Which language a file counts towards, or null when it counts towards none. */
|
|
120
|
+
function languageOf(file, ext) {
|
|
121
|
+
for (const collision of EXTENSION_COLLISIONS) {
|
|
122
|
+
if (ext === collision.ext) {
|
|
123
|
+
return collision.notUnder.test(file) ? null : collision.lang;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
for (const lang of Object.keys(EXTENSIONS)) {
|
|
127
|
+
if (EXTENSIONS[lang].includes(ext))
|
|
128
|
+
return lang;
|
|
129
|
+
}
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
async function countTrackedFiles(root, ignoreDirs, excludePaths) {
|
|
75
133
|
const counts = {
|
|
76
134
|
typescript: 0,
|
|
77
135
|
javascript: 0,
|
|
@@ -89,6 +147,7 @@ async function countTrackedFiles(root, ignoreDirs) {
|
|
|
89
147
|
const { stdout } = await (0, execa_1.execa)('git', ['ls-files'], { cwd: root });
|
|
90
148
|
const files = stdout.split('\n').map(f => f.trim());
|
|
91
149
|
const ignored = new Set(ignoreDirs);
|
|
150
|
+
const markers = markerFilenames();
|
|
92
151
|
for (const file of files) {
|
|
93
152
|
if (!file)
|
|
94
153
|
continue;
|
|
@@ -100,13 +159,16 @@ async function countTrackedFiles(root, ignoreDirs) {
|
|
|
100
159
|
// check's file list already applies via `excludeIgnoredDirs`.
|
|
101
160
|
if ((0, glob_1.isUnderIgnoredDir)(file, ignored))
|
|
102
161
|
continue;
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
162
|
+
// Generated reports and vendored bootstrap scripts are not the repo's code.
|
|
163
|
+
if ((0, glob_1.matchesAnyGlob)(file, excludePaths))
|
|
164
|
+
continue;
|
|
165
|
+
// A manifest that happens to be source-shaped (`Package.swift`) declares a
|
|
166
|
+
// language; it is not a line of it.
|
|
167
|
+
if (markers.has(node_path_1.default.basename(file)))
|
|
168
|
+
continue;
|
|
169
|
+
const lang = languageOf(file, node_path_1.default.extname(file).toLowerCase());
|
|
170
|
+
if (lang)
|
|
171
|
+
counts[lang] += 1;
|
|
110
172
|
}
|
|
111
173
|
}
|
|
112
174
|
catch {
|
|
@@ -115,23 +177,55 @@ async function countTrackedFiles(root, ignoreDirs) {
|
|
|
115
177
|
return counts;
|
|
116
178
|
}
|
|
117
179
|
/**
|
|
118
|
-
* Detects
|
|
119
|
-
*
|
|
120
|
-
*
|
|
180
|
+
* Detects the languages a repository uses, and *why* each one was detected.
|
|
181
|
+
*
|
|
182
|
+
* Two different decisions read this, and they should not read it the same way:
|
|
183
|
+
*
|
|
184
|
+
* - **Which checks to run.** A marker is enough. A repo scaffolded from a
|
|
185
|
+
* template has `pyproject.toml` and no `.py` yet, and running Python checks on
|
|
186
|
+
* it costs nothing but a skip.
|
|
187
|
+
* - **Which toolchains to download** (ticket 064). A marker is *not* enough — a
|
|
188
|
+
* `Cargo.toml` beside zero `.rs` files would pull a Rust profile on every CI
|
|
189
|
+
* run for a language nobody has written yet. That decision wants `files > 0`.
|
|
190
|
+
*
|
|
191
|
+
* Returning the evidence rather than a bare list is what lets each caller pick.
|
|
121
192
|
*
|
|
122
193
|
* @param root Repository root.
|
|
123
194
|
* @param ignoreDirs Directory names whose contents do not count as the repo's own
|
|
124
195
|
* code. Defaults to the curated list; `buildContext` passes the client's
|
|
125
196
|
* `ignoreDirs` so a repo that vendors dependencies is described by its own
|
|
126
197
|
* sources. Marker files are unaffected — they are only ever read from `root`.
|
|
198
|
+
* @param detect Client overrides for the heuristics.
|
|
127
199
|
* @returns languages ordered by tracked file count (most-used first). `.h` headers
|
|
128
200
|
* are intentionally excluded from the C++ count since they are ambiguous with C.
|
|
129
201
|
*/
|
|
130
|
-
async function
|
|
131
|
-
const
|
|
132
|
-
const
|
|
133
|
-
|
|
202
|
+
async function detectLanguageUsage(root = process.cwd(), ignoreDirs = constants_1.IGNORED_DIRS, detect = {}) {
|
|
203
|
+
const excludePaths = [...DEFAULT_DETECT_EXCLUDES, ...(detect.excludePaths ?? [])];
|
|
204
|
+
const counts = await countTrackedFiles(root, ignoreDirs, excludePaths);
|
|
205
|
+
// 1 by default: with the causes of the false positives fixed above, a single
|
|
206
|
+
// real source file is a real language. The knob exists for repositories that
|
|
207
|
+
// carry stray one-off scripts they would rather not have checked.
|
|
208
|
+
const minFiles = Math.max(1, detect.minFiles ?? 1);
|
|
209
|
+
const disabled = new Set(detect.disable ?? []);
|
|
210
|
+
const detected = [];
|
|
211
|
+
for (const lang of Object.keys(EXTENSIONS)) {
|
|
212
|
+
if (disabled.has(lang))
|
|
213
|
+
continue;
|
|
214
|
+
const marker = hasMarker(root, lang);
|
|
215
|
+
if (marker || counts[lang] >= minFiles)
|
|
216
|
+
detected.push({ lang, files: counts[lang], marker });
|
|
217
|
+
}
|
|
218
|
+
detected.sort((a, b) => b.files - a.files);
|
|
134
219
|
const countStr = Object.keys(counts).map(l => `${l}=${counts[l]}`).join(', ');
|
|
135
|
-
|
|
220
|
+
const names = detected.map(u => u.lang).join(', ');
|
|
221
|
+
logger_1.logger.info(`[detectLanguages]: Detected ${names || 'none'} (counts: ${countStr})`);
|
|
136
222
|
return detected;
|
|
137
223
|
}
|
|
224
|
+
/**
|
|
225
|
+
* The languages a repository uses, most-used first — the answer to "which checks
|
|
226
|
+
* should run". See {@link detectLanguageUsage} when the file counts matter, as
|
|
227
|
+
* they do for deciding what to download.
|
|
228
|
+
*/
|
|
229
|
+
async function detectLanguages(root = process.cwd(), ignoreDirs = constants_1.IGNORED_DIRS, detect = {}) {
|
|
230
|
+
return (await detectLanguageUsage(root, ignoreDirs, detect)).map(u => u.lang);
|
|
231
|
+
}
|