@rainy-updates/cli 0.5.1-rc.3 → 0.5.2-rc.1
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/CHANGELOG.md +145 -2
- package/README.md +92 -26
- package/dist/bin/cli.js +87 -7
- package/dist/commands/audit/fetcher.d.ts +6 -0
- package/dist/commands/audit/fetcher.js +79 -0
- package/dist/commands/audit/mapper.d.ts +16 -0
- package/dist/commands/audit/mapper.js +61 -0
- package/dist/commands/audit/parser.d.ts +3 -0
- package/dist/commands/audit/parser.js +87 -0
- package/dist/commands/audit/runner.d.ts +7 -0
- package/dist/commands/audit/runner.js +64 -0
- package/dist/commands/bisect/engine.d.ts +12 -0
- package/dist/commands/bisect/engine.js +89 -0
- package/dist/commands/bisect/oracle.d.ts +7 -0
- package/dist/commands/bisect/oracle.js +36 -0
- package/dist/commands/bisect/parser.d.ts +2 -0
- package/dist/commands/bisect/parser.js +73 -0
- package/dist/commands/bisect/runner.d.ts +6 -0
- package/dist/commands/bisect/runner.js +27 -0
- package/dist/commands/health/parser.d.ts +2 -0
- package/dist/commands/health/parser.js +90 -0
- package/dist/commands/health/runner.d.ts +7 -0
- package/dist/commands/health/runner.js +130 -0
- package/dist/commands/licenses/parser.d.ts +2 -0
- package/dist/commands/licenses/parser.js +116 -0
- package/dist/commands/licenses/runner.d.ts +9 -0
- package/dist/commands/licenses/runner.js +163 -0
- package/dist/commands/licenses/sbom.d.ts +10 -0
- package/dist/commands/licenses/sbom.js +70 -0
- package/dist/commands/resolve/graph/builder.d.ts +20 -0
- package/dist/commands/resolve/graph/builder.js +183 -0
- package/dist/commands/resolve/graph/conflict.d.ts +20 -0
- package/dist/commands/resolve/graph/conflict.js +52 -0
- package/dist/commands/resolve/graph/resolver.d.ts +17 -0
- package/dist/commands/resolve/graph/resolver.js +71 -0
- package/dist/commands/resolve/parser.d.ts +2 -0
- package/dist/commands/resolve/parser.js +89 -0
- package/dist/commands/resolve/runner.d.ts +13 -0
- package/dist/commands/resolve/runner.js +136 -0
- package/dist/commands/snapshot/parser.d.ts +2 -0
- package/dist/commands/snapshot/parser.js +80 -0
- package/dist/commands/snapshot/runner.d.ts +11 -0
- package/dist/commands/snapshot/runner.js +115 -0
- package/dist/commands/snapshot/store.d.ts +35 -0
- package/dist/commands/snapshot/store.js +158 -0
- package/dist/commands/unused/matcher.d.ts +22 -0
- package/dist/commands/unused/matcher.js +95 -0
- package/dist/commands/unused/parser.d.ts +2 -0
- package/dist/commands/unused/parser.js +95 -0
- package/dist/commands/unused/runner.d.ts +11 -0
- package/dist/commands/unused/runner.js +113 -0
- package/dist/commands/unused/scanner.d.ts +18 -0
- package/dist/commands/unused/scanner.js +129 -0
- package/dist/config/loader.d.ts +5 -1
- package/dist/config/policy.d.ts +4 -0
- package/dist/config/policy.js +2 -0
- package/dist/core/check.js +56 -3
- package/dist/core/fix-pr-batch.js +3 -2
- package/dist/core/fix-pr.js +19 -4
- package/dist/core/impact.d.ts +36 -0
- package/dist/core/impact.js +82 -0
- package/dist/core/init-ci.js +3 -3
- package/dist/core/options.d.ts +22 -1
- package/dist/core/options.js +151 -13
- package/dist/core/summary.d.ts +1 -0
- package/dist/core/summary.js +11 -1
- package/dist/core/upgrade.js +10 -0
- package/dist/core/warm-cache.js +19 -1
- package/dist/output/format.js +4 -0
- package/dist/output/github.js +3 -0
- package/dist/registry/npm.d.ts +9 -2
- package/dist/registry/npm.js +87 -17
- package/dist/types/index.d.ts +236 -0
- package/dist/utils/lockfile.d.ts +5 -0
- package/dist/utils/lockfile.js +44 -0
- package/dist/utils/semver.d.ts +18 -0
- package/dist/utils/semver.js +88 -3
- package/package.json +13 -4
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
/**
|
|
4
|
+
* Extracts all imported package names from a single source file.
|
|
5
|
+
*
|
|
6
|
+
* Handles:
|
|
7
|
+
* - ESM static: import ... from "pkg"
|
|
8
|
+
* - ESM dynamic: import("pkg")
|
|
9
|
+
* - CJS: require("pkg")
|
|
10
|
+
*
|
|
11
|
+
* Strips subpath imports (e.g. "lodash/merge" → "lodash"),
|
|
12
|
+
* skips relative imports and node: builtins.
|
|
13
|
+
*/
|
|
14
|
+
export function extractImportsFromSource(source) {
|
|
15
|
+
const names = new Set();
|
|
16
|
+
// ESM static import: from "pkg" or from 'pkg'
|
|
17
|
+
const staticImport = /from\s+['"]([^'"]+)['"]/g;
|
|
18
|
+
for (const match of source.matchAll(staticImport)) {
|
|
19
|
+
addPackageName(names, match[1]);
|
|
20
|
+
}
|
|
21
|
+
// ESM dynamic import: import("pkg") or import('pkg')
|
|
22
|
+
const dynamicImport = /\bimport\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
23
|
+
for (const match of source.matchAll(dynamicImport)) {
|
|
24
|
+
addPackageName(names, match[1]);
|
|
25
|
+
}
|
|
26
|
+
// CJS require: require("pkg") or require('pkg')
|
|
27
|
+
const cjsRequire = /\brequire\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
28
|
+
for (const match of source.matchAll(cjsRequire)) {
|
|
29
|
+
addPackageName(names, match[1]);
|
|
30
|
+
}
|
|
31
|
+
// export ... from "pkg"
|
|
32
|
+
const reExport = /\bexport\s+(?:\*|\{[^}]*\})\s+from\s+['"]([^'"]+)['"]/g;
|
|
33
|
+
for (const match of source.matchAll(reExport)) {
|
|
34
|
+
addPackageName(names, match[1]);
|
|
35
|
+
}
|
|
36
|
+
return names;
|
|
37
|
+
}
|
|
38
|
+
function addPackageName(names, specifier) {
|
|
39
|
+
// Skip relative imports, node: builtins, and bare file paths
|
|
40
|
+
if (specifier.startsWith(".") || specifier.startsWith("/"))
|
|
41
|
+
return;
|
|
42
|
+
if (specifier.startsWith("node:"))
|
|
43
|
+
return;
|
|
44
|
+
if (specifier.startsWith("bun:"))
|
|
45
|
+
return;
|
|
46
|
+
// Normalize package name (strip subpath): "lodash/merge" → "lodash"
|
|
47
|
+
// "@scope/pkg/subpath" → "@scope/pkg"
|
|
48
|
+
const name = extractPackageName(specifier);
|
|
49
|
+
if (name)
|
|
50
|
+
names.add(name);
|
|
51
|
+
}
|
|
52
|
+
export function extractPackageName(specifier) {
|
|
53
|
+
if (!specifier)
|
|
54
|
+
return null;
|
|
55
|
+
if (specifier.startsWith("@")) {
|
|
56
|
+
// Scoped: @scope/pkg or @scope/pkg/subpath
|
|
57
|
+
const parts = specifier.split("/");
|
|
58
|
+
if (parts.length < 2)
|
|
59
|
+
return null;
|
|
60
|
+
return `${parts[0]}/${parts[1]}`;
|
|
61
|
+
}
|
|
62
|
+
// Unscoped: pkg or pkg/subpath
|
|
63
|
+
return specifier.split("/")[0] ?? null;
|
|
64
|
+
}
|
|
65
|
+
const SOURCE_EXTENSIONS = new Set([
|
|
66
|
+
".ts",
|
|
67
|
+
".tsx",
|
|
68
|
+
".js",
|
|
69
|
+
".jsx",
|
|
70
|
+
".mjs",
|
|
71
|
+
".cjs",
|
|
72
|
+
".mts",
|
|
73
|
+
".cts",
|
|
74
|
+
]);
|
|
75
|
+
const IGNORED_DIRS = new Set([
|
|
76
|
+
"node_modules",
|
|
77
|
+
".git",
|
|
78
|
+
"dist",
|
|
79
|
+
"build",
|
|
80
|
+
"out",
|
|
81
|
+
".next",
|
|
82
|
+
".nuxt",
|
|
83
|
+
]);
|
|
84
|
+
/**
|
|
85
|
+
* Recursively scans a directory and returns all imported package names
|
|
86
|
+
* found across all source files.
|
|
87
|
+
*/
|
|
88
|
+
export async function scanDirectory(dir) {
|
|
89
|
+
const allImports = new Set();
|
|
90
|
+
await walkDirectory(dir, allImports);
|
|
91
|
+
return allImports;
|
|
92
|
+
}
|
|
93
|
+
async function walkDirectory(dir, collector) {
|
|
94
|
+
let entries;
|
|
95
|
+
try {
|
|
96
|
+
entries = await fs.readdir(dir);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const tasks = [];
|
|
102
|
+
for (const entryName of entries) {
|
|
103
|
+
if (IGNORED_DIRS.has(entryName))
|
|
104
|
+
continue;
|
|
105
|
+
const fullPath = path.join(dir, entryName);
|
|
106
|
+
tasks.push(fs
|
|
107
|
+
.stat(fullPath)
|
|
108
|
+
.then((stat) => {
|
|
109
|
+
if (stat.isDirectory()) {
|
|
110
|
+
return walkDirectory(fullPath, collector);
|
|
111
|
+
}
|
|
112
|
+
if (stat.isFile()) {
|
|
113
|
+
const ext = path.extname(entryName).toLowerCase();
|
|
114
|
+
if (!SOURCE_EXTENSIONS.has(ext))
|
|
115
|
+
return;
|
|
116
|
+
return fs
|
|
117
|
+
.readFile(fullPath, "utf8")
|
|
118
|
+
.then((source) => {
|
|
119
|
+
for (const name of extractImportsFromSource(source)) {
|
|
120
|
+
collector.add(name);
|
|
121
|
+
}
|
|
122
|
+
})
|
|
123
|
+
.catch(() => undefined);
|
|
124
|
+
}
|
|
125
|
+
})
|
|
126
|
+
.catch(() => undefined));
|
|
127
|
+
}
|
|
128
|
+
await Promise.all(tasks);
|
|
129
|
+
}
|
package/dist/config/loader.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CiProfile, DependencyKind, FailOnLevel, GroupBy, LogLevel, OutputFormat, TargetLevel } from "../types/index.js";
|
|
1
|
+
import type { CiProfile, DependencyKind, FailOnLevel, GroupBy, LockfileMode, LogLevel, OutputFormat, TargetLevel } from "../types/index.js";
|
|
2
2
|
export interface FileConfig {
|
|
3
3
|
target?: TargetLevel;
|
|
4
4
|
filter?: string;
|
|
@@ -12,7 +12,10 @@ export interface FileConfig {
|
|
|
12
12
|
githubOutputFile?: string;
|
|
13
13
|
sarifFile?: string;
|
|
14
14
|
concurrency?: number;
|
|
15
|
+
registryTimeoutMs?: number;
|
|
16
|
+
registryRetries?: number;
|
|
15
17
|
offline?: boolean;
|
|
18
|
+
stream?: boolean;
|
|
16
19
|
policyFile?: string;
|
|
17
20
|
prReportFile?: string;
|
|
18
21
|
failOn?: FailOnLevel;
|
|
@@ -31,6 +34,7 @@ export interface FileConfig {
|
|
|
31
34
|
prLimit?: number;
|
|
32
35
|
onlyChanged?: boolean;
|
|
33
36
|
ciProfile?: CiProfile;
|
|
37
|
+
lockfileMode?: LockfileMode;
|
|
34
38
|
install?: boolean;
|
|
35
39
|
packageManager?: "auto" | "npm" | "pnpm";
|
|
36
40
|
sync?: boolean;
|
package/dist/config/policy.d.ts
CHANGED
|
@@ -11,6 +11,8 @@ export interface PolicyConfig {
|
|
|
11
11
|
allowPrerelease?: boolean;
|
|
12
12
|
group?: string;
|
|
13
13
|
priority?: number;
|
|
14
|
+
target?: TargetLevel;
|
|
15
|
+
autofix?: boolean;
|
|
14
16
|
}>;
|
|
15
17
|
}
|
|
16
18
|
export interface PolicyRule {
|
|
@@ -22,6 +24,8 @@ export interface PolicyRule {
|
|
|
22
24
|
allowPrerelease?: boolean;
|
|
23
25
|
group?: string;
|
|
24
26
|
priority?: number;
|
|
27
|
+
target?: TargetLevel;
|
|
28
|
+
autofix?: boolean;
|
|
25
29
|
}
|
|
26
30
|
export interface ResolvedPolicy {
|
|
27
31
|
ignorePatterns: string[];
|
package/dist/config/policy.js
CHANGED
|
@@ -46,6 +46,8 @@ function normalizeRule(rule) {
|
|
|
46
46
|
allowPrerelease: rule.allowPrerelease === true,
|
|
47
47
|
group: typeof rule.group === "string" && rule.group.trim().length > 0 ? rule.group.trim() : undefined,
|
|
48
48
|
priority: asNonNegativeInt(rule.priority),
|
|
49
|
+
target: rule.target,
|
|
50
|
+
autofix: rule.autofix !== false,
|
|
49
51
|
};
|
|
50
52
|
}
|
|
51
53
|
function asNonNegativeInt(value) {
|
package/dist/core/check.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
+
import process from "node:process";
|
|
2
3
|
import { collectDependencies, readManifest } from "../parsers/package-json.js";
|
|
3
4
|
import { matchesPattern } from "../utils/pattern.js";
|
|
4
5
|
import { applyRangeStyle, classifyDiff, clampTarget, pickTargetVersionFromAvailable } from "../utils/semver.js";
|
|
@@ -18,7 +19,10 @@ export async function check(options) {
|
|
|
18
19
|
const packageDirs = await discoverPackageDirs(options.cwd, options.workspace);
|
|
19
20
|
discoveryMs += Date.now() - discoveryStartedAt;
|
|
20
21
|
const cache = await VersionCache.create();
|
|
21
|
-
const registryClient = new NpmRegistryClient(options.cwd
|
|
22
|
+
const registryClient = new NpmRegistryClient(options.cwd, {
|
|
23
|
+
timeoutMs: options.registryTimeoutMs,
|
|
24
|
+
retries: options.registryRetries,
|
|
25
|
+
});
|
|
22
26
|
const policy = await loadPolicy(options.cwd, options.policyFile);
|
|
23
27
|
const updates = [];
|
|
24
28
|
const errors = [];
|
|
@@ -30,6 +34,14 @@ export async function check(options) {
|
|
|
30
34
|
const tasks = [];
|
|
31
35
|
let skipped = 0;
|
|
32
36
|
let cooldownSkipped = 0;
|
|
37
|
+
let streamedEvents = 0;
|
|
38
|
+
let policyOverridesApplied = 0;
|
|
39
|
+
const emitStream = (message) => {
|
|
40
|
+
if (!options.stream)
|
|
41
|
+
return;
|
|
42
|
+
streamedEvents += 1;
|
|
43
|
+
process.stdout.write(`${message}\n`);
|
|
44
|
+
};
|
|
33
45
|
for (const packageDir of packageDirs) {
|
|
34
46
|
let manifest;
|
|
35
47
|
try {
|
|
@@ -49,10 +61,14 @@ export async function check(options) {
|
|
|
49
61
|
const rule = resolvePolicyRule(dep.name, policy);
|
|
50
62
|
if (rule?.ignore === true) {
|
|
51
63
|
skipped += 1;
|
|
64
|
+
policyOverridesApplied += 1;
|
|
65
|
+
emitStream(`[policy-ignore] ${dep.name}`);
|
|
52
66
|
continue;
|
|
53
67
|
}
|
|
54
68
|
if (policy.ignorePatterns.some((pattern) => matchesPattern(dep.name, pattern))) {
|
|
55
69
|
skipped += 1;
|
|
70
|
+
policyOverridesApplied += 1;
|
|
71
|
+
emitStream(`[policy-ignore-pattern] ${dep.name}`);
|
|
56
72
|
continue;
|
|
57
73
|
}
|
|
58
74
|
tasks.push({ packageDir, dependency: dep });
|
|
@@ -99,6 +115,8 @@ export async function check(options) {
|
|
|
99
115
|
const registryStartedAt = Date.now();
|
|
100
116
|
const fetched = await registryClient.resolveManyPackageMetadata(unresolvedPackages, {
|
|
101
117
|
concurrency: options.concurrency,
|
|
118
|
+
retries: options.registryRetries,
|
|
119
|
+
timeoutMs: options.registryTimeoutMs,
|
|
102
120
|
});
|
|
103
121
|
registryMs += Date.now() - registryStartedAt;
|
|
104
122
|
const cacheWriteStartedAt = Date.now();
|
|
@@ -126,6 +144,7 @@ export async function check(options) {
|
|
|
126
144
|
}
|
|
127
145
|
else {
|
|
128
146
|
errors.push(`Unable to resolve ${packageName}: ${error}`);
|
|
147
|
+
emitStream(`[error] Unable to resolve ${packageName}: ${error}`);
|
|
129
148
|
}
|
|
130
149
|
}
|
|
131
150
|
cacheMs += Date.now() - cacheStaleStartedAt;
|
|
@@ -136,12 +155,17 @@ export async function check(options) {
|
|
|
136
155
|
if (!metadata?.latestVersion)
|
|
137
156
|
continue;
|
|
138
157
|
const rule = resolvePolicyRule(task.dependency.name, policy);
|
|
139
|
-
const
|
|
158
|
+
const baseTarget = rule?.target ?? options.target;
|
|
159
|
+
const effectiveTarget = clampTarget(baseTarget, rule?.maxTarget);
|
|
160
|
+
if (rule?.target || rule?.maxTarget || rule?.autofix === false) {
|
|
161
|
+
policyOverridesApplied += 1;
|
|
162
|
+
}
|
|
140
163
|
const picked = pickTargetVersionFromAvailable(task.dependency.range, metadata.availableVersions, metadata.latestVersion, effectiveTarget);
|
|
141
164
|
if (!picked)
|
|
142
165
|
continue;
|
|
143
166
|
if (shouldSkipByCooldown(picked, metadata.publishedAtByVersion, options.cooldownDays, policy.cooldownDays, rule?.cooldownDays)) {
|
|
144
167
|
cooldownSkipped += 1;
|
|
168
|
+
emitStream(`[cooldown-skip] ${task.dependency.name}@${picked}`);
|
|
145
169
|
continue;
|
|
146
170
|
}
|
|
147
171
|
const nextRange = applyRangeStyle(task.dependency.range, picked);
|
|
@@ -156,14 +180,17 @@ export async function check(options) {
|
|
|
156
180
|
toVersionResolved: picked,
|
|
157
181
|
diffType: classifyDiff(task.dependency.range, picked),
|
|
158
182
|
filtered: false,
|
|
183
|
+
autofix: rule?.autofix !== false,
|
|
159
184
|
reason: rule?.maxTarget ? `policy maxTarget=${rule.maxTarget}` : undefined,
|
|
160
185
|
});
|
|
186
|
+
emitStream(`[update] ${task.dependency.name} ${task.dependency.range} -> ${nextRange} (${classifyDiff(task.dependency.range, picked)})`);
|
|
161
187
|
}
|
|
162
188
|
const grouped = groupUpdates(updates, options.groupBy);
|
|
163
189
|
const groupedUpdates = grouped.length;
|
|
164
190
|
const groupedSorted = sortUpdates(grouped.flatMap((group) => group.items));
|
|
165
191
|
const groupedCapped = typeof options.groupMax === "number" ? groupedSorted.slice(0, options.groupMax) : groupedSorted;
|
|
166
|
-
const
|
|
192
|
+
const prioritized = applyPolicyPrioritySort(groupedCapped, policy);
|
|
193
|
+
const ruleLimited = applyRuleUpdateCaps(prioritized, policy);
|
|
167
194
|
const prLimited = typeof options.prLimit === "number" ? ruleLimited.slice(0, options.prLimit) : ruleLimited;
|
|
168
195
|
const limitedUpdates = sortUpdates(prLimited);
|
|
169
196
|
const prLimitHit = typeof options.prLimit === "number" && groupedSorted.length > options.prLimit;
|
|
@@ -189,7 +216,9 @@ export async function check(options) {
|
|
|
189
216
|
cooldownSkipped,
|
|
190
217
|
ciProfile: options.ciProfile,
|
|
191
218
|
prLimitHit,
|
|
219
|
+
policyOverridesApplied,
|
|
192
220
|
}));
|
|
221
|
+
summary.streamedEvents = streamedEvents;
|
|
193
222
|
return {
|
|
194
223
|
projectPath: options.cwd,
|
|
195
224
|
packagePaths: packageDirs,
|
|
@@ -266,6 +295,30 @@ function applyRuleUpdateCaps(updates, policy) {
|
|
|
266
295
|
}
|
|
267
296
|
return limited;
|
|
268
297
|
}
|
|
298
|
+
function applyPolicyPrioritySort(updates, policy) {
|
|
299
|
+
return [...updates].sort((left, right) => {
|
|
300
|
+
const leftPriority = resolvePolicyRule(left.name, policy)?.priority ?? 0;
|
|
301
|
+
const rightPriority = resolvePolicyRule(right.name, policy)?.priority ?? 0;
|
|
302
|
+
if (leftPriority !== rightPriority)
|
|
303
|
+
return rightPriority - leftPriority;
|
|
304
|
+
const byRisk = riskRank(left.diffType) - riskRank(right.diffType);
|
|
305
|
+
if (byRisk !== 0)
|
|
306
|
+
return byRisk;
|
|
307
|
+
const byName = left.name.localeCompare(right.name);
|
|
308
|
+
if (byName !== 0)
|
|
309
|
+
return byName;
|
|
310
|
+
return left.packagePath.localeCompare(right.packagePath);
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
function riskRank(value) {
|
|
314
|
+
if (value === "patch")
|
|
315
|
+
return 0;
|
|
316
|
+
if (value === "minor")
|
|
317
|
+
return 1;
|
|
318
|
+
if (value === "major")
|
|
319
|
+
return 2;
|
|
320
|
+
return 3;
|
|
321
|
+
}
|
|
269
322
|
function sortUpdates(updates) {
|
|
270
323
|
return [...updates].sort((left, right) => {
|
|
271
324
|
const byPath = left.packagePath.localeCompare(right.packagePath);
|
|
@@ -4,11 +4,12 @@ import os from "node:os";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { readManifest, writeManifest } from "../parsers/package-json.js";
|
|
6
6
|
export async function applyFixPrBatches(options, result) {
|
|
7
|
-
|
|
7
|
+
const autofixUpdates = result.updates.filter((update) => update.autofix !== false);
|
|
8
|
+
if (!options.fixPr || autofixUpdates.length === 0) {
|
|
8
9
|
return { applied: false, branches: [], commits: [] };
|
|
9
10
|
}
|
|
10
11
|
const baseRef = await resolveBaseRef(options.cwd, options.fixPrNoCheckout);
|
|
11
|
-
const groups = groupUpdates(
|
|
12
|
+
const groups = groupUpdates(autofixUpdates, options.groupBy);
|
|
12
13
|
const plans = planFixPrBatches(groups, options.fixBranch ?? "chore/rainy-updates", options.fixPrBatchSize ?? 1);
|
|
13
14
|
if (options.fixDryRun) {
|
|
14
15
|
return { applied: false, branches: plans.map((plan) => plan.branchName), commits: [] };
|
package/dist/core/fix-pr.js
CHANGED
|
@@ -3,7 +3,8 @@ import path from "node:path";
|
|
|
3
3
|
export async function applyFixPr(options, result, extraFiles) {
|
|
4
4
|
if (!options.fixPr)
|
|
5
5
|
return { applied: false };
|
|
6
|
-
|
|
6
|
+
const autofixUpdates = result.updates.filter((update) => update.autofix !== false);
|
|
7
|
+
if (autofixUpdates.length === 0) {
|
|
7
8
|
return {
|
|
8
9
|
applied: false,
|
|
9
10
|
branchName: options.fixBranch ?? "chore/rainy-updates",
|
|
@@ -35,8 +36,11 @@ export async function applyFixPr(options, result, extraFiles) {
|
|
|
35
36
|
commitSha: "",
|
|
36
37
|
};
|
|
37
38
|
}
|
|
38
|
-
const manifestFiles = Array.from(new Set(
|
|
39
|
-
const
|
|
39
|
+
const manifestFiles = Array.from(new Set(autofixUpdates.map((update) => path.resolve(update.packagePath, "package.json"))));
|
|
40
|
+
const lockfileFiles = options.lockfileMode === "update"
|
|
41
|
+
? (await collectChangedLockfiles(options.cwd)).map((entry) => path.resolve(options.cwd, entry))
|
|
42
|
+
: [];
|
|
43
|
+
const filesToStage = Array.from(new Set([...manifestFiles, ...extraFiles, ...lockfileFiles]
|
|
40
44
|
.map((entry) => path.resolve(options.cwd, entry))
|
|
41
45
|
.filter((entry) => entry.startsWith(path.resolve(options.cwd) + path.sep) || entry === path.resolve(options.cwd)))).sort((a, b) => a.localeCompare(b));
|
|
42
46
|
if (filesToStage.length > 0) {
|
|
@@ -50,7 +54,7 @@ export async function applyFixPr(options, result, extraFiles) {
|
|
|
50
54
|
commitSha: "",
|
|
51
55
|
};
|
|
52
56
|
}
|
|
53
|
-
const message = options.fixCommitMessage ?? `chore(deps): apply rainy-updates (${
|
|
57
|
+
const message = options.fixCommitMessage ?? `chore(deps): apply rainy-updates (${autofixUpdates.length} updates)`;
|
|
54
58
|
await runGit(options.cwd, ["commit", "-m", message]);
|
|
55
59
|
const rev = await runGit(options.cwd, ["rev-parse", "HEAD"]);
|
|
56
60
|
return {
|
|
@@ -81,3 +85,14 @@ async function runGit(cwd, args, allowNonZero = false) {
|
|
|
81
85
|
});
|
|
82
86
|
});
|
|
83
87
|
}
|
|
88
|
+
async function collectChangedLockfiles(cwd) {
|
|
89
|
+
const status = await runGit(cwd, ["status", "--porcelain"], true);
|
|
90
|
+
const allowed = new Set(["package-lock.json", "npm-shrinkwrap.json", "pnpm-lock.yaml", "yarn.lock", "bun.lock"]);
|
|
91
|
+
const changed = status.stdout
|
|
92
|
+
.split(/\r?\n/)
|
|
93
|
+
.map((line) => line.trim())
|
|
94
|
+
.filter(Boolean)
|
|
95
|
+
.map((line) => line.slice(3).trim())
|
|
96
|
+
.filter((entry) => allowed.has(path.basename(entry)));
|
|
97
|
+
return Array.from(new Set(changed)).sort((a, b) => a.localeCompare(b));
|
|
98
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { ImpactScore, PackageUpdate } from "../types/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* Context fed into the impact scorer from the outer check/upgrade pipeline.
|
|
4
|
+
* All fields are optional — missing data degrades gracefully.
|
|
5
|
+
*/
|
|
6
|
+
export interface ImpactContext {
|
|
7
|
+
/** Names of packages that have a known CVE advisory (from `rup audit` cache). */
|
|
8
|
+
advisoryPackages?: ReadonlySet<string>;
|
|
9
|
+
/** How many workspace packages depend on this package (reverse-dep count). */
|
|
10
|
+
workspaceDependentCount?: (name: string) => number;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Compute an `ImpactScore` for a single pending package update.
|
|
14
|
+
*
|
|
15
|
+
* Algorithm:
|
|
16
|
+
* score = diffWeight
|
|
17
|
+
* + (advisoryBonus if CVE exists)
|
|
18
|
+
* + min(workspaceCount × perPkg, cap)
|
|
19
|
+
*
|
|
20
|
+
* Clamped to [0, 100]. Rank thresholds:
|
|
21
|
+
* ≥ 70 → critical
|
|
22
|
+
* ≥ 45 → high
|
|
23
|
+
* ≥ 20 → medium
|
|
24
|
+
* < 20 → low
|
|
25
|
+
*/
|
|
26
|
+
export declare function computeImpactScore(update: PackageUpdate, context?: ImpactContext): ImpactScore;
|
|
27
|
+
/**
|
|
28
|
+
* Batch-compute impact scores for all updates, returning a new array with
|
|
29
|
+
* `impactScore` populated on each entry. Non-mutating.
|
|
30
|
+
*/
|
|
31
|
+
export declare function applyImpactScores(updates: PackageUpdate[], context?: ImpactContext): PackageUpdate[];
|
|
32
|
+
/**
|
|
33
|
+
* Returns the ANSI color badge string for terminal output.
|
|
34
|
+
* Used by the table renderer when --show-impact is active.
|
|
35
|
+
*/
|
|
36
|
+
export declare function impactBadge(score: ImpactScore): string;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/** Weights for each semver diff type. */
|
|
2
|
+
const DIFF_WEIGHT = {
|
|
3
|
+
patch: 10,
|
|
4
|
+
minor: 25,
|
|
5
|
+
major: 55,
|
|
6
|
+
latest: 30,
|
|
7
|
+
};
|
|
8
|
+
/** Advisory presence bonus — lifts total score significantly. */
|
|
9
|
+
const ADVISORY_BONUS = 35;
|
|
10
|
+
/** Points added per additional workspace package that uses this dep. */
|
|
11
|
+
const WORKSPACE_SPREAD_PER_PKG = 5;
|
|
12
|
+
/** Maximum workspace spread contribution (cap). */
|
|
13
|
+
const WORKSPACE_SPREAD_CAP = 20;
|
|
14
|
+
/**
|
|
15
|
+
* Compute an `ImpactScore` for a single pending package update.
|
|
16
|
+
*
|
|
17
|
+
* Algorithm:
|
|
18
|
+
* score = diffWeight
|
|
19
|
+
* + (advisoryBonus if CVE exists)
|
|
20
|
+
* + min(workspaceCount × perPkg, cap)
|
|
21
|
+
*
|
|
22
|
+
* Clamped to [0, 100]. Rank thresholds:
|
|
23
|
+
* ≥ 70 → critical
|
|
24
|
+
* ≥ 45 → high
|
|
25
|
+
* ≥ 20 → medium
|
|
26
|
+
* < 20 → low
|
|
27
|
+
*/
|
|
28
|
+
export function computeImpactScore(update, context = {}) {
|
|
29
|
+
const diffTypeWeight = DIFF_WEIGHT[update.diffType] ?? DIFF_WEIGHT.latest;
|
|
30
|
+
const hasAdvisory = context.advisoryPackages?.has(update.name) ?? false;
|
|
31
|
+
const rawWorkspaceCount = context.workspaceDependentCount?.(update.name) ?? 0;
|
|
32
|
+
const affectedWorkspaceCount = Math.max(0, rawWorkspaceCount);
|
|
33
|
+
const advisoryPoints = hasAdvisory ? ADVISORY_BONUS : 0;
|
|
34
|
+
const workspacePoints = Math.min(affectedWorkspaceCount * WORKSPACE_SPREAD_PER_PKG, WORKSPACE_SPREAD_CAP);
|
|
35
|
+
const rawScore = diffTypeWeight + advisoryPoints + workspacePoints;
|
|
36
|
+
const score = Math.min(100, Math.max(0, rawScore));
|
|
37
|
+
const rank = scoreToRank(score);
|
|
38
|
+
return {
|
|
39
|
+
rank,
|
|
40
|
+
score,
|
|
41
|
+
factors: {
|
|
42
|
+
diffTypeWeight,
|
|
43
|
+
hasAdvisory,
|
|
44
|
+
affectedWorkspaceCount,
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Batch-compute impact scores for all updates, returning a new array with
|
|
50
|
+
* `impactScore` populated on each entry. Non-mutating.
|
|
51
|
+
*/
|
|
52
|
+
export function applyImpactScores(updates, context = {}) {
|
|
53
|
+
return updates.map((u) => ({
|
|
54
|
+
...u,
|
|
55
|
+
impactScore: computeImpactScore(u, context),
|
|
56
|
+
}));
|
|
57
|
+
}
|
|
58
|
+
function scoreToRank(score) {
|
|
59
|
+
if (score >= 70)
|
|
60
|
+
return "critical";
|
|
61
|
+
if (score >= 45)
|
|
62
|
+
return "high";
|
|
63
|
+
if (score >= 20)
|
|
64
|
+
return "medium";
|
|
65
|
+
return "low";
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Returns the ANSI color badge string for terminal output.
|
|
69
|
+
* Used by the table renderer when --show-impact is active.
|
|
70
|
+
*/
|
|
71
|
+
export function impactBadge(score) {
|
|
72
|
+
switch (score.rank) {
|
|
73
|
+
case "critical":
|
|
74
|
+
return "\x1b[41m\x1b[97m CRITICAL \x1b[0m";
|
|
75
|
+
case "high":
|
|
76
|
+
return "\x1b[31m HIGH \x1b[0m";
|
|
77
|
+
case "medium":
|
|
78
|
+
return "\x1b[33m MED \x1b[0m";
|
|
79
|
+
case "low":
|
|
80
|
+
return "\x1b[32m LOW \x1b[0m";
|
|
81
|
+
}
|
|
82
|
+
}
|
package/dist/core/init-ci.js
CHANGED
|
@@ -46,14 +46,14 @@ function installStep(packageManager) {
|
|
|
46
46
|
return ` - name: Install dependencies\n run: npm ci`;
|
|
47
47
|
}
|
|
48
48
|
function minimalWorkflowTemplate(scheduleBlock, packageManager) {
|
|
49
|
-
return `name: Rainy Updates\n\non:\n${scheduleBlock}\n\njobs:\n dependency-check:\n runs-on: ubuntu-latest\n steps:\n - name: Checkout\n uses: actions/checkout@v4\n\n - name: Setup Node\n uses: actions/setup-node@v4\n with:\n node-version: '20'\n\n${installStep(packageManager)}\n\n - name: Run dependency check\n run: |\n npx @rainy-updates/cli ci \\\n --workspace \\\n --mode minimal \\\n --ci \\\n --format table\n`;
|
|
49
|
+
return `name: Rainy Updates\n\non:\n${scheduleBlock}\n\njobs:\n dependency-check:\n runs-on: ubuntu-latest\n steps:\n - name: Checkout\n uses: actions/checkout@v4\n\n - name: Setup Node\n uses: actions/setup-node@v4\n with:\n node-version: '20'\n\n${installStep(packageManager)}\n\n - name: Run dependency check\n run: |\n npx @rainy-updates/cli ci \\\n --workspace \\\n --mode minimal \\\n --ci \\\n --stream \\\n --registry-timeout-ms 12000 \\\n --registry-retries 4 \\\n --format table\n`;
|
|
50
50
|
}
|
|
51
51
|
function strictWorkflowTemplate(scheduleBlock, packageManager) {
|
|
52
|
-
return `name: Rainy Updates\n\non:\n${scheduleBlock}\n\npermissions:\n contents: read\n security-events: write\n\njobs:\n dependency-check:\n runs-on: ubuntu-latest\n steps:\n - name: Checkout\n uses: actions/checkout@v4\n\n - name: Setup Node\n uses: actions/setup-node@v4\n with:\n node-version: '20'\n\n${installStep(packageManager)}\n\n - name: Warm cache\n run: npx @rainy-updates/cli warm-cache --workspace --concurrency 32\n\n - name: Run strict dependency check\n run: |\n npx @rainy-updates/cli ci \\\n --workspace \\\n --mode strict \\\n --ci \\\n --concurrency 32 \\\n --format github \\\n --json-file .artifacts/deps-report.json \\\n --pr-report-file .artifacts/deps-report.md \\\n --sarif-file .artifacts/deps-report.sarif \\\n --github-output $GITHUB_OUTPUT\n\n - name: Upload report artifacts\n uses: actions/upload-artifact@v4\n with:\n name: rainy-updates-report\n path: .artifacts/\n\n - name: Upload SARIF\n uses: github/codeql-action/upload-sarif@v3\n with:\n sarif_file: .artifacts/deps-report.sarif\n`;
|
|
52
|
+
return `name: Rainy Updates\n\non:\n${scheduleBlock}\n\npermissions:\n contents: read\n security-events: write\n\njobs:\n dependency-check:\n runs-on: ubuntu-latest\n steps:\n - name: Checkout\n uses: actions/checkout@v4\n\n - name: Setup Node\n uses: actions/setup-node@v4\n with:\n node-version: '20'\n\n${installStep(packageManager)}\n\n - name: Warm cache\n run: npx @rainy-updates/cli warm-cache --workspace --concurrency 32 --registry-timeout-ms 12000 --registry-retries 4\n\n - name: Run strict dependency check\n run: |\n npx @rainy-updates/cli ci \\\n --workspace \\\n --mode strict \\\n --ci \\\n --concurrency 32 \\\n --stream \\\n --registry-timeout-ms 12000 \\\n --registry-retries 4 \\\n --format github \\\n --json-file .artifacts/deps-report.json \\\n --pr-report-file .artifacts/deps-report.md \\\n --sarif-file .artifacts/deps-report.sarif \\\n --github-output $GITHUB_OUTPUT\n\n - name: Upload report artifacts\n uses: actions/upload-artifact@v4\n with:\n name: rainy-updates-report\n path: .artifacts/\n\n - name: Upload SARIF\n uses: github/codeql-action/upload-sarif@v3\n with:\n sarif_file: .artifacts/deps-report.sarif\n`;
|
|
53
53
|
}
|
|
54
54
|
function enterpriseWorkflowTemplate(scheduleBlock, packageManager) {
|
|
55
55
|
const detectedPmInstall = packageManager === "pnpm"
|
|
56
56
|
? "corepack enable && corepack prepare pnpm@9 --activate && pnpm install --frozen-lockfile"
|
|
57
57
|
: "npm ci";
|
|
58
|
-
return `name: Rainy Updates Enterprise\n\non:\n${scheduleBlock}\n\npermissions:\n contents: read\n security-events: write\n actions: read\n\nconcurrency:\n group: rainy-updates-\${{ github.ref }}\n cancel-in-progress: false\n\njobs:\n dependency-check:\n runs-on: ubuntu-latest\n strategy:\n fail-fast: false\n matrix:\n node: [20, 22]\n steps:\n - name: Checkout\n uses: actions/checkout@v4\n\n - name: Setup Node\n uses: actions/setup-node@v4\n with:\n node-version: \${{ matrix.node }}\n\n - name: Install dependencies\n run: ${detectedPmInstall}\n\n - name: Warm cache\n run: npx @rainy-updates/cli warm-cache --workspace --concurrency 32\n\n - name: Check updates with rollout controls\n run: |\n npx @rainy-updates/cli ci \\\n --workspace \\\n --mode enterprise \\\n --concurrency 32 \\\n --format github \\\n --fail-on minor \\\n --max-updates 50 \\\n --json-file .artifacts/deps-report-node-\${{ matrix.node }}.json \\\n --pr-report-file .artifacts/deps-report-node-\${{ matrix.node }}.md \\\n --sarif-file .artifacts/deps-report-node-\${{ matrix.node }}.sarif \\\n --github-output $GITHUB_OUTPUT\n\n - name: Upload report artifacts\n uses: actions/upload-artifact@v4\n with:\n name: rainy-updates-report-node-\${{ matrix.node }}\n path: .artifacts/\n retention-days: 14\n\n - name: Upload SARIF\n uses: github/codeql-action/upload-sarif@v3\n with:\n sarif_file: .artifacts/deps-report-node-\${{ matrix.node }}.sarif\n`;
|
|
58
|
+
return `name: Rainy Updates Enterprise\n\non:\n${scheduleBlock}\n\npermissions:\n contents: read\n security-events: write\n actions: read\n\nconcurrency:\n group: rainy-updates-\${{ github.ref }}\n cancel-in-progress: false\n\njobs:\n dependency-check:\n runs-on: ubuntu-latest\n strategy:\n fail-fast: false\n matrix:\n node: [20, 22]\n steps:\n - name: Checkout\n uses: actions/checkout@v4\n\n - name: Setup Node\n uses: actions/setup-node@v4\n with:\n node-version: \${{ matrix.node }}\n\n - name: Install dependencies\n run: ${detectedPmInstall}\n\n - name: Warm cache\n run: npx @rainy-updates/cli warm-cache --workspace --concurrency 32 --registry-timeout-ms 12000 --registry-retries 4\n\n - name: Check updates with rollout controls\n run: |\n npx @rainy-updates/cli ci \\\n --workspace \\\n --mode enterprise \\\n --concurrency 32 \\\n --stream \\\n --registry-timeout-ms 12000 \\\n --registry-retries 4 \\\n --lockfile-mode preserve \\\n --format github \\\n --fail-on minor \\\n --max-updates 50 \\\n --json-file .artifacts/deps-report-node-\${{ matrix.node }}.json \\\n --pr-report-file .artifacts/deps-report-node-\${{ matrix.node }}.md \\\n --sarif-file .artifacts/deps-report-node-\${{ matrix.node }}.sarif \\\n --github-output $GITHUB_OUTPUT\n\n - name: Upload report artifacts\n uses: actions/upload-artifact@v4\n with:\n name: rainy-updates-report-node-\${{ matrix.node }}\n path: .artifacts/\n retention-days: 14\n\n - name: Upload SARIF\n uses: github/codeql-action/upload-sarif@v3\n with:\n sarif_file: .artifacts/deps-report-node-\${{ matrix.node }}.sarif\n`;
|
|
59
59
|
}
|
package/dist/core/options.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { BaselineOptions, CheckOptions, UpgradeOptions } from "../types/index.js";
|
|
1
|
+
import type { BaselineOptions, CheckOptions, UpgradeOptions, AuditOptions, BisectOptions, HealthOptions, UnusedOptions, ResolveOptions, LicenseOptions, SnapshotOptions } from "../types/index.js";
|
|
2
2
|
import type { InitCiMode, InitCiSchedule } from "./init-ci.js";
|
|
3
3
|
export type ParsedCliArgs = {
|
|
4
4
|
command: "check";
|
|
@@ -25,5 +25,26 @@ export type ParsedCliArgs = {
|
|
|
25
25
|
options: BaselineOptions & {
|
|
26
26
|
action: "save" | "check";
|
|
27
27
|
};
|
|
28
|
+
} | {
|
|
29
|
+
command: "bisect";
|
|
30
|
+
options: BisectOptions;
|
|
31
|
+
} | {
|
|
32
|
+
command: "audit";
|
|
33
|
+
options: AuditOptions;
|
|
34
|
+
} | {
|
|
35
|
+
command: "health";
|
|
36
|
+
options: HealthOptions;
|
|
37
|
+
} | {
|
|
38
|
+
command: "unused";
|
|
39
|
+
options: UnusedOptions;
|
|
40
|
+
} | {
|
|
41
|
+
command: "resolve";
|
|
42
|
+
options: ResolveOptions;
|
|
43
|
+
} | {
|
|
44
|
+
command: "licenses";
|
|
45
|
+
options: LicenseOptions;
|
|
46
|
+
} | {
|
|
47
|
+
command: "snapshot";
|
|
48
|
+
options: SnapshotOptions;
|
|
28
49
|
};
|
|
29
50
|
export declare function parseCliArgs(argv: string[]): Promise<ParsedCliArgs>;
|