@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.
@@ -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
+ }
@@ -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
- base.jsonFile = path.resolve(next);
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
- base.githubOutputFile = path.resolve(next);
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
- base.sarifFile = path.resolve(next);
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
- base.policyFile = path.resolve(next);
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
- base.prReportFile = path.resolve(next);
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("--format must be table, json, minimal or github");
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
+ }
@@ -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 summary = {
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
  }
@@ -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 JSON.stringify(result, null, 2);
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
  }
@@ -1,26 +1,40 @@
1
- import { promises as fs } from "node:fs";
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.errors.length}`,
7
- `warnings_count=${result.warnings.length}`,
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 fs.mkdir(path.dirname(filePath), { recursive: true });
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
- for (const update of result.updates) {
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) {
@@ -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 updateResults = result.updates.map((update) => ({
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
  };
@@ -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[];