@rainy-updates/cli 0.4.4 → 0.5.0-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 +29 -0
- package/README.md +15 -1
- package/dist/bin/cli.js +63 -8
- package/dist/cache/cache.d.ts +1 -1
- package/dist/cache/cache.js +62 -9
- package/dist/config/loader.d.ts +3 -1
- package/dist/core/baseline.d.ts +23 -0
- package/dist/core/baseline.js +72 -0
- package/dist/core/check.js +24 -12
- package/dist/core/init-ci.d.ts +1 -1
- package/dist/core/init-ci.js +9 -1
- package/dist/core/options.d.ts +6 -1
- package/dist/core/options.js +133 -4
- package/dist/core/warm-cache.js +4 -4
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/output/sarif.js +21 -1
- package/dist/registry/npm.d.ts +13 -2
- package/dist/registry/npm.js +29 -12
- package/dist/types/index.d.ts +11 -0
- package/dist/utils/semver.d.ts +1 -0
- package/dist/utils/semver.js +24 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,35 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project are documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.5.0-rc.1] - 2026-02-27
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- New CI rollout controls:
|
|
10
|
+
- `--fail-on none|patch|minor|major|any`
|
|
11
|
+
- `--max-updates <n>`
|
|
12
|
+
- New baseline workflow command:
|
|
13
|
+
- `baseline --save --file <path>` to snapshot dependency state
|
|
14
|
+
- `baseline --check --file <path>` to detect dependency drift
|
|
15
|
+
- New `init-ci --mode enterprise` template:
|
|
16
|
+
- Node runtime matrix (`20`, `22`)
|
|
17
|
+
- stricter default permissions
|
|
18
|
+
- artifact retention policy
|
|
19
|
+
- built-in rollout gate flags (`--fail-on`, `--max-updates`)
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- Dependency target selection now evaluates available package versions from registry metadata, improving `patch|minor|major` accuracy.
|
|
24
|
+
- CLI parser now rejects unknown options and missing option values with explicit errors (safer CI behavior).
|
|
25
|
+
- SARIF output now reports the actual package version dynamically.
|
|
26
|
+
|
|
27
|
+
### Tests
|
|
28
|
+
|
|
29
|
+
- Added baseline snapshot/diff tests.
|
|
30
|
+
- Added enterprise workflow generation tests.
|
|
31
|
+
- Added semver target selection tests using available version sets.
|
|
32
|
+
- Added parser tests for baseline command, rollout flags, and unknown option rejection.
|
|
33
|
+
|
|
5
34
|
## [0.4.4] - 2026-02-27
|
|
6
35
|
|
|
7
36
|
### Changed
|
package/README.md
CHANGED
|
@@ -25,6 +25,7 @@ pnpm add -D @rainy-updates/cli
|
|
|
25
25
|
- `check`: analyze dependencies and report available updates.
|
|
26
26
|
- `upgrade`: rewrite dependency ranges in manifests, optionally install lockfile updates.
|
|
27
27
|
- `warm-cache`: prefetch package metadata for fast and offline checks.
|
|
28
|
+
- `baseline`: save and compare dependency baseline snapshots.
|
|
28
29
|
|
|
29
30
|
## Quick usage
|
|
30
31
|
|
|
@@ -41,6 +42,10 @@ npx @rainy-updates/cli upgrade --target latest --workspace --sync --install
|
|
|
41
42
|
# 4) Warm cache for deterministic offline checks
|
|
42
43
|
npx @rainy-updates/cli warm-cache --workspace --concurrency 32
|
|
43
44
|
npx @rainy-updates/cli check --workspace --offline --ci
|
|
45
|
+
|
|
46
|
+
# 5) Save and compare baseline drift in CI
|
|
47
|
+
npx @rainy-updates/cli baseline --save --file .artifacts/deps-baseline.json --workspace
|
|
48
|
+
npx @rainy-updates/cli baseline --check --file .artifacts/deps-baseline.json --workspace --ci
|
|
44
49
|
```
|
|
45
50
|
|
|
46
51
|
## What it does in production
|
|
@@ -108,7 +113,7 @@ Generate a workflow in the target project automatically:
|
|
|
108
113
|
|
|
109
114
|
```bash
|
|
110
115
|
# strict mode (recommended)
|
|
111
|
-
npx @rainy-updates/cli init-ci --mode
|
|
116
|
+
npx @rainy-updates/cli init-ci --mode enterprise --schedule weekly
|
|
112
117
|
|
|
113
118
|
# lightweight mode
|
|
114
119
|
npx @rainy-updates/cli init-ci --mode minimal --schedule daily
|
|
@@ -121,6 +126,7 @@ Generated file:
|
|
|
121
126
|
Modes:
|
|
122
127
|
|
|
123
128
|
- `strict`: warm-cache + offline check + artifacts + SARIF upload.
|
|
129
|
+
- `enterprise`: strict checks + runtime matrix + retention policy + rollout gates.
|
|
124
130
|
- `minimal`: fast check-only workflow for quick adoption.
|
|
125
131
|
|
|
126
132
|
Schedule:
|
|
@@ -140,6 +146,8 @@ Schedule:
|
|
|
140
146
|
- `--concurrency <n>`
|
|
141
147
|
- `--cache-ttl <seconds>`
|
|
142
148
|
- `--offline`
|
|
149
|
+
- `--fail-on none|patch|minor|major|any`
|
|
150
|
+
- `--max-updates <n>`
|
|
143
151
|
- `--policy-file <path>`
|
|
144
152
|
- `--format table|json|minimal|github`
|
|
145
153
|
- `--json-file <path>`
|
|
@@ -154,6 +162,12 @@ Schedule:
|
|
|
154
162
|
- `--pm auto|npm|pnpm`
|
|
155
163
|
- `--sync`
|
|
156
164
|
|
|
165
|
+
### Baseline-only
|
|
166
|
+
|
|
167
|
+
- `--save`
|
|
168
|
+
- `--check`
|
|
169
|
+
- `--file <path>`
|
|
170
|
+
|
|
157
171
|
## Config support
|
|
158
172
|
|
|
159
173
|
Configuration can be loaded from:
|
package/dist/bin/cli.js
CHANGED
|
@@ -8,6 +8,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
10
|
import { initCiWorkflow } from "../core/init-ci.js";
|
|
11
|
+
import { diffBaseline, saveBaseline } from "../core/baseline.js";
|
|
11
12
|
import { renderResult } from "../output/format.js";
|
|
12
13
|
import { writeGitHubOutput } from "../output/github.js";
|
|
13
14
|
import { createSarifReport } from "../output/sarif.js";
|
|
@@ -34,6 +35,31 @@ async function main() {
|
|
|
34
35
|
: `CI workflow already exists at ${workflow.path}. Use --force to overwrite.\n`);
|
|
35
36
|
return;
|
|
36
37
|
}
|
|
38
|
+
if (parsed.command === "baseline") {
|
|
39
|
+
if (parsed.options.action === "save") {
|
|
40
|
+
const saved = await saveBaseline(parsed.options);
|
|
41
|
+
process.stdout.write(`Saved baseline at ${saved.filePath} (${saved.entries} entries)\n`);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const diff = await diffBaseline(parsed.options);
|
|
45
|
+
const changes = diff.added.length + diff.removed.length + diff.changed.length;
|
|
46
|
+
if (changes === 0) {
|
|
47
|
+
process.stdout.write(`No baseline drift detected (${diff.filePath}).\n`);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
process.stdout.write(`Baseline drift detected (${diff.filePath}).\n`);
|
|
51
|
+
if (diff.added.length > 0) {
|
|
52
|
+
process.stdout.write(`Added: ${diff.added.length}\n`);
|
|
53
|
+
}
|
|
54
|
+
if (diff.removed.length > 0) {
|
|
55
|
+
process.stdout.write(`Removed: ${diff.removed.length}\n`);
|
|
56
|
+
}
|
|
57
|
+
if (diff.changed.length > 0) {
|
|
58
|
+
process.stdout.write(`Changed: ${diff.changed.length}\n`);
|
|
59
|
+
}
|
|
60
|
+
process.exitCode = 1;
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
37
63
|
const result = parsed.command === "upgrade"
|
|
38
64
|
? await upgrade(parsed.options)
|
|
39
65
|
: parsed.command === "warm-cache"
|
|
@@ -58,13 +84,7 @@ async function main() {
|
|
|
58
84
|
await fs.mkdir(path.dirname(parsed.options.sarifFile), { recursive: true });
|
|
59
85
|
await fs.writeFile(parsed.options.sarifFile, JSON.stringify(sarif, null, 2) + "\n", "utf8");
|
|
60
86
|
}
|
|
61
|
-
|
|
62
|
-
process.exitCode = 1;
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
65
|
-
if (result.errors.length > 0) {
|
|
66
|
-
process.exitCode = 2;
|
|
67
|
-
}
|
|
87
|
+
process.exitCode = resolveExitCode(result, parsed.options.failOn, parsed.options.maxUpdates, parsed.options.ci);
|
|
68
88
|
}
|
|
69
89
|
catch (error) {
|
|
70
90
|
process.stderr.write(`rainy-updates: ${String(error)}\n`);
|
|
@@ -117,8 +137,21 @@ Create a GitHub Actions workflow template at:
|
|
|
117
137
|
|
|
118
138
|
Options:
|
|
119
139
|
--force
|
|
120
|
-
--mode minimal|strict
|
|
140
|
+
--mode minimal|strict|enterprise
|
|
121
141
|
--schedule weekly|daily|off`;
|
|
142
|
+
}
|
|
143
|
+
if (isCommand && command === "baseline") {
|
|
144
|
+
return `rainy-updates baseline [options]
|
|
145
|
+
|
|
146
|
+
Save or compare dependency baseline snapshots.
|
|
147
|
+
|
|
148
|
+
Options:
|
|
149
|
+
--save
|
|
150
|
+
--check
|
|
151
|
+
--file <path>
|
|
152
|
+
--workspace
|
|
153
|
+
--dep-kinds deps,dev,optional,peer
|
|
154
|
+
--ci`;
|
|
122
155
|
}
|
|
123
156
|
return `rainy-updates <command> [options]
|
|
124
157
|
|
|
@@ -127,6 +160,7 @@ Commands:
|
|
|
127
160
|
upgrade Apply updates to manifests
|
|
128
161
|
warm-cache Warm local cache for fast/offline checks
|
|
129
162
|
init-ci Scaffold GitHub Actions workflow
|
|
163
|
+
baseline Save/check dependency baseline snapshots
|
|
130
164
|
|
|
131
165
|
Global options:
|
|
132
166
|
--cwd <path>
|
|
@@ -138,6 +172,8 @@ Global options:
|
|
|
138
172
|
--sarif-file <path>
|
|
139
173
|
--pr-report-file <path>
|
|
140
174
|
--policy-file <path>
|
|
175
|
+
--fail-on none|patch|minor|major|any
|
|
176
|
+
--max-updates <n>
|
|
141
177
|
--concurrency <n>
|
|
142
178
|
--cache-ttl <seconds>
|
|
143
179
|
--offline
|
|
@@ -152,3 +188,22 @@ async function readPackageVersion() {
|
|
|
152
188
|
const parsed = JSON.parse(content);
|
|
153
189
|
return parsed.version ?? "0.0.0";
|
|
154
190
|
}
|
|
191
|
+
function resolveExitCode(result, failOn, maxUpdates, ciMode) {
|
|
192
|
+
if (result.errors.length > 0)
|
|
193
|
+
return 2;
|
|
194
|
+
if (typeof maxUpdates === "number" && result.updates.length > maxUpdates)
|
|
195
|
+
return 1;
|
|
196
|
+
const effectiveFailOn = failOn && failOn !== "none" ? failOn : ciMode ? "any" : "none";
|
|
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");
|
|
209
|
+
}
|
package/dist/cache/cache.d.ts
CHANGED
|
@@ -5,5 +5,5 @@ export declare class VersionCache {
|
|
|
5
5
|
static create(customPath?: string): Promise<VersionCache>;
|
|
6
6
|
getValid(packageName: string, target: TargetLevel): Promise<CachedVersion | null>;
|
|
7
7
|
getAny(packageName: string, target: TargetLevel): Promise<CachedVersion | null>;
|
|
8
|
-
set(packageName: string, target: TargetLevel, latestVersion: string, ttlSeconds: number): Promise<void>;
|
|
8
|
+
set(packageName: string, target: TargetLevel, latestVersion: string, availableVersions: string[], ttlSeconds: number): Promise<void>;
|
|
9
9
|
}
|
package/dist/cache/cache.js
CHANGED
|
@@ -9,7 +9,13 @@ class FileCacheStore {
|
|
|
9
9
|
async get(packageName, target) {
|
|
10
10
|
const entries = await this.readEntries();
|
|
11
11
|
const key = this.getKey(packageName, target);
|
|
12
|
-
|
|
12
|
+
const entry = entries[key];
|
|
13
|
+
if (!entry)
|
|
14
|
+
return null;
|
|
15
|
+
return {
|
|
16
|
+
...entry,
|
|
17
|
+
availableVersions: Array.isArray(entry.availableVersions) ? entry.availableVersions : [entry.latestVersion],
|
|
18
|
+
};
|
|
13
19
|
}
|
|
14
20
|
async set(entry) {
|
|
15
21
|
const entries = await this.readEntries();
|
|
@@ -39,31 +45,63 @@ class SqliteCacheStore {
|
|
|
39
45
|
package_name TEXT NOT NULL,
|
|
40
46
|
target TEXT NOT NULL,
|
|
41
47
|
latest_version TEXT NOT NULL,
|
|
48
|
+
available_versions TEXT NOT NULL,
|
|
42
49
|
fetched_at INTEGER NOT NULL,
|
|
43
50
|
ttl_seconds INTEGER NOT NULL,
|
|
44
51
|
PRIMARY KEY (package_name, target)
|
|
45
52
|
);
|
|
46
53
|
`);
|
|
54
|
+
this.ensureSchema();
|
|
47
55
|
}
|
|
48
56
|
async get(packageName, target) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
.
|
|
57
|
+
let row;
|
|
58
|
+
try {
|
|
59
|
+
row = this.db
|
|
60
|
+
.prepare(`SELECT package_name, target, latest_version, available_versions, fetched_at, ttl_seconds FROM versions WHERE package_name = ? AND target = ?`)
|
|
61
|
+
.get(packageName, target);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
row = this.db
|
|
65
|
+
.prepare(`SELECT package_name, target, latest_version, fetched_at, ttl_seconds FROM versions WHERE package_name = ? AND target = ?`)
|
|
66
|
+
.get(packageName, target);
|
|
67
|
+
}
|
|
52
68
|
if (!row)
|
|
53
69
|
return null;
|
|
54
70
|
return {
|
|
55
71
|
packageName: row.package_name,
|
|
56
72
|
target: row.target,
|
|
57
73
|
latestVersion: row.latest_version,
|
|
74
|
+
availableVersions: parseJsonArray(row.available_versions ?? row.latest_version, row.latest_version),
|
|
58
75
|
fetchedAt: row.fetched_at,
|
|
59
76
|
ttlSeconds: row.ttl_seconds,
|
|
60
77
|
};
|
|
61
78
|
}
|
|
62
79
|
async set(entry) {
|
|
63
|
-
|
|
64
|
-
.
|
|
65
|
-
|
|
66
|
-
|
|
80
|
+
try {
|
|
81
|
+
this.db
|
|
82
|
+
.prepare(`INSERT OR REPLACE INTO versions (package_name, target, latest_version, available_versions, fetched_at, ttl_seconds)
|
|
83
|
+
VALUES (?, ?, ?, ?, ?, ?)`)
|
|
84
|
+
.run(entry.packageName, entry.target, entry.latestVersion, JSON.stringify(entry.availableVersions), entry.fetchedAt, entry.ttlSeconds);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
this.db
|
|
88
|
+
.prepare(`INSERT OR REPLACE INTO versions (package_name, target, latest_version, fetched_at, ttl_seconds)
|
|
89
|
+
VALUES (?, ?, ?, ?, ?)`)
|
|
90
|
+
.run(entry.packageName, entry.target, entry.latestVersion, entry.fetchedAt, entry.ttlSeconds);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
ensureSchema() {
|
|
94
|
+
try {
|
|
95
|
+
const columns = this.db.prepare("PRAGMA table_info(versions);").all();
|
|
96
|
+
const hasAvailableVersions = columns.some((column) => column.name === "available_versions");
|
|
97
|
+
if (!hasAvailableVersions) {
|
|
98
|
+
this.db.exec("ALTER TABLE versions ADD COLUMN available_versions TEXT;");
|
|
99
|
+
}
|
|
100
|
+
this.db.exec("UPDATE versions SET available_versions = latest_version WHERE available_versions IS NULL;");
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
// Best-effort migration.
|
|
104
|
+
}
|
|
67
105
|
}
|
|
68
106
|
}
|
|
69
107
|
export class VersionCache {
|
|
@@ -92,11 +130,12 @@ export class VersionCache {
|
|
|
92
130
|
async getAny(packageName, target) {
|
|
93
131
|
return this.store.get(packageName, target);
|
|
94
132
|
}
|
|
95
|
-
async set(packageName, target, latestVersion, ttlSeconds) {
|
|
133
|
+
async set(packageName, target, latestVersion, availableVersions, ttlSeconds) {
|
|
96
134
|
await this.store.set({
|
|
97
135
|
packageName,
|
|
98
136
|
target,
|
|
99
137
|
latestVersion,
|
|
138
|
+
availableVersions,
|
|
100
139
|
fetchedAt: Date.now(),
|
|
101
140
|
ttlSeconds,
|
|
102
141
|
});
|
|
@@ -115,3 +154,17 @@ async function tryCreateSqliteStore(dbPath) {
|
|
|
115
154
|
}
|
|
116
155
|
return null;
|
|
117
156
|
}
|
|
157
|
+
function parseJsonArray(raw, fallback) {
|
|
158
|
+
if (typeof raw !== "string")
|
|
159
|
+
return [fallback];
|
|
160
|
+
try {
|
|
161
|
+
const parsed = JSON.parse(raw);
|
|
162
|
+
if (!Array.isArray(parsed))
|
|
163
|
+
return [fallback];
|
|
164
|
+
const values = parsed.filter((value) => typeof value === "string");
|
|
165
|
+
return values.length > 0 ? values : [fallback];
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
return [fallback];
|
|
169
|
+
}
|
|
170
|
+
}
|
package/dist/config/loader.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { DependencyKind, OutputFormat, TargetLevel } from "../types/index.js";
|
|
1
|
+
import type { DependencyKind, FailOnLevel, OutputFormat, TargetLevel } from "../types/index.js";
|
|
2
2
|
export interface FileConfig {
|
|
3
3
|
target?: TargetLevel;
|
|
4
4
|
filter?: string;
|
|
@@ -15,6 +15,8 @@ export interface FileConfig {
|
|
|
15
15
|
offline?: boolean;
|
|
16
16
|
policyFile?: string;
|
|
17
17
|
prReportFile?: string;
|
|
18
|
+
failOn?: FailOnLevel;
|
|
19
|
+
maxUpdates?: number;
|
|
18
20
|
install?: boolean;
|
|
19
21
|
packageManager?: "auto" | "npm" | "pnpm";
|
|
20
22
|
sync?: boolean;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { BaselineOptions, DependencyKind } from "../types/index.js";
|
|
2
|
+
interface BaselineEntry {
|
|
3
|
+
packagePath: string;
|
|
4
|
+
kind: DependencyKind;
|
|
5
|
+
name: string;
|
|
6
|
+
range: string;
|
|
7
|
+
}
|
|
8
|
+
export interface BaselineSaveResult {
|
|
9
|
+
filePath: string;
|
|
10
|
+
entries: number;
|
|
11
|
+
}
|
|
12
|
+
export interface BaselineDiffResult {
|
|
13
|
+
filePath: string;
|
|
14
|
+
added: BaselineEntry[];
|
|
15
|
+
removed: BaselineEntry[];
|
|
16
|
+
changed: Array<{
|
|
17
|
+
before: BaselineEntry;
|
|
18
|
+
after: BaselineEntry;
|
|
19
|
+
}>;
|
|
20
|
+
}
|
|
21
|
+
export declare function saveBaseline(options: BaselineOptions): Promise<BaselineSaveResult>;
|
|
22
|
+
export declare function diffBaseline(options: BaselineOptions): Promise<BaselineDiffResult>;
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { collectDependencies, readManifest } from "../parsers/package-json.js";
|
|
4
|
+
import { discoverPackageDirs } from "../workspace/discover.js";
|
|
5
|
+
export async function saveBaseline(options) {
|
|
6
|
+
const entries = await collectBaselineEntries(options.cwd, options.workspace, options.includeKinds);
|
|
7
|
+
const payload = {
|
|
8
|
+
version: 1,
|
|
9
|
+
createdAt: new Date().toISOString(),
|
|
10
|
+
entries,
|
|
11
|
+
};
|
|
12
|
+
await fs.mkdir(path.dirname(options.filePath), { recursive: true });
|
|
13
|
+
await fs.writeFile(options.filePath, JSON.stringify(payload, null, 2) + "\n", "utf8");
|
|
14
|
+
return {
|
|
15
|
+
filePath: options.filePath,
|
|
16
|
+
entries: entries.length,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export async function diffBaseline(options) {
|
|
20
|
+
const content = await fs.readFile(options.filePath, "utf8");
|
|
21
|
+
const baseline = JSON.parse(content);
|
|
22
|
+
const currentEntries = await collectBaselineEntries(options.cwd, options.workspace, options.includeKinds);
|
|
23
|
+
const baselineMap = new Map(baseline.entries.map((entry) => [toKey(entry), entry]));
|
|
24
|
+
const currentMap = new Map(currentEntries.map((entry) => [toKey(entry), entry]));
|
|
25
|
+
const added = [];
|
|
26
|
+
const removed = [];
|
|
27
|
+
const changed = [];
|
|
28
|
+
for (const [key, current] of currentMap) {
|
|
29
|
+
const base = baselineMap.get(key);
|
|
30
|
+
if (!base) {
|
|
31
|
+
added.push(current);
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (base.range !== current.range) {
|
|
35
|
+
changed.push({ before: base, after: current });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
for (const [key, base] of baselineMap) {
|
|
39
|
+
if (!currentMap.has(key)) {
|
|
40
|
+
removed.push(base);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
filePath: options.filePath,
|
|
45
|
+
added: sortEntries(added),
|
|
46
|
+
removed: sortEntries(removed),
|
|
47
|
+
changed: changed.sort((a, b) => toKey(a.after).localeCompare(toKey(b.after))),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
async function collectBaselineEntries(cwd, workspace, includeKinds) {
|
|
51
|
+
const packageDirs = await discoverPackageDirs(cwd, workspace);
|
|
52
|
+
const entries = [];
|
|
53
|
+
for (const packageDir of packageDirs) {
|
|
54
|
+
const manifest = await readManifest(packageDir);
|
|
55
|
+
const deps = collectDependencies(manifest, includeKinds);
|
|
56
|
+
for (const dep of deps) {
|
|
57
|
+
entries.push({
|
|
58
|
+
packagePath: path.relative(cwd, packageDir) || ".",
|
|
59
|
+
kind: dep.kind,
|
|
60
|
+
name: dep.name,
|
|
61
|
+
range: dep.range,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return sortEntries(entries);
|
|
66
|
+
}
|
|
67
|
+
function toKey(entry) {
|
|
68
|
+
return `${entry.packagePath}::${entry.kind}::${entry.name}`;
|
|
69
|
+
}
|
|
70
|
+
function sortEntries(entries) {
|
|
71
|
+
return [...entries].sort((a, b) => toKey(a).localeCompare(toKey(b)));
|
|
72
|
+
}
|
package/dist/core/check.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { collectDependencies, readManifest } from "../parsers/package-json.js";
|
|
3
3
|
import { matchesPattern } from "../utils/pattern.js";
|
|
4
|
-
import { applyRangeStyle, classifyDiff, clampTarget,
|
|
4
|
+
import { applyRangeStyle, classifyDiff, clampTarget, pickTargetVersionFromAvailable } from "../utils/semver.js";
|
|
5
5
|
import { VersionCache } from "../cache/cache.js";
|
|
6
6
|
import { NpmRegistryClient } from "../registry/npm.js";
|
|
7
7
|
import { detectPackageManager } from "../pm/detect.js";
|
|
@@ -53,7 +53,10 @@ export async function check(options) {
|
|
|
53
53
|
for (const packageName of uniquePackageNames) {
|
|
54
54
|
const cached = await cache.getValid(packageName, options.target);
|
|
55
55
|
if (cached) {
|
|
56
|
-
resolvedVersions.set(packageName,
|
|
56
|
+
resolvedVersions.set(packageName, {
|
|
57
|
+
latestVersion: cached.latestVersion,
|
|
58
|
+
availableVersions: cached.availableVersions,
|
|
59
|
+
});
|
|
57
60
|
}
|
|
58
61
|
else {
|
|
59
62
|
unresolvedPackages.push(packageName);
|
|
@@ -64,7 +67,10 @@ export async function check(options) {
|
|
|
64
67
|
for (const packageName of unresolvedPackages) {
|
|
65
68
|
const stale = await cache.getAny(packageName, options.target);
|
|
66
69
|
if (stale) {
|
|
67
|
-
resolvedVersions.set(packageName,
|
|
70
|
+
resolvedVersions.set(packageName, {
|
|
71
|
+
latestVersion: stale.latestVersion,
|
|
72
|
+
availableVersions: stale.availableVersions,
|
|
73
|
+
});
|
|
68
74
|
warnings.push(`Using stale cache for ${packageName} because --offline is enabled.`);
|
|
69
75
|
}
|
|
70
76
|
else {
|
|
@@ -73,19 +79,25 @@ export async function check(options) {
|
|
|
73
79
|
}
|
|
74
80
|
}
|
|
75
81
|
else {
|
|
76
|
-
const fetched = await registryClient.
|
|
82
|
+
const fetched = await registryClient.resolveManyPackageMetadata(unresolvedPackages, {
|
|
77
83
|
concurrency: options.concurrency,
|
|
78
84
|
});
|
|
79
|
-
for (const [packageName,
|
|
80
|
-
resolvedVersions.set(packageName,
|
|
81
|
-
|
|
82
|
-
|
|
85
|
+
for (const [packageName, metadata] of fetched.metadata) {
|
|
86
|
+
resolvedVersions.set(packageName, {
|
|
87
|
+
latestVersion: metadata.latestVersion,
|
|
88
|
+
availableVersions: metadata.versions,
|
|
89
|
+
});
|
|
90
|
+
if (metadata.latestVersion) {
|
|
91
|
+
await cache.set(packageName, options.target, metadata.latestVersion, metadata.versions, options.cacheTtlSeconds);
|
|
83
92
|
}
|
|
84
93
|
}
|
|
85
94
|
for (const [packageName, error] of fetched.errors) {
|
|
86
95
|
const stale = await cache.getAny(packageName, options.target);
|
|
87
96
|
if (stale) {
|
|
88
|
-
resolvedVersions.set(packageName,
|
|
97
|
+
resolvedVersions.set(packageName, {
|
|
98
|
+
latestVersion: stale.latestVersion,
|
|
99
|
+
availableVersions: stale.availableVersions,
|
|
100
|
+
});
|
|
89
101
|
warnings.push(`Using stale cache for ${packageName} due to registry error: ${error}`);
|
|
90
102
|
}
|
|
91
103
|
else {
|
|
@@ -95,12 +107,12 @@ export async function check(options) {
|
|
|
95
107
|
}
|
|
96
108
|
}
|
|
97
109
|
for (const task of tasks) {
|
|
98
|
-
const
|
|
99
|
-
if (!latestVersion)
|
|
110
|
+
const metadata = resolvedVersions.get(task.dependency.name);
|
|
111
|
+
if (!metadata?.latestVersion)
|
|
100
112
|
continue;
|
|
101
113
|
const rule = policy.packageRules.get(task.dependency.name);
|
|
102
114
|
const effectiveTarget = clampTarget(options.target, rule?.maxTarget);
|
|
103
|
-
const picked =
|
|
115
|
+
const picked = pickTargetVersionFromAvailable(task.dependency.range, metadata.availableVersions, metadata.latestVersion, effectiveTarget);
|
|
104
116
|
if (!picked)
|
|
105
117
|
continue;
|
|
106
118
|
const nextRange = applyRangeStyle(task.dependency.range, picked);
|
package/dist/core/init-ci.d.ts
CHANGED
package/dist/core/init-ci.js
CHANGED
|
@@ -15,7 +15,9 @@ export async function initCiWorkflow(cwd, force, options) {
|
|
|
15
15
|
const scheduleBlock = renderScheduleBlock(options.schedule);
|
|
16
16
|
const workflow = options.mode === "minimal"
|
|
17
17
|
? minimalWorkflowTemplate(scheduleBlock, packageManager)
|
|
18
|
-
:
|
|
18
|
+
: options.mode === "strict"
|
|
19
|
+
? strictWorkflowTemplate(scheduleBlock, packageManager)
|
|
20
|
+
: enterpriseWorkflowTemplate(scheduleBlock, packageManager);
|
|
19
21
|
await mkdir(path.dirname(workflowPath), { recursive: true });
|
|
20
22
|
await writeFile(workflowPath, workflow, "utf8");
|
|
21
23
|
return { path: workflowPath, created: true };
|
|
@@ -49,3 +51,9 @@ function minimalWorkflowTemplate(scheduleBlock, packageManager) {
|
|
|
49
51
|
function strictWorkflowTemplate(scheduleBlock, packageManager) {
|
|
50
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`;
|
|
51
53
|
}
|
|
54
|
+
function enterpriseWorkflowTemplate(scheduleBlock, packageManager) {
|
|
55
|
+
const detectedPmInstall = packageManager === "pnpm"
|
|
56
|
+
? "corepack enable && corepack prepare pnpm@9 --activate && pnpm install --frozen-lockfile"
|
|
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`;
|
|
59
|
+
}
|
package/dist/core/options.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CheckOptions, UpgradeOptions } from "../types/index.js";
|
|
1
|
+
import type { BaselineOptions, CheckOptions, UpgradeOptions } from "../types/index.js";
|
|
2
2
|
import type { InitCiMode, InitCiSchedule } from "./init-ci.js";
|
|
3
3
|
export type ParsedCliArgs = {
|
|
4
4
|
command: "check";
|
|
@@ -17,5 +17,10 @@ export type ParsedCliArgs = {
|
|
|
17
17
|
mode: InitCiMode;
|
|
18
18
|
schedule: InitCiSchedule;
|
|
19
19
|
};
|
|
20
|
+
} | {
|
|
21
|
+
command: "baseline";
|
|
22
|
+
options: BaselineOptions & {
|
|
23
|
+
action: "save" | "check";
|
|
24
|
+
};
|
|
20
25
|
};
|
|
21
26
|
export declare function parseCliArgs(argv: string[]): Promise<ParsedCliArgs>;
|
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"];
|
|
10
|
+
const KNOWN_COMMANDS = ["check", "upgrade", "warm-cache", "init-ci", "baseline"];
|
|
11
11
|
export async function parseCliArgs(argv) {
|
|
12
12
|
const firstArg = argv[0];
|
|
13
13
|
const isKnownCommand = KNOWN_COMMANDS.includes(firstArg);
|
|
@@ -34,10 +34,14 @@ export async function parseCliArgs(argv) {
|
|
|
34
34
|
offline: false,
|
|
35
35
|
policyFile: undefined,
|
|
36
36
|
prReportFile: undefined,
|
|
37
|
+
failOn: "none",
|
|
38
|
+
maxUpdates: undefined,
|
|
37
39
|
};
|
|
38
40
|
let force = false;
|
|
39
|
-
let initCiMode = "
|
|
41
|
+
let initCiMode = "enterprise";
|
|
40
42
|
let initCiSchedule = "weekly";
|
|
43
|
+
let baselineAction = "check";
|
|
44
|
+
let baselineFilePath = path.resolve(base.cwd, ".rainy-updates-baseline.json");
|
|
41
45
|
let resolvedConfig = await loadConfig(base.cwd);
|
|
42
46
|
applyConfig(base, resolvedConfig);
|
|
43
47
|
for (let index = 0; index < args.length; index += 1) {
|
|
@@ -48,23 +52,36 @@ export async function parseCliArgs(argv) {
|
|
|
48
52
|
index += 1;
|
|
49
53
|
continue;
|
|
50
54
|
}
|
|
55
|
+
if (current === "--target") {
|
|
56
|
+
throw new Error("Missing value for --target");
|
|
57
|
+
}
|
|
51
58
|
if (current === "--filter" && next) {
|
|
52
59
|
base.filter = next;
|
|
53
60
|
index += 1;
|
|
54
61
|
continue;
|
|
55
62
|
}
|
|
63
|
+
if (current === "--filter") {
|
|
64
|
+
throw new Error("Missing value for --filter");
|
|
65
|
+
}
|
|
56
66
|
if (current === "--reject" && next) {
|
|
57
67
|
base.reject = next;
|
|
58
68
|
index += 1;
|
|
59
69
|
continue;
|
|
60
70
|
}
|
|
71
|
+
if (current === "--reject") {
|
|
72
|
+
throw new Error("Missing value for --reject");
|
|
73
|
+
}
|
|
61
74
|
if (current === "--cwd" && next) {
|
|
62
75
|
base.cwd = path.resolve(next);
|
|
63
76
|
resolvedConfig = await loadConfig(base.cwd);
|
|
64
77
|
applyConfig(base, resolvedConfig);
|
|
78
|
+
baselineFilePath = path.resolve(base.cwd, ".rainy-updates-baseline.json");
|
|
65
79
|
index += 1;
|
|
66
80
|
continue;
|
|
67
81
|
}
|
|
82
|
+
if (current === "--cwd") {
|
|
83
|
+
throw new Error("Missing value for --cwd");
|
|
84
|
+
}
|
|
68
85
|
if (current === "--cache-ttl" && next) {
|
|
69
86
|
const parsed = Number(next);
|
|
70
87
|
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
@@ -74,11 +91,17 @@ export async function parseCliArgs(argv) {
|
|
|
74
91
|
index += 1;
|
|
75
92
|
continue;
|
|
76
93
|
}
|
|
94
|
+
if (current === "--cache-ttl") {
|
|
95
|
+
throw new Error("Missing value for --cache-ttl");
|
|
96
|
+
}
|
|
77
97
|
if (current === "--format" && next) {
|
|
78
98
|
base.format = ensureFormat(next);
|
|
79
99
|
index += 1;
|
|
80
100
|
continue;
|
|
81
101
|
}
|
|
102
|
+
if (current === "--format") {
|
|
103
|
+
throw new Error("Missing value for --format");
|
|
104
|
+
}
|
|
82
105
|
if (current === "--ci") {
|
|
83
106
|
base.ci = true;
|
|
84
107
|
continue;
|
|
@@ -92,16 +115,25 @@ export async function parseCliArgs(argv) {
|
|
|
92
115
|
index += 1;
|
|
93
116
|
continue;
|
|
94
117
|
}
|
|
118
|
+
if (current === "--json-file") {
|
|
119
|
+
throw new Error("Missing value for --json-file");
|
|
120
|
+
}
|
|
95
121
|
if (current === "--github-output" && next) {
|
|
96
122
|
base.githubOutputFile = path.resolve(next);
|
|
97
123
|
index += 1;
|
|
98
124
|
continue;
|
|
99
125
|
}
|
|
126
|
+
if (current === "--github-output") {
|
|
127
|
+
throw new Error("Missing value for --github-output");
|
|
128
|
+
}
|
|
100
129
|
if (current === "--sarif-file" && next) {
|
|
101
130
|
base.sarifFile = path.resolve(next);
|
|
102
131
|
index += 1;
|
|
103
132
|
continue;
|
|
104
133
|
}
|
|
134
|
+
if (current === "--sarif-file") {
|
|
135
|
+
throw new Error("Missing value for --sarif-file");
|
|
136
|
+
}
|
|
105
137
|
if (current === "--concurrency" && next) {
|
|
106
138
|
const parsed = Number(next);
|
|
107
139
|
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
@@ -111,6 +143,9 @@ export async function parseCliArgs(argv) {
|
|
|
111
143
|
index += 1;
|
|
112
144
|
continue;
|
|
113
145
|
}
|
|
146
|
+
if (current === "--concurrency") {
|
|
147
|
+
throw new Error("Missing value for --concurrency");
|
|
148
|
+
}
|
|
114
149
|
if (current === "--offline") {
|
|
115
150
|
base.offline = true;
|
|
116
151
|
continue;
|
|
@@ -120,30 +155,99 @@ export async function parseCliArgs(argv) {
|
|
|
120
155
|
index += 1;
|
|
121
156
|
continue;
|
|
122
157
|
}
|
|
158
|
+
if (current === "--policy-file") {
|
|
159
|
+
throw new Error("Missing value for --policy-file");
|
|
160
|
+
}
|
|
123
161
|
if (current === "--pr-report-file" && next) {
|
|
124
162
|
base.prReportFile = path.resolve(next);
|
|
125
163
|
index += 1;
|
|
126
164
|
continue;
|
|
127
165
|
}
|
|
166
|
+
if (current === "--pr-report-file") {
|
|
167
|
+
throw new Error("Missing value for --pr-report-file");
|
|
168
|
+
}
|
|
128
169
|
if (current === "--force") {
|
|
129
170
|
force = true;
|
|
130
171
|
continue;
|
|
131
172
|
}
|
|
173
|
+
if (current === "--install" && command === "upgrade") {
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (current === "--sync" && command === "upgrade") {
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
if (current === "--pm" && next && command === "upgrade") {
|
|
180
|
+
parsePackageManager(args);
|
|
181
|
+
index += 1;
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (current === "--pm" && command === "upgrade") {
|
|
185
|
+
throw new Error("Missing value for --pm");
|
|
186
|
+
}
|
|
132
187
|
if (current === "--mode" && next) {
|
|
133
188
|
initCiMode = ensureInitCiMode(next);
|
|
134
189
|
index += 1;
|
|
135
190
|
continue;
|
|
136
191
|
}
|
|
192
|
+
if (current === "--mode") {
|
|
193
|
+
throw new Error("Missing value for --mode");
|
|
194
|
+
}
|
|
137
195
|
if (current === "--schedule" && next) {
|
|
138
196
|
initCiSchedule = ensureInitCiSchedule(next);
|
|
139
197
|
index += 1;
|
|
140
198
|
continue;
|
|
141
199
|
}
|
|
200
|
+
if (current === "--schedule") {
|
|
201
|
+
throw new Error("Missing value for --schedule");
|
|
202
|
+
}
|
|
142
203
|
if (current === "--dep-kinds" && next) {
|
|
143
204
|
base.includeKinds = parseDependencyKinds(next);
|
|
144
205
|
index += 1;
|
|
145
206
|
continue;
|
|
146
207
|
}
|
|
208
|
+
if (current === "--dep-kinds") {
|
|
209
|
+
throw new Error("Missing value for --dep-kinds");
|
|
210
|
+
}
|
|
211
|
+
if (current === "--fail-on" && next) {
|
|
212
|
+
base.failOn = ensureFailOn(next);
|
|
213
|
+
index += 1;
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
if (current === "--fail-on") {
|
|
217
|
+
throw new Error("Missing value for --fail-on");
|
|
218
|
+
}
|
|
219
|
+
if (current === "--max-updates" && next) {
|
|
220
|
+
const parsed = Number(next);
|
|
221
|
+
if (!Number.isInteger(parsed) || parsed < 0) {
|
|
222
|
+
throw new Error("--max-updates must be a non-negative integer");
|
|
223
|
+
}
|
|
224
|
+
base.maxUpdates = parsed;
|
|
225
|
+
index += 1;
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
if (current === "--max-updates") {
|
|
229
|
+
throw new Error("Missing value for --max-updates");
|
|
230
|
+
}
|
|
231
|
+
if (current === "--save") {
|
|
232
|
+
baselineAction = "save";
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
if (current === "--check") {
|
|
236
|
+
baselineAction = "check";
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
if (current === "--file" && next) {
|
|
240
|
+
baselineFilePath = path.resolve(base.cwd, next);
|
|
241
|
+
index += 1;
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
if (current === "--file") {
|
|
245
|
+
throw new Error("Missing value for --file");
|
|
246
|
+
}
|
|
247
|
+
if (current.startsWith("-")) {
|
|
248
|
+
throw new Error(`Unknown option: ${current}`);
|
|
249
|
+
}
|
|
250
|
+
throw new Error(`Unexpected argument: ${current}`);
|
|
147
251
|
}
|
|
148
252
|
if (command === "upgrade") {
|
|
149
253
|
const configPm = resolvedConfig.packageManager;
|
|
@@ -170,6 +274,19 @@ export async function parseCliArgs(argv) {
|
|
|
170
274
|
},
|
|
171
275
|
};
|
|
172
276
|
}
|
|
277
|
+
if (command === "baseline") {
|
|
278
|
+
return {
|
|
279
|
+
command,
|
|
280
|
+
options: {
|
|
281
|
+
action: baselineAction,
|
|
282
|
+
cwd: base.cwd,
|
|
283
|
+
workspace: base.workspace,
|
|
284
|
+
includeKinds: base.includeKinds,
|
|
285
|
+
filePath: baselineFilePath,
|
|
286
|
+
ci: base.ci,
|
|
287
|
+
},
|
|
288
|
+
};
|
|
289
|
+
}
|
|
173
290
|
return {
|
|
174
291
|
command: "check",
|
|
175
292
|
options: base,
|
|
@@ -212,6 +329,12 @@ function applyConfig(base, config) {
|
|
|
212
329
|
if (typeof config.prReportFile === "string") {
|
|
213
330
|
base.prReportFile = path.resolve(base.cwd, config.prReportFile);
|
|
214
331
|
}
|
|
332
|
+
if (typeof config.failOn === "string") {
|
|
333
|
+
base.failOn = ensureFailOn(config.failOn);
|
|
334
|
+
}
|
|
335
|
+
if (typeof config.maxUpdates === "number" && Number.isInteger(config.maxUpdates) && config.maxUpdates >= 0) {
|
|
336
|
+
base.maxUpdates = config.maxUpdates;
|
|
337
|
+
}
|
|
215
338
|
}
|
|
216
339
|
function parsePackageManager(args) {
|
|
217
340
|
const index = args.indexOf("--pm");
|
|
@@ -257,10 +380,10 @@ function parseDependencyKinds(value) {
|
|
|
257
380
|
return Array.from(new Set(mapped));
|
|
258
381
|
}
|
|
259
382
|
function ensureInitCiMode(value) {
|
|
260
|
-
if (value === "minimal" || value === "strict") {
|
|
383
|
+
if (value === "minimal" || value === "strict" || value === "enterprise") {
|
|
261
384
|
return value;
|
|
262
385
|
}
|
|
263
|
-
throw new Error("--mode must be minimal or
|
|
386
|
+
throw new Error("--mode must be minimal, strict or enterprise");
|
|
264
387
|
}
|
|
265
388
|
function ensureInitCiSchedule(value) {
|
|
266
389
|
if (value === "weekly" || value === "daily" || value === "off") {
|
|
@@ -268,3 +391,9 @@ function ensureInitCiSchedule(value) {
|
|
|
268
391
|
}
|
|
269
392
|
throw new Error("--schedule must be weekly, daily or off");
|
|
270
393
|
}
|
|
394
|
+
function ensureFailOn(value) {
|
|
395
|
+
if (value === "none" || value === "patch" || value === "minor" || value === "major" || value === "any") {
|
|
396
|
+
return value;
|
|
397
|
+
}
|
|
398
|
+
throw new Error("--fail-on must be none, patch, minor, major or any");
|
|
399
|
+
}
|
package/dist/core/warm-cache.js
CHANGED
|
@@ -52,12 +52,12 @@ export async function warmCache(options) {
|
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
else {
|
|
55
|
-
const fetched = await registryClient.
|
|
55
|
+
const fetched = await registryClient.resolveManyPackageMetadata(needsFetch, {
|
|
56
56
|
concurrency: options.concurrency,
|
|
57
57
|
});
|
|
58
|
-
for (const [pkg,
|
|
59
|
-
if (
|
|
60
|
-
await cache.set(pkg, options.target,
|
|
58
|
+
for (const [pkg, metadata] of fetched.metadata) {
|
|
59
|
+
if (metadata.latestVersion) {
|
|
60
|
+
await cache.set(pkg, options.target, metadata.latestVersion, metadata.versions, options.cacheTtlSeconds);
|
|
61
61
|
warmed += 1;
|
|
62
62
|
}
|
|
63
63
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,8 @@ export { check } from "./core/check.js";
|
|
|
2
2
|
export { upgrade } from "./core/upgrade.js";
|
|
3
3
|
export { warmCache } from "./core/warm-cache.js";
|
|
4
4
|
export { initCiWorkflow } from "./core/init-ci.js";
|
|
5
|
+
export { saveBaseline, diffBaseline } from "./core/baseline.js";
|
|
5
6
|
export { createSarifReport } from "./output/sarif.js";
|
|
6
7
|
export { writeGitHubOutput, renderGitHubAnnotations } from "./output/github.js";
|
|
7
8
|
export { renderPrReport } from "./output/pr-report.js";
|
|
8
|
-
export type { CheckOptions, CheckResult, DependencyKind, OutputFormat, PackageUpdate, RunOptions, TargetLevel, UpgradeOptions, UpgradeResult, } from "./types/index.js";
|
|
9
|
+
export type { CheckOptions, CheckResult, DependencyKind, FailOnLevel, OutputFormat, PackageUpdate, RunOptions, TargetLevel, UpgradeOptions, UpgradeResult, } from "./types/index.js";
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,7 @@ export { check } from "./core/check.js";
|
|
|
2
2
|
export { upgrade } from "./core/upgrade.js";
|
|
3
3
|
export { warmCache } from "./core/warm-cache.js";
|
|
4
4
|
export { initCiWorkflow } from "./core/init-ci.js";
|
|
5
|
+
export { saveBaseline, diffBaseline } from "./core/baseline.js";
|
|
5
6
|
export { createSarifReport } from "./output/sarif.js";
|
|
6
7
|
export { writeGitHubOutput, renderGitHubAnnotations } from "./output/github.js";
|
|
7
8
|
export { renderPrReport } from "./output/pr-report.js";
|
package/dist/output/sarif.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
1
4
|
export function createSarifReport(result) {
|
|
2
5
|
const dependencyRuleId = "rainy-updates/dependency-update";
|
|
3
6
|
const runtimeRuleId = "rainy-updates/runtime-error";
|
|
@@ -38,7 +41,7 @@ export function createSarifReport(result) {
|
|
|
38
41
|
tool: {
|
|
39
42
|
driver: {
|
|
40
43
|
name: "@rainy-updates/cli",
|
|
41
|
-
version:
|
|
44
|
+
version: getToolVersion(),
|
|
42
45
|
rules: [
|
|
43
46
|
{
|
|
44
47
|
id: dependencyRuleId,
|
|
@@ -58,3 +61,20 @@ export function createSarifReport(result) {
|
|
|
58
61
|
],
|
|
59
62
|
};
|
|
60
63
|
}
|
|
64
|
+
let TOOL_VERSION_CACHE = null;
|
|
65
|
+
function getToolVersion() {
|
|
66
|
+
if (TOOL_VERSION_CACHE)
|
|
67
|
+
return TOOL_VERSION_CACHE;
|
|
68
|
+
try {
|
|
69
|
+
const currentFile = fileURLToPath(import.meta.url);
|
|
70
|
+
const packageJsonPath = path.resolve(path.dirname(currentFile), "../../package.json");
|
|
71
|
+
const content = readFileSync(packageJsonPath, "utf8");
|
|
72
|
+
const parsed = JSON.parse(content);
|
|
73
|
+
TOOL_VERSION_CACHE = parsed.version ?? "0.0.0";
|
|
74
|
+
return TOOL_VERSION_CACHE;
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
TOOL_VERSION_CACHE = "0.0.0";
|
|
78
|
+
return TOOL_VERSION_CACHE;
|
|
79
|
+
}
|
|
80
|
+
}
|
package/dist/registry/npm.d.ts
CHANGED
|
@@ -3,12 +3,23 @@ export interface ResolveManyOptions {
|
|
|
3
3
|
timeoutMs?: number;
|
|
4
4
|
}
|
|
5
5
|
export interface ResolveManyResult {
|
|
6
|
-
|
|
6
|
+
metadata: Map<string, {
|
|
7
|
+
latestVersion: string | null;
|
|
8
|
+
versions: string[];
|
|
9
|
+
}>;
|
|
7
10
|
errors: Map<string, string>;
|
|
8
11
|
}
|
|
9
12
|
export declare class NpmRegistryClient {
|
|
10
13
|
private readonly requesterPromise;
|
|
11
14
|
constructor();
|
|
15
|
+
resolvePackageMetadata(packageName: string, timeoutMs?: number): Promise<{
|
|
16
|
+
latestVersion: string | null;
|
|
17
|
+
versions: string[];
|
|
18
|
+
}>;
|
|
12
19
|
resolveLatestVersion(packageName: string, timeoutMs?: number): Promise<string | null>;
|
|
13
|
-
|
|
20
|
+
resolveManyPackageMetadata(packageNames: string[], options: ResolveManyOptions): Promise<ResolveManyResult>;
|
|
21
|
+
resolveManyLatestVersions(packageNames: string[], options: ResolveManyOptions): Promise<{
|
|
22
|
+
versions: Map<string, string | null>;
|
|
23
|
+
errors: Map<string, string>;
|
|
24
|
+
}>;
|
|
14
25
|
}
|
package/dist/registry/npm.js
CHANGED
|
@@ -6,21 +6,23 @@ export class NpmRegistryClient {
|
|
|
6
6
|
constructor() {
|
|
7
7
|
this.requesterPromise = createRequester();
|
|
8
8
|
}
|
|
9
|
-
async
|
|
9
|
+
async resolvePackageMetadata(packageName, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
10
10
|
const requester = await this.requesterPromise;
|
|
11
11
|
let lastError = null;
|
|
12
12
|
for (let attempt = 1; attempt <= 3; attempt += 1) {
|
|
13
13
|
try {
|
|
14
14
|
const response = await requester(packageName, timeoutMs);
|
|
15
|
-
if (response.status === 404)
|
|
16
|
-
return null;
|
|
15
|
+
if (response.status === 404) {
|
|
16
|
+
return { latestVersion: null, versions: [] };
|
|
17
|
+
}
|
|
17
18
|
if (response.status === 429 || response.status >= 500) {
|
|
18
19
|
throw new Error(`Registry temporary error: ${response.status}`);
|
|
19
20
|
}
|
|
20
21
|
if (response.status < 200 || response.status >= 300) {
|
|
21
22
|
throw new Error(`Registry request failed: ${response.status}`);
|
|
22
23
|
}
|
|
23
|
-
|
|
24
|
+
const versions = Object.keys(response.data?.versions ?? {});
|
|
25
|
+
return { latestVersion: response.data?.["dist-tags"]?.latest ?? null, versions };
|
|
24
26
|
}
|
|
25
27
|
catch (error) {
|
|
26
28
|
lastError = String(error);
|
|
@@ -31,18 +33,22 @@ export class NpmRegistryClient {
|
|
|
31
33
|
}
|
|
32
34
|
throw new Error(`Unable to resolve ${packageName}: ${lastError ?? "unknown error"}`);
|
|
33
35
|
}
|
|
34
|
-
async
|
|
36
|
+
async resolveLatestVersion(packageName, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
37
|
+
const metadata = await this.resolvePackageMetadata(packageName, timeoutMs);
|
|
38
|
+
return metadata.latestVersion;
|
|
39
|
+
}
|
|
40
|
+
async resolveManyPackageMetadata(packageNames, options) {
|
|
35
41
|
const unique = Array.from(new Set(packageNames));
|
|
36
|
-
const
|
|
42
|
+
const metadata = new Map();
|
|
37
43
|
const errors = new Map();
|
|
38
44
|
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
39
45
|
const results = await asyncPool(options.concurrency, unique.map((pkg) => async () => {
|
|
40
46
|
try {
|
|
41
|
-
const
|
|
42
|
-
return { pkg,
|
|
47
|
+
const packageMetadata = await this.resolvePackageMetadata(pkg, timeoutMs);
|
|
48
|
+
return { pkg, packageMetadata, error: null };
|
|
43
49
|
}
|
|
44
50
|
catch (error) {
|
|
45
|
-
return { pkg,
|
|
51
|
+
return { pkg, packageMetadata: null, error: String(error) };
|
|
46
52
|
}
|
|
47
53
|
}));
|
|
48
54
|
for (const result of results) {
|
|
@@ -52,11 +58,22 @@ export class NpmRegistryClient {
|
|
|
52
58
|
if (result.error) {
|
|
53
59
|
errors.set(result.pkg, result.error);
|
|
54
60
|
}
|
|
55
|
-
else {
|
|
56
|
-
|
|
61
|
+
else if (result.packageMetadata) {
|
|
62
|
+
metadata.set(result.pkg, result.packageMetadata);
|
|
57
63
|
}
|
|
58
64
|
}
|
|
59
|
-
return {
|
|
65
|
+
return { metadata, errors };
|
|
66
|
+
}
|
|
67
|
+
async resolveManyLatestVersions(packageNames, options) {
|
|
68
|
+
const metadataResult = await this.resolveManyPackageMetadata(packageNames, options);
|
|
69
|
+
const versions = new Map();
|
|
70
|
+
for (const [name, value] of metadataResult.metadata) {
|
|
71
|
+
versions.set(name, value.latestVersion);
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
versions,
|
|
75
|
+
errors: metadataResult.errors,
|
|
76
|
+
};
|
|
60
77
|
}
|
|
61
78
|
}
|
|
62
79
|
function sleep(ms) {
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export type DependencyKind = "dependencies" | "devDependencies" | "optionalDependencies" | "peerDependencies";
|
|
2
2
|
export type TargetLevel = "patch" | "minor" | "major" | "latest";
|
|
3
3
|
export type OutputFormat = "table" | "json" | "minimal" | "github";
|
|
4
|
+
export type FailOnLevel = "none" | "patch" | "minor" | "major" | "any";
|
|
4
5
|
export interface RunOptions {
|
|
5
6
|
cwd: string;
|
|
6
7
|
target: TargetLevel;
|
|
@@ -18,6 +19,8 @@ export interface RunOptions {
|
|
|
18
19
|
offline: boolean;
|
|
19
20
|
policyFile?: string;
|
|
20
21
|
prReportFile?: string;
|
|
22
|
+
failOn?: FailOnLevel;
|
|
23
|
+
maxUpdates?: number;
|
|
21
24
|
}
|
|
22
25
|
export interface CheckOptions extends RunOptions {
|
|
23
26
|
}
|
|
@@ -26,6 +29,13 @@ export interface UpgradeOptions extends RunOptions {
|
|
|
26
29
|
packageManager: "auto" | "npm" | "pnpm";
|
|
27
30
|
sync: boolean;
|
|
28
31
|
}
|
|
32
|
+
export interface BaselineOptions {
|
|
33
|
+
cwd: string;
|
|
34
|
+
workspace: boolean;
|
|
35
|
+
includeKinds: DependencyKind[];
|
|
36
|
+
filePath: string;
|
|
37
|
+
ci: boolean;
|
|
38
|
+
}
|
|
29
39
|
export interface PackageDependency {
|
|
30
40
|
name: string;
|
|
31
41
|
range: string;
|
|
@@ -78,6 +88,7 @@ export interface CachedVersion {
|
|
|
78
88
|
packageName: string;
|
|
79
89
|
target: TargetLevel;
|
|
80
90
|
latestVersion: string;
|
|
91
|
+
availableVersions: string[];
|
|
81
92
|
fetchedAt: number;
|
|
82
93
|
ttlSeconds: number;
|
|
83
94
|
}
|
package/dist/utils/semver.d.ts
CHANGED
|
@@ -9,5 +9,6 @@ export declare function parseVersion(raw: string): ParsedVersion | null;
|
|
|
9
9
|
export declare function compareVersions(a: ParsedVersion, b: ParsedVersion): number;
|
|
10
10
|
export declare function classifyDiff(currentRange: string, nextVersion: string): TargetLevel;
|
|
11
11
|
export declare function pickTargetVersion(currentRange: string, latestVersion: string, target: TargetLevel): string | null;
|
|
12
|
+
export declare function pickTargetVersionFromAvailable(currentRange: string, availableVersions: string[], latestVersion: string, target: TargetLevel): string | null;
|
|
12
13
|
export declare function applyRangeStyle(previousRange: string, version: string): string;
|
|
13
14
|
export declare function clampTarget(requested: TargetLevel, maxAllowed?: TargetLevel): TargetLevel;
|
package/dist/utils/semver.js
CHANGED
|
@@ -64,6 +64,30 @@ export function pickTargetVersion(currentRange, latestVersion, target) {
|
|
|
64
64
|
}
|
|
65
65
|
return latestVersion;
|
|
66
66
|
}
|
|
67
|
+
export function pickTargetVersionFromAvailable(currentRange, availableVersions, latestVersion, target) {
|
|
68
|
+
const current = parseVersion(currentRange);
|
|
69
|
+
if (!current || target === "latest")
|
|
70
|
+
return latestVersion;
|
|
71
|
+
const parsed = availableVersions
|
|
72
|
+
.map((version) => ({ raw: version, parsed: parseVersion(version) }))
|
|
73
|
+
.filter((item) => item.parsed !== null)
|
|
74
|
+
.filter((item) => compareVersions(item.parsed, current) > 0)
|
|
75
|
+
.sort((a, b) => compareVersions(a.parsed, b.parsed));
|
|
76
|
+
if (parsed.length === 0)
|
|
77
|
+
return null;
|
|
78
|
+
if (target === "major") {
|
|
79
|
+
return parsed[parsed.length - 1]?.raw ?? null;
|
|
80
|
+
}
|
|
81
|
+
if (target === "minor") {
|
|
82
|
+
const sameMajor = parsed.filter((item) => item.parsed.major === current.major);
|
|
83
|
+
return sameMajor.length > 0 ? sameMajor[sameMajor.length - 1].raw : null;
|
|
84
|
+
}
|
|
85
|
+
if (target === "patch") {
|
|
86
|
+
const sameLine = parsed.filter((item) => item.parsed.major === current.major && item.parsed.minor === current.minor);
|
|
87
|
+
return sameLine.length > 0 ? sameLine[sameLine.length - 1].raw : null;
|
|
88
|
+
}
|
|
89
|
+
return latestVersion;
|
|
90
|
+
}
|
|
67
91
|
export function applyRangeStyle(previousRange, version) {
|
|
68
92
|
const prefix = normalizeRangePrefix(previousRange);
|
|
69
93
|
return `${prefix}${version}`;
|