@rainy-updates/cli 0.5.0 → 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 +54 -0
- package/README.md +14 -0
- package/dist/bin/cli.js +77 -5
- package/dist/config/loader.d.ts +8 -1
- package/dist/config/policy.d.ts +6 -0
- package/dist/config/policy.js +4 -0
- package/dist/core/check.js +66 -1
- package/dist/core/ci.d.ts +2 -0
- package/dist/core/ci.js +20 -0
- package/dist/core/fix-pr-batch.d.ts +19 -0
- package/dist/core/fix-pr-batch.js +172 -0
- package/dist/core/init-ci.js +3 -3
- package/dist/core/options.d.ts +3 -0
- package/dist/core/options.js +110 -2
- package/dist/core/summary.d.ts +4 -0
- package/dist/core/summary.js +5 -0
- package/dist/core/warm-cache.js +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/output/format.js +7 -0
- package/dist/output/github.js +5 -0
- package/dist/output/pr-report.js +5 -0
- package/dist/output/sarif.js +5 -0
- package/dist/registry/npm.d.ts +2 -0
- package/dist/registry/npm.js +20 -2
- package/dist/types/index.d.ts +14 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,60 @@
|
|
|
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
|
+
|
|
26
|
+
## [0.5.1-rc.1] - 2026-02-27
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
|
|
30
|
+
- New `ci` command for CI-first orchestration:
|
|
31
|
+
- profile-driven automation with `--mode minimal|strict|enterprise`,
|
|
32
|
+
- warm-cache + check/upgrade flow with deterministic artifacts.
|
|
33
|
+
- New rollout and orchestration flags:
|
|
34
|
+
- `--group-by none|name|scope|kind|risk`
|
|
35
|
+
- `--group-max <n>`
|
|
36
|
+
- `--cooldown-days <n>`
|
|
37
|
+
- `--pr-limit <n>`
|
|
38
|
+
- `--only-changed`
|
|
39
|
+
- Additive summary/output contract fields:
|
|
40
|
+
- `groupedUpdates`
|
|
41
|
+
- `cooldownSkipped`
|
|
42
|
+
- `ciProfile`
|
|
43
|
+
- `prLimitHit`
|
|
44
|
+
- Policy schema extensions:
|
|
45
|
+
- global `cooldownDays`
|
|
46
|
+
- package rule `group` and `priority`
|
|
47
|
+
|
|
48
|
+
### Changed
|
|
49
|
+
|
|
50
|
+
- `check` now supports cooldown-aware filtering when publish timestamps are available.
|
|
51
|
+
- CI workflow templates generated by `init-ci` now use `rainy-updates ci` with explicit profile mode.
|
|
52
|
+
- GitHub output, metrics output, SARIF properties, and PR report include new CI orchestration metadata.
|
|
53
|
+
|
|
54
|
+
### Tests
|
|
55
|
+
|
|
56
|
+
- Added parser coverage for `ci` command orchestration flags.
|
|
57
|
+
- Extended workflow, policy, summary, and output tests for new metadata and profile behavior.
|
|
58
|
+
|
|
5
59
|
## [0.5.0] - 2026-02-27
|
|
6
60
|
|
|
7
61
|
### 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,12 @@ 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
|
+
|
|
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
|
+
|
|
39
46
|
# 3) Apply upgrades with workspace sync
|
|
40
47
|
npx @rainy-updates/cli upgrade --target latest --workspace --sync --install
|
|
41
48
|
|
|
@@ -151,6 +158,13 @@ Schedule:
|
|
|
151
158
|
- `--offline`
|
|
152
159
|
- `--fail-on none|patch|minor|major|any`
|
|
153
160
|
- `--max-updates <n>`
|
|
161
|
+
- `--group-by none|name|scope|kind|risk`
|
|
162
|
+
- `--group-max <n>`
|
|
163
|
+
- `--cooldown-days <n>`
|
|
164
|
+
- `--pr-limit <n>`
|
|
165
|
+
- `--only-changed`
|
|
166
|
+
- `--mode minimal|strict|enterprise` (for `ci`)
|
|
167
|
+
- `--fix-pr-batch-size <n>` (for batched fix branches in `ci`)
|
|
154
168
|
- `--policy-file <path>`
|
|
155
169
|
- `--format table|json|minimal|github`
|
|
156
170
|
- `--json-file <path>`
|
package/dist/bin/cli.js
CHANGED
|
@@ -7,9 +7,11 @@ 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";
|
|
14
|
+
import { applyFixPrBatches } from "../core/fix-pr-batch.js";
|
|
13
15
|
import { renderResult } from "../output/format.js";
|
|
14
16
|
import { writeGitHubOutput } from "../output/github.js";
|
|
15
17
|
import { createSarifReport } from "../output/sarif.js";
|
|
@@ -69,14 +71,28 @@ async function main() {
|
|
|
69
71
|
const markdown = renderPrReport(result);
|
|
70
72
|
await writeFileAtomic(parsed.options.prReportFile, markdown + "\n");
|
|
71
73
|
}
|
|
72
|
-
if (parsed.options.fixPr && (parsed.command === "check" || parsed.command === "upgrade")) {
|
|
74
|
+
if (parsed.options.fixPr && (parsed.command === "check" || parsed.command === "upgrade" || parsed.command === "ci")) {
|
|
73
75
|
result.summary.fixPrApplied = false;
|
|
74
76
|
result.summary.fixBranchName = parsed.options.fixBranch ?? "chore/rainy-updates";
|
|
75
77
|
result.summary.fixCommitSha = "";
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
+
}
|
|
80
96
|
}
|
|
81
97
|
result.summary.failReason = resolveFailReason(result.updates, result.errors, parsed.options.failOn, parsed.options.maxUpdates, parsed.options.ci);
|
|
82
98
|
const renderStartedAt = Date.now();
|
|
@@ -85,6 +101,13 @@ async function main() {
|
|
|
85
101
|
if (parsed.options.format === "json" || parsed.options.format === "metrics") {
|
|
86
102
|
rendered = renderResult(result, parsed.options.format);
|
|
87
103
|
}
|
|
104
|
+
if (parsed.options.onlyChanged &&
|
|
105
|
+
result.updates.length === 0 &&
|
|
106
|
+
result.errors.length === 0 &&
|
|
107
|
+
result.warnings.length === 0 &&
|
|
108
|
+
(parsed.options.format === "table" || parsed.options.format === "minimal" || parsed.options.format === "github")) {
|
|
109
|
+
rendered = "";
|
|
110
|
+
}
|
|
88
111
|
if (parsed.options.jsonFile) {
|
|
89
112
|
await writeFileAtomic(parsed.options.jsonFile, stableStringify(result, 2) + "\n");
|
|
90
113
|
}
|
|
@@ -126,6 +149,7 @@ Options:
|
|
|
126
149
|
--fix-commit-message <text>
|
|
127
150
|
--fix-dry-run
|
|
128
151
|
--fix-pr-no-checkout
|
|
152
|
+
--fix-pr-batch-size <n>
|
|
129
153
|
--no-pr-report
|
|
130
154
|
--json-file <path>
|
|
131
155
|
--github-output <path>
|
|
@@ -133,6 +157,11 @@ Options:
|
|
|
133
157
|
--pr-report-file <path>
|
|
134
158
|
--fail-on none|patch|minor|major|any
|
|
135
159
|
--max-updates <n>
|
|
160
|
+
--group-by none|name|scope|kind|risk
|
|
161
|
+
--group-max <n>
|
|
162
|
+
--cooldown-days <n>
|
|
163
|
+
--pr-limit <n>
|
|
164
|
+
--only-changed
|
|
136
165
|
--log-level error|warn|info|debug
|
|
137
166
|
--ci`;
|
|
138
167
|
}
|
|
@@ -173,9 +202,41 @@ Options:
|
|
|
173
202
|
--fix-commit-message <text>
|
|
174
203
|
--fix-dry-run
|
|
175
204
|
--fix-pr-no-checkout
|
|
205
|
+
--fix-pr-batch-size <n>
|
|
176
206
|
--no-pr-report
|
|
177
207
|
--json-file <path>
|
|
178
208
|
--pr-report-file <path>`;
|
|
209
|
+
}
|
|
210
|
+
if (isCommand && command === "ci") {
|
|
211
|
+
return `rainy-updates ci [options]
|
|
212
|
+
|
|
213
|
+
Run CI-oriented dependency automation pipeline.
|
|
214
|
+
|
|
215
|
+
Options:
|
|
216
|
+
--workspace
|
|
217
|
+
--mode minimal|strict|enterprise
|
|
218
|
+
--group-by none|name|scope|kind|risk
|
|
219
|
+
--group-max <n>
|
|
220
|
+
--cooldown-days <n>
|
|
221
|
+
--pr-limit <n>
|
|
222
|
+
--only-changed
|
|
223
|
+
--offline
|
|
224
|
+
--concurrency <n>
|
|
225
|
+
--fix-pr
|
|
226
|
+
--fix-branch <name>
|
|
227
|
+
--fix-commit-message <text>
|
|
228
|
+
--fix-dry-run
|
|
229
|
+
--fix-pr-no-checkout
|
|
230
|
+
--fix-pr-batch-size <n>
|
|
231
|
+
--no-pr-report
|
|
232
|
+
--json-file <path>
|
|
233
|
+
--github-output <path>
|
|
234
|
+
--sarif-file <path>
|
|
235
|
+
--pr-report-file <path>
|
|
236
|
+
--fail-on none|patch|minor|major|any
|
|
237
|
+
--max-updates <n>
|
|
238
|
+
--log-level error|warn|info|debug
|
|
239
|
+
--ci`;
|
|
179
240
|
}
|
|
180
241
|
if (isCommand && command === "init-ci") {
|
|
181
242
|
return `rainy-updates init-ci [options]
|
|
@@ -206,6 +267,7 @@ Options:
|
|
|
206
267
|
Commands:
|
|
207
268
|
check Detect available updates
|
|
208
269
|
upgrade Apply updates to manifests
|
|
270
|
+
ci Run CI-focused update pipeline
|
|
209
271
|
warm-cache Warm local cache for fast/offline checks
|
|
210
272
|
init-ci Scaffold GitHub Actions workflow
|
|
211
273
|
baseline Save/check dependency baseline snapshots
|
|
@@ -222,11 +284,18 @@ Global options:
|
|
|
222
284
|
--policy-file <path>
|
|
223
285
|
--fail-on none|patch|minor|major|any
|
|
224
286
|
--max-updates <n>
|
|
287
|
+
--group-by none|name|scope|kind|risk
|
|
288
|
+
--group-max <n>
|
|
289
|
+
--cooldown-days <n>
|
|
290
|
+
--pr-limit <n>
|
|
291
|
+
--only-changed
|
|
292
|
+
--mode minimal|strict|enterprise
|
|
225
293
|
--fix-pr
|
|
226
294
|
--fix-branch <name>
|
|
227
295
|
--fix-commit-message <text>
|
|
228
296
|
--fix-dry-run
|
|
229
297
|
--fix-pr-no-checkout
|
|
298
|
+
--fix-pr-batch-size <n>
|
|
230
299
|
--no-pr-report
|
|
231
300
|
--log-level error|warn|info|debug
|
|
232
301
|
--concurrency <n>
|
|
@@ -243,6 +312,9 @@ async function runCommand(parsed) {
|
|
|
243
312
|
if (parsed.command === "warm-cache") {
|
|
244
313
|
return await warmCache(parsed.options);
|
|
245
314
|
}
|
|
315
|
+
if (parsed.command === "ci") {
|
|
316
|
+
return await runCi(parsed.options);
|
|
317
|
+
}
|
|
246
318
|
if (parsed.options.fixPr) {
|
|
247
319
|
const upgradeOptions = {
|
|
248
320
|
...parsed.options,
|
package/dist/config/loader.d.ts
CHANGED
|
@@ -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;
|
|
@@ -22,8 +22,15 @@ 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;
|
|
28
|
+
groupBy?: GroupBy;
|
|
29
|
+
groupMax?: number;
|
|
30
|
+
cooldownDays?: number;
|
|
31
|
+
prLimit?: number;
|
|
32
|
+
onlyChanged?: boolean;
|
|
33
|
+
ciProfile?: CiProfile;
|
|
27
34
|
install?: boolean;
|
|
28
35
|
packageManager?: "auto" | "npm" | "pnpm";
|
|
29
36
|
sync?: boolean;
|
package/dist/config/policy.d.ts
CHANGED
|
@@ -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
|
}
|
package/dist/config/policy.js
CHANGED
|
@@ -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) {
|
package/dist/core/check.js
CHANGED
|
@@ -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
|
|
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();
|
package/dist/core/ci.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { check } from "./check.js";
|
|
2
|
+
import { warmCache } from "./warm-cache.js";
|
|
3
|
+
export async function runCi(options) {
|
|
4
|
+
const profile = options.ciProfile;
|
|
5
|
+
if (profile !== "minimal") {
|
|
6
|
+
await warmCache({
|
|
7
|
+
...options,
|
|
8
|
+
offline: false,
|
|
9
|
+
ci: true,
|
|
10
|
+
format: "minimal",
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
const checkOptions = {
|
|
14
|
+
...options,
|
|
15
|
+
ci: true,
|
|
16
|
+
offline: profile === "minimal" ? options.offline : true,
|
|
17
|
+
concurrency: profile === "enterprise" ? Math.max(options.concurrency, 32) : options.concurrency,
|
|
18
|
+
};
|
|
19
|
+
return await check(checkOptions);
|
|
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
|
+
}
|
package/dist/core/init-ci.js
CHANGED
|
@@ -46,14 +46,14 @@ function installStep(packageManager) {
|
|
|
46
46
|
return ` - name: Install dependencies\n run: npm ci`;
|
|
47
47
|
}
|
|
48
48
|
function minimalWorkflowTemplate(scheduleBlock, packageManager) {
|
|
49
|
-
return `name: Rainy Updates\n\non:\n${scheduleBlock}\n\njobs:\n dependency-check:\n runs-on: ubuntu-latest\n steps:\n - name: Checkout\n uses: actions/checkout@v4\n\n - name: Setup Node\n uses: actions/setup-node@v4\n with:\n node-version: '20'\n\n${installStep(packageManager)}\n\n - name: Run dependency check\n run: |\n npx @rainy-updates/cli
|
|
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
|
|
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
|
|
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
|
}
|
package/dist/core/options.d.ts
CHANGED
package/dist/core/options.js
CHANGED
|
@@ -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);
|
|
@@ -41,8 +41,15 @@ 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",
|
|
47
|
+
groupBy: "none",
|
|
48
|
+
groupMax: undefined,
|
|
49
|
+
cooldownDays: undefined,
|
|
50
|
+
prLimit: undefined,
|
|
51
|
+
onlyChanged: false,
|
|
52
|
+
ciProfile: "minimal",
|
|
46
53
|
};
|
|
47
54
|
let force = false;
|
|
48
55
|
let initCiMode = "enterprise";
|
|
@@ -214,6 +221,18 @@ export async function parseCliArgs(argv) {
|
|
|
214
221
|
base.fixPrNoCheckout = true;
|
|
215
222
|
continue;
|
|
216
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
|
+
}
|
|
217
236
|
if (current === "--log-level" && next) {
|
|
218
237
|
base.logLevel = ensureLogLevel(next);
|
|
219
238
|
index += 1;
|
|
@@ -237,7 +256,12 @@ export async function parseCliArgs(argv) {
|
|
|
237
256
|
throw new Error("Missing value for --pm");
|
|
238
257
|
}
|
|
239
258
|
if (current === "--mode" && next) {
|
|
240
|
-
|
|
259
|
+
if (command === "init-ci") {
|
|
260
|
+
initCiMode = ensureInitCiMode(next);
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
base.ciProfile = ensureCiProfile(next);
|
|
264
|
+
}
|
|
241
265
|
index += 1;
|
|
242
266
|
continue;
|
|
243
267
|
}
|
|
@@ -280,6 +304,54 @@ export async function parseCliArgs(argv) {
|
|
|
280
304
|
if (current === "--max-updates") {
|
|
281
305
|
throw new Error("Missing value for --max-updates");
|
|
282
306
|
}
|
|
307
|
+
if (current === "--group-by" && next) {
|
|
308
|
+
base.groupBy = ensureGroupBy(next);
|
|
309
|
+
index += 1;
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
if (current === "--group-by") {
|
|
313
|
+
throw new Error("Missing value for --group-by");
|
|
314
|
+
}
|
|
315
|
+
if (current === "--group-max" && next) {
|
|
316
|
+
const parsed = Number(next);
|
|
317
|
+
if (!Number.isInteger(parsed) || parsed < 1) {
|
|
318
|
+
throw new Error("--group-max must be a positive integer");
|
|
319
|
+
}
|
|
320
|
+
base.groupMax = parsed;
|
|
321
|
+
index += 1;
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
if (current === "--group-max") {
|
|
325
|
+
throw new Error("Missing value for --group-max");
|
|
326
|
+
}
|
|
327
|
+
if (current === "--cooldown-days" && next) {
|
|
328
|
+
const parsed = Number(next);
|
|
329
|
+
if (!Number.isInteger(parsed) || parsed < 0) {
|
|
330
|
+
throw new Error("--cooldown-days must be a non-negative integer");
|
|
331
|
+
}
|
|
332
|
+
base.cooldownDays = parsed;
|
|
333
|
+
index += 1;
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
if (current === "--cooldown-days") {
|
|
337
|
+
throw new Error("Missing value for --cooldown-days");
|
|
338
|
+
}
|
|
339
|
+
if (current === "--pr-limit" && next) {
|
|
340
|
+
const parsed = Number(next);
|
|
341
|
+
if (!Number.isInteger(parsed) || parsed < 1) {
|
|
342
|
+
throw new Error("--pr-limit must be a positive integer");
|
|
343
|
+
}
|
|
344
|
+
base.prLimit = parsed;
|
|
345
|
+
index += 1;
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
if (current === "--pr-limit") {
|
|
349
|
+
throw new Error("Missing value for --pr-limit");
|
|
350
|
+
}
|
|
351
|
+
if (current === "--only-changed") {
|
|
352
|
+
base.onlyChanged = true;
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
283
355
|
if (current === "--save") {
|
|
284
356
|
baselineAction = "save";
|
|
285
357
|
continue;
|
|
@@ -336,6 +408,9 @@ export async function parseCliArgs(argv) {
|
|
|
336
408
|
if (command === "warm-cache") {
|
|
337
409
|
return { command, options: base };
|
|
338
410
|
}
|
|
411
|
+
if (command === "ci") {
|
|
412
|
+
return { command, options: base };
|
|
413
|
+
}
|
|
339
414
|
if (command === "init-ci") {
|
|
340
415
|
return {
|
|
341
416
|
command,
|
|
@@ -423,12 +498,33 @@ function applyConfig(base, config) {
|
|
|
423
498
|
if (typeof config.fixPrNoCheckout === "boolean") {
|
|
424
499
|
base.fixPrNoCheckout = config.fixPrNoCheckout;
|
|
425
500
|
}
|
|
501
|
+
if (typeof config.fixPrBatchSize === "number" && Number.isInteger(config.fixPrBatchSize) && config.fixPrBatchSize > 0) {
|
|
502
|
+
base.fixPrBatchSize = config.fixPrBatchSize;
|
|
503
|
+
}
|
|
426
504
|
if (typeof config.noPrReport === "boolean") {
|
|
427
505
|
base.noPrReport = config.noPrReport;
|
|
428
506
|
}
|
|
429
507
|
if (typeof config.logLevel === "string") {
|
|
430
508
|
base.logLevel = ensureLogLevel(config.logLevel);
|
|
431
509
|
}
|
|
510
|
+
if (typeof config.groupBy === "string") {
|
|
511
|
+
base.groupBy = ensureGroupBy(config.groupBy);
|
|
512
|
+
}
|
|
513
|
+
if (typeof config.groupMax === "number" && Number.isInteger(config.groupMax) && config.groupMax > 0) {
|
|
514
|
+
base.groupMax = config.groupMax;
|
|
515
|
+
}
|
|
516
|
+
if (typeof config.cooldownDays === "number" && Number.isInteger(config.cooldownDays) && config.cooldownDays >= 0) {
|
|
517
|
+
base.cooldownDays = config.cooldownDays;
|
|
518
|
+
}
|
|
519
|
+
if (typeof config.prLimit === "number" && Number.isInteger(config.prLimit) && config.prLimit > 0) {
|
|
520
|
+
base.prLimit = config.prLimit;
|
|
521
|
+
}
|
|
522
|
+
if (typeof config.onlyChanged === "boolean") {
|
|
523
|
+
base.onlyChanged = config.onlyChanged;
|
|
524
|
+
}
|
|
525
|
+
if (typeof config.ciProfile === "string") {
|
|
526
|
+
base.ciProfile = ensureCiProfile(config.ciProfile);
|
|
527
|
+
}
|
|
432
528
|
}
|
|
433
529
|
function parsePackageManager(args) {
|
|
434
530
|
const index = args.indexOf("--pm");
|
|
@@ -497,3 +593,15 @@ function ensureFailOn(value) {
|
|
|
497
593
|
}
|
|
498
594
|
throw new Error("--fail-on must be none, patch, minor, major or any");
|
|
499
595
|
}
|
|
596
|
+
function ensureGroupBy(value) {
|
|
597
|
+
if (value === "none" || value === "name" || value === "scope" || value === "kind" || value === "risk") {
|
|
598
|
+
return value;
|
|
599
|
+
}
|
|
600
|
+
throw new Error("--group-by must be none, name, scope, kind or risk");
|
|
601
|
+
}
|
|
602
|
+
function ensureCiProfile(value) {
|
|
603
|
+
if (value === "minimal" || value === "strict" || value === "enterprise") {
|
|
604
|
+
return value;
|
|
605
|
+
}
|
|
606
|
+
throw new Error("--mode must be minimal, strict or enterprise");
|
|
607
|
+
}
|
package/dist/core/summary.d.ts
CHANGED
|
@@ -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;
|
package/dist/core/summary.js
CHANGED
|
@@ -33,6 +33,11 @@ export function createSummary(input) {
|
|
|
33
33
|
fixPrApplied: false,
|
|
34
34
|
fixBranchName: "",
|
|
35
35
|
fixCommitSha: "",
|
|
36
|
+
fixPrBranchesCreated: 0,
|
|
37
|
+
groupedUpdates: Math.max(0, Math.round(input.groupedUpdates ?? 0)),
|
|
38
|
+
cooldownSkipped: Math.max(0, Math.round(input.cooldownSkipped ?? 0)),
|
|
39
|
+
ciProfile: input.ciProfile ?? "minimal",
|
|
40
|
+
prLimitHit: input.prLimitHit === true,
|
|
36
41
|
};
|
|
37
42
|
}
|
|
38
43
|
export function finalizeSummary(summary) {
|
package/dist/core/warm-cache.js
CHANGED
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";
|
package/dist/output/format.js
CHANGED
|
@@ -32,6 +32,11 @@ 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"}`,
|
|
39
|
+
`fix_pr_branches_created=${result.summary.fixPrBranchesCreated}`,
|
|
35
40
|
].join("\n");
|
|
36
41
|
}
|
|
37
42
|
const lines = [];
|
|
@@ -70,9 +75,11 @@ export function renderResult(result, format) {
|
|
|
70
75
|
}
|
|
71
76
|
lines.push("");
|
|
72
77
|
lines.push(`Summary: ${result.summary.updatesFound} updates, ${result.summary.checkedDependencies}/${result.summary.totalDependencies} checked, ${result.summary.warmedPackages} warmed`);
|
|
78
|
+
lines.push(`Groups=${result.summary.groupedUpdates}, cooldownSkipped=${result.summary.cooldownSkipped}, ciProfile=${result.summary.ciProfile}, prLimitHit=${result.summary.prLimitHit ? "yes" : "no"}`);
|
|
73
79
|
lines.push(`Contract v${result.summary.contractVersion}, failReason=${result.summary.failReason}, duration=${result.summary.durationMs.total}ms`);
|
|
74
80
|
if (result.summary.fixPrApplied) {
|
|
75
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}`);
|
|
76
83
|
}
|
|
77
84
|
return lines.join("\n");
|
|
78
85
|
}
|
package/dist/output/github.js
CHANGED
|
@@ -14,7 +14,12 @@ 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"}`,
|
|
22
|
+
`fix_pr_branches_created=${result.summary.fixPrBranchesCreated}`,
|
|
18
23
|
`fix_pr_branch=${result.summary.fixBranchName ?? ""}`,
|
|
19
24
|
`fix_pr_commit=${result.summary.fixCommitSha ?? ""}`,
|
|
20
25
|
];
|
package/dist/output/pr-report.js
CHANGED
|
@@ -7,6 +7,11 @@ 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"}`);
|
|
14
|
+
lines.push(`- Fix PR branches created: ${result.summary.fixPrBranchesCreated}`);
|
|
10
15
|
lines.push("");
|
|
11
16
|
if (result.updates.length > 0) {
|
|
12
17
|
lines.push("## Proposed Updates");
|
package/dist/output/sarif.js
CHANGED
|
@@ -67,8 +67,13 @@ 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,
|
|
76
|
+
fixPrBranchesCreated: result.summary.fixPrBranchesCreated,
|
|
72
77
|
durationMs: result.summary.durationMs,
|
|
73
78
|
},
|
|
74
79
|
},
|
package/dist/registry/npm.d.ts
CHANGED
|
@@ -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>;
|
package/dist/registry/npm.js
CHANGED
|
@@ -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 {
|
|
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
|
+
}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -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";
|
|
@@ -28,8 +30,15 @@ export interface RunOptions {
|
|
|
28
30
|
fixCommitMessage?: string;
|
|
29
31
|
fixDryRun?: boolean;
|
|
30
32
|
fixPrNoCheckout?: boolean;
|
|
33
|
+
fixPrBatchSize?: number;
|
|
31
34
|
noPrReport?: boolean;
|
|
32
35
|
logLevel: LogLevel;
|
|
36
|
+
groupBy: GroupBy;
|
|
37
|
+
groupMax?: number;
|
|
38
|
+
cooldownDays?: number;
|
|
39
|
+
prLimit?: number;
|
|
40
|
+
onlyChanged: boolean;
|
|
41
|
+
ciProfile: CiProfile;
|
|
33
42
|
}
|
|
34
43
|
export interface CheckOptions extends RunOptions {
|
|
35
44
|
}
|
|
@@ -92,6 +101,11 @@ export interface Summary {
|
|
|
92
101
|
fixPrApplied: boolean;
|
|
93
102
|
fixBranchName: string;
|
|
94
103
|
fixCommitSha: string;
|
|
104
|
+
fixPrBranchesCreated: number;
|
|
105
|
+
groupedUpdates: number;
|
|
106
|
+
cooldownSkipped: number;
|
|
107
|
+
ciProfile: CiProfile;
|
|
108
|
+
prLimitHit: boolean;
|
|
95
109
|
}
|
|
96
110
|
export interface CheckResult {
|
|
97
111
|
projectPath: string;
|