@rainy-updates/cli 0.5.1-rc.1 → 0.5.1-rc.2

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 CHANGED
@@ -2,6 +2,27 @@
2
2
 
3
3
  All notable changes to this project are documented in this file.
4
4
 
5
+ ## [0.5.1-rc.2] - 2026-02-27
6
+
7
+ ### Added
8
+
9
+ - CI fix-PR batch automation:
10
+ - `ci --fix-pr` now creates batched branches from a shared base ref using git worktrees.
11
+ - New flag: `--fix-pr-batch-size <n>` to control groups per branch batch.
12
+ - New summary/output metadata:
13
+ - `fixPrBranchesCreated`
14
+ - `fix_pr_branches_created` GitHub output key.
15
+
16
+ ### Changed
17
+
18
+ - `ci --fix-pr --group-by scope` now supports multi-branch batch creation for scoped dependency flows.
19
+ - `runCi` now consistently performs CI analysis flow first, with fix-PR handled by dedicated batch automation.
20
+
21
+ ### Tests
22
+
23
+ - Added batch planning tests for fix-PR branch creation.
24
+ - Extended parser tests for `--fix-pr-batch-size`.
25
+
5
26
  ## [0.5.1-rc.1] - 2026-02-27
6
27
 
7
28
  ### Added
package/README.md CHANGED
@@ -40,6 +40,9 @@ npx @rainy-updates/cli check --workspace --ci --format json --json-file .artifac
40
40
  # 2b) CI orchestration mode
41
41
  npx @rainy-updates/cli ci --workspace --mode strict --format github --json-file .artifacts/updates.json
42
42
 
43
+ # 2c) Batch fix branches by scope
44
+ npx @rainy-updates/cli ci --workspace --mode enterprise --group-by scope --fix-pr --fix-pr-batch-size 2
45
+
43
46
  # 3) Apply upgrades with workspace sync
44
47
  npx @rainy-updates/cli upgrade --target latest --workspace --sync --install
45
48
 
@@ -161,6 +164,7 @@ Schedule:
161
164
  - `--pr-limit <n>`
162
165
  - `--only-changed`
163
166
  - `--mode minimal|strict|enterprise` (for `ci`)
167
+ - `--fix-pr-batch-size <n>` (for batched fix branches in `ci`)
164
168
  - `--policy-file <path>`
165
169
  - `--format table|json|minimal|github`
166
170
  - `--json-file <path>`
package/dist/bin/cli.js CHANGED
@@ -11,6 +11,7 @@ import { runCi } from "../core/ci.js";
11
11
  import { initCiWorkflow } from "../core/init-ci.js";
12
12
  import { diffBaseline, saveBaseline } from "../core/baseline.js";
13
13
  import { applyFixPr } from "../core/fix-pr.js";
14
+ import { applyFixPrBatches } from "../core/fix-pr-batch.js";
14
15
  import { renderResult } from "../output/format.js";
15
16
  import { writeGitHubOutput } from "../output/github.js";
16
17
  import { createSarifReport } from "../output/sarif.js";
@@ -74,10 +75,24 @@ async function main() {
74
75
  result.summary.fixPrApplied = false;
75
76
  result.summary.fixBranchName = parsed.options.fixBranch ?? "chore/rainy-updates";
76
77
  result.summary.fixCommitSha = "";
77
- const fixResult = await applyFixPr(parsed.options, result, parsed.options.prReportFile ? [parsed.options.prReportFile] : []);
78
- result.summary.fixPrApplied = fixResult.applied;
79
- result.summary.fixBranchName = fixResult.branchName ?? "";
80
- result.summary.fixCommitSha = fixResult.commitSha ?? "";
78
+ result.summary.fixPrBranchesCreated = 0;
79
+ if (parsed.command === "ci") {
80
+ const batched = await applyFixPrBatches(parsed.options, result);
81
+ result.summary.fixPrApplied = batched.applied;
82
+ result.summary.fixBranchName = batched.branches[0] ?? (parsed.options.fixBranch ?? "chore/rainy-updates");
83
+ result.summary.fixCommitSha = batched.commits[0] ?? "";
84
+ result.summary.fixPrBranchesCreated = batched.branches.length;
85
+ if (batched.branches.length > 1) {
86
+ result.warnings.push(`Created ${batched.branches.length} fix-pr batch branches.`);
87
+ }
88
+ }
89
+ else {
90
+ const fixResult = await applyFixPr(parsed.options, result, parsed.options.prReportFile ? [parsed.options.prReportFile] : []);
91
+ result.summary.fixPrApplied = fixResult.applied;
92
+ result.summary.fixBranchName = fixResult.branchName ?? "";
93
+ result.summary.fixCommitSha = fixResult.commitSha ?? "";
94
+ result.summary.fixPrBranchesCreated = fixResult.applied ? 1 : 0;
95
+ }
81
96
  }
82
97
  result.summary.failReason = resolveFailReason(result.updates, result.errors, parsed.options.failOn, parsed.options.maxUpdates, parsed.options.ci);
83
98
  const renderStartedAt = Date.now();
@@ -134,6 +149,7 @@ Options:
134
149
  --fix-commit-message <text>
135
150
  --fix-dry-run
136
151
  --fix-pr-no-checkout
152
+ --fix-pr-batch-size <n>
137
153
  --no-pr-report
138
154
  --json-file <path>
139
155
  --github-output <path>
@@ -186,6 +202,7 @@ Options:
186
202
  --fix-commit-message <text>
187
203
  --fix-dry-run
188
204
  --fix-pr-no-checkout
205
+ --fix-pr-batch-size <n>
189
206
  --no-pr-report
190
207
  --json-file <path>
191
208
  --pr-report-file <path>`;
@@ -210,6 +227,7 @@ Options:
210
227
  --fix-commit-message <text>
211
228
  --fix-dry-run
212
229
  --fix-pr-no-checkout
230
+ --fix-pr-batch-size <n>
213
231
  --no-pr-report
214
232
  --json-file <path>
215
233
  --github-output <path>
@@ -277,6 +295,7 @@ Global options:
277
295
  --fix-commit-message <text>
278
296
  --fix-dry-run
279
297
  --fix-pr-no-checkout
298
+ --fix-pr-batch-size <n>
280
299
  --no-pr-report
281
300
  --log-level error|warn|info|debug
282
301
  --concurrency <n>
@@ -22,6 +22,7 @@ export interface FileConfig {
22
22
  fixCommitMessage?: string;
23
23
  fixDryRun?: boolean;
24
24
  fixPrNoCheckout?: boolean;
25
+ fixPrBatchSize?: number;
25
26
  noPrReport?: boolean;
26
27
  logLevel?: LogLevel;
27
28
  groupBy?: GroupBy;
package/dist/core/ci.js CHANGED
@@ -1,6 +1,5 @@
1
1
  import { check } from "./check.js";
2
2
  import { warmCache } from "./warm-cache.js";
3
- import { upgrade } from "./upgrade.js";
4
3
  export async function runCi(options) {
5
4
  const profile = options.ciProfile;
6
5
  if (profile !== "minimal") {
@@ -17,14 +16,5 @@ export async function runCi(options) {
17
16
  offline: profile === "minimal" ? options.offline : true,
18
17
  concurrency: profile === "enterprise" ? Math.max(options.concurrency, 32) : options.concurrency,
19
18
  };
20
- if (options.fixPr) {
21
- const upgradeOptions = {
22
- ...checkOptions,
23
- install: false,
24
- packageManager: "auto",
25
- sync: false,
26
- };
27
- return await upgrade(upgradeOptions);
28
- }
29
19
  return await check(checkOptions);
30
20
  }
@@ -0,0 +1,19 @@
1
+ import type { CheckResult, PackageUpdate, RunOptions } from "../types/index.js";
2
+ export interface FixPrBatchResult {
3
+ applied: boolean;
4
+ branches: string[];
5
+ commits: string[];
6
+ }
7
+ interface UpdateGroup {
8
+ key: string;
9
+ items: PackageUpdate[];
10
+ }
11
+ interface PlannedBatch {
12
+ index: number;
13
+ groups: UpdateGroup[];
14
+ updates: PackageUpdate[];
15
+ branchName: string;
16
+ }
17
+ export declare function applyFixPrBatches(options: RunOptions, result: CheckResult): Promise<FixPrBatchResult>;
18
+ export declare function planFixPrBatches(groups: UpdateGroup[], baseBranch: string, batchSize: number): PlannedBatch[];
19
+ export {};
@@ -0,0 +1,172 @@
1
+ import { spawn } from "node:child_process";
2
+ import { promises as fs } from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { readManifest, writeManifest } from "../parsers/package-json.js";
6
+ export async function applyFixPrBatches(options, result) {
7
+ if (!options.fixPr || result.updates.length === 0) {
8
+ return { applied: false, branches: [], commits: [] };
9
+ }
10
+ const baseRef = await resolveBaseRef(options.cwd, options.fixPrNoCheckout);
11
+ const groups = groupUpdates(result.updates, options.groupBy);
12
+ const plans = planFixPrBatches(groups, options.fixBranch ?? "chore/rainy-updates", options.fixPrBatchSize ?? 1);
13
+ if (options.fixDryRun) {
14
+ return { applied: false, branches: plans.map((plan) => plan.branchName), commits: [] };
15
+ }
16
+ const branches = [];
17
+ const commits = [];
18
+ for (const plan of plans) {
19
+ const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "rainy-fix-pr-batch-"));
20
+ try {
21
+ await runGit(options.cwd, ["worktree", "add", "-B", plan.branchName, tempDir, baseRef]);
22
+ await applyUpdatesInWorktree(options.cwd, tempDir, plan.updates);
23
+ await stageUpdatedManifests(options.cwd, tempDir, plan.updates);
24
+ const message = renderCommitMessage(options, plan, plans.length);
25
+ await runGit(tempDir, ["commit", "-m", message]);
26
+ const rev = await runGit(tempDir, ["rev-parse", "HEAD"]);
27
+ branches.push(plan.branchName);
28
+ commits.push(rev.stdout.trim());
29
+ }
30
+ finally {
31
+ await runGit(options.cwd, ["worktree", "remove", "--force", tempDir], true);
32
+ }
33
+ }
34
+ return {
35
+ applied: branches.length > 0,
36
+ branches,
37
+ commits,
38
+ };
39
+ }
40
+ export function planFixPrBatches(groups, baseBranch, batchSize) {
41
+ if (groups.length === 0)
42
+ return [];
43
+ const size = Math.max(1, Math.floor(batchSize));
44
+ const chunks = [];
45
+ for (let index = 0; index < groups.length; index += size) {
46
+ chunks.push(groups.slice(index, index + size));
47
+ }
48
+ return chunks.map((chunk, index) => {
49
+ const suffix = chunk.length === 1 ? sanitizeBranchToken(chunk[0]?.key ?? `batch-${index + 1}`) : `batch-${index + 1}`;
50
+ return {
51
+ index: index + 1,
52
+ groups: chunk,
53
+ updates: chunk.flatMap((item) => item.items),
54
+ branchName: `${baseBranch}-${suffix}`,
55
+ };
56
+ });
57
+ }
58
+ function groupUpdates(updates, groupBy) {
59
+ if (updates.length === 0)
60
+ return [];
61
+ if (groupBy === "none") {
62
+ return [{ key: "all", items: sortUpdates(updates) }];
63
+ }
64
+ const byGroup = new Map();
65
+ for (const update of updates) {
66
+ const key = groupKey(update, groupBy);
67
+ byGroup.set(key, [...(byGroup.get(key) ?? []), update]);
68
+ }
69
+ return Array.from(byGroup.entries())
70
+ .map(([key, items]) => ({ key, items: sortUpdates(items) }))
71
+ .sort((left, right) => left.key.localeCompare(right.key));
72
+ }
73
+ function groupKey(update, groupBy) {
74
+ if (groupBy === "name")
75
+ return update.name;
76
+ if (groupBy === "kind")
77
+ return update.kind;
78
+ if (groupBy === "risk")
79
+ return update.diffType;
80
+ if (groupBy === "scope") {
81
+ if (update.name.startsWith("@")) {
82
+ const slash = update.name.indexOf("/");
83
+ if (slash > 1)
84
+ return update.name.slice(0, slash);
85
+ }
86
+ return "unscoped";
87
+ }
88
+ return "all";
89
+ }
90
+ function sortUpdates(updates) {
91
+ return [...updates].sort((left, right) => {
92
+ const byPath = left.packagePath.localeCompare(right.packagePath);
93
+ if (byPath !== 0)
94
+ return byPath;
95
+ const byName = left.name.localeCompare(right.name);
96
+ if (byName !== 0)
97
+ return byName;
98
+ return left.kind.localeCompare(right.kind);
99
+ });
100
+ }
101
+ async function resolveBaseRef(cwd, allowDetached) {
102
+ const status = await runGit(cwd, ["status", "--porcelain"]);
103
+ if (status.stdout.trim().length > 0) {
104
+ throw new Error("Cannot run --fix-pr with a dirty git working tree.");
105
+ }
106
+ const headRef = await runGit(cwd, ["symbolic-ref", "--quiet", "--short", "HEAD"], true);
107
+ if (headRef.code === 0) {
108
+ return headRef.stdout.trim();
109
+ }
110
+ if (!allowDetached) {
111
+ throw new Error("Cannot run --fix-pr in detached HEAD state without --fix-pr-no-checkout.");
112
+ }
113
+ const rev = await runGit(cwd, ["rev-parse", "HEAD"]);
114
+ return rev.stdout.trim();
115
+ }
116
+ async function applyUpdatesInWorktree(rootCwd, worktreeCwd, updates) {
117
+ const manifestsByPath = new Map();
118
+ for (const update of updates) {
119
+ const relativePackagePath = path.relative(rootCwd, update.packagePath);
120
+ const targetPackagePath = path.resolve(worktreeCwd, relativePackagePath);
121
+ let manifest = manifestsByPath.get(targetPackagePath);
122
+ if (!manifest) {
123
+ manifest = await readManifest(targetPackagePath);
124
+ manifestsByPath.set(targetPackagePath, manifest);
125
+ }
126
+ const section = manifest[update.kind];
127
+ if (!section || !section[update.name])
128
+ continue;
129
+ section[update.name] = update.toRange;
130
+ }
131
+ for (const [manifestPath, manifest] of manifestsByPath) {
132
+ await writeManifest(manifestPath, manifest);
133
+ }
134
+ }
135
+ async function stageUpdatedManifests(rootCwd, worktreeCwd, updates) {
136
+ const files = Array.from(new Set(updates.map((update) => {
137
+ const relativePackagePath = path.relative(rootCwd, update.packagePath);
138
+ return path.resolve(worktreeCwd, relativePackagePath, "package.json");
139
+ }))).sort((a, b) => a.localeCompare(b));
140
+ if (files.length > 0) {
141
+ await runGit(worktreeCwd, ["add", "--", ...files]);
142
+ }
143
+ }
144
+ function renderCommitMessage(options, plan, totalBatches) {
145
+ const baseMessage = options.fixCommitMessage ?? `chore(deps): apply rainy-updates batch`;
146
+ return `${baseMessage} (${plan.index}/${totalBatches}, ${plan.updates.length} updates)`;
147
+ }
148
+ function sanitizeBranchToken(value) {
149
+ return value.replace(/^@/, "").replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "batch";
150
+ }
151
+ async function runGit(cwd, args, allowNonZero = false) {
152
+ return await new Promise((resolve, reject) => {
153
+ const child = spawn("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
154
+ let stdout = "";
155
+ let stderr = "";
156
+ child.stdout.on("data", (chunk) => {
157
+ stdout += typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8");
158
+ });
159
+ child.stderr.on("data", (chunk) => {
160
+ stderr += typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8");
161
+ });
162
+ child.on("error", reject);
163
+ child.on("exit", (code) => {
164
+ const normalized = code ?? 1;
165
+ if (normalized !== 0 && !allowNonZero) {
166
+ reject(new Error(`git ${args.join(" ")} failed (${normalized}): ${stderr.trim()}`));
167
+ return;
168
+ }
169
+ resolve({ code: normalized, stdout, stderr });
170
+ });
171
+ });
172
+ }
@@ -41,6 +41,7 @@ export async function parseCliArgs(argv) {
41
41
  fixCommitMessage: undefined,
42
42
  fixDryRun: false,
43
43
  fixPrNoCheckout: false,
44
+ fixPrBatchSize: undefined,
44
45
  noPrReport: false,
45
46
  logLevel: "info",
46
47
  groupBy: "none",
@@ -220,6 +221,18 @@ export async function parseCliArgs(argv) {
220
221
  base.fixPrNoCheckout = true;
221
222
  continue;
222
223
  }
224
+ if (current === "--fix-pr-batch-size" && next) {
225
+ const parsed = Number(next);
226
+ if (!Number.isInteger(parsed) || parsed < 1) {
227
+ throw new Error("--fix-pr-batch-size must be a positive integer");
228
+ }
229
+ base.fixPrBatchSize = parsed;
230
+ index += 1;
231
+ continue;
232
+ }
233
+ if (current === "--fix-pr-batch-size") {
234
+ throw new Error("Missing value for --fix-pr-batch-size");
235
+ }
223
236
  if (current === "--log-level" && next) {
224
237
  base.logLevel = ensureLogLevel(next);
225
238
  index += 1;
@@ -485,6 +498,9 @@ function applyConfig(base, config) {
485
498
  if (typeof config.fixPrNoCheckout === "boolean") {
486
499
  base.fixPrNoCheckout = config.fixPrNoCheckout;
487
500
  }
501
+ if (typeof config.fixPrBatchSize === "number" && Number.isInteger(config.fixPrBatchSize) && config.fixPrBatchSize > 0) {
502
+ base.fixPrBatchSize = config.fixPrBatchSize;
503
+ }
488
504
  if (typeof config.noPrReport === "boolean") {
489
505
  base.noPrReport = config.noPrReport;
490
506
  }
@@ -33,6 +33,7 @@ export function createSummary(input) {
33
33
  fixPrApplied: false,
34
34
  fixBranchName: "",
35
35
  fixCommitSha: "",
36
+ fixPrBranchesCreated: 0,
36
37
  groupedUpdates: Math.max(0, Math.round(input.groupedUpdates ?? 0)),
37
38
  cooldownSkipped: Math.max(0, Math.round(input.cooldownSkipped ?? 0)),
38
39
  ciProfile: input.ciProfile ?? "minimal",
@@ -36,6 +36,7 @@ export function renderResult(result, format) {
36
36
  `cooldown_skipped=${result.summary.cooldownSkipped}`,
37
37
  `ci_profile=${result.summary.ciProfile}`,
38
38
  `pr_limit_hit=${result.summary.prLimitHit ? "1" : "0"}`,
39
+ `fix_pr_branches_created=${result.summary.fixPrBranchesCreated}`,
39
40
  ].join("\n");
40
41
  }
41
42
  const lines = [];
@@ -78,6 +79,7 @@ export function renderResult(result, format) {
78
79
  lines.push(`Contract v${result.summary.contractVersion}, failReason=${result.summary.failReason}, duration=${result.summary.durationMs.total}ms`);
79
80
  if (result.summary.fixPrApplied) {
80
81
  lines.push(`Fix PR: applied on branch ${result.summary.fixBranchName ?? "unknown"} (${result.summary.fixCommitSha ?? "no-commit"})`);
82
+ lines.push(`Fix PR batches created: ${result.summary.fixPrBranchesCreated}`);
81
83
  }
82
84
  return lines.join("\n");
83
85
  }
@@ -19,6 +19,7 @@ export async function writeGitHubOutput(filePath, result) {
19
19
  `ci_profile=${result.summary.ciProfile}`,
20
20
  `pr_limit_hit=${result.summary.prLimitHit === true ? "1" : "0"}`,
21
21
  `fix_pr_applied=${result.summary.fixPrApplied === true ? "1" : "0"}`,
22
+ `fix_pr_branches_created=${result.summary.fixPrBranchesCreated}`,
22
23
  `fix_pr_branch=${result.summary.fixBranchName ?? ""}`,
23
24
  `fix_pr_commit=${result.summary.fixCommitSha ?? ""}`,
24
25
  ];
@@ -11,6 +11,7 @@ export function renderPrReport(result) {
11
11
  lines.push(`- Cooldown skipped: ${result.summary.cooldownSkipped}`);
12
12
  lines.push(`- CI profile: ${result.summary.ciProfile}`);
13
13
  lines.push(`- PR limit hit: ${result.summary.prLimitHit ? "yes" : "no"}`);
14
+ lines.push(`- Fix PR branches created: ${result.summary.fixPrBranchesCreated}`);
14
15
  lines.push("");
15
16
  if (result.updates.length > 0) {
16
17
  lines.push("## Proposed Updates");
@@ -73,6 +73,7 @@ export function createSarifReport(result) {
73
73
  warningsCount: result.summary.warningCounts.total,
74
74
  ciProfile: result.summary.ciProfile,
75
75
  prLimitHit: result.summary.prLimitHit,
76
+ fixPrBranchesCreated: result.summary.fixPrBranchesCreated,
76
77
  durationMs: result.summary.durationMs,
77
78
  },
78
79
  },
@@ -30,6 +30,7 @@ export interface RunOptions {
30
30
  fixCommitMessage?: string;
31
31
  fixDryRun?: boolean;
32
32
  fixPrNoCheckout?: boolean;
33
+ fixPrBatchSize?: number;
33
34
  noPrReport?: boolean;
34
35
  logLevel: LogLevel;
35
36
  groupBy: GroupBy;
@@ -100,6 +101,7 @@ export interface Summary {
100
101
  fixPrApplied: boolean;
101
102
  fixBranchName: string;
102
103
  fixCommitSha: string;
104
+ fixPrBranchesCreated: number;
103
105
  groupedUpdates: number;
104
106
  cooldownSkipped: number;
105
107
  ciProfile: CiProfile;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rainy-updates/cli",
3
- "version": "0.5.1-rc.1",
3
+ "version": "0.5.1-rc.2",
4
4
  "description": "Agentic CLI to check and upgrade npm/pnpm dependencies for CI workflows",
5
5
  "type": "module",
6
6
  "private": false,