@rainy-updates/cli 0.5.0-rc.1 → 0.5.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/CHANGELOG.md +68 -0
- package/README.md +10 -0
- package/dist/bin/cli.js +93 -32
- package/dist/cache/cache.d.ts +2 -0
- package/dist/cache/cache.js +8 -3
- package/dist/config/loader.d.ts +8 -1
- package/dist/config/policy.d.ts +15 -4
- package/dist/config/policy.js +35 -7
- package/dist/core/check.js +78 -11
- package/dist/core/fix-pr.d.ts +7 -0
- package/dist/core/fix-pr.js +83 -0
- package/dist/core/options.js +107 -7
- package/dist/core/summary.d.ts +22 -0
- package/dist/core/summary.js +78 -0
- package/dist/core/warm-cache.js +34 -6
- package/dist/output/format.js +23 -1
- package/dist/output/github.js +23 -9
- package/dist/output/sarif.js +16 -2
- package/dist/registry/npm.d.ts +1 -1
- package/dist/registry/npm.js +136 -17
- package/dist/types/index.d.ts +33 -1
- package/dist/utils/io.d.ts +1 -0
- package/dist/utils/io.js +10 -0
- package/dist/utils/stable-json.d.ts +1 -0
- package/dist/utils/stable-json.js +20 -0
- package/dist/workspace/discover.js +56 -18
- package/package.json +2 -1
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
export async function applyFixPr(options, result, extraFiles) {
|
|
4
|
+
if (!options.fixPr)
|
|
5
|
+
return { applied: false };
|
|
6
|
+
if (result.updates.length === 0) {
|
|
7
|
+
return {
|
|
8
|
+
applied: false,
|
|
9
|
+
branchName: options.fixBranch ?? "chore/rainy-updates",
|
|
10
|
+
commitSha: "",
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
const status = await runGit(options.cwd, ["status", "--porcelain"]);
|
|
14
|
+
if (status.stdout.trim().length > 0) {
|
|
15
|
+
throw new Error("Cannot run --fix-pr with a dirty git working tree.");
|
|
16
|
+
}
|
|
17
|
+
const branch = options.fixBranch ?? "chore/rainy-updates";
|
|
18
|
+
const headRef = await runGit(options.cwd, ["symbolic-ref", "--quiet", "--short", "HEAD"], true);
|
|
19
|
+
if (headRef.code !== 0 && !options.fixPrNoCheckout) {
|
|
20
|
+
throw new Error("Cannot run --fix-pr in detached HEAD state without --fix-pr-no-checkout.");
|
|
21
|
+
}
|
|
22
|
+
if (!options.fixPrNoCheckout) {
|
|
23
|
+
const branchCheck = await runGit(options.cwd, ["rev-parse", "--verify", "--quiet", branch], true);
|
|
24
|
+
if (branchCheck.code === 0) {
|
|
25
|
+
await runGit(options.cwd, ["checkout", branch]);
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
await runGit(options.cwd, ["checkout", "-b", branch]);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (options.fixDryRun) {
|
|
32
|
+
return {
|
|
33
|
+
applied: false,
|
|
34
|
+
branchName: branch,
|
|
35
|
+
commitSha: "",
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
const manifestFiles = Array.from(new Set(result.updates.map((update) => path.resolve(update.packagePath, "package.json"))));
|
|
39
|
+
const filesToStage = Array.from(new Set([...manifestFiles, ...extraFiles]
|
|
40
|
+
.map((entry) => path.resolve(options.cwd, entry))
|
|
41
|
+
.filter((entry) => entry.startsWith(path.resolve(options.cwd) + path.sep) || entry === path.resolve(options.cwd)))).sort((a, b) => a.localeCompare(b));
|
|
42
|
+
if (filesToStage.length > 0) {
|
|
43
|
+
await runGit(options.cwd, ["add", "--", ...filesToStage]);
|
|
44
|
+
}
|
|
45
|
+
const stagedCheck = await runGit(options.cwd, ["diff", "--cached", "--quiet"], true);
|
|
46
|
+
if (stagedCheck.code === 0) {
|
|
47
|
+
return {
|
|
48
|
+
applied: false,
|
|
49
|
+
branchName: branch,
|
|
50
|
+
commitSha: "",
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
const message = options.fixCommitMessage ?? `chore(deps): apply rainy-updates (${result.updates.length} updates)`;
|
|
54
|
+
await runGit(options.cwd, ["commit", "-m", message]);
|
|
55
|
+
const rev = await runGit(options.cwd, ["rev-parse", "HEAD"]);
|
|
56
|
+
return {
|
|
57
|
+
applied: true,
|
|
58
|
+
branchName: branch,
|
|
59
|
+
commitSha: rev.stdout.trim(),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
async function runGit(cwd, args, allowNonZero = false) {
|
|
63
|
+
return await new Promise((resolve, reject) => {
|
|
64
|
+
const child = spawn("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
65
|
+
let stdout = "";
|
|
66
|
+
let stderr = "";
|
|
67
|
+
child.stdout.on("data", (chunk) => {
|
|
68
|
+
stdout += typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8");
|
|
69
|
+
});
|
|
70
|
+
child.stderr.on("data", (chunk) => {
|
|
71
|
+
stderr += typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8");
|
|
72
|
+
});
|
|
73
|
+
child.on("error", reject);
|
|
74
|
+
child.on("exit", (code) => {
|
|
75
|
+
const normalized = code ?? 1;
|
|
76
|
+
if (normalized !== 0 && !allowNonZero) {
|
|
77
|
+
reject(new Error(`git ${args.join(" ")} failed (${normalized}): ${stderr.trim()}`));
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
resolve({ code: normalized, stdout, stderr });
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
}
|
package/dist/core/options.js
CHANGED
|
@@ -36,12 +36,24 @@ export async function parseCliArgs(argv) {
|
|
|
36
36
|
prReportFile: undefined,
|
|
37
37
|
failOn: "none",
|
|
38
38
|
maxUpdates: undefined,
|
|
39
|
+
fixPr: false,
|
|
40
|
+
fixBranch: "chore/rainy-updates",
|
|
41
|
+
fixCommitMessage: undefined,
|
|
42
|
+
fixDryRun: false,
|
|
43
|
+
fixPrNoCheckout: false,
|
|
44
|
+
noPrReport: false,
|
|
45
|
+
logLevel: "info",
|
|
39
46
|
};
|
|
40
47
|
let force = false;
|
|
41
48
|
let initCiMode = "enterprise";
|
|
42
49
|
let initCiSchedule = "weekly";
|
|
43
50
|
let baselineAction = "check";
|
|
44
51
|
let baselineFilePath = path.resolve(base.cwd, ".rainy-updates-baseline.json");
|
|
52
|
+
let jsonFileRaw;
|
|
53
|
+
let githubOutputRaw;
|
|
54
|
+
let sarifFileRaw;
|
|
55
|
+
let policyFileRaw;
|
|
56
|
+
let prReportFileRaw;
|
|
45
57
|
let resolvedConfig = await loadConfig(base.cwd);
|
|
46
58
|
applyConfig(base, resolvedConfig);
|
|
47
59
|
for (let index = 0; index < args.length; index += 1) {
|
|
@@ -111,7 +123,7 @@ export async function parseCliArgs(argv) {
|
|
|
111
123
|
continue;
|
|
112
124
|
}
|
|
113
125
|
if (current === "--json-file" && next) {
|
|
114
|
-
|
|
126
|
+
jsonFileRaw = next;
|
|
115
127
|
index += 1;
|
|
116
128
|
continue;
|
|
117
129
|
}
|
|
@@ -119,7 +131,7 @@ export async function parseCliArgs(argv) {
|
|
|
119
131
|
throw new Error("Missing value for --json-file");
|
|
120
132
|
}
|
|
121
133
|
if (current === "--github-output" && next) {
|
|
122
|
-
|
|
134
|
+
githubOutputRaw = next;
|
|
123
135
|
index += 1;
|
|
124
136
|
continue;
|
|
125
137
|
}
|
|
@@ -127,7 +139,7 @@ export async function parseCliArgs(argv) {
|
|
|
127
139
|
throw new Error("Missing value for --github-output");
|
|
128
140
|
}
|
|
129
141
|
if (current === "--sarif-file" && next) {
|
|
130
|
-
|
|
142
|
+
sarifFileRaw = next;
|
|
131
143
|
index += 1;
|
|
132
144
|
continue;
|
|
133
145
|
}
|
|
@@ -151,7 +163,7 @@ export async function parseCliArgs(argv) {
|
|
|
151
163
|
continue;
|
|
152
164
|
}
|
|
153
165
|
if (current === "--policy-file" && next) {
|
|
154
|
-
|
|
166
|
+
policyFileRaw = next;
|
|
155
167
|
index += 1;
|
|
156
168
|
continue;
|
|
157
169
|
}
|
|
@@ -159,7 +171,7 @@ export async function parseCliArgs(argv) {
|
|
|
159
171
|
throw new Error("Missing value for --policy-file");
|
|
160
172
|
}
|
|
161
173
|
if (current === "--pr-report-file" && next) {
|
|
162
|
-
|
|
174
|
+
prReportFileRaw = next;
|
|
163
175
|
index += 1;
|
|
164
176
|
continue;
|
|
165
177
|
}
|
|
@@ -170,6 +182,46 @@ export async function parseCliArgs(argv) {
|
|
|
170
182
|
force = true;
|
|
171
183
|
continue;
|
|
172
184
|
}
|
|
185
|
+
if (current === "--fix-pr") {
|
|
186
|
+
base.fixPr = true;
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
if (current === "--fix-branch" && next) {
|
|
190
|
+
base.fixBranch = next;
|
|
191
|
+
index += 1;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (current === "--fix-branch") {
|
|
195
|
+
throw new Error("Missing value for --fix-branch");
|
|
196
|
+
}
|
|
197
|
+
if (current === "--fix-commit-message" && next) {
|
|
198
|
+
base.fixCommitMessage = next;
|
|
199
|
+
index += 1;
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (current === "--fix-commit-message") {
|
|
203
|
+
throw new Error("Missing value for --fix-commit-message");
|
|
204
|
+
}
|
|
205
|
+
if (current === "--fix-dry-run") {
|
|
206
|
+
base.fixDryRun = true;
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
if (current === "--no-pr-report") {
|
|
210
|
+
base.noPrReport = true;
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
if (current === "--fix-pr-no-checkout") {
|
|
214
|
+
base.fixPrNoCheckout = true;
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
if (current === "--log-level" && next) {
|
|
218
|
+
base.logLevel = ensureLogLevel(next);
|
|
219
|
+
index += 1;
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
if (current === "--log-level") {
|
|
223
|
+
throw new Error("Missing value for --log-level");
|
|
224
|
+
}
|
|
173
225
|
if (current === "--install" && command === "upgrade") {
|
|
174
226
|
continue;
|
|
175
227
|
}
|
|
@@ -249,6 +301,27 @@ export async function parseCliArgs(argv) {
|
|
|
249
301
|
}
|
|
250
302
|
throw new Error(`Unexpected argument: ${current}`);
|
|
251
303
|
}
|
|
304
|
+
if (jsonFileRaw) {
|
|
305
|
+
base.jsonFile = path.resolve(base.cwd, jsonFileRaw);
|
|
306
|
+
}
|
|
307
|
+
if (githubOutputRaw) {
|
|
308
|
+
base.githubOutputFile = path.resolve(base.cwd, githubOutputRaw);
|
|
309
|
+
}
|
|
310
|
+
if (sarifFileRaw) {
|
|
311
|
+
base.sarifFile = path.resolve(base.cwd, sarifFileRaw);
|
|
312
|
+
}
|
|
313
|
+
if (policyFileRaw) {
|
|
314
|
+
base.policyFile = path.resolve(base.cwd, policyFileRaw);
|
|
315
|
+
}
|
|
316
|
+
if (prReportFileRaw) {
|
|
317
|
+
base.prReportFile = path.resolve(base.cwd, prReportFileRaw);
|
|
318
|
+
}
|
|
319
|
+
if (base.noPrReport) {
|
|
320
|
+
base.prReportFile = undefined;
|
|
321
|
+
}
|
|
322
|
+
else if (base.fixPr && !base.prReportFile) {
|
|
323
|
+
base.prReportFile = path.resolve(base.cwd, ".artifacts/deps-report.md");
|
|
324
|
+
}
|
|
252
325
|
if (command === "upgrade") {
|
|
253
326
|
const configPm = resolvedConfig.packageManager;
|
|
254
327
|
const cliPm = parsePackageManager(args);
|
|
@@ -335,6 +408,27 @@ function applyConfig(base, config) {
|
|
|
335
408
|
if (typeof config.maxUpdates === "number" && Number.isInteger(config.maxUpdates) && config.maxUpdates >= 0) {
|
|
336
409
|
base.maxUpdates = config.maxUpdates;
|
|
337
410
|
}
|
|
411
|
+
if (typeof config.fixPr === "boolean") {
|
|
412
|
+
base.fixPr = config.fixPr;
|
|
413
|
+
}
|
|
414
|
+
if (typeof config.fixBranch === "string" && config.fixBranch.length > 0) {
|
|
415
|
+
base.fixBranch = config.fixBranch;
|
|
416
|
+
}
|
|
417
|
+
if (typeof config.fixCommitMessage === "string" && config.fixCommitMessage.length > 0) {
|
|
418
|
+
base.fixCommitMessage = config.fixCommitMessage;
|
|
419
|
+
}
|
|
420
|
+
if (typeof config.fixDryRun === "boolean") {
|
|
421
|
+
base.fixDryRun = config.fixDryRun;
|
|
422
|
+
}
|
|
423
|
+
if (typeof config.fixPrNoCheckout === "boolean") {
|
|
424
|
+
base.fixPrNoCheckout = config.fixPrNoCheckout;
|
|
425
|
+
}
|
|
426
|
+
if (typeof config.noPrReport === "boolean") {
|
|
427
|
+
base.noPrReport = config.noPrReport;
|
|
428
|
+
}
|
|
429
|
+
if (typeof config.logLevel === "string") {
|
|
430
|
+
base.logLevel = ensureLogLevel(config.logLevel);
|
|
431
|
+
}
|
|
338
432
|
}
|
|
339
433
|
function parsePackageManager(args) {
|
|
340
434
|
const index = args.indexOf("--pm");
|
|
@@ -353,10 +447,16 @@ function ensureTarget(value) {
|
|
|
353
447
|
throw new Error("--target must be patch, minor, major, latest");
|
|
354
448
|
}
|
|
355
449
|
function ensureFormat(value) {
|
|
356
|
-
if (value === "table" || value === "json" || value === "minimal" || value === "github") {
|
|
450
|
+
if (value === "table" || value === "json" || value === "minimal" || value === "github" || value === "metrics") {
|
|
451
|
+
return value;
|
|
452
|
+
}
|
|
453
|
+
throw new Error("--format must be table, json, minimal, github or metrics");
|
|
454
|
+
}
|
|
455
|
+
function ensureLogLevel(value) {
|
|
456
|
+
if (value === "error" || value === "warn" || value === "info" || value === "debug") {
|
|
357
457
|
return value;
|
|
358
458
|
}
|
|
359
|
-
throw new Error("--
|
|
459
|
+
throw new Error("--log-level must be error, warn, info or debug");
|
|
360
460
|
}
|
|
361
461
|
function parseDependencyKinds(value) {
|
|
362
462
|
const mapped = value
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { FailOnLevel, FailReason, PackageUpdate, Summary } from "../types/index.js";
|
|
2
|
+
export interface DurationInput {
|
|
3
|
+
totalMs: number;
|
|
4
|
+
discoveryMs: number;
|
|
5
|
+
registryMs: number;
|
|
6
|
+
cacheMs: number;
|
|
7
|
+
}
|
|
8
|
+
export declare function createSummary(input: {
|
|
9
|
+
scannedPackages: number;
|
|
10
|
+
totalDependencies: number;
|
|
11
|
+
checkedDependencies: number;
|
|
12
|
+
updatesFound: number;
|
|
13
|
+
upgraded: number;
|
|
14
|
+
skipped: number;
|
|
15
|
+
warmedPackages: number;
|
|
16
|
+
errors: string[];
|
|
17
|
+
warnings: string[];
|
|
18
|
+
durations: DurationInput;
|
|
19
|
+
}): Summary;
|
|
20
|
+
export declare function finalizeSummary(summary: Summary): Summary;
|
|
21
|
+
export declare function resolveFailReason(updates: PackageUpdate[], errors: string[], failOn: FailOnLevel | undefined, maxUpdates: number | undefined, ciMode: boolean): FailReason;
|
|
22
|
+
export declare function shouldFailForUpdates(updates: PackageUpdate[], failOn: FailOnLevel): boolean;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
export function createSummary(input) {
|
|
2
|
+
const offlineCacheMiss = input.errors.filter((error) => isOfflineCacheMissError(error)).length;
|
|
3
|
+
const registryFailure = input.errors.filter((error) => isRegistryFailureError(error)).length;
|
|
4
|
+
const staleCache = input.warnings.filter((warning) => warning.includes("Using stale cache")).length;
|
|
5
|
+
return {
|
|
6
|
+
contractVersion: "2",
|
|
7
|
+
scannedPackages: input.scannedPackages,
|
|
8
|
+
totalDependencies: input.totalDependencies,
|
|
9
|
+
checkedDependencies: input.checkedDependencies,
|
|
10
|
+
updatesFound: input.updatesFound,
|
|
11
|
+
upgraded: input.upgraded,
|
|
12
|
+
skipped: input.skipped,
|
|
13
|
+
warmedPackages: input.warmedPackages,
|
|
14
|
+
failReason: "none",
|
|
15
|
+
errorCounts: {
|
|
16
|
+
total: input.errors.length,
|
|
17
|
+
offlineCacheMiss,
|
|
18
|
+
registryFailure,
|
|
19
|
+
other: 0,
|
|
20
|
+
},
|
|
21
|
+
warningCounts: {
|
|
22
|
+
total: input.warnings.length,
|
|
23
|
+
staleCache,
|
|
24
|
+
other: 0,
|
|
25
|
+
},
|
|
26
|
+
durationMs: {
|
|
27
|
+
total: Math.max(0, Math.round(input.durations.totalMs)),
|
|
28
|
+
discovery: Math.max(0, Math.round(input.durations.discoveryMs)),
|
|
29
|
+
registry: Math.max(0, Math.round(input.durations.registryMs)),
|
|
30
|
+
cache: Math.max(0, Math.round(input.durations.cacheMs)),
|
|
31
|
+
render: 0,
|
|
32
|
+
},
|
|
33
|
+
fixPrApplied: false,
|
|
34
|
+
fixBranchName: "",
|
|
35
|
+
fixCommitSha: "",
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export function finalizeSummary(summary) {
|
|
39
|
+
const errorOther = summary.errorCounts.total - summary.errorCounts.offlineCacheMiss - summary.errorCounts.registryFailure;
|
|
40
|
+
const warningOther = summary.warningCounts.total - summary.warningCounts.staleCache;
|
|
41
|
+
summary.errorCounts.other = Math.max(0, errorOther);
|
|
42
|
+
summary.warningCounts.other = Math.max(0, warningOther);
|
|
43
|
+
return summary;
|
|
44
|
+
}
|
|
45
|
+
export function resolveFailReason(updates, errors, failOn, maxUpdates, ciMode) {
|
|
46
|
+
if (errors.some((error) => isOfflineCacheMissError(error))) {
|
|
47
|
+
return "offline-cache-miss";
|
|
48
|
+
}
|
|
49
|
+
if (errors.some((error) => isRegistryFailureError(error))) {
|
|
50
|
+
return "registry-failure";
|
|
51
|
+
}
|
|
52
|
+
if (typeof maxUpdates === "number" && updates.length > maxUpdates) {
|
|
53
|
+
return "updates-threshold";
|
|
54
|
+
}
|
|
55
|
+
const effectiveFailOn = failOn && failOn !== "none" ? failOn : ciMode ? "any" : "none";
|
|
56
|
+
if (shouldFailForUpdates(updates, effectiveFailOn)) {
|
|
57
|
+
return "severity-threshold";
|
|
58
|
+
}
|
|
59
|
+
return "none";
|
|
60
|
+
}
|
|
61
|
+
export function shouldFailForUpdates(updates, failOn) {
|
|
62
|
+
if (failOn === "none")
|
|
63
|
+
return false;
|
|
64
|
+
if (failOn === "any" || failOn === "patch")
|
|
65
|
+
return updates.length > 0;
|
|
66
|
+
if (failOn === "minor")
|
|
67
|
+
return updates.some((update) => update.diffType === "minor" || update.diffType === "major");
|
|
68
|
+
return updates.some((update) => update.diffType === "major");
|
|
69
|
+
}
|
|
70
|
+
function isOfflineCacheMissError(value) {
|
|
71
|
+
return value.includes("Offline cache miss");
|
|
72
|
+
}
|
|
73
|
+
function isRegistryFailureError(value) {
|
|
74
|
+
return (value.includes("Unable to resolve") ||
|
|
75
|
+
value.includes("Unable to warm") ||
|
|
76
|
+
value.includes("Registry request failed") ||
|
|
77
|
+
value.includes("Registry temporary error"));
|
|
78
|
+
}
|
package/dist/core/warm-cache.js
CHANGED
|
@@ -4,13 +4,23 @@ import { VersionCache } from "../cache/cache.js";
|
|
|
4
4
|
import { NpmRegistryClient } from "../registry/npm.js";
|
|
5
5
|
import { detectPackageManager } from "../pm/detect.js";
|
|
6
6
|
import { discoverPackageDirs } from "../workspace/discover.js";
|
|
7
|
+
import { createSummary, finalizeSummary } from "./summary.js";
|
|
7
8
|
export async function warmCache(options) {
|
|
9
|
+
const startedAt = Date.now();
|
|
10
|
+
let discoveryMs = 0;
|
|
11
|
+
let cacheMs = 0;
|
|
12
|
+
let registryMs = 0;
|
|
13
|
+
const discoveryStartedAt = Date.now();
|
|
8
14
|
const packageManager = await detectPackageManager(options.cwd);
|
|
9
15
|
const packageDirs = await discoverPackageDirs(options.cwd, options.workspace);
|
|
16
|
+
discoveryMs += Date.now() - discoveryStartedAt;
|
|
10
17
|
const cache = await VersionCache.create();
|
|
11
|
-
const registryClient = new NpmRegistryClient();
|
|
18
|
+
const registryClient = new NpmRegistryClient(options.cwd);
|
|
12
19
|
const errors = [];
|
|
13
20
|
const warnings = [];
|
|
21
|
+
if (cache.degraded) {
|
|
22
|
+
warnings.push("SQLite cache backend unavailable in Bun runtime. Falling back to file cache backend.");
|
|
23
|
+
}
|
|
14
24
|
let totalDependencies = 0;
|
|
15
25
|
const packageNames = new Set();
|
|
16
26
|
for (const packageDir of packageDirs) {
|
|
@@ -30,16 +40,19 @@ export async function warmCache(options) {
|
|
|
30
40
|
errors.push(`Failed to read package.json in ${packageDir}: ${String(error)}`);
|
|
31
41
|
}
|
|
32
42
|
}
|
|
33
|
-
const names = Array.from(packageNames);
|
|
43
|
+
const names = Array.from(packageNames).sort((a, b) => a.localeCompare(b));
|
|
34
44
|
const needsFetch = [];
|
|
45
|
+
const cacheLookupStartedAt = Date.now();
|
|
35
46
|
for (const pkg of names) {
|
|
36
47
|
const valid = await cache.getValid(pkg, options.target);
|
|
37
48
|
if (!valid)
|
|
38
49
|
needsFetch.push(pkg);
|
|
39
50
|
}
|
|
51
|
+
cacheMs += Date.now() - cacheLookupStartedAt;
|
|
40
52
|
let warmed = 0;
|
|
41
53
|
if (needsFetch.length > 0) {
|
|
42
54
|
if (options.offline) {
|
|
55
|
+
const cacheFallbackStartedAt = Date.now();
|
|
43
56
|
for (const pkg of needsFetch) {
|
|
44
57
|
const stale = await cache.getAny(pkg, options.target);
|
|
45
58
|
if (stale) {
|
|
@@ -50,23 +63,30 @@ export async function warmCache(options) {
|
|
|
50
63
|
errors.push(`Offline cache miss for ${pkg}. Cannot warm cache in --offline mode.`);
|
|
51
64
|
}
|
|
52
65
|
}
|
|
66
|
+
cacheMs += Date.now() - cacheFallbackStartedAt;
|
|
53
67
|
}
|
|
54
68
|
else {
|
|
69
|
+
const registryStartedAt = Date.now();
|
|
55
70
|
const fetched = await registryClient.resolveManyPackageMetadata(needsFetch, {
|
|
56
71
|
concurrency: options.concurrency,
|
|
57
72
|
});
|
|
73
|
+
registryMs += Date.now() - registryStartedAt;
|
|
74
|
+
const cacheWriteStartedAt = Date.now();
|
|
58
75
|
for (const [pkg, metadata] of fetched.metadata) {
|
|
59
76
|
if (metadata.latestVersion) {
|
|
60
77
|
await cache.set(pkg, options.target, metadata.latestVersion, metadata.versions, options.cacheTtlSeconds);
|
|
61
78
|
warmed += 1;
|
|
62
79
|
}
|
|
63
80
|
}
|
|
81
|
+
cacheMs += Date.now() - cacheWriteStartedAt;
|
|
64
82
|
for (const [pkg, error] of fetched.errors) {
|
|
65
83
|
errors.push(`Unable to warm ${pkg}: ${error}`);
|
|
66
84
|
}
|
|
67
85
|
}
|
|
68
86
|
}
|
|
69
|
-
const
|
|
87
|
+
const sortedErrors = [...errors].sort((a, b) => a.localeCompare(b));
|
|
88
|
+
const sortedWarnings = [...warnings].sort((a, b) => a.localeCompare(b));
|
|
89
|
+
const summary = finalizeSummary(createSummary({
|
|
70
90
|
scannedPackages: packageDirs.length,
|
|
71
91
|
totalDependencies,
|
|
72
92
|
checkedDependencies: names.length,
|
|
@@ -74,7 +94,15 @@ export async function warmCache(options) {
|
|
|
74
94
|
upgraded: 0,
|
|
75
95
|
skipped: 0,
|
|
76
96
|
warmedPackages: warmed,
|
|
77
|
-
|
|
97
|
+
errors: sortedErrors,
|
|
98
|
+
warnings: sortedWarnings,
|
|
99
|
+
durations: {
|
|
100
|
+
totalMs: Date.now() - startedAt,
|
|
101
|
+
discoveryMs,
|
|
102
|
+
registryMs,
|
|
103
|
+
cacheMs,
|
|
104
|
+
},
|
|
105
|
+
}));
|
|
78
106
|
return {
|
|
79
107
|
projectPath: options.cwd,
|
|
80
108
|
packagePaths: packageDirs,
|
|
@@ -83,7 +111,7 @@ export async function warmCache(options) {
|
|
|
83
111
|
timestamp: new Date().toISOString(),
|
|
84
112
|
summary,
|
|
85
113
|
updates: [],
|
|
86
|
-
errors,
|
|
87
|
-
warnings,
|
|
114
|
+
errors: sortedErrors,
|
|
115
|
+
warnings: sortedWarnings,
|
|
88
116
|
};
|
|
89
117
|
}
|
package/dist/output/format.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { renderGitHubAnnotations } from "./github.js";
|
|
2
|
+
import { stableStringify } from "../utils/stable-json.js";
|
|
2
3
|
export function renderResult(result, format) {
|
|
3
4
|
if (format === "json") {
|
|
4
|
-
return
|
|
5
|
+
return stableStringify(result, 2);
|
|
5
6
|
}
|
|
6
7
|
if (format === "minimal") {
|
|
7
8
|
if (result.updates.length === 0 && result.summary.warmedPackages > 0) {
|
|
@@ -16,6 +17,23 @@ export function renderResult(result, format) {
|
|
|
16
17
|
if (format === "github") {
|
|
17
18
|
return renderGitHubAnnotations(result);
|
|
18
19
|
}
|
|
20
|
+
if (format === "metrics") {
|
|
21
|
+
return [
|
|
22
|
+
`contract_version=${result.summary.contractVersion}`,
|
|
23
|
+
`updates_found=${result.summary.updatesFound}`,
|
|
24
|
+
`errors_count=${result.summary.errorCounts.total}`,
|
|
25
|
+
`warnings_count=${result.summary.warningCounts.total}`,
|
|
26
|
+
`checked_dependencies=${result.summary.checkedDependencies}`,
|
|
27
|
+
`scanned_packages=${result.summary.scannedPackages}`,
|
|
28
|
+
`warmed_packages=${result.summary.warmedPackages}`,
|
|
29
|
+
`fail_reason=${result.summary.failReason}`,
|
|
30
|
+
`duration_total_ms=${result.summary.durationMs.total}`,
|
|
31
|
+
`duration_discovery_ms=${result.summary.durationMs.discovery}`,
|
|
32
|
+
`duration_registry_ms=${result.summary.durationMs.registry}`,
|
|
33
|
+
`duration_cache_ms=${result.summary.durationMs.cache}`,
|
|
34
|
+
`duration_render_ms=${result.summary.durationMs.render}`,
|
|
35
|
+
].join("\n");
|
|
36
|
+
}
|
|
19
37
|
const lines = [];
|
|
20
38
|
lines.push(`Project: ${result.projectPath}`);
|
|
21
39
|
lines.push(`Scanned packages: ${result.summary.scannedPackages}`);
|
|
@@ -52,5 +70,9 @@ export function renderResult(result, format) {
|
|
|
52
70
|
}
|
|
53
71
|
lines.push("");
|
|
54
72
|
lines.push(`Summary: ${result.summary.updatesFound} updates, ${result.summary.checkedDependencies}/${result.summary.totalDependencies} checked, ${result.summary.warmedPackages} warmed`);
|
|
73
|
+
lines.push(`Contract v${result.summary.contractVersion}, failReason=${result.summary.failReason}, duration=${result.summary.durationMs.total}ms`);
|
|
74
|
+
if (result.summary.fixPrApplied) {
|
|
75
|
+
lines.push(`Fix PR: applied on branch ${result.summary.fixBranchName ?? "unknown"} (${result.summary.fixCommitSha ?? "no-commit"})`);
|
|
76
|
+
}
|
|
55
77
|
return lines.join("\n");
|
|
56
78
|
}
|
package/dist/output/github.js
CHANGED
|
@@ -1,26 +1,40 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import path from "node:path";
|
|
1
|
+
import { writeFileAtomic } from "../utils/io.js";
|
|
3
2
|
export async function writeGitHubOutput(filePath, result) {
|
|
4
3
|
const lines = [
|
|
4
|
+
`contract_version=${result.summary.contractVersion}`,
|
|
5
5
|
`updates_found=${result.summary.updatesFound}`,
|
|
6
|
-
`errors_count=${result.
|
|
7
|
-
`warnings_count=${result.
|
|
6
|
+
`errors_count=${result.summary.errorCounts.total}`,
|
|
7
|
+
`warnings_count=${result.summary.warningCounts.total}`,
|
|
8
8
|
`checked_dependencies=${result.summary.checkedDependencies}`,
|
|
9
9
|
`scanned_packages=${result.summary.scannedPackages}`,
|
|
10
10
|
`warmed_packages=${result.summary.warmedPackages}`,
|
|
11
|
+
`fail_reason=${result.summary.failReason}`,
|
|
12
|
+
`duration_total_ms=${result.summary.durationMs.total}`,
|
|
13
|
+
`duration_discovery_ms=${result.summary.durationMs.discovery}`,
|
|
14
|
+
`duration_registry_ms=${result.summary.durationMs.registry}`,
|
|
15
|
+
`duration_cache_ms=${result.summary.durationMs.cache}`,
|
|
16
|
+
`duration_render_ms=${result.summary.durationMs.render}`,
|
|
17
|
+
`fix_pr_applied=${result.summary.fixPrApplied === true ? "1" : "0"}`,
|
|
18
|
+
`fix_pr_branch=${result.summary.fixBranchName ?? ""}`,
|
|
19
|
+
`fix_pr_commit=${result.summary.fixCommitSha ?? ""}`,
|
|
11
20
|
];
|
|
12
|
-
await
|
|
13
|
-
await fs.writeFile(filePath, lines.join("\n") + "\n", "utf8");
|
|
21
|
+
await writeFileAtomic(filePath, lines.join("\n") + "\n");
|
|
14
22
|
}
|
|
15
23
|
export function renderGitHubAnnotations(result) {
|
|
16
24
|
const lines = [];
|
|
17
|
-
|
|
25
|
+
const sortedUpdates = [...result.updates].sort((left, right) => {
|
|
26
|
+
const byName = left.name.localeCompare(right.name);
|
|
27
|
+
if (byName !== 0)
|
|
28
|
+
return byName;
|
|
29
|
+
return left.packagePath.localeCompare(right.packagePath);
|
|
30
|
+
});
|
|
31
|
+
for (const update of sortedUpdates) {
|
|
18
32
|
lines.push(`::notice title=Dependency Update::${update.name} ${update.fromRange} -> ${update.toRange} (${update.packagePath})`);
|
|
19
33
|
}
|
|
20
|
-
for (const warning of result.warnings) {
|
|
34
|
+
for (const warning of [...result.warnings].sort((a, b) => a.localeCompare(b))) {
|
|
21
35
|
lines.push(`::warning title=Rainy Updates::${warning}`);
|
|
22
36
|
}
|
|
23
|
-
for (const error of result.errors) {
|
|
37
|
+
for (const error of [...result.errors].sort((a, b) => a.localeCompare(b))) {
|
|
24
38
|
lines.push(`::error title=Rainy Updates::${error}`);
|
|
25
39
|
}
|
|
26
40
|
if (lines.length === 0) {
|
package/dist/output/sarif.js
CHANGED
|
@@ -4,7 +4,13 @@ import { fileURLToPath } from "node:url";
|
|
|
4
4
|
export function createSarifReport(result) {
|
|
5
5
|
const dependencyRuleId = "rainy-updates/dependency-update";
|
|
6
6
|
const runtimeRuleId = "rainy-updates/runtime-error";
|
|
7
|
-
const
|
|
7
|
+
const sortedUpdates = [...result.updates].sort((left, right) => {
|
|
8
|
+
const byName = left.name.localeCompare(right.name);
|
|
9
|
+
if (byName !== 0)
|
|
10
|
+
return byName;
|
|
11
|
+
return left.packagePath.localeCompare(right.packagePath);
|
|
12
|
+
});
|
|
13
|
+
const updateResults = sortedUpdates.map((update) => ({
|
|
8
14
|
ruleId: dependencyRuleId,
|
|
9
15
|
level: "warning",
|
|
10
16
|
message: {
|
|
@@ -26,7 +32,7 @@ export function createSarifReport(result) {
|
|
|
26
32
|
resolvedVersion: update.toVersionResolved,
|
|
27
33
|
},
|
|
28
34
|
}));
|
|
29
|
-
const errorResults = result.errors.map((error) => ({
|
|
35
|
+
const errorResults = [...result.errors].sort((a, b) => a.localeCompare(b)).map((error) => ({
|
|
30
36
|
ruleId: runtimeRuleId,
|
|
31
37
|
level: "error",
|
|
32
38
|
message: {
|
|
@@ -57,6 +63,14 @@ export function createSarifReport(result) {
|
|
|
57
63
|
},
|
|
58
64
|
},
|
|
59
65
|
results: [...updateResults, ...errorResults],
|
|
66
|
+
properties: {
|
|
67
|
+
contractVersion: result.summary.contractVersion,
|
|
68
|
+
failReason: result.summary.failReason,
|
|
69
|
+
updatesFound: result.summary.updatesFound,
|
|
70
|
+
errorsCount: result.summary.errorCounts.total,
|
|
71
|
+
warningsCount: result.summary.warningCounts.total,
|
|
72
|
+
durationMs: result.summary.durationMs,
|
|
73
|
+
},
|
|
60
74
|
},
|
|
61
75
|
],
|
|
62
76
|
};
|
package/dist/registry/npm.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ export interface ResolveManyResult {
|
|
|
11
11
|
}
|
|
12
12
|
export declare class NpmRegistryClient {
|
|
13
13
|
private readonly requesterPromise;
|
|
14
|
-
constructor();
|
|
14
|
+
constructor(cwd?: string);
|
|
15
15
|
resolvePackageMetadata(packageName: string, timeoutMs?: number): Promise<{
|
|
16
16
|
latestVersion: string | null;
|
|
17
17
|
versions: string[];
|