@rainy-updates/cli 0.5.0 → 0.5.1-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 CHANGED
@@ -2,6 +2,39 @@
2
2
 
3
3
  All notable changes to this project are documented in this file.
4
4
 
5
+ ## [0.5.1-rc.1] - 2026-02-27
6
+
7
+ ### Added
8
+
9
+ - New `ci` command for CI-first orchestration:
10
+ - profile-driven automation with `--mode minimal|strict|enterprise`,
11
+ - warm-cache + check/upgrade flow with deterministic artifacts.
12
+ - New rollout and orchestration flags:
13
+ - `--group-by none|name|scope|kind|risk`
14
+ - `--group-max <n>`
15
+ - `--cooldown-days <n>`
16
+ - `--pr-limit <n>`
17
+ - `--only-changed`
18
+ - Additive summary/output contract fields:
19
+ - `groupedUpdates`
20
+ - `cooldownSkipped`
21
+ - `ciProfile`
22
+ - `prLimitHit`
23
+ - Policy schema extensions:
24
+ - global `cooldownDays`
25
+ - package rule `group` and `priority`
26
+
27
+ ### Changed
28
+
29
+ - `check` now supports cooldown-aware filtering when publish timestamps are available.
30
+ - CI workflow templates generated by `init-ci` now use `rainy-updates ci` with explicit profile mode.
31
+ - GitHub output, metrics output, SARIF properties, and PR report include new CI orchestration metadata.
32
+
33
+ ### Tests
34
+
35
+ - Added parser coverage for `ci` command orchestration flags.
36
+ - Extended workflow, policy, summary, and output tests for new metadata and profile behavior.
37
+
5
38
  ## [0.5.0] - 2026-02-27
6
39
 
7
40
  ### Changed
package/README.md CHANGED
@@ -24,6 +24,7 @@ pnpm add -D @rainy-updates/cli
24
24
 
25
25
  - `check`: analyze dependencies and report available updates.
26
26
  - `upgrade`: rewrite dependency ranges in manifests, optionally install lockfile updates.
27
+ - `ci`: run CI-focused dependency automation (warm cache, check/upgrade, policy gates).
27
28
  - `warm-cache`: prefetch package metadata for fast and offline checks.
28
29
  - `baseline`: save and compare dependency baseline snapshots.
29
30
 
@@ -36,6 +37,9 @@ npx @rainy-updates/cli check --format table
36
37
  # 2) Strict CI mode (non-zero when updates exist)
37
38
  npx @rainy-updates/cli check --workspace --ci --format json --json-file .artifacts/updates.json
38
39
 
40
+ # 2b) CI orchestration mode
41
+ npx @rainy-updates/cli ci --workspace --mode strict --format github --json-file .artifacts/updates.json
42
+
39
43
  # 3) Apply upgrades with workspace sync
40
44
  npx @rainy-updates/cli upgrade --target latest --workspace --sync --install
41
45
 
@@ -151,6 +155,12 @@ Schedule:
151
155
  - `--offline`
152
156
  - `--fail-on none|patch|minor|major|any`
153
157
  - `--max-updates <n>`
158
+ - `--group-by none|name|scope|kind|risk`
159
+ - `--group-max <n>`
160
+ - `--cooldown-days <n>`
161
+ - `--pr-limit <n>`
162
+ - `--only-changed`
163
+ - `--mode minimal|strict|enterprise` (for `ci`)
154
164
  - `--policy-file <path>`
155
165
  - `--format table|json|minimal|github`
156
166
  - `--json-file <path>`
package/dist/bin/cli.js CHANGED
@@ -7,6 +7,7 @@ import { parseCliArgs } from "../core/options.js";
7
7
  import { check } from "../core/check.js";
8
8
  import { upgrade } from "../core/upgrade.js";
9
9
  import { warmCache } from "../core/warm-cache.js";
10
+ import { runCi } from "../core/ci.js";
10
11
  import { initCiWorkflow } from "../core/init-ci.js";
11
12
  import { diffBaseline, saveBaseline } from "../core/baseline.js";
12
13
  import { applyFixPr } from "../core/fix-pr.js";
@@ -69,7 +70,7 @@ async function main() {
69
70
  const markdown = renderPrReport(result);
70
71
  await writeFileAtomic(parsed.options.prReportFile, markdown + "\n");
71
72
  }
72
- if (parsed.options.fixPr && (parsed.command === "check" || parsed.command === "upgrade")) {
73
+ if (parsed.options.fixPr && (parsed.command === "check" || parsed.command === "upgrade" || parsed.command === "ci")) {
73
74
  result.summary.fixPrApplied = false;
74
75
  result.summary.fixBranchName = parsed.options.fixBranch ?? "chore/rainy-updates";
75
76
  result.summary.fixCommitSha = "";
@@ -85,6 +86,13 @@ async function main() {
85
86
  if (parsed.options.format === "json" || parsed.options.format === "metrics") {
86
87
  rendered = renderResult(result, parsed.options.format);
87
88
  }
89
+ if (parsed.options.onlyChanged &&
90
+ result.updates.length === 0 &&
91
+ result.errors.length === 0 &&
92
+ result.warnings.length === 0 &&
93
+ (parsed.options.format === "table" || parsed.options.format === "minimal" || parsed.options.format === "github")) {
94
+ rendered = "";
95
+ }
88
96
  if (parsed.options.jsonFile) {
89
97
  await writeFileAtomic(parsed.options.jsonFile, stableStringify(result, 2) + "\n");
90
98
  }
@@ -133,6 +141,11 @@ Options:
133
141
  --pr-report-file <path>
134
142
  --fail-on none|patch|minor|major|any
135
143
  --max-updates <n>
144
+ --group-by none|name|scope|kind|risk
145
+ --group-max <n>
146
+ --cooldown-days <n>
147
+ --pr-limit <n>
148
+ --only-changed
136
149
  --log-level error|warn|info|debug
137
150
  --ci`;
138
151
  }
@@ -176,6 +189,36 @@ Options:
176
189
  --no-pr-report
177
190
  --json-file <path>
178
191
  --pr-report-file <path>`;
192
+ }
193
+ if (isCommand && command === "ci") {
194
+ return `rainy-updates ci [options]
195
+
196
+ Run CI-oriented dependency automation pipeline.
197
+
198
+ Options:
199
+ --workspace
200
+ --mode minimal|strict|enterprise
201
+ --group-by none|name|scope|kind|risk
202
+ --group-max <n>
203
+ --cooldown-days <n>
204
+ --pr-limit <n>
205
+ --only-changed
206
+ --offline
207
+ --concurrency <n>
208
+ --fix-pr
209
+ --fix-branch <name>
210
+ --fix-commit-message <text>
211
+ --fix-dry-run
212
+ --fix-pr-no-checkout
213
+ --no-pr-report
214
+ --json-file <path>
215
+ --github-output <path>
216
+ --sarif-file <path>
217
+ --pr-report-file <path>
218
+ --fail-on none|patch|minor|major|any
219
+ --max-updates <n>
220
+ --log-level error|warn|info|debug
221
+ --ci`;
179
222
  }
180
223
  if (isCommand && command === "init-ci") {
181
224
  return `rainy-updates init-ci [options]
@@ -206,6 +249,7 @@ Options:
206
249
  Commands:
207
250
  check Detect available updates
208
251
  upgrade Apply updates to manifests
252
+ ci Run CI-focused update pipeline
209
253
  warm-cache Warm local cache for fast/offline checks
210
254
  init-ci Scaffold GitHub Actions workflow
211
255
  baseline Save/check dependency baseline snapshots
@@ -222,6 +266,12 @@ Global options:
222
266
  --policy-file <path>
223
267
  --fail-on none|patch|minor|major|any
224
268
  --max-updates <n>
269
+ --group-by none|name|scope|kind|risk
270
+ --group-max <n>
271
+ --cooldown-days <n>
272
+ --pr-limit <n>
273
+ --only-changed
274
+ --mode minimal|strict|enterprise
225
275
  --fix-pr
226
276
  --fix-branch <name>
227
277
  --fix-commit-message <text>
@@ -243,6 +293,9 @@ async function runCommand(parsed) {
243
293
  if (parsed.command === "warm-cache") {
244
294
  return await warmCache(parsed.options);
245
295
  }
296
+ if (parsed.command === "ci") {
297
+ return await runCi(parsed.options);
298
+ }
246
299
  if (parsed.options.fixPr) {
247
300
  const upgradeOptions = {
248
301
  ...parsed.options,
@@ -1,4 +1,4 @@
1
- import type { DependencyKind, FailOnLevel, LogLevel, OutputFormat, TargetLevel } from "../types/index.js";
1
+ import type { CiProfile, DependencyKind, FailOnLevel, GroupBy, LogLevel, OutputFormat, TargetLevel } from "../types/index.js";
2
2
  export interface FileConfig {
3
3
  target?: TargetLevel;
4
4
  filter?: string;
@@ -24,6 +24,12 @@ export interface FileConfig {
24
24
  fixPrNoCheckout?: boolean;
25
25
  noPrReport?: boolean;
26
26
  logLevel?: LogLevel;
27
+ groupBy?: GroupBy;
28
+ groupMax?: number;
29
+ cooldownDays?: number;
30
+ prLimit?: number;
31
+ onlyChanged?: boolean;
32
+ ciProfile?: CiProfile;
27
33
  install?: boolean;
28
34
  packageManager?: "auto" | "npm" | "pnpm";
29
35
  sync?: boolean;
@@ -1,6 +1,7 @@
1
1
  import type { TargetLevel } from "../types/index.js";
2
2
  export interface PolicyConfig {
3
3
  ignore?: string[];
4
+ cooldownDays?: number;
4
5
  packageRules?: Record<string, {
5
6
  match?: string;
6
7
  maxTarget?: TargetLevel;
@@ -8,6 +9,8 @@ export interface PolicyConfig {
8
9
  maxUpdatesPerRun?: number;
9
10
  cooldownDays?: number;
10
11
  allowPrerelease?: boolean;
12
+ group?: string;
13
+ priority?: number;
11
14
  }>;
12
15
  }
13
16
  export interface PolicyRule {
@@ -17,9 +20,12 @@ export interface PolicyRule {
17
20
  maxUpdatesPerRun?: number;
18
21
  cooldownDays?: number;
19
22
  allowPrerelease?: boolean;
23
+ group?: string;
24
+ priority?: number;
20
25
  }
21
26
  export interface ResolvedPolicy {
22
27
  ignorePatterns: string[];
28
+ cooldownDays?: number;
23
29
  packageRules: Map<string, PolicyRule>;
24
30
  matchRules: PolicyRule[];
25
31
  }
@@ -12,6 +12,7 @@ export async function loadPolicy(cwd, policyFile) {
12
12
  const parsed = JSON.parse(content);
13
13
  return {
14
14
  ignorePatterns: parsed.ignore ?? [],
15
+ cooldownDays: asNonNegativeInt(parsed.cooldownDays),
15
16
  packageRules: new Map(Object.entries(parsed.packageRules ?? {}).map(([pkg, rule]) => [pkg, normalizeRule(rule)])),
16
17
  matchRules: Object.values(parsed.packageRules ?? {})
17
18
  .map((rule) => normalizeRule(rule))
@@ -24,6 +25,7 @@ export async function loadPolicy(cwd, policyFile) {
24
25
  }
25
26
  return {
26
27
  ignorePatterns: [],
28
+ cooldownDays: undefined,
27
29
  packageRules: new Map(),
28
30
  matchRules: [],
29
31
  };
@@ -42,6 +44,8 @@ function normalizeRule(rule) {
42
44
  maxUpdatesPerRun: asNonNegativeInt(rule.maxUpdatesPerRun),
43
45
  cooldownDays: asNonNegativeInt(rule.cooldownDays),
44
46
  allowPrerelease: rule.allowPrerelease === true,
47
+ group: typeof rule.group === "string" && rule.group.trim().length > 0 ? rule.group.trim() : undefined,
48
+ priority: asNonNegativeInt(rule.priority),
45
49
  };
46
50
  }
47
51
  function asNonNegativeInt(value) {
@@ -29,6 +29,7 @@ export async function check(options) {
29
29
  let totalDependencies = 0;
30
30
  const tasks = [];
31
31
  let skipped = 0;
32
+ let cooldownSkipped = 0;
32
33
  for (const packageDir of packageDirs) {
33
34
  let manifest;
34
35
  try {
@@ -67,6 +68,7 @@ export async function check(options) {
67
68
  resolvedVersions.set(packageName, {
68
69
  latestVersion: cached.latestVersion,
69
70
  availableVersions: cached.availableVersions,
71
+ publishedAtByVersion: {},
70
72
  });
71
73
  }
72
74
  else {
@@ -83,6 +85,7 @@ export async function check(options) {
83
85
  resolvedVersions.set(packageName, {
84
86
  latestVersion: stale.latestVersion,
85
87
  availableVersions: stale.availableVersions,
88
+ publishedAtByVersion: {},
86
89
  });
87
90
  warnings.push(`Using stale cache for ${packageName} because --offline is enabled.`);
88
91
  }
@@ -103,6 +106,7 @@ export async function check(options) {
103
106
  resolvedVersions.set(packageName, {
104
107
  latestVersion: metadata.latestVersion,
105
108
  availableVersions: metadata.versions,
109
+ publishedAtByVersion: metadata.publishedAtByVersion,
106
110
  });
107
111
  if (metadata.latestVersion) {
108
112
  await cache.set(packageName, options.target, metadata.latestVersion, metadata.versions, options.cacheTtlSeconds);
@@ -116,6 +120,7 @@ export async function check(options) {
116
120
  resolvedVersions.set(packageName, {
117
121
  latestVersion: stale.latestVersion,
118
122
  availableVersions: stale.availableVersions,
123
+ publishedAtByVersion: {},
119
124
  });
120
125
  warnings.push(`Using stale cache for ${packageName} due to registry error: ${error}`);
121
126
  }
@@ -135,6 +140,10 @@ export async function check(options) {
135
140
  const picked = pickTargetVersionFromAvailable(task.dependency.range, metadata.availableVersions, metadata.latestVersion, effectiveTarget);
136
141
  if (!picked)
137
142
  continue;
143
+ if (shouldSkipByCooldown(picked, metadata.publishedAtByVersion, options.cooldownDays, policy.cooldownDays, rule?.cooldownDays)) {
144
+ cooldownSkipped += 1;
145
+ continue;
146
+ }
138
147
  const nextRange = applyRangeStyle(task.dependency.range, picked);
139
148
  if (nextRange === task.dependency.range)
140
149
  continue;
@@ -150,7 +159,14 @@ export async function check(options) {
150
159
  reason: rule?.maxTarget ? `policy maxTarget=${rule.maxTarget}` : undefined,
151
160
  });
152
161
  }
153
- const limitedUpdates = sortUpdates(applyRuleUpdateCaps(updates, policy));
162
+ const grouped = groupUpdates(updates, options.groupBy);
163
+ const groupedUpdates = grouped.length;
164
+ const groupedSorted = sortUpdates(grouped.flatMap((group) => group.items));
165
+ const groupedCapped = typeof options.groupMax === "number" ? groupedSorted.slice(0, options.groupMax) : groupedSorted;
166
+ const ruleLimited = applyRuleUpdateCaps(groupedCapped, policy);
167
+ const prLimited = typeof options.prLimit === "number" ? ruleLimited.slice(0, options.prLimit) : ruleLimited;
168
+ const limitedUpdates = sortUpdates(prLimited);
169
+ const prLimitHit = typeof options.prLimit === "number" && groupedSorted.length > options.prLimit;
154
170
  const sortedErrors = [...errors].sort((a, b) => a.localeCompare(b));
155
171
  const sortedWarnings = [...warnings].sort((a, b) => a.localeCompare(b));
156
172
  const summary = finalizeSummary(createSummary({
@@ -169,6 +185,10 @@ export async function check(options) {
169
185
  registryMs,
170
186
  cacheMs,
171
187
  },
188
+ groupedUpdates,
189
+ cooldownSkipped,
190
+ ciProfile: options.ciProfile,
191
+ prLimitHit,
172
192
  }));
173
193
  return {
174
194
  projectPath: options.cwd,
@@ -182,6 +202,51 @@ export async function check(options) {
182
202
  warnings: sortedWarnings,
183
203
  };
184
204
  }
205
+ function groupUpdates(updates, groupBy) {
206
+ if (updates.length === 0) {
207
+ return [];
208
+ }
209
+ if (groupBy === "none") {
210
+ return [{ key: "all", items: [...updates] }];
211
+ }
212
+ const byGroup = new Map();
213
+ for (const update of updates) {
214
+ const key = groupKey(update, groupBy);
215
+ const current = byGroup.get(key) ?? [];
216
+ current.push(update);
217
+ byGroup.set(key, current);
218
+ }
219
+ return Array.from(byGroup.entries())
220
+ .map(([key, items]) => ({ key, items: sortUpdates(items) }))
221
+ .sort((left, right) => left.key.localeCompare(right.key));
222
+ }
223
+ function groupKey(update, groupBy) {
224
+ if (groupBy === "name")
225
+ return update.name;
226
+ if (groupBy === "kind")
227
+ return update.kind;
228
+ if (groupBy === "risk")
229
+ return update.diffType;
230
+ if (groupBy === "scope") {
231
+ if (update.name.startsWith("@")) {
232
+ const slash = update.name.indexOf("/");
233
+ if (slash > 1)
234
+ return update.name.slice(0, slash);
235
+ }
236
+ return "unscoped";
237
+ }
238
+ return "all";
239
+ }
240
+ function shouldSkipByCooldown(pickedVersion, publishedAtByVersion, optionCooldownDays, policyCooldownDays, ruleCooldownDays) {
241
+ const cooldownDays = ruleCooldownDays ?? optionCooldownDays ?? policyCooldownDays;
242
+ if (typeof cooldownDays !== "number" || cooldownDays <= 0)
243
+ return false;
244
+ const publishedAt = publishedAtByVersion[pickedVersion];
245
+ if (typeof publishedAt !== "number" || !Number.isFinite(publishedAt))
246
+ return false;
247
+ const threshold = Date.now() - cooldownDays * 24 * 60 * 60 * 1000;
248
+ return publishedAt > threshold;
249
+ }
185
250
  function applyRuleUpdateCaps(updates, policy) {
186
251
  const limited = [];
187
252
  const seenPerPackage = new Map();
@@ -0,0 +1,2 @@
1
+ import type { CheckOptions, CheckResult } from "../types/index.js";
2
+ export declare function runCi(options: CheckOptions): Promise<CheckResult>;
@@ -0,0 +1,30 @@
1
+ import { check } from "./check.js";
2
+ import { warmCache } from "./warm-cache.js";
3
+ import { upgrade } from "./upgrade.js";
4
+ export async function runCi(options) {
5
+ const profile = options.ciProfile;
6
+ if (profile !== "minimal") {
7
+ await warmCache({
8
+ ...options,
9
+ offline: false,
10
+ ci: true,
11
+ format: "minimal",
12
+ });
13
+ }
14
+ const checkOptions = {
15
+ ...options,
16
+ ci: true,
17
+ offline: profile === "minimal" ? options.offline : true,
18
+ concurrency: profile === "enterprise" ? Math.max(options.concurrency, 32) : options.concurrency,
19
+ };
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
+ return await check(checkOptions);
30
+ }
@@ -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 check \\\n --workspace \\\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 --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 check \\\n --workspace \\\n --offline \\\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\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`;
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 check \\\n --workspace \\\n --offline \\\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\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`;
59
59
  }
@@ -9,6 +9,9 @@ export type ParsedCliArgs = {
9
9
  } | {
10
10
  command: "warm-cache";
11
11
  options: CheckOptions;
12
+ } | {
13
+ command: "ci";
14
+ options: CheckOptions;
12
15
  } | {
13
16
  command: "init-ci";
14
17
  options: {
@@ -7,7 +7,7 @@ const DEFAULT_INCLUDE_KINDS = [
7
7
  "optionalDependencies",
8
8
  "peerDependencies",
9
9
  ];
10
- const KNOWN_COMMANDS = ["check", "upgrade", "warm-cache", "init-ci", "baseline"];
10
+ const KNOWN_COMMANDS = ["check", "upgrade", "warm-cache", "init-ci", "baseline", "ci"];
11
11
  export async function parseCliArgs(argv) {
12
12
  const firstArg = argv[0];
13
13
  const isKnownCommand = KNOWN_COMMANDS.includes(firstArg);
@@ -43,6 +43,12 @@ export async function parseCliArgs(argv) {
43
43
  fixPrNoCheckout: false,
44
44
  noPrReport: false,
45
45
  logLevel: "info",
46
+ groupBy: "none",
47
+ groupMax: undefined,
48
+ cooldownDays: undefined,
49
+ prLimit: undefined,
50
+ onlyChanged: false,
51
+ ciProfile: "minimal",
46
52
  };
47
53
  let force = false;
48
54
  let initCiMode = "enterprise";
@@ -237,7 +243,12 @@ export async function parseCliArgs(argv) {
237
243
  throw new Error("Missing value for --pm");
238
244
  }
239
245
  if (current === "--mode" && next) {
240
- initCiMode = ensureInitCiMode(next);
246
+ if (command === "init-ci") {
247
+ initCiMode = ensureInitCiMode(next);
248
+ }
249
+ else {
250
+ base.ciProfile = ensureCiProfile(next);
251
+ }
241
252
  index += 1;
242
253
  continue;
243
254
  }
@@ -280,6 +291,54 @@ export async function parseCliArgs(argv) {
280
291
  if (current === "--max-updates") {
281
292
  throw new Error("Missing value for --max-updates");
282
293
  }
294
+ if (current === "--group-by" && next) {
295
+ base.groupBy = ensureGroupBy(next);
296
+ index += 1;
297
+ continue;
298
+ }
299
+ if (current === "--group-by") {
300
+ throw new Error("Missing value for --group-by");
301
+ }
302
+ if (current === "--group-max" && next) {
303
+ const parsed = Number(next);
304
+ if (!Number.isInteger(parsed) || parsed < 1) {
305
+ throw new Error("--group-max must be a positive integer");
306
+ }
307
+ base.groupMax = parsed;
308
+ index += 1;
309
+ continue;
310
+ }
311
+ if (current === "--group-max") {
312
+ throw new Error("Missing value for --group-max");
313
+ }
314
+ if (current === "--cooldown-days" && next) {
315
+ const parsed = Number(next);
316
+ if (!Number.isInteger(parsed) || parsed < 0) {
317
+ throw new Error("--cooldown-days must be a non-negative integer");
318
+ }
319
+ base.cooldownDays = parsed;
320
+ index += 1;
321
+ continue;
322
+ }
323
+ if (current === "--cooldown-days") {
324
+ throw new Error("Missing value for --cooldown-days");
325
+ }
326
+ if (current === "--pr-limit" && next) {
327
+ const parsed = Number(next);
328
+ if (!Number.isInteger(parsed) || parsed < 1) {
329
+ throw new Error("--pr-limit must be a positive integer");
330
+ }
331
+ base.prLimit = parsed;
332
+ index += 1;
333
+ continue;
334
+ }
335
+ if (current === "--pr-limit") {
336
+ throw new Error("Missing value for --pr-limit");
337
+ }
338
+ if (current === "--only-changed") {
339
+ base.onlyChanged = true;
340
+ continue;
341
+ }
283
342
  if (current === "--save") {
284
343
  baselineAction = "save";
285
344
  continue;
@@ -336,6 +395,9 @@ export async function parseCliArgs(argv) {
336
395
  if (command === "warm-cache") {
337
396
  return { command, options: base };
338
397
  }
398
+ if (command === "ci") {
399
+ return { command, options: base };
400
+ }
339
401
  if (command === "init-ci") {
340
402
  return {
341
403
  command,
@@ -429,6 +491,24 @@ function applyConfig(base, config) {
429
491
  if (typeof config.logLevel === "string") {
430
492
  base.logLevel = ensureLogLevel(config.logLevel);
431
493
  }
494
+ if (typeof config.groupBy === "string") {
495
+ base.groupBy = ensureGroupBy(config.groupBy);
496
+ }
497
+ if (typeof config.groupMax === "number" && Number.isInteger(config.groupMax) && config.groupMax > 0) {
498
+ base.groupMax = config.groupMax;
499
+ }
500
+ if (typeof config.cooldownDays === "number" && Number.isInteger(config.cooldownDays) && config.cooldownDays >= 0) {
501
+ base.cooldownDays = config.cooldownDays;
502
+ }
503
+ if (typeof config.prLimit === "number" && Number.isInteger(config.prLimit) && config.prLimit > 0) {
504
+ base.prLimit = config.prLimit;
505
+ }
506
+ if (typeof config.onlyChanged === "boolean") {
507
+ base.onlyChanged = config.onlyChanged;
508
+ }
509
+ if (typeof config.ciProfile === "string") {
510
+ base.ciProfile = ensureCiProfile(config.ciProfile);
511
+ }
432
512
  }
433
513
  function parsePackageManager(args) {
434
514
  const index = args.indexOf("--pm");
@@ -497,3 +577,15 @@ function ensureFailOn(value) {
497
577
  }
498
578
  throw new Error("--fail-on must be none, patch, minor, major or any");
499
579
  }
580
+ function ensureGroupBy(value) {
581
+ if (value === "none" || value === "name" || value === "scope" || value === "kind" || value === "risk") {
582
+ return value;
583
+ }
584
+ throw new Error("--group-by must be none, name, scope, kind or risk");
585
+ }
586
+ function ensureCiProfile(value) {
587
+ if (value === "minimal" || value === "strict" || value === "enterprise") {
588
+ return value;
589
+ }
590
+ throw new Error("--mode must be minimal, strict or enterprise");
591
+ }
@@ -16,6 +16,10 @@ export declare function createSummary(input: {
16
16
  errors: string[];
17
17
  warnings: string[];
18
18
  durations: DurationInput;
19
+ groupedUpdates?: number;
20
+ cooldownSkipped?: number;
21
+ ciProfile?: Summary["ciProfile"];
22
+ prLimitHit?: boolean;
19
23
  }): Summary;
20
24
  export declare function finalizeSummary(summary: Summary): Summary;
21
25
  export declare function resolveFailReason(updates: PackageUpdate[], errors: string[], failOn: FailOnLevel | undefined, maxUpdates: number | undefined, ciMode: boolean): FailReason;
@@ -33,6 +33,10 @@ export function createSummary(input) {
33
33
  fixPrApplied: false,
34
34
  fixBranchName: "",
35
35
  fixCommitSha: "",
36
+ groupedUpdates: Math.max(0, Math.round(input.groupedUpdates ?? 0)),
37
+ cooldownSkipped: Math.max(0, Math.round(input.cooldownSkipped ?? 0)),
38
+ ciProfile: input.ciProfile ?? "minimal",
39
+ prLimitHit: input.prLimitHit === true,
36
40
  };
37
41
  }
38
42
  export function finalizeSummary(summary) {
@@ -102,6 +102,7 @@ export async function warmCache(options) {
102
102
  registryMs,
103
103
  cacheMs,
104
104
  },
105
+ ciProfile: options.ciProfile,
105
106
  }));
106
107
  return {
107
108
  projectPath: options.cwd,
package/dist/index.d.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  export { check } from "./core/check.js";
2
2
  export { upgrade } from "./core/upgrade.js";
3
3
  export { warmCache } from "./core/warm-cache.js";
4
+ export { runCi } from "./core/ci.js";
4
5
  export { initCiWorkflow } from "./core/init-ci.js";
5
6
  export { saveBaseline, diffBaseline } from "./core/baseline.js";
6
7
  export { createSarifReport } from "./output/sarif.js";
7
8
  export { writeGitHubOutput, renderGitHubAnnotations } from "./output/github.js";
8
9
  export { renderPrReport } from "./output/pr-report.js";
9
- export type { CheckOptions, CheckResult, DependencyKind, FailOnLevel, OutputFormat, PackageUpdate, RunOptions, TargetLevel, UpgradeOptions, UpgradeResult, } from "./types/index.js";
10
+ export type { CheckOptions, CheckResult, CiProfile, DependencyKind, FailOnLevel, GroupBy, OutputFormat, PackageUpdate, RunOptions, TargetLevel, UpgradeOptions, UpgradeResult, } from "./types/index.js";
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  export { check } from "./core/check.js";
2
2
  export { upgrade } from "./core/upgrade.js";
3
3
  export { warmCache } from "./core/warm-cache.js";
4
+ export { runCi } from "./core/ci.js";
4
5
  export { initCiWorkflow } from "./core/init-ci.js";
5
6
  export { saveBaseline, diffBaseline } from "./core/baseline.js";
6
7
  export { createSarifReport } from "./output/sarif.js";
@@ -32,6 +32,10 @@ export function renderResult(result, format) {
32
32
  `duration_registry_ms=${result.summary.durationMs.registry}`,
33
33
  `duration_cache_ms=${result.summary.durationMs.cache}`,
34
34
  `duration_render_ms=${result.summary.durationMs.render}`,
35
+ `grouped_updates=${result.summary.groupedUpdates}`,
36
+ `cooldown_skipped=${result.summary.cooldownSkipped}`,
37
+ `ci_profile=${result.summary.ciProfile}`,
38
+ `pr_limit_hit=${result.summary.prLimitHit ? "1" : "0"}`,
35
39
  ].join("\n");
36
40
  }
37
41
  const lines = [];
@@ -70,6 +74,7 @@ export function renderResult(result, format) {
70
74
  }
71
75
  lines.push("");
72
76
  lines.push(`Summary: ${result.summary.updatesFound} updates, ${result.summary.checkedDependencies}/${result.summary.totalDependencies} checked, ${result.summary.warmedPackages} warmed`);
77
+ lines.push(`Groups=${result.summary.groupedUpdates}, cooldownSkipped=${result.summary.cooldownSkipped}, ciProfile=${result.summary.ciProfile}, prLimitHit=${result.summary.prLimitHit ? "yes" : "no"}`);
73
78
  lines.push(`Contract v${result.summary.contractVersion}, failReason=${result.summary.failReason}, duration=${result.summary.durationMs.total}ms`);
74
79
  if (result.summary.fixPrApplied) {
75
80
  lines.push(`Fix PR: applied on branch ${result.summary.fixBranchName ?? "unknown"} (${result.summary.fixCommitSha ?? "no-commit"})`);
@@ -14,6 +14,10 @@ export async function writeGitHubOutput(filePath, result) {
14
14
  `duration_registry_ms=${result.summary.durationMs.registry}`,
15
15
  `duration_cache_ms=${result.summary.durationMs.cache}`,
16
16
  `duration_render_ms=${result.summary.durationMs.render}`,
17
+ `grouped_updates=${result.summary.groupedUpdates}`,
18
+ `cooldown_skipped=${result.summary.cooldownSkipped}`,
19
+ `ci_profile=${result.summary.ciProfile}`,
20
+ `pr_limit_hit=${result.summary.prLimitHit === true ? "1" : "0"}`,
17
21
  `fix_pr_applied=${result.summary.fixPrApplied === true ? "1" : "0"}`,
18
22
  `fix_pr_branch=${result.summary.fixBranchName ?? ""}`,
19
23
  `fix_pr_commit=${result.summary.fixCommitSha ?? ""}`,
@@ -7,6 +7,10 @@ export function renderPrReport(result) {
7
7
  lines.push(`- Updates found: ${result.summary.updatesFound}`);
8
8
  lines.push(`- Errors: ${result.errors.length}`);
9
9
  lines.push(`- Warnings: ${result.warnings.length}`);
10
+ lines.push(`- Grouped updates: ${result.summary.groupedUpdates}`);
11
+ lines.push(`- Cooldown skipped: ${result.summary.cooldownSkipped}`);
12
+ lines.push(`- CI profile: ${result.summary.ciProfile}`);
13
+ lines.push(`- PR limit hit: ${result.summary.prLimitHit ? "yes" : "no"}`);
10
14
  lines.push("");
11
15
  if (result.updates.length > 0) {
12
16
  lines.push("## Proposed Updates");
@@ -67,8 +67,12 @@ export function createSarifReport(result) {
67
67
  contractVersion: result.summary.contractVersion,
68
68
  failReason: result.summary.failReason,
69
69
  updatesFound: result.summary.updatesFound,
70
+ groupedUpdates: result.summary.groupedUpdates,
71
+ cooldownSkipped: result.summary.cooldownSkipped,
70
72
  errorsCount: result.summary.errorCounts.total,
71
73
  warningsCount: result.summary.warningCounts.total,
74
+ ciProfile: result.summary.ciProfile,
75
+ prLimitHit: result.summary.prLimitHit,
72
76
  durationMs: result.summary.durationMs,
73
77
  },
74
78
  },
@@ -6,6 +6,7 @@ export interface ResolveManyResult {
6
6
  metadata: Map<string, {
7
7
  latestVersion: string | null;
8
8
  versions: string[];
9
+ publishedAtByVersion: Record<string, number>;
9
10
  }>;
10
11
  errors: Map<string, string>;
11
12
  }
@@ -15,6 +16,7 @@ export declare class NpmRegistryClient {
15
16
  resolvePackageMetadata(packageName: string, timeoutMs?: number): Promise<{
16
17
  latestVersion: string | null;
17
18
  versions: string[];
19
+ publishedAtByVersion: Record<string, number>;
18
20
  }>;
19
21
  resolveLatestVersion(packageName: string, timeoutMs?: number): Promise<string | null>;
20
22
  resolveManyPackageMetadata(packageNames: string[], options: ResolveManyOptions): Promise<ResolveManyResult>;
@@ -18,7 +18,7 @@ export class NpmRegistryClient {
18
18
  try {
19
19
  const response = await requester(packageName, timeoutMs);
20
20
  if (response.status === 404) {
21
- return { latestVersion: null, versions: [] };
21
+ return { latestVersion: null, versions: [], publishedAtByVersion: {} };
22
22
  }
23
23
  if (response.status === 429 || response.status >= 500) {
24
24
  throw new RetryableRegistryError(`Registry temporary error: ${response.status}`, response.retryAfterMs ?? computeBackoffMs(attempt));
@@ -27,7 +27,11 @@ export class NpmRegistryClient {
27
27
  throw new Error(`Registry request failed: ${response.status}`);
28
28
  }
29
29
  const versions = Object.keys(response.data?.versions ?? {});
30
- return { latestVersion: response.data?.["dist-tags"]?.latest ?? null, versions };
30
+ return {
31
+ latestVersion: response.data?.["dist-tags"]?.latest ?? null,
32
+ versions,
33
+ publishedAtByVersion: extractPublishTimes(response.data?.time),
34
+ };
31
35
  }
32
36
  catch (error) {
33
37
  lastError = String(error);
@@ -262,3 +266,17 @@ function parseRetryAfterHeader(value) {
262
266
  return 0;
263
267
  return delta;
264
268
  }
269
+ function extractPublishTimes(timeMap) {
270
+ if (!timeMap)
271
+ return {};
272
+ const publishedAtByVersion = {};
273
+ for (const [version, rawDate] of Object.entries(timeMap)) {
274
+ if (version === "created" || version === "modified")
275
+ continue;
276
+ const timestamp = Date.parse(rawDate);
277
+ if (Number.isFinite(timestamp)) {
278
+ publishedAtByVersion[version] = timestamp;
279
+ }
280
+ }
281
+ return publishedAtByVersion;
282
+ }
@@ -1,5 +1,7 @@
1
1
  export type DependencyKind = "dependencies" | "devDependencies" | "optionalDependencies" | "peerDependencies";
2
2
  export type TargetLevel = "patch" | "minor" | "major" | "latest";
3
+ export type GroupBy = "none" | "name" | "scope" | "kind" | "risk";
4
+ export type CiProfile = "minimal" | "strict" | "enterprise";
3
5
  export type OutputFormat = "table" | "json" | "minimal" | "github" | "metrics";
4
6
  export type FailOnLevel = "none" | "patch" | "minor" | "major" | "any";
5
7
  export type LogLevel = "error" | "warn" | "info" | "debug";
@@ -30,6 +32,12 @@ export interface RunOptions {
30
32
  fixPrNoCheckout?: boolean;
31
33
  noPrReport?: boolean;
32
34
  logLevel: LogLevel;
35
+ groupBy: GroupBy;
36
+ groupMax?: number;
37
+ cooldownDays?: number;
38
+ prLimit?: number;
39
+ onlyChanged: boolean;
40
+ ciProfile: CiProfile;
33
41
  }
34
42
  export interface CheckOptions extends RunOptions {
35
43
  }
@@ -92,6 +100,10 @@ export interface Summary {
92
100
  fixPrApplied: boolean;
93
101
  fixBranchName: string;
94
102
  fixCommitSha: string;
103
+ groupedUpdates: number;
104
+ cooldownSkipped: number;
105
+ ciProfile: CiProfile;
106
+ prLimitHit: boolean;
95
107
  }
96
108
  export interface CheckResult {
97
109
  projectPath: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rainy-updates/cli",
3
- "version": "0.5.0",
3
+ "version": "0.5.1-rc.1",
4
4
  "description": "Agentic CLI to check and upgrade npm/pnpm dependencies for CI workflows",
5
5
  "type": "module",
6
6
  "private": false,