@rainy-updates/cli 0.6.0 → 0.6.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 +111 -0
- package/README.md +12 -0
- package/dist/bin/cli.js +12 -127
- package/dist/bin/dispatch.js +6 -0
- package/dist/bin/help.js +48 -0
- package/dist/bin/main.d.ts +1 -0
- package/dist/bin/main.js +126 -0
- package/dist/commands/audit/parser.js +36 -0
- package/dist/commands/audit/runner.js +17 -18
- package/dist/commands/bisect/oracle.js +21 -17
- package/dist/commands/bisect/runner.js +4 -3
- package/dist/commands/dashboard/parser.js +41 -0
- package/dist/commands/dashboard/runner.js +3 -0
- package/dist/commands/doctor/parser.js +44 -0
- package/dist/commands/ga/parser.js +39 -0
- package/dist/commands/ga/runner.js +73 -9
- package/dist/commands/health/parser.js +36 -0
- package/dist/commands/health/runner.js +5 -1
- package/dist/commands/hook/parser.d.ts +2 -0
- package/dist/commands/hook/parser.js +40 -0
- package/dist/commands/hook/runner.d.ts +2 -0
- package/dist/commands/hook/runner.js +174 -0
- package/dist/commands/licenses/parser.js +39 -0
- package/dist/commands/licenses/runner.js +5 -1
- package/dist/commands/resolve/graph/builder.js +5 -1
- package/dist/commands/resolve/parser.js +39 -0
- package/dist/commands/resolve/runner.js +5 -0
- package/dist/commands/review/parser.js +44 -0
- package/dist/commands/snapshot/parser.js +39 -0
- package/dist/commands/snapshot/runner.js +4 -1
- package/dist/commands/unused/parser.js +39 -0
- package/dist/commands/unused/runner.js +4 -1
- package/dist/commands/unused/scanner.d.ts +2 -1
- package/dist/commands/unused/scanner.js +60 -44
- package/dist/core/check.js +5 -1
- package/dist/core/doctor/findings.js +4 -4
- package/dist/core/init-ci.js +28 -26
- package/dist/core/options.d.ts +4 -1
- package/dist/core/options.js +57 -0
- package/dist/core/verification.js +11 -9
- package/dist/core/warm-cache.js +5 -1
- package/dist/generated/version.d.ts +1 -0
- package/dist/generated/version.js +2 -0
- package/dist/git/scope.d.ts +19 -0
- package/dist/git/scope.js +167 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/output/sarif.js +2 -8
- package/dist/pm/detect.d.ts +37 -0
- package/dist/pm/detect.js +133 -2
- package/dist/pm/install.d.ts +2 -1
- package/dist/pm/install.js +7 -5
- package/dist/rup +0 -0
- package/dist/types/index.d.ts +59 -1
- package/dist/ui/dashboard-state.d.ts +7 -0
- package/dist/ui/dashboard-state.js +44 -0
- package/dist/ui/tui.d.ts +3 -0
- package/dist/ui/tui.js +311 -111
- package/dist/utils/shell.d.ts +6 -0
- package/dist/utils/shell.js +18 -0
- package/dist/workspace/discover.d.ts +7 -1
- package/dist/workspace/discover.js +12 -3
- package/package.json +16 -8
|
@@ -1,37 +1,63 @@
|
|
|
1
|
-
import { promises as fs } from "node:fs";
|
|
2
1
|
import path from "node:path";
|
|
2
|
+
import { parseSync } from "oxc-parser";
|
|
3
3
|
/**
|
|
4
|
-
* Extracts all imported package names from a single source file.
|
|
4
|
+
* Extracts all imported package names from a single source file using AST.
|
|
5
5
|
*
|
|
6
6
|
* Handles:
|
|
7
7
|
* - ESM static: import ... from "pkg"
|
|
8
8
|
* - ESM dynamic: import("pkg")
|
|
9
9
|
* - CJS: require("pkg")
|
|
10
|
+
* - ESM re-export: export ... from "pkg"
|
|
10
11
|
*
|
|
11
12
|
* Strips subpath imports (e.g. "lodash/merge" → "lodash"),
|
|
12
13
|
* skips relative imports and node: builtins.
|
|
13
14
|
*/
|
|
14
15
|
export function extractImportsFromSource(source) {
|
|
15
16
|
const names = new Set();
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
17
|
+
try {
|
|
18
|
+
const parseResult = parseSync("unknown.ts", source, {
|
|
19
|
+
sourceType: "module",
|
|
20
|
+
});
|
|
21
|
+
const walk = (node) => {
|
|
22
|
+
if (!node)
|
|
23
|
+
return;
|
|
24
|
+
if (node.type === "ImportDeclaration" && node.source?.value) {
|
|
25
|
+
addPackageName(names, node.source.value);
|
|
26
|
+
}
|
|
27
|
+
else if (node.type === "ExportNamedDeclaration" && node.source?.value) {
|
|
28
|
+
addPackageName(names, node.source.value);
|
|
29
|
+
}
|
|
30
|
+
else if (node.type === "ExportAllDeclaration" && node.source?.value) {
|
|
31
|
+
addPackageName(names, node.source.value);
|
|
32
|
+
}
|
|
33
|
+
else if (node.type === "ImportExpression" && node.source?.value) {
|
|
34
|
+
addPackageName(names, node.source.value);
|
|
35
|
+
}
|
|
36
|
+
else if (node.type === "CallExpression") {
|
|
37
|
+
if (node.callee?.type === "Identifier" &&
|
|
38
|
+
node.callee.name === "require" &&
|
|
39
|
+
node.arguments?.[0]?.type === "StringLiteral") {
|
|
40
|
+
addPackageName(names, node.arguments[0].value);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// Traverse children
|
|
44
|
+
for (const key in node) {
|
|
45
|
+
if (node[key] && typeof node[key] === "object") {
|
|
46
|
+
if (Array.isArray(node[key])) {
|
|
47
|
+
for (const child of node[key]) {
|
|
48
|
+
walk(child);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
walk(node[key]);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
walk(parseResult.program);
|
|
30
58
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
for (const match of source.matchAll(reExport)) {
|
|
34
|
-
addPackageName(names, match[1]);
|
|
59
|
+
catch (err) {
|
|
60
|
+
// Fallback or ignore parse errors
|
|
35
61
|
}
|
|
36
62
|
return names;
|
|
37
63
|
}
|
|
@@ -87,40 +113,30 @@ const IGNORED_DIRS = new Set([
|
|
|
87
113
|
*/
|
|
88
114
|
export async function scanDirectory(dir) {
|
|
89
115
|
const allImports = new Set();
|
|
90
|
-
|
|
91
|
-
return allImports;
|
|
92
|
-
}
|
|
93
|
-
async function walkDirectory(dir, collector) {
|
|
94
|
-
let entries;
|
|
95
|
-
try {
|
|
96
|
-
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
97
|
-
}
|
|
98
|
-
catch {
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
116
|
+
const glob = new Bun.Glob("**/*.{ts,tsx,js,jsx,mjs,cjs,mts,cts}");
|
|
101
117
|
const tasks = [];
|
|
102
|
-
for (const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
118
|
+
for await (const file of glob.scan(dir)) {
|
|
119
|
+
// Bun.Glob returns relative paths
|
|
120
|
+
const fullPath = path.join(dir, file);
|
|
121
|
+
// Quick check to ignore certain directories in the path
|
|
122
|
+
if (fullPath.includes("/node_modules/") ||
|
|
123
|
+
fullPath.includes("/.git/") ||
|
|
124
|
+
fullPath.includes("/dist/") ||
|
|
125
|
+
fullPath.includes("/build/") ||
|
|
126
|
+
fullPath.includes("/out/") ||
|
|
127
|
+
fullPath.includes("/.next/") ||
|
|
128
|
+
fullPath.includes("/.nuxt/")) {
|
|
109
129
|
continue;
|
|
110
130
|
}
|
|
111
|
-
if (!entry.isFile())
|
|
112
|
-
continue;
|
|
113
|
-
const ext = path.extname(entryName).toLowerCase();
|
|
114
|
-
if (!SOURCE_EXTENSIONS.has(ext))
|
|
115
|
-
continue;
|
|
116
131
|
tasks.push(Bun.file(fullPath)
|
|
117
132
|
.text()
|
|
118
133
|
.then((source) => {
|
|
119
134
|
for (const name of extractImportsFromSource(source)) {
|
|
120
|
-
|
|
135
|
+
allImports.add(name);
|
|
121
136
|
}
|
|
122
137
|
})
|
|
123
138
|
.catch(() => undefined));
|
|
124
139
|
}
|
|
125
140
|
await Promise.all(tasks);
|
|
141
|
+
return allImports;
|
|
126
142
|
}
|
package/dist/core/check.js
CHANGED
|
@@ -18,7 +18,11 @@ export async function check(options) {
|
|
|
18
18
|
let registryMs = 0;
|
|
19
19
|
const discoveryStartedAt = Date.now();
|
|
20
20
|
const packageManager = await detectPackageManager(options.cwd);
|
|
21
|
-
const packageDirs = await discoverPackageDirs(options.cwd, options.workspace
|
|
21
|
+
const packageDirs = await discoverPackageDirs(options.cwd, options.workspace, {
|
|
22
|
+
git: options,
|
|
23
|
+
includeKinds: options.includeKinds,
|
|
24
|
+
includeDependents: options.affected === true,
|
|
25
|
+
});
|
|
22
26
|
discoveryMs += Date.now() - discoveryStartedAt;
|
|
23
27
|
const cache = await VersionCache.create();
|
|
24
28
|
const registryClient = new NpmRegistryClient(options.cwd, {
|
|
@@ -11,7 +11,7 @@ export function buildDoctorFindings(review) {
|
|
|
11
11
|
summary: error,
|
|
12
12
|
details: "Execution errors make the scan incomplete or require operator review.",
|
|
13
13
|
help: "Resolve execution and registry issues before treating the run as clean.",
|
|
14
|
-
recommendedAction: "Run `rup
|
|
14
|
+
recommendedAction: "Run `rup dashboard --mode review` after fixing execution failures.",
|
|
15
15
|
evidence: [error],
|
|
16
16
|
});
|
|
17
17
|
}
|
|
@@ -43,7 +43,7 @@ export function buildDoctorFindings(review) {
|
|
|
43
43
|
summary: `${item.update.name} has ${item.update.peerConflictSeverity} peer conflicts after the proposed upgrade.`,
|
|
44
44
|
details: item.peerConflicts[0]?.suggestion,
|
|
45
45
|
help: "Inspect peer dependency requirements before applying the update.",
|
|
46
|
-
recommendedAction: item.update.recommendedAction ?? "Review peer requirements in `rup
|
|
46
|
+
recommendedAction: item.update.recommendedAction ?? "Review peer requirements in `rup dashboard --mode review`.",
|
|
47
47
|
evidence: item.peerConflicts.map((conflict) => `${conflict.requester} -> ${conflict.peer}@${conflict.requiredRange}`),
|
|
48
48
|
});
|
|
49
49
|
}
|
|
@@ -59,7 +59,7 @@ export function buildDoctorFindings(review) {
|
|
|
59
59
|
summary: `${item.update.name} violates the current license policy.`,
|
|
60
60
|
details: item.license?.license,
|
|
61
61
|
help: "Keep denied licenses out of the approved update set.",
|
|
62
|
-
recommendedAction: item.update.recommendedAction ?? "Block this package in `rup review --
|
|
62
|
+
recommendedAction: item.update.recommendedAction ?? "Block this package in `rup dashboard --mode review --focus blocked`.",
|
|
63
63
|
});
|
|
64
64
|
}
|
|
65
65
|
if ((item.update.advisoryCount ?? 0) > 0) {
|
|
@@ -106,7 +106,7 @@ export function buildDoctorFindings(review) {
|
|
|
106
106
|
workspace,
|
|
107
107
|
summary: `${item.update.name} is a major version upgrade.`,
|
|
108
108
|
help: "Major upgrades should be reviewed explicitly before being applied.",
|
|
109
|
-
recommendedAction: item.update.recommendedAction ?? "Review major changes in `rup review --
|
|
109
|
+
recommendedAction: item.update.recommendedAction ?? "Review major changes in `rup dashboard --mode review --focus major`.",
|
|
110
110
|
});
|
|
111
111
|
}
|
|
112
112
|
if (item.update.healthStatus === "stale" || item.update.healthStatus === "archived") {
|
package/dist/core/init-ci.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { mkdir } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import { buildInstallInvocation, buildTestCommand, createPackageManagerProfile, detectPackageManagerDetails, } from "../pm/detect.js";
|
|
4
4
|
export async function initCiWorkflow(cwd, force, options) {
|
|
5
5
|
const workflowPath = path.join(cwd, ".github", "workflows", "rainy-updates.yml");
|
|
6
6
|
try {
|
|
@@ -13,8 +13,8 @@ export async function initCiWorkflow(cwd, force, options) {
|
|
|
13
13
|
catch {
|
|
14
14
|
// missing file, continue create
|
|
15
15
|
}
|
|
16
|
-
const detected = await
|
|
17
|
-
const packageManager =
|
|
16
|
+
const detected = await detectPackageManagerDetails(cwd);
|
|
17
|
+
const packageManager = createPackageManagerProfile("auto", detected);
|
|
18
18
|
const scheduleBlock = renderScheduleBlock(options.schedule);
|
|
19
19
|
const workflow = options.mode === "minimal"
|
|
20
20
|
? minimalWorkflowTemplate(scheduleBlock, packageManager)
|
|
@@ -32,32 +32,34 @@ function renderScheduleBlock(schedule) {
|
|
|
32
32
|
const cron = schedule === "daily" ? "0 8 * * *" : "0 8 * * 1";
|
|
33
33
|
return ` schedule:\n - cron: '${cron}'\n workflow_dispatch:`;
|
|
34
34
|
}
|
|
35
|
-
function installStep(
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
function installStep(profile) {
|
|
36
|
+
const install = buildInstallInvocation(profile, { frozen: true, ci: true });
|
|
37
|
+
return ` - name: Install dependencies\n run: ${install.display}`;
|
|
38
|
+
}
|
|
39
|
+
function runtimeSetupSteps(profile) {
|
|
40
|
+
const lines = [
|
|
41
|
+
` - name: Checkout\n uses: actions/checkout@v4`,
|
|
42
|
+
];
|
|
43
|
+
if (profile.manager !== "bun") {
|
|
44
|
+
lines.push(` - name: Setup Node\n uses: actions/setup-node@v4\n with:\n node-version: 22`);
|
|
45
|
+
}
|
|
46
|
+
lines.push(` - name: Setup Bun\n uses: oven-sh/setup-bun@v1`);
|
|
47
|
+
if (profile.manager === "pnpm" || profile.manager === "yarn") {
|
|
48
|
+
lines.push(` - name: Enable Corepack\n run: corepack enable`);
|
|
38
49
|
}
|
|
39
|
-
if (
|
|
40
|
-
|
|
50
|
+
if (profile.manager === "pnpm") {
|
|
51
|
+
lines.push(` - name: Prepare pnpm\n run: corepack prepare pnpm@9 --activate`);
|
|
41
52
|
}
|
|
42
|
-
return
|
|
53
|
+
return lines.join("\n\n");
|
|
43
54
|
}
|
|
44
|
-
function minimalWorkflowTemplate(scheduleBlock,
|
|
45
|
-
return `name: Rainy Updates\n\non:\n${scheduleBlock}\n\njobs:\n dependency-check:\n runs-on: ubuntu-latest\n steps:\n
|
|
55
|
+
function minimalWorkflowTemplate(scheduleBlock, profile) {
|
|
56
|
+
return `name: Rainy Updates\n\non:\n${scheduleBlock}\n\njobs:\n dependency-check:\n runs-on: ubuntu-latest\n steps:\n${runtimeSetupSteps(profile)}\n\n${installStep(profile)}\n\n - name: Run dependency check\n run: |\n bunx --bun @rainy-updates/cli ci \\\n --workspace \\\n --mode minimal \\\n --gate check \\\n --ci \\\n --stream \\\n --registry-timeout-ms 12000 \\\n --registry-retries 4 \\\n --format table\n`;
|
|
46
57
|
}
|
|
47
|
-
function strictWorkflowTemplate(scheduleBlock,
|
|
48
|
-
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
|
|
58
|
+
function strictWorkflowTemplate(scheduleBlock, profile) {
|
|
59
|
+
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${runtimeSetupSteps(profile)}\n\n${installStep(profile)}\n\n - name: Warm cache\n run: bunx --bun @rainy-updates/cli warm-cache --workspace --concurrency 32 --registry-timeout-ms 12000 --registry-retries 4\n\n - name: Generate reviewed decision plan\n run: |\n bunx --bun @rainy-updates/cli ci \\\n --workspace \\\n --mode strict \\\n --gate review \\\n --plan-file .artifacts/decision-plan.json \\\n --ci \\\n --concurrency 32 \\\n --stream \\\n --registry-timeout-ms 12000 \\\n --registry-retries 4 \\\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`;
|
|
49
60
|
}
|
|
50
|
-
function enterpriseWorkflowTemplate(scheduleBlock,
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
detectedPmInstall =
|
|
55
|
-
"corepack enable && corepack prepare pnpm@9 --activate && pnpm install --frozen-lockfile";
|
|
56
|
-
testCmd = "pnpm test";
|
|
57
|
-
}
|
|
58
|
-
else if (packageManager === "bun") {
|
|
59
|
-
detectedPmInstall = "bun install";
|
|
60
|
-
testCmd = "bun test";
|
|
61
|
-
}
|
|
62
|
-
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: Setup Bun\n uses: oven-sh/setup-bun@v1\n\n - name: Install dependencies\n run: ${detectedPmInstall}\n\n - name: Warm cache\n run: bunx --bun @rainy-updates/cli warm-cache --workspace --concurrency 32 --registry-timeout-ms 12000 --registry-retries 4\n\n - name: Generate reviewed decision plan\n run: |\n bunx --bun @rainy-updates/cli ci \\\n --workspace \\\n --mode enterprise \\\n --gate review \\\n --plan-file .artifacts/decision-plan.json \\\n --concurrency 32 \\\n --stream \\\n --registry-timeout-ms 12000 \\\n --registry-retries 4 \\\n --lockfile-mode preserve \\\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: Replay approved plan with verification\n run: |\n bunx --bun @rainy-updates/cli ci \\\n --workspace \\\n --mode enterprise \\\n --gate upgrade \\\n --from-plan .artifacts/decision-plan.json \\\n --verify test \\\n --test-command "${testCmd}" \\\n --verification-report-file .artifacts/verification-node-\${{ matrix.node }}.json\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`;
|
|
61
|
+
function enterpriseWorkflowTemplate(scheduleBlock, profile) {
|
|
62
|
+
const install = buildInstallInvocation(profile, { frozen: true, ci: true });
|
|
63
|
+
const testCmd = buildTestCommand(profile);
|
|
64
|
+
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: Setup Bun\n uses: oven-sh/setup-bun@v1\n\n${profile.manager === "pnpm" || profile.manager === "yarn" ? ' - name: Enable Corepack\n run: corepack enable\n\n' : ""}${profile.manager === "pnpm" ? ' - name: Prepare pnpm\n run: corepack prepare pnpm@9 --activate\n\n' : ""} - name: Install dependencies\n run: ${install.display}\n\n - name: Warm cache\n run: bunx --bun @rainy-updates/cli warm-cache --workspace --concurrency 32 --registry-timeout-ms 12000 --registry-retries 4\n\n - name: Generate reviewed decision plan\n run: |\n bunx --bun @rainy-updates/cli ci \\\n --workspace \\\n --mode enterprise \\\n --gate review \\\n --plan-file .artifacts/decision-plan.json \\\n --concurrency 32 \\\n --stream \\\n --registry-timeout-ms 12000 \\\n --registry-retries 4 \\\n --lockfile-mode preserve \\\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: Replay approved plan with verification\n run: |\n bunx --bun @rainy-updates/cli ci \\\n --workspace \\\n --mode enterprise \\\n --gate upgrade \\\n --from-plan .artifacts/decision-plan.json \\\n --verify test \\\n --test-command "${testCmd}" \\\n --verification-report-file .artifacts/verification-node-\${{ matrix.node }}.json\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`;
|
|
63
65
|
}
|
package/dist/core/options.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { BaselineOptions, CheckOptions, UpgradeOptions, AuditOptions, BisectOptions, HealthOptions, UnusedOptions, ResolveOptions, LicenseOptions, SnapshotOptions, ReviewOptions, DoctorOptions, RiskLevel, DashboardOptions, GaOptions } from "../types/index.js";
|
|
1
|
+
import type { BaselineOptions, CheckOptions, UpgradeOptions, AuditOptions, BisectOptions, HealthOptions, UnusedOptions, ResolveOptions, LicenseOptions, SnapshotOptions, ReviewOptions, DoctorOptions, RiskLevel, DashboardOptions, GaOptions, HookOptions } from "../types/index.js";
|
|
2
2
|
import type { InitCiMode, InitCiSchedule } from "./init-ci.js";
|
|
3
3
|
export type ParsedCliArgs = {
|
|
4
4
|
command: "check";
|
|
@@ -58,6 +58,9 @@ export type ParsedCliArgs = {
|
|
|
58
58
|
} | {
|
|
59
59
|
command: "ga";
|
|
60
60
|
options: GaOptions;
|
|
61
|
+
} | {
|
|
62
|
+
command: "hook";
|
|
63
|
+
options: HookOptions;
|
|
61
64
|
};
|
|
62
65
|
export declare function parseCliArgs(argv: string[]): Promise<ParsedCliArgs>;
|
|
63
66
|
export declare function ensureRiskLevel(value: string): RiskLevel;
|
package/dist/core/options.js
CHANGED
|
@@ -25,6 +25,7 @@ const KNOWN_COMMANDS = [
|
|
|
25
25
|
"doctor",
|
|
26
26
|
"dashboard",
|
|
27
27
|
"ga",
|
|
28
|
+
"hook",
|
|
28
29
|
];
|
|
29
30
|
export async function parseCliArgs(argv) {
|
|
30
31
|
const firstArg = argv[0];
|
|
@@ -82,6 +83,10 @@ export async function parseCliArgs(argv) {
|
|
|
82
83
|
const { parseGaArgs } = await import("../commands/ga/parser.js");
|
|
83
84
|
return { command, options: parseGaArgs(args) };
|
|
84
85
|
}
|
|
86
|
+
if (command === "hook") {
|
|
87
|
+
const { parseHookArgs } = await import("../commands/hook/parser.js");
|
|
88
|
+
return { command, options: parseHookArgs(args) };
|
|
89
|
+
}
|
|
85
90
|
const base = {
|
|
86
91
|
cwd: getRuntimeCwd(),
|
|
87
92
|
target: "latest",
|
|
@@ -117,6 +122,11 @@ export async function parseCliArgs(argv) {
|
|
|
117
122
|
cooldownDays: undefined,
|
|
118
123
|
prLimit: undefined,
|
|
119
124
|
onlyChanged: false,
|
|
125
|
+
affected: false,
|
|
126
|
+
staged: false,
|
|
127
|
+
baseRef: undefined,
|
|
128
|
+
headRef: undefined,
|
|
129
|
+
sinceRef: undefined,
|
|
120
130
|
ciProfile: "minimal",
|
|
121
131
|
lockfileMode: "preserve",
|
|
122
132
|
interactive: false,
|
|
@@ -457,6 +467,38 @@ export async function parseCliArgs(argv) {
|
|
|
457
467
|
base.onlyChanged = true;
|
|
458
468
|
continue;
|
|
459
469
|
}
|
|
470
|
+
if (current === "--affected") {
|
|
471
|
+
base.affected = true;
|
|
472
|
+
continue;
|
|
473
|
+
}
|
|
474
|
+
if (current === "--staged") {
|
|
475
|
+
base.staged = true;
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
if (current === "--base" && next) {
|
|
479
|
+
base.baseRef = next;
|
|
480
|
+
index += 1;
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
if (current === "--base") {
|
|
484
|
+
throw new Error("Missing value for --base");
|
|
485
|
+
}
|
|
486
|
+
if (current === "--head" && next) {
|
|
487
|
+
base.headRef = next;
|
|
488
|
+
index += 1;
|
|
489
|
+
continue;
|
|
490
|
+
}
|
|
491
|
+
if (current === "--head") {
|
|
492
|
+
throw new Error("Missing value for --head");
|
|
493
|
+
}
|
|
494
|
+
if (current === "--since" && next) {
|
|
495
|
+
base.sinceRef = next;
|
|
496
|
+
index += 1;
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
499
|
+
if (current === "--since") {
|
|
500
|
+
throw new Error("Missing value for --since");
|
|
501
|
+
}
|
|
460
502
|
if (current === "--interactive") {
|
|
461
503
|
base.interactive = true;
|
|
462
504
|
continue;
|
|
@@ -726,6 +768,21 @@ function applyConfig(base, config) {
|
|
|
726
768
|
if (typeof config.onlyChanged === "boolean") {
|
|
727
769
|
base.onlyChanged = config.onlyChanged;
|
|
728
770
|
}
|
|
771
|
+
if (typeof config.affected === "boolean") {
|
|
772
|
+
base.affected = config.affected;
|
|
773
|
+
}
|
|
774
|
+
if (typeof config.staged === "boolean") {
|
|
775
|
+
base.staged = config.staged;
|
|
776
|
+
}
|
|
777
|
+
if (typeof config.baseRef === "string" && config.baseRef.length > 0) {
|
|
778
|
+
base.baseRef = config.baseRef;
|
|
779
|
+
}
|
|
780
|
+
if (typeof config.headRef === "string" && config.headRef.length > 0) {
|
|
781
|
+
base.headRef = config.headRef;
|
|
782
|
+
}
|
|
783
|
+
if (typeof config.sinceRef === "string" && config.sinceRef.length > 0) {
|
|
784
|
+
base.sinceRef = config.sinceRef;
|
|
785
|
+
}
|
|
729
786
|
if (typeof config.ciProfile === "string") {
|
|
730
787
|
base.ciProfile = ensureCiProfile(config.ciProfile);
|
|
731
788
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { buildInstallInvocation, buildTestCommand, createPackageManagerProfile, detectPackageManagerDetails, } from "../pm/detect.js";
|
|
2
2
|
import { installDependencies } from "../pm/install.js";
|
|
3
3
|
import { stableStringify } from "../utils/stable-json.js";
|
|
4
4
|
import { writeFileAtomic } from "../utils/io.js";
|
|
5
|
-
import {
|
|
5
|
+
import { buildShellInvocation } from "../utils/shell.js";
|
|
6
6
|
export async function runVerification(options) {
|
|
7
7
|
const mode = options.verify;
|
|
8
8
|
if (mode === "none") {
|
|
@@ -15,15 +15,17 @@ export async function runVerification(options) {
|
|
|
15
15
|
return result;
|
|
16
16
|
}
|
|
17
17
|
const checks = [];
|
|
18
|
-
const detected = await
|
|
18
|
+
const detected = await detectPackageManagerDetails(options.cwd);
|
|
19
|
+
const profile = createPackageManagerProfile(options.packageManager, detected, "bun");
|
|
19
20
|
if (includesInstall(mode)) {
|
|
20
|
-
|
|
21
|
+
const installInvocation = buildInstallInvocation(profile);
|
|
22
|
+
checks.push(await runCheck("install", installInvocation.display, async () => {
|
|
21
23
|
await installDependencies(options.cwd, options.packageManager, detected);
|
|
22
24
|
}));
|
|
23
25
|
}
|
|
24
26
|
if (includesTest(mode)) {
|
|
25
27
|
const command = options.testCommand ??
|
|
26
|
-
defaultTestCommand(
|
|
28
|
+
defaultTestCommand(profile);
|
|
27
29
|
checks.push(await runShellCheck(options.cwd, command));
|
|
28
30
|
}
|
|
29
31
|
const result = {
|
|
@@ -40,14 +42,14 @@ function includesInstall(mode) {
|
|
|
40
42
|
function includesTest(mode) {
|
|
41
43
|
return mode === "test" || mode === "install,test";
|
|
42
44
|
}
|
|
43
|
-
function defaultTestCommand(
|
|
44
|
-
return
|
|
45
|
+
function defaultTestCommand(profile) {
|
|
46
|
+
return buildTestCommand(profile);
|
|
45
47
|
}
|
|
46
48
|
async function runShellCheck(cwd, command) {
|
|
47
49
|
const startedAt = Date.now();
|
|
48
50
|
try {
|
|
49
|
-
const
|
|
50
|
-
const proc = Bun.spawn([shell,
|
|
51
|
+
const invocation = buildShellInvocation(command);
|
|
52
|
+
const proc = Bun.spawn([invocation.shell, ...invocation.args], {
|
|
51
53
|
cwd,
|
|
52
54
|
stdin: "inherit",
|
|
53
55
|
stdout: "inherit",
|
package/dist/core/warm-cache.js
CHANGED
|
@@ -14,7 +14,11 @@ export async function warmCache(options) {
|
|
|
14
14
|
let registryMs = 0;
|
|
15
15
|
const discoveryStartedAt = Date.now();
|
|
16
16
|
const packageManager = await detectPackageManager(options.cwd);
|
|
17
|
-
const packageDirs = await discoverPackageDirs(options.cwd, options.workspace
|
|
17
|
+
const packageDirs = await discoverPackageDirs(options.cwd, options.workspace, {
|
|
18
|
+
git: options,
|
|
19
|
+
includeKinds: options.includeKinds,
|
|
20
|
+
includeDependents: options.affected === true,
|
|
21
|
+
});
|
|
18
22
|
discoveryMs += Date.now() - discoveryStartedAt;
|
|
19
23
|
const cache = await VersionCache.create();
|
|
20
24
|
const registryClient = new NpmRegistryClient(options.cwd, {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const CLI_VERSION = "0.6.2";
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { DependencyKind } from "../types/index.js";
|
|
2
|
+
export interface GitScopeOptions {
|
|
3
|
+
onlyChanged?: boolean;
|
|
4
|
+
affected?: boolean;
|
|
5
|
+
staged?: boolean;
|
|
6
|
+
baseRef?: string;
|
|
7
|
+
headRef?: string;
|
|
8
|
+
sinceRef?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface ScopedPackageDirsResult {
|
|
11
|
+
packageDirs: string[];
|
|
12
|
+
warnings: string[];
|
|
13
|
+
changedFiles: string[];
|
|
14
|
+
}
|
|
15
|
+
export declare function hasGitScope(options: GitScopeOptions): boolean;
|
|
16
|
+
export declare function scopePackageDirsByGit(cwd: string, packageDirs: string[], options: GitScopeOptions, config?: {
|
|
17
|
+
includeKinds?: DependencyKind[];
|
|
18
|
+
includeDependents?: boolean;
|
|
19
|
+
}): Promise<ScopedPackageDirsResult>;
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { readManifest } from "../parsers/package-json.js";
|
|
3
|
+
import { buildWorkspaceGraph } from "../workspace/graph.js";
|
|
4
|
+
export function hasGitScope(options) {
|
|
5
|
+
return (options.onlyChanged === true ||
|
|
6
|
+
options.affected === true ||
|
|
7
|
+
options.staged === true ||
|
|
8
|
+
typeof options.baseRef === "string" ||
|
|
9
|
+
typeof options.headRef === "string" ||
|
|
10
|
+
typeof options.sinceRef === "string");
|
|
11
|
+
}
|
|
12
|
+
export async function scopePackageDirsByGit(cwd, packageDirs, options, config = {}) {
|
|
13
|
+
if (!hasGitScope(options)) {
|
|
14
|
+
return {
|
|
15
|
+
packageDirs,
|
|
16
|
+
warnings: [],
|
|
17
|
+
changedFiles: [],
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
const changedFiles = await listChangedFiles(cwd, options);
|
|
21
|
+
if ("warnings" in changedFiles) {
|
|
22
|
+
return {
|
|
23
|
+
packageDirs,
|
|
24
|
+
warnings: changedFiles.warnings,
|
|
25
|
+
changedFiles: [],
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
const changedPackageDirs = mapFilesToPackageDirs(cwd, packageDirs, changedFiles.files);
|
|
29
|
+
if (changedPackageDirs.length === 0) {
|
|
30
|
+
return {
|
|
31
|
+
packageDirs: [],
|
|
32
|
+
warnings: [],
|
|
33
|
+
changedFiles: changedFiles.files,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
if (!config.includeDependents) {
|
|
37
|
+
return {
|
|
38
|
+
packageDirs: changedPackageDirs,
|
|
39
|
+
warnings: [],
|
|
40
|
+
changedFiles: changedFiles.files,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
const affectedPackageDirs = await expandToDependents(changedPackageDirs, packageDirs, config.includeKinds ?? ["dependencies", "devDependencies"]);
|
|
44
|
+
return {
|
|
45
|
+
packageDirs: affectedPackageDirs,
|
|
46
|
+
warnings: [],
|
|
47
|
+
changedFiles: changedFiles.files,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
async function listChangedFiles(cwd, options) {
|
|
51
|
+
const primaryArgs = buildGitDiffArgs(options);
|
|
52
|
+
const diff = await runGit(cwd, primaryArgs);
|
|
53
|
+
if (!diff.ok) {
|
|
54
|
+
return {
|
|
55
|
+
warnings: [
|
|
56
|
+
`Git scope could not be resolved (${diff.error}). Falling back to full workspace scan.`,
|
|
57
|
+
],
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
const untracked = await runGit(cwd, ["ls-files", "--others", "--exclude-standard"]);
|
|
61
|
+
const files = new Set();
|
|
62
|
+
for (const value of [...diff.lines, ...(untracked.ok ? untracked.lines : [])]) {
|
|
63
|
+
const trimmed = value.trim();
|
|
64
|
+
if (trimmed.length === 0)
|
|
65
|
+
continue;
|
|
66
|
+
files.add(trimmed);
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
files: Array.from(files).sort((left, right) => left.localeCompare(right)),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function buildGitDiffArgs(options) {
|
|
73
|
+
if (options.staged) {
|
|
74
|
+
return ["diff", "--name-only", "--cached"];
|
|
75
|
+
}
|
|
76
|
+
if (options.baseRef && options.headRef) {
|
|
77
|
+
return ["diff", "--name-only", `${options.baseRef}...${options.headRef}`];
|
|
78
|
+
}
|
|
79
|
+
if (options.baseRef) {
|
|
80
|
+
return ["diff", "--name-only", `${options.baseRef}...HEAD`];
|
|
81
|
+
}
|
|
82
|
+
if (options.sinceRef) {
|
|
83
|
+
return ["diff", "--name-only", `${options.sinceRef}..HEAD`];
|
|
84
|
+
}
|
|
85
|
+
return ["diff", "--name-only", "HEAD"];
|
|
86
|
+
}
|
|
87
|
+
async function runGit(cwd, args) {
|
|
88
|
+
try {
|
|
89
|
+
const proc = Bun.spawn(["git", ...args], {
|
|
90
|
+
cwd,
|
|
91
|
+
stdout: "pipe",
|
|
92
|
+
stderr: "pipe",
|
|
93
|
+
});
|
|
94
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
95
|
+
new Response(proc.stdout).text(),
|
|
96
|
+
new Response(proc.stderr).text(),
|
|
97
|
+
proc.exited,
|
|
98
|
+
]);
|
|
99
|
+
if (exitCode !== 0) {
|
|
100
|
+
const message = stderr.trim() || `git ${args.join(" ")} exited with code ${exitCode}`;
|
|
101
|
+
return { ok: false, error: message };
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
ok: true,
|
|
105
|
+
lines: stdout.split(/\r?\n/).filter(Boolean),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
return { ok: false, error: String(error) };
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function mapFilesToPackageDirs(cwd, packageDirs, files) {
|
|
113
|
+
const sortedDirs = [...packageDirs].sort((left, right) => right.length - left.length);
|
|
114
|
+
const matched = new Set();
|
|
115
|
+
for (const file of files) {
|
|
116
|
+
const absoluteFile = path.resolve(cwd, file);
|
|
117
|
+
let bestMatch;
|
|
118
|
+
for (const packageDir of sortedDirs) {
|
|
119
|
+
const relative = path.relative(packageDir, absoluteFile);
|
|
120
|
+
if (relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative))) {
|
|
121
|
+
bestMatch = packageDir;
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (bestMatch) {
|
|
126
|
+
matched.add(bestMatch);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return Array.from(matched).sort((left, right) => left.localeCompare(right));
|
|
130
|
+
}
|
|
131
|
+
async function expandToDependents(changedPackageDirs, packageDirs, includeKinds) {
|
|
132
|
+
const manifestsByPath = new Map();
|
|
133
|
+
for (const packageDir of packageDirs) {
|
|
134
|
+
try {
|
|
135
|
+
manifestsByPath.set(packageDir, await readManifest(packageDir));
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
// Skip unreadable manifests; callers already handle manifest read failures.
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const graph = buildWorkspaceGraph(manifestsByPath, includeKinds);
|
|
142
|
+
const pathByName = new Map(graph.nodes.map((node) => [node.packageName, node.packagePath]));
|
|
143
|
+
const nameByPath = new Map(graph.nodes.map((node) => [node.packagePath, node.packageName]));
|
|
144
|
+
const dependents = new Map();
|
|
145
|
+
for (const node of graph.nodes) {
|
|
146
|
+
for (const dependency of node.dependsOn) {
|
|
147
|
+
const list = dependents.get(dependency) ?? [];
|
|
148
|
+
list.push(node.packageName);
|
|
149
|
+
dependents.set(dependency, list);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const selected = new Set(changedPackageDirs);
|
|
153
|
+
const queue = changedPackageDirs
|
|
154
|
+
.map((packageDir) => nameByPath.get(packageDir))
|
|
155
|
+
.filter((value) => typeof value === "string");
|
|
156
|
+
while (queue.length > 0) {
|
|
157
|
+
const current = queue.shift();
|
|
158
|
+
for (const dependent of dependents.get(current) ?? []) {
|
|
159
|
+
const dependentPath = pathByName.get(dependent);
|
|
160
|
+
if (!dependentPath || selected.has(dependentPath))
|
|
161
|
+
continue;
|
|
162
|
+
selected.add(dependentPath);
|
|
163
|
+
queue.push(dependent);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return Array.from(selected).sort((left, right) => left.localeCompare(right));
|
|
167
|
+
}
|