@rainy-updates/cli 0.5.0-rc.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +68 -0
- package/README.md +10 -0
- package/dist/bin/cli.js +93 -32
- package/dist/cache/cache.d.ts +2 -0
- package/dist/cache/cache.js +8 -3
- package/dist/config/loader.d.ts +8 -1
- package/dist/config/policy.d.ts +15 -4
- package/dist/config/policy.js +35 -7
- package/dist/core/check.js +78 -11
- package/dist/core/fix-pr.d.ts +7 -0
- package/dist/core/fix-pr.js +83 -0
- package/dist/core/options.js +107 -7
- package/dist/core/summary.d.ts +22 -0
- package/dist/core/summary.js +78 -0
- package/dist/core/warm-cache.js +34 -6
- package/dist/output/format.js +23 -1
- package/dist/output/github.js +23 -9
- package/dist/output/sarif.js +16 -2
- package/dist/registry/npm.d.ts +1 -1
- package/dist/registry/npm.js +136 -17
- package/dist/types/index.d.ts +33 -1
- package/dist/utils/io.d.ts +1 -0
- package/dist/utils/io.js +10 -0
- package/dist/utils/stable-json.d.ts +1 -0
- package/dist/utils/stable-json.js +20 -0
- package/dist/workspace/discover.js +56 -18
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,74 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project are documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.5.0] - 2026-02-27
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- Promoted `0.5.0-rc.4` to General Availability.
|
|
10
|
+
- Stabilized deterministic CI artifact behavior for JSON, SARIF, and GitHub outputs.
|
|
11
|
+
- Finalized fix-PR summary metadata contract defaults for automation pipelines.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- GA release gate includes `perf:smoke` CI check for regression protection.
|
|
16
|
+
|
|
17
|
+
## [0.5.0-rc.4] - 2026-02-27
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- Hardened deterministic CI artifacts:
|
|
22
|
+
- stable key ordering for JSON and SARIF files,
|
|
23
|
+
- deterministic sorting for updates, warnings, and errors in output pipelines.
|
|
24
|
+
- Improved fail-reason classification consistency for registry/runtime failures across commands.
|
|
25
|
+
- Fix-PR metadata in summary now has stable defaults (`fixPrApplied`, `fixBranchName`, `fixCommitSha`) to reduce contract drift.
|
|
26
|
+
- Fix-PR staging now includes only updated manifests plus explicit report files, with deterministic file ordering.
|
|
27
|
+
- Added warning when Bun runtime falls back from SQLite cache backend to file cache backend.
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
|
|
31
|
+
- Added `perf:smoke` script and CI gate to enforce a basic performance regression threshold.
|
|
32
|
+
- Added deterministic output and summary regression tests.
|
|
33
|
+
|
|
34
|
+
## [0.5.0-rc.2] - 2026-02-27
|
|
35
|
+
|
|
36
|
+
### Added
|
|
37
|
+
|
|
38
|
+
- New fix-PR automation flags for CI branch workflows:
|
|
39
|
+
- `--fix-pr`
|
|
40
|
+
- `--fix-branch <name>`
|
|
41
|
+
- `--fix-commit-message <text>`
|
|
42
|
+
- `--fix-dry-run`
|
|
43
|
+
- `--no-pr-report`
|
|
44
|
+
- New summary metadata for fix-PR execution:
|
|
45
|
+
- `fixPrApplied`
|
|
46
|
+
- `fixBranchName`
|
|
47
|
+
- `fixCommitSha`
|
|
48
|
+
- New GitHub output values for fix-PR state:
|
|
49
|
+
- `fix_pr_applied`
|
|
50
|
+
- `fix_pr_branch`
|
|
51
|
+
- `fix_pr_commit`
|
|
52
|
+
- Added command-specific help output for `check --help`.
|
|
53
|
+
|
|
54
|
+
### Changed
|
|
55
|
+
|
|
56
|
+
- `check --fix-pr` now executes update application flow to support branch+commit automation without requiring `upgrade`.
|
|
57
|
+
- Default PR report path is auto-assigned when `--fix-pr` is enabled: `.artifacts/deps-report.md`.
|
|
58
|
+
- CLI path-like options are resolved against the final effective `--cwd` value (stable behavior when option order varies).
|
|
59
|
+
- Workspace discovery now supports recursive patterns (`**`) and negated patterns (`!pattern`) with safer directory traversal defaults.
|
|
60
|
+
- Registry resolution now loads `.npmrc` scope mappings (`@scope:registry=...`) from user and project config.
|
|
61
|
+
|
|
62
|
+
### Fixed
|
|
63
|
+
|
|
64
|
+
- Prevented stale output contracts by writing fix-PR metadata into JSON/GitHub/SARIF artifact flow after git automation is resolved.
|
|
65
|
+
|
|
66
|
+
### Tests
|
|
67
|
+
|
|
68
|
+
- Added parser tests for fix-PR flags and final-cwd path resolution.
|
|
69
|
+
- Added workspace discovery coverage for recursive and negated patterns.
|
|
70
|
+
- Added fix-PR dry-run workflow test in temporary git repos.
|
|
71
|
+
- Extended GitHub output tests for new fix-PR keys.
|
|
72
|
+
|
|
5
73
|
## [0.5.0-rc.1] - 2026-02-27
|
|
6
74
|
|
|
7
75
|
### Added
|
package/README.md
CHANGED
|
@@ -39,6 +39,9 @@ npx @rainy-updates/cli check --workspace --ci --format json --json-file .artifac
|
|
|
39
39
|
# 3) Apply upgrades with workspace sync
|
|
40
40
|
npx @rainy-updates/cli upgrade --target latest --workspace --sync --install
|
|
41
41
|
|
|
42
|
+
# 3b) Generate a fix branch + commit for CI automation
|
|
43
|
+
npx @rainy-updates/cli check --workspace --fix-pr --fix-branch chore/rainy-updates
|
|
44
|
+
|
|
42
45
|
# 4) Warm cache for deterministic offline checks
|
|
43
46
|
npx @rainy-updates/cli warm-cache --workspace --concurrency 32
|
|
44
47
|
npx @rainy-updates/cli check --workspace --offline --ci
|
|
@@ -154,6 +157,11 @@ Schedule:
|
|
|
154
157
|
- `--github-output <path>`
|
|
155
158
|
- `--sarif-file <path>`
|
|
156
159
|
- `--pr-report-file <path>`
|
|
160
|
+
- `--fix-pr`
|
|
161
|
+
- `--fix-branch <name>`
|
|
162
|
+
- `--fix-commit-message <text>`
|
|
163
|
+
- `--fix-dry-run`
|
|
164
|
+
- `--no-pr-report`
|
|
157
165
|
- `--ci`
|
|
158
166
|
|
|
159
167
|
### Upgrade-only
|
|
@@ -189,6 +197,7 @@ rainy-updates --version
|
|
|
189
197
|
- Node.js 20+ runtime.
|
|
190
198
|
- Works with npm and pnpm workflows.
|
|
191
199
|
- Uses optional `undici` pool path for high-throughput HTTP.
|
|
200
|
+
- Reads `.npmrc` default and scoped registries for private package environments.
|
|
192
201
|
- Cache-first architecture for speed and resilience.
|
|
193
202
|
|
|
194
203
|
## CI/CD included
|
|
@@ -196,6 +205,7 @@ rainy-updates --version
|
|
|
196
205
|
This package ships with production CI/CD pipelines in the repository:
|
|
197
206
|
|
|
198
207
|
- Continuous integration pipeline for typecheck, tests, build, and production smoke checks.
|
|
208
|
+
- Performance smoke gate (`perf:smoke`) to catch startup/runtime regressions in CI.
|
|
199
209
|
- Tag-driven release pipeline for npm publishing with provenance.
|
|
200
210
|
- Release preflight validation for npm auth/scope checks before publishing.
|
|
201
211
|
|
package/dist/bin/cli.js
CHANGED
|
@@ -9,10 +9,14 @@ import { upgrade } from "../core/upgrade.js";
|
|
|
9
9
|
import { warmCache } from "../core/warm-cache.js";
|
|
10
10
|
import { initCiWorkflow } from "../core/init-ci.js";
|
|
11
11
|
import { diffBaseline, saveBaseline } from "../core/baseline.js";
|
|
12
|
+
import { applyFixPr } from "../core/fix-pr.js";
|
|
12
13
|
import { renderResult } from "../output/format.js";
|
|
13
14
|
import { writeGitHubOutput } from "../output/github.js";
|
|
14
15
|
import { createSarifReport } from "../output/sarif.js";
|
|
15
16
|
import { renderPrReport } from "../output/pr-report.js";
|
|
17
|
+
import { writeFileAtomic } from "../utils/io.js";
|
|
18
|
+
import { resolveFailReason } from "../core/summary.js";
|
|
19
|
+
import { stableStringify } from "../utils/stable-json.js";
|
|
16
20
|
async function main() {
|
|
17
21
|
try {
|
|
18
22
|
const argv = process.argv.slice(2);
|
|
@@ -60,31 +64,39 @@ async function main() {
|
|
|
60
64
|
process.exitCode = 1;
|
|
61
65
|
return;
|
|
62
66
|
}
|
|
63
|
-
const result = parsed
|
|
64
|
-
? await upgrade(parsed.options)
|
|
65
|
-
: parsed.command === "warm-cache"
|
|
66
|
-
? await warmCache(parsed.options)
|
|
67
|
-
: await check(parsed.options);
|
|
68
|
-
const rendered = renderResult(result, parsed.options.format);
|
|
69
|
-
process.stdout.write(rendered + "\n");
|
|
70
|
-
if (parsed.options.jsonFile) {
|
|
71
|
-
await fs.mkdir(path.dirname(parsed.options.jsonFile), { recursive: true });
|
|
72
|
-
await fs.writeFile(parsed.options.jsonFile, JSON.stringify(result, null, 2) + "\n", "utf8");
|
|
73
|
-
}
|
|
67
|
+
const result = await runCommand(parsed);
|
|
74
68
|
if (parsed.options.prReportFile) {
|
|
75
69
|
const markdown = renderPrReport(result);
|
|
76
|
-
await
|
|
77
|
-
|
|
70
|
+
await writeFileAtomic(parsed.options.prReportFile, markdown + "\n");
|
|
71
|
+
}
|
|
72
|
+
if (parsed.options.fixPr && (parsed.command === "check" || parsed.command === "upgrade")) {
|
|
73
|
+
result.summary.fixPrApplied = false;
|
|
74
|
+
result.summary.fixBranchName = parsed.options.fixBranch ?? "chore/rainy-updates";
|
|
75
|
+
result.summary.fixCommitSha = "";
|
|
76
|
+
const fixResult = await applyFixPr(parsed.options, result, parsed.options.prReportFile ? [parsed.options.prReportFile] : []);
|
|
77
|
+
result.summary.fixPrApplied = fixResult.applied;
|
|
78
|
+
result.summary.fixBranchName = fixResult.branchName ?? "";
|
|
79
|
+
result.summary.fixCommitSha = fixResult.commitSha ?? "";
|
|
80
|
+
}
|
|
81
|
+
result.summary.failReason = resolveFailReason(result.updates, result.errors, parsed.options.failOn, parsed.options.maxUpdates, parsed.options.ci);
|
|
82
|
+
const renderStartedAt = Date.now();
|
|
83
|
+
let rendered = renderResult(result, parsed.options.format);
|
|
84
|
+
result.summary.durationMs.render = Math.max(0, Date.now() - renderStartedAt);
|
|
85
|
+
if (parsed.options.format === "json" || parsed.options.format === "metrics") {
|
|
86
|
+
rendered = renderResult(result, parsed.options.format);
|
|
87
|
+
}
|
|
88
|
+
if (parsed.options.jsonFile) {
|
|
89
|
+
await writeFileAtomic(parsed.options.jsonFile, stableStringify(result, 2) + "\n");
|
|
78
90
|
}
|
|
79
91
|
if (parsed.options.githubOutputFile) {
|
|
80
92
|
await writeGitHubOutput(parsed.options.githubOutputFile, result);
|
|
81
93
|
}
|
|
82
94
|
if (parsed.options.sarifFile) {
|
|
83
95
|
const sarif = createSarifReport(result);
|
|
84
|
-
await
|
|
85
|
-
await fs.writeFile(parsed.options.sarifFile, JSON.stringify(sarif, null, 2) + "\n", "utf8");
|
|
96
|
+
await writeFileAtomic(parsed.options.sarifFile, stableStringify(sarif, 2) + "\n");
|
|
86
97
|
}
|
|
87
|
-
process.
|
|
98
|
+
process.stdout.write(rendered + "\n");
|
|
99
|
+
process.exitCode = resolveExitCode(result, result.summary.failReason);
|
|
88
100
|
}
|
|
89
101
|
catch (error) {
|
|
90
102
|
process.stderr.write(`rainy-updates: ${String(error)}\n`);
|
|
@@ -94,6 +106,36 @@ async function main() {
|
|
|
94
106
|
void main();
|
|
95
107
|
function renderHelp(command) {
|
|
96
108
|
const isCommand = command && !command.startsWith("-");
|
|
109
|
+
if (isCommand && command === "check") {
|
|
110
|
+
return `rainy-updates check [options]
|
|
111
|
+
|
|
112
|
+
Detect available dependency updates.
|
|
113
|
+
|
|
114
|
+
Options:
|
|
115
|
+
--workspace
|
|
116
|
+
--target patch|minor|major|latest
|
|
117
|
+
--filter <pattern>
|
|
118
|
+
--reject <pattern>
|
|
119
|
+
--dep-kinds deps,dev,optional,peer
|
|
120
|
+
--concurrency <n>
|
|
121
|
+
--cache-ttl <seconds>
|
|
122
|
+
--policy-file <path>
|
|
123
|
+
--offline
|
|
124
|
+
--fix-pr
|
|
125
|
+
--fix-branch <name>
|
|
126
|
+
--fix-commit-message <text>
|
|
127
|
+
--fix-dry-run
|
|
128
|
+
--fix-pr-no-checkout
|
|
129
|
+
--no-pr-report
|
|
130
|
+
--json-file <path>
|
|
131
|
+
--github-output <path>
|
|
132
|
+
--sarif-file <path>
|
|
133
|
+
--pr-report-file <path>
|
|
134
|
+
--fail-on none|patch|minor|major|any
|
|
135
|
+
--max-updates <n>
|
|
136
|
+
--log-level error|warn|info|debug
|
|
137
|
+
--ci`;
|
|
138
|
+
}
|
|
97
139
|
if (isCommand && command === "warm-cache") {
|
|
98
140
|
return `rainy-updates warm-cache [options]
|
|
99
141
|
|
|
@@ -126,6 +168,12 @@ Options:
|
|
|
126
168
|
--target patch|minor|major|latest
|
|
127
169
|
--policy-file <path>
|
|
128
170
|
--concurrency <n>
|
|
171
|
+
--fix-pr
|
|
172
|
+
--fix-branch <name>
|
|
173
|
+
--fix-commit-message <text>
|
|
174
|
+
--fix-dry-run
|
|
175
|
+
--fix-pr-no-checkout
|
|
176
|
+
--no-pr-report
|
|
129
177
|
--json-file <path>
|
|
130
178
|
--pr-report-file <path>`;
|
|
131
179
|
}
|
|
@@ -166,7 +214,7 @@ Global options:
|
|
|
166
214
|
--cwd <path>
|
|
167
215
|
--workspace
|
|
168
216
|
--target patch|minor|major|latest
|
|
169
|
-
--format table|json|minimal|github
|
|
217
|
+
--format table|json|minimal|github|metrics
|
|
170
218
|
--json-file <path>
|
|
171
219
|
--github-output <path>
|
|
172
220
|
--sarif-file <path>
|
|
@@ -174,6 +222,13 @@ Global options:
|
|
|
174
222
|
--policy-file <path>
|
|
175
223
|
--fail-on none|patch|minor|major|any
|
|
176
224
|
--max-updates <n>
|
|
225
|
+
--fix-pr
|
|
226
|
+
--fix-branch <name>
|
|
227
|
+
--fix-commit-message <text>
|
|
228
|
+
--fix-dry-run
|
|
229
|
+
--fix-pr-no-checkout
|
|
230
|
+
--no-pr-report
|
|
231
|
+
--log-level error|warn|info|debug
|
|
177
232
|
--concurrency <n>
|
|
178
233
|
--cache-ttl <seconds>
|
|
179
234
|
--offline
|
|
@@ -181,6 +236,24 @@ Global options:
|
|
|
181
236
|
--help, -h
|
|
182
237
|
--version, -v`;
|
|
183
238
|
}
|
|
239
|
+
async function runCommand(parsed) {
|
|
240
|
+
if (parsed.command === "upgrade") {
|
|
241
|
+
return await upgrade(parsed.options);
|
|
242
|
+
}
|
|
243
|
+
if (parsed.command === "warm-cache") {
|
|
244
|
+
return await warmCache(parsed.options);
|
|
245
|
+
}
|
|
246
|
+
if (parsed.options.fixPr) {
|
|
247
|
+
const upgradeOptions = {
|
|
248
|
+
...parsed.options,
|
|
249
|
+
install: false,
|
|
250
|
+
packageManager: "auto",
|
|
251
|
+
sync: false,
|
|
252
|
+
};
|
|
253
|
+
return await upgrade(upgradeOptions);
|
|
254
|
+
}
|
|
255
|
+
return await check(parsed.options);
|
|
256
|
+
}
|
|
184
257
|
async function readPackageVersion() {
|
|
185
258
|
const currentFile = fileURLToPath(import.meta.url);
|
|
186
259
|
const packageJsonPath = path.resolve(path.dirname(currentFile), "../../package.json");
|
|
@@ -188,22 +261,10 @@ async function readPackageVersion() {
|
|
|
188
261
|
const parsed = JSON.parse(content);
|
|
189
262
|
return parsed.version ?? "0.0.0";
|
|
190
263
|
}
|
|
191
|
-
function resolveExitCode(result,
|
|
264
|
+
function resolveExitCode(result, failReason) {
|
|
192
265
|
if (result.errors.length > 0)
|
|
193
266
|
return 2;
|
|
194
|
-
if (
|
|
267
|
+
if (failReason !== "none")
|
|
195
268
|
return 1;
|
|
196
|
-
|
|
197
|
-
if (!shouldFailForUpdates(result.updates, effectiveFailOn))
|
|
198
|
-
return 0;
|
|
199
|
-
return 1;
|
|
200
|
-
}
|
|
201
|
-
function shouldFailForUpdates(updates, failOn) {
|
|
202
|
-
if (failOn === "none")
|
|
203
|
-
return false;
|
|
204
|
-
if (failOn === "any" || failOn === "patch")
|
|
205
|
-
return updates.length > 0;
|
|
206
|
-
if (failOn === "minor")
|
|
207
|
-
return updates.some((update) => update.diffType === "minor" || update.diffType === "major");
|
|
208
|
-
return updates.some((update) => update.diffType === "major");
|
|
269
|
+
return 0;
|
|
209
270
|
}
|
package/dist/cache/cache.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { CachedVersion, TargetLevel } from "../types/index.js";
|
|
2
2
|
export declare class VersionCache {
|
|
3
3
|
private readonly store;
|
|
4
|
+
readonly backend: "sqlite" | "file";
|
|
5
|
+
readonly degraded: boolean;
|
|
4
6
|
private constructor();
|
|
5
7
|
static create(customPath?: string): Promise<VersionCache>;
|
|
6
8
|
getValid(packageName: string, target: TargetLevel): Promise<CachedVersion | null>;
|
package/dist/cache/cache.js
CHANGED
|
@@ -106,17 +106,22 @@ class SqliteCacheStore {
|
|
|
106
106
|
}
|
|
107
107
|
export class VersionCache {
|
|
108
108
|
store;
|
|
109
|
-
|
|
109
|
+
backend;
|
|
110
|
+
degraded;
|
|
111
|
+
constructor(store, backend, degraded) {
|
|
110
112
|
this.store = store;
|
|
113
|
+
this.backend = backend;
|
|
114
|
+
this.degraded = degraded;
|
|
111
115
|
}
|
|
112
116
|
static async create(customPath) {
|
|
113
117
|
const basePath = customPath ?? path.join(os.homedir(), ".cache", "rainy-updates");
|
|
114
118
|
const sqlitePath = path.join(basePath, "cache.db");
|
|
115
119
|
const sqliteStore = await tryCreateSqliteStore(sqlitePath);
|
|
116
120
|
if (sqliteStore)
|
|
117
|
-
return new VersionCache(sqliteStore);
|
|
121
|
+
return new VersionCache(sqliteStore, "sqlite", false);
|
|
118
122
|
const jsonPath = path.join(basePath, "cache.json");
|
|
119
|
-
|
|
123
|
+
const degraded = typeof Bun !== "undefined";
|
|
124
|
+
return new VersionCache(new FileCacheStore(jsonPath), "file", degraded);
|
|
120
125
|
}
|
|
121
126
|
async getValid(packageName, target) {
|
|
122
127
|
const entry = await this.store.get(packageName, target);
|
package/dist/config/loader.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { DependencyKind, FailOnLevel, OutputFormat, TargetLevel } from "../types/index.js";
|
|
1
|
+
import type { DependencyKind, FailOnLevel, LogLevel, OutputFormat, TargetLevel } from "../types/index.js";
|
|
2
2
|
export interface FileConfig {
|
|
3
3
|
target?: TargetLevel;
|
|
4
4
|
filter?: string;
|
|
@@ -17,6 +17,13 @@ export interface FileConfig {
|
|
|
17
17
|
prReportFile?: string;
|
|
18
18
|
failOn?: FailOnLevel;
|
|
19
19
|
maxUpdates?: number;
|
|
20
|
+
fixPr?: boolean;
|
|
21
|
+
fixBranch?: string;
|
|
22
|
+
fixCommitMessage?: string;
|
|
23
|
+
fixDryRun?: boolean;
|
|
24
|
+
fixPrNoCheckout?: boolean;
|
|
25
|
+
noPrReport?: boolean;
|
|
26
|
+
logLevel?: LogLevel;
|
|
20
27
|
install?: boolean;
|
|
21
28
|
packageManager?: "auto" | "npm" | "pnpm";
|
|
22
29
|
sync?: boolean;
|
package/dist/config/policy.d.ts
CHANGED
|
@@ -2,15 +2,26 @@ import type { TargetLevel } from "../types/index.js";
|
|
|
2
2
|
export interface PolicyConfig {
|
|
3
3
|
ignore?: string[];
|
|
4
4
|
packageRules?: Record<string, {
|
|
5
|
+
match?: string;
|
|
5
6
|
maxTarget?: TargetLevel;
|
|
6
7
|
ignore?: boolean;
|
|
8
|
+
maxUpdatesPerRun?: number;
|
|
9
|
+
cooldownDays?: number;
|
|
10
|
+
allowPrerelease?: boolean;
|
|
7
11
|
}>;
|
|
8
12
|
}
|
|
13
|
+
export interface PolicyRule {
|
|
14
|
+
match?: string;
|
|
15
|
+
maxTarget?: TargetLevel;
|
|
16
|
+
ignore: boolean;
|
|
17
|
+
maxUpdatesPerRun?: number;
|
|
18
|
+
cooldownDays?: number;
|
|
19
|
+
allowPrerelease?: boolean;
|
|
20
|
+
}
|
|
9
21
|
export interface ResolvedPolicy {
|
|
10
22
|
ignorePatterns: string[];
|
|
11
|
-
packageRules: Map<string,
|
|
12
|
-
|
|
13
|
-
ignore: boolean;
|
|
14
|
-
}>;
|
|
23
|
+
packageRules: Map<string, PolicyRule>;
|
|
24
|
+
matchRules: PolicyRule[];
|
|
15
25
|
}
|
|
16
26
|
export declare function loadPolicy(cwd: string, policyFile?: string): Promise<ResolvedPolicy>;
|
|
27
|
+
export declare function resolvePolicyRule(packageName: string, policy: ResolvedPolicy): PolicyRule | undefined;
|
package/dist/config/policy.js
CHANGED
|
@@ -12,13 +12,10 @@ export async function loadPolicy(cwd, policyFile) {
|
|
|
12
12
|
const parsed = JSON.parse(content);
|
|
13
13
|
return {
|
|
14
14
|
ignorePatterns: parsed.ignore ?? [],
|
|
15
|
-
packageRules: new Map(Object.entries(parsed.packageRules ?? {}).map(([pkg, rule]) => [
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
ignore: rule.ignore === true,
|
|
20
|
-
},
|
|
21
|
-
])),
|
|
15
|
+
packageRules: new Map(Object.entries(parsed.packageRules ?? {}).map(([pkg, rule]) => [pkg, normalizeRule(rule)])),
|
|
16
|
+
matchRules: Object.values(parsed.packageRules ?? {})
|
|
17
|
+
.map((rule) => normalizeRule(rule))
|
|
18
|
+
.filter((rule) => typeof rule.match === "string" && rule.match.length > 0),
|
|
22
19
|
};
|
|
23
20
|
}
|
|
24
21
|
catch {
|
|
@@ -28,5 +25,36 @@ export async function loadPolicy(cwd, policyFile) {
|
|
|
28
25
|
return {
|
|
29
26
|
ignorePatterns: [],
|
|
30
27
|
packageRules: new Map(),
|
|
28
|
+
matchRules: [],
|
|
31
29
|
};
|
|
32
30
|
}
|
|
31
|
+
export function resolvePolicyRule(packageName, policy) {
|
|
32
|
+
const exact = policy.packageRules.get(packageName);
|
|
33
|
+
if (exact)
|
|
34
|
+
return exact;
|
|
35
|
+
return policy.matchRules.find((rule) => matchesPattern(packageName, rule.match));
|
|
36
|
+
}
|
|
37
|
+
function normalizeRule(rule) {
|
|
38
|
+
return {
|
|
39
|
+
match: typeof rule.match === "string" ? rule.match : undefined,
|
|
40
|
+
maxTarget: rule.maxTarget,
|
|
41
|
+
ignore: rule.ignore === true,
|
|
42
|
+
maxUpdatesPerRun: asNonNegativeInt(rule.maxUpdatesPerRun),
|
|
43
|
+
cooldownDays: asNonNegativeInt(rule.cooldownDays),
|
|
44
|
+
allowPrerelease: rule.allowPrerelease === true,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function asNonNegativeInt(value) {
|
|
48
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 0)
|
|
49
|
+
return undefined;
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
function matchesPattern(value, pattern) {
|
|
53
|
+
if (!pattern || pattern.length === 0)
|
|
54
|
+
return false;
|
|
55
|
+
if (pattern === "*")
|
|
56
|
+
return true;
|
|
57
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
58
|
+
const regex = new RegExp(`^${escaped}$`);
|
|
59
|
+
return regex.test(value);
|
|
60
|
+
}
|
package/dist/core/check.js
CHANGED
|
@@ -6,16 +6,26 @@ import { VersionCache } from "../cache/cache.js";
|
|
|
6
6
|
import { NpmRegistryClient } from "../registry/npm.js";
|
|
7
7
|
import { detectPackageManager } from "../pm/detect.js";
|
|
8
8
|
import { discoverPackageDirs } from "../workspace/discover.js";
|
|
9
|
-
import { loadPolicy } from "../config/policy.js";
|
|
9
|
+
import { loadPolicy, resolvePolicyRule } from "../config/policy.js";
|
|
10
|
+
import { createSummary, finalizeSummary } from "./summary.js";
|
|
10
11
|
export async function check(options) {
|
|
12
|
+
const startedAt = Date.now();
|
|
13
|
+
let discoveryMs = 0;
|
|
14
|
+
let cacheMs = 0;
|
|
15
|
+
let registryMs = 0;
|
|
16
|
+
const discoveryStartedAt = Date.now();
|
|
11
17
|
const packageManager = await detectPackageManager(options.cwd);
|
|
12
18
|
const packageDirs = await discoverPackageDirs(options.cwd, options.workspace);
|
|
19
|
+
discoveryMs += Date.now() - discoveryStartedAt;
|
|
13
20
|
const cache = await VersionCache.create();
|
|
14
|
-
const registryClient = new NpmRegistryClient();
|
|
21
|
+
const registryClient = new NpmRegistryClient(options.cwd);
|
|
15
22
|
const policy = await loadPolicy(options.cwd, options.policyFile);
|
|
16
23
|
const updates = [];
|
|
17
24
|
const errors = [];
|
|
18
25
|
const warnings = [];
|
|
26
|
+
if (cache.degraded) {
|
|
27
|
+
warnings.push("SQLite cache backend unavailable in Bun runtime. Falling back to file cache backend.");
|
|
28
|
+
}
|
|
19
29
|
let totalDependencies = 0;
|
|
20
30
|
const tasks = [];
|
|
21
31
|
let skipped = 0;
|
|
@@ -35,7 +45,7 @@ export async function check(options) {
|
|
|
35
45
|
continue;
|
|
36
46
|
if (options.reject && matchesPattern(dep.name, options.reject))
|
|
37
47
|
continue;
|
|
38
|
-
const rule =
|
|
48
|
+
const rule = resolvePolicyRule(dep.name, policy);
|
|
39
49
|
if (rule?.ignore === true) {
|
|
40
50
|
skipped += 1;
|
|
41
51
|
continue;
|
|
@@ -47,9 +57,10 @@ export async function check(options) {
|
|
|
47
57
|
tasks.push({ packageDir, dependency: dep });
|
|
48
58
|
}
|
|
49
59
|
}
|
|
50
|
-
const uniquePackageNames = Array.from(new Set(tasks.map((task) => task.dependency.name)));
|
|
60
|
+
const uniquePackageNames = Array.from(new Set(tasks.map((task) => task.dependency.name))).sort((a, b) => a.localeCompare(b));
|
|
51
61
|
const resolvedVersions = new Map();
|
|
52
62
|
const unresolvedPackages = [];
|
|
63
|
+
const cacheLookupStartedAt = Date.now();
|
|
53
64
|
for (const packageName of uniquePackageNames) {
|
|
54
65
|
const cached = await cache.getValid(packageName, options.target);
|
|
55
66
|
if (cached) {
|
|
@@ -62,8 +73,10 @@ export async function check(options) {
|
|
|
62
73
|
unresolvedPackages.push(packageName);
|
|
63
74
|
}
|
|
64
75
|
}
|
|
76
|
+
cacheMs += Date.now() - cacheLookupStartedAt;
|
|
65
77
|
if (unresolvedPackages.length > 0) {
|
|
66
78
|
if (options.offline) {
|
|
79
|
+
const cacheFallbackStartedAt = Date.now();
|
|
67
80
|
for (const packageName of unresolvedPackages) {
|
|
68
81
|
const stale = await cache.getAny(packageName, options.target);
|
|
69
82
|
if (stale) {
|
|
@@ -77,11 +90,15 @@ export async function check(options) {
|
|
|
77
90
|
errors.push(`Offline cache miss for ${packageName}. Run once without --offline to warm cache.`);
|
|
78
91
|
}
|
|
79
92
|
}
|
|
93
|
+
cacheMs += Date.now() - cacheFallbackStartedAt;
|
|
80
94
|
}
|
|
81
95
|
else {
|
|
96
|
+
const registryStartedAt = Date.now();
|
|
82
97
|
const fetched = await registryClient.resolveManyPackageMetadata(unresolvedPackages, {
|
|
83
98
|
concurrency: options.concurrency,
|
|
84
99
|
});
|
|
100
|
+
registryMs += Date.now() - registryStartedAt;
|
|
101
|
+
const cacheWriteStartedAt = Date.now();
|
|
85
102
|
for (const [packageName, metadata] of fetched.metadata) {
|
|
86
103
|
resolvedVersions.set(packageName, {
|
|
87
104
|
latestVersion: metadata.latestVersion,
|
|
@@ -91,6 +108,8 @@ export async function check(options) {
|
|
|
91
108
|
await cache.set(packageName, options.target, metadata.latestVersion, metadata.versions, options.cacheTtlSeconds);
|
|
92
109
|
}
|
|
93
110
|
}
|
|
111
|
+
cacheMs += Date.now() - cacheWriteStartedAt;
|
|
112
|
+
const cacheStaleStartedAt = Date.now();
|
|
94
113
|
for (const [packageName, error] of fetched.errors) {
|
|
95
114
|
const stale = await cache.getAny(packageName, options.target);
|
|
96
115
|
if (stale) {
|
|
@@ -104,13 +123,14 @@ export async function check(options) {
|
|
|
104
123
|
errors.push(`Unable to resolve ${packageName}: ${error}`);
|
|
105
124
|
}
|
|
106
125
|
}
|
|
126
|
+
cacheMs += Date.now() - cacheStaleStartedAt;
|
|
107
127
|
}
|
|
108
128
|
}
|
|
109
129
|
for (const task of tasks) {
|
|
110
130
|
const metadata = resolvedVersions.get(task.dependency.name);
|
|
111
131
|
if (!metadata?.latestVersion)
|
|
112
132
|
continue;
|
|
113
|
-
const rule =
|
|
133
|
+
const rule = resolvePolicyRule(task.dependency.name, policy);
|
|
114
134
|
const effectiveTarget = clampTarget(options.target, rule?.maxTarget);
|
|
115
135
|
const picked = pickTargetVersionFromAvailable(task.dependency.range, metadata.availableVersions, metadata.latestVersion, effectiveTarget);
|
|
116
136
|
if (!picked)
|
|
@@ -130,15 +150,26 @@ export async function check(options) {
|
|
|
130
150
|
reason: rule?.maxTarget ? `policy maxTarget=${rule.maxTarget}` : undefined,
|
|
131
151
|
});
|
|
132
152
|
}
|
|
133
|
-
const
|
|
153
|
+
const limitedUpdates = sortUpdates(applyRuleUpdateCaps(updates, policy));
|
|
154
|
+
const sortedErrors = [...errors].sort((a, b) => a.localeCompare(b));
|
|
155
|
+
const sortedWarnings = [...warnings].sort((a, b) => a.localeCompare(b));
|
|
156
|
+
const summary = finalizeSummary(createSummary({
|
|
134
157
|
scannedPackages: packageDirs.length,
|
|
135
158
|
totalDependencies,
|
|
136
159
|
checkedDependencies: tasks.length,
|
|
137
|
-
updatesFound:
|
|
160
|
+
updatesFound: limitedUpdates.length,
|
|
138
161
|
upgraded: 0,
|
|
139
162
|
skipped,
|
|
140
163
|
warmedPackages: 0,
|
|
141
|
-
|
|
164
|
+
errors: sortedErrors,
|
|
165
|
+
warnings: sortedWarnings,
|
|
166
|
+
durations: {
|
|
167
|
+
totalMs: Date.now() - startedAt,
|
|
168
|
+
discoveryMs,
|
|
169
|
+
registryMs,
|
|
170
|
+
cacheMs,
|
|
171
|
+
},
|
|
172
|
+
}));
|
|
142
173
|
return {
|
|
143
174
|
projectPath: options.cwd,
|
|
144
175
|
packagePaths: packageDirs,
|
|
@@ -146,8 +177,44 @@ export async function check(options) {
|
|
|
146
177
|
target: options.target,
|
|
147
178
|
timestamp: new Date().toISOString(),
|
|
148
179
|
summary,
|
|
149
|
-
updates,
|
|
150
|
-
errors,
|
|
151
|
-
warnings,
|
|
180
|
+
updates: limitedUpdates,
|
|
181
|
+
errors: sortedErrors,
|
|
182
|
+
warnings: sortedWarnings,
|
|
152
183
|
};
|
|
153
184
|
}
|
|
185
|
+
function applyRuleUpdateCaps(updates, policy) {
|
|
186
|
+
const limited = [];
|
|
187
|
+
const seenPerPackage = new Map();
|
|
188
|
+
for (const update of updates) {
|
|
189
|
+
const rule = resolvePolicyRule(update.name, policy);
|
|
190
|
+
const cap = rule?.maxUpdatesPerRun;
|
|
191
|
+
if (typeof cap !== "number") {
|
|
192
|
+
limited.push(update);
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
const seen = seenPerPackage.get(update.name) ?? 0;
|
|
196
|
+
if (seen >= cap) {
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
seenPerPackage.set(update.name, seen + 1);
|
|
200
|
+
limited.push(update);
|
|
201
|
+
}
|
|
202
|
+
return limited;
|
|
203
|
+
}
|
|
204
|
+
function sortUpdates(updates) {
|
|
205
|
+
return [...updates].sort((left, right) => {
|
|
206
|
+
const byPath = left.packagePath.localeCompare(right.packagePath);
|
|
207
|
+
if (byPath !== 0)
|
|
208
|
+
return byPath;
|
|
209
|
+
const byName = left.name.localeCompare(right.name);
|
|
210
|
+
if (byName !== 0)
|
|
211
|
+
return byName;
|
|
212
|
+
const byKind = left.kind.localeCompare(right.kind);
|
|
213
|
+
if (byKind !== 0)
|
|
214
|
+
return byKind;
|
|
215
|
+
const byFrom = left.fromRange.localeCompare(right.fromRange);
|
|
216
|
+
if (byFrom !== 0)
|
|
217
|
+
return byFrom;
|
|
218
|
+
return left.toRange.localeCompare(right.toRange);
|
|
219
|
+
});
|
|
220
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { CheckResult, RunOptions } from "../types/index.js";
|
|
2
|
+
export interface FixPrResult {
|
|
3
|
+
applied: boolean;
|
|
4
|
+
branchName?: string;
|
|
5
|
+
commitSha?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function applyFixPr(options: RunOptions, result: CheckResult, extraFiles: string[]): Promise<FixPrResult>;
|