renovate-mcp 1.7.2 → 1.8.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/README.md +3 -2
- package/dist/index.js +5 -2
- package/dist/lib/configLinter.js +14 -15
- package/dist/lib/customManagerPreview.js +2 -2
- package/dist/lib/dependencyExplainer.js +1 -1
- package/dist/lib/externalPresetFetcher.js +6 -1
- package/dist/lib/pathSelection.js +266 -0
- package/dist/lib/setupCheck.js +1 -0
- package/dist/lib/version.js +1 -1
- package/dist/prompts/workflows.js +2 -2
- package/dist/tools/explainPaths.js +58 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -30,7 +30,7 @@ Then try the prompt: *"List the namespaces available under `renovate://presets`.
|
|
|
30
30
|
|
|
31
31
|
## Tools & resources
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
Nineteen tools, seven resources, and four workflow prompts. Each tool name below links to its full reference in [`docs/tools.md`](docs/tools.md).
|
|
34
34
|
|
|
35
35
|
| Tool | Purpose |
|
|
36
36
|
| --- | --- |
|
|
@@ -50,6 +50,7 @@ Eighteen tools, seven resources, and four workflow prompts. Each tool name below
|
|
|
50
50
|
| [`annotate_dry_run`](docs/tools.md#annotate_dry_run) | Attribute each proposed update in a `dry_run` report to the `packageRules` that caused it; flags rules that never matched and report fields the matchers needed but the report lacked. Stateless + offline. |
|
|
51
51
|
| [`explain_dependency`](docs/tools.md#explain_dependency) | Answer "why was/wasn't dependency X updated?" from a `dry_run` report: per-occurrence `skipReason`, proposed `updates`, `warnings`, a one-line verdict and remedy hints; optionally the `packageRules` that matched it. Stateless + offline. |
|
|
52
52
|
| [`explain_schedule`](docs/tools.md#explain_schedule) | Answer "when will this `schedule` actually run?": active-now plus the next run windows in UTC and the config's `timezone`, computed with Renovate's own schedule matcher; invalid schedules return Renovate's message and the fact that it then runs at any time. Offline. |
|
|
53
|
+
| [`explain_paths`](docs/tools.md#explain_paths) | Show which files Renovate scans under `includePaths` / `ignorePaths` (preset-added ones included, each attributed to its source), flag dead patterns and `ignorePaths` entries that drop files only by substring (`test` dropping `src/latest/`), and count each `matchFileNames` entry against the real repo. Offline. |
|
|
53
54
|
| [`migrate_config`](docs/tools.md#migrate_config) | Apply Renovate's built-in migrations and return the migrated config plus a unified diff. Does not write. |
|
|
54
55
|
| [`write_config`](docs/tools.md#write_config) | Validate, then atomically write a config to disk. Preserves comments/key order on existing JSON-with-comments files; for `package.json` edits only the nested `renovate` key. |
|
|
55
56
|
| [`renovate://presets`](docs/tools.md#renovatepresets) (resource) | Markdown index of all built-in presets grouped by namespace. |
|
|
@@ -74,7 +75,7 @@ Eighteen tools, seven resources, and four workflow prompts. Each tool name below
|
|
|
74
75
|
- **Linux or macOS.** Windows is not supported — `package.json` declares `"os": ["darwin", "linux"]`, so `npm i` surfaces an `EBADPLATFORM` warning on Windows and the server exits with a clear stderr message at startup. Use WSL2 or a Linux/macOS host instead.
|
|
75
76
|
- **Node.js ≥ 24.11** (aligns with Renovate's own engine requirement).
|
|
76
77
|
|
|
77
|
-
Renovate ships bundled — the `renovate` package is a runtime dependency, so `validate_config`, `dry_run`, and `write_config` work out of the box with no separate install. The offline tools (`read_config`, `suggest_presets`, `resolve_config`, `explain_config`, `resolve_config_diff`, `test_package_rules`, `preview_custom_manager`, `lint_config`, `dry_run_diff`, `annotate_dry_run`, `explain_dependency`, `explain_schedule`, `migrate_config`) never spawn Renovate at all — nor do `check_setup` (by default it reads version metadata from the bundled install instead of spawning) or `get_version` (it only reports the renovate-mcp server's own version).
|
|
78
|
+
Renovate ships bundled — the `renovate` package is a runtime dependency, so `validate_config`, `dry_run`, and `write_config` work out of the box with no separate install. The offline tools (`read_config`, `suggest_presets`, `resolve_config`, `explain_config`, `resolve_config_diff`, `test_package_rules`, `preview_custom_manager`, `lint_config`, `dry_run_diff`, `annotate_dry_run`, `explain_dependency`, `explain_schedule`, `explain_paths`, `migrate_config`) never spawn Renovate at all — nor do `check_setup` (by default it reads version metadata from the bundled install instead of spawning) or `get_version` (it only reports the renovate-mcp server's own version).
|
|
78
79
|
|
|
79
80
|
**Optional env vars:**
|
|
80
81
|
|
package/dist/index.js
CHANGED
|
@@ -16,6 +16,7 @@ import { registerTestPackageRules } from "./tools/testPackageRules.js";
|
|
|
16
16
|
import { registerAnnotateDryRun } from "./tools/annotateDryRun.js";
|
|
17
17
|
import { registerExplainDependency } from "./tools/explainDependency.js";
|
|
18
18
|
import { registerExplainSchedule } from "./tools/explainSchedule.js";
|
|
19
|
+
import { registerExplainPaths } from "./tools/explainPaths.js";
|
|
19
20
|
import { registerWriteConfig } from "./tools/writeConfig.js";
|
|
20
21
|
import { registerMigrateConfig } from "./tools/migrateConfig.js";
|
|
21
22
|
import { registerGetVersion } from "./tools/getVersion.js";
|
|
@@ -52,8 +53,9 @@ const BASE_INSTRUCTIONS = [
|
|
|
52
53
|
" 12. annotate_dry_run — attribute each proposed update in a dry_run report to the packageRules that caused it (and flag rules that never matched)",
|
|
53
54
|
" 13. explain_dependency — why was/wasn't a given dependency updated: skipReason, updates, warnings and matched rules, read from a dry_run report (offline)",
|
|
54
55
|
" 14. explain_schedule — when will a `schedule` actually run: active-now plus the next windows in UTC and the config's timezone, or Renovate's own invalid-schedule message (offline)",
|
|
55
|
-
" 15.
|
|
56
|
-
" 16.
|
|
56
|
+
" 15. explain_paths — which files Renovate scans under includePaths / ignorePaths (presets included), with dead patterns, substring-only ignorePaths surprises and per-rule matchFileNames counts (offline)",
|
|
57
|
+
" 16. migrate_config — apply Renovate's built-in migrations (deprecated keys → current schema) and return the migrated config",
|
|
58
|
+
" 17. write_config — save the agreed-upon config (validates first)",
|
|
57
59
|
"",
|
|
58
60
|
"Before the first repo-touching tool call in a session (read_config, resolve_config, dry_run, write_config, …), call check_setup with the same repoPath. It surfaces token / endpoint / connectivity problems up front (e.g. \"set GITHUB_TOKEN or github-actions deps will be skipped\") instead of waiting for dry_run to fail. Skip if the user has already confirmed setup or is doing offline-only work that doesn't depend on git origin or registries.",
|
|
59
61
|
"If any tool fails unexpectedly, call check_setup to diagnose CLI availability.",
|
|
@@ -87,6 +89,7 @@ registerTestPackageRules(server);
|
|
|
87
89
|
registerAnnotateDryRun(server);
|
|
88
90
|
registerExplainDependency(server);
|
|
89
91
|
registerExplainSchedule(server);
|
|
92
|
+
registerExplainPaths(server);
|
|
90
93
|
registerMigrateConfig(server);
|
|
91
94
|
registerWriteConfig(server);
|
|
92
95
|
registerGetVersion(server);
|
package/dist/lib/configLinter.js
CHANGED
|
@@ -338,23 +338,22 @@ function checkAutomergeIncludesMajor(config, findings) {
|
|
|
338
338
|
const pkgRules = config.packageRules;
|
|
339
339
|
if (!Array.isArray(pkgRules))
|
|
340
340
|
return;
|
|
341
|
-
//
|
|
342
|
-
//
|
|
343
|
-
// guard
|
|
344
|
-
//
|
|
345
|
-
//
|
|
341
|
+
// Renovate applies packageRules in order and a later match overrides an
|
|
342
|
+
// earlier one, so only a `matchUpdateTypes` including "major" + `automerge:
|
|
343
|
+
// false` guard AFTER an offending entry protects it; an earlier guard is
|
|
344
|
+
// itself overridden.
|
|
345
|
+
// ponytail: an entry counts as guarded by any later guard, without checking
|
|
346
|
+
// that the guard's selectors actually cover the same deps as the offending
|
|
347
|
+
// entry. Ceiling: a guard scoped to a different dep subset silently
|
|
348
|
+
// suppresses findings for unrelated earlier entries. Upgrade path: track
|
|
346
349
|
// selector overlap per-entry if false negatives show up in practice.
|
|
347
|
-
const
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
return false;
|
|
352
|
-
return (Array.isArray(entry.matchUpdateTypes) &&
|
|
353
|
-
entry.matchUpdateTypes.includes("major"));
|
|
354
|
-
});
|
|
355
|
-
if (hasLaterMajorGuard)
|
|
356
|
-
return;
|
|
350
|
+
const lastGuardIndex = pkgRules.findLastIndex((entry) => isRecord(entry) &&
|
|
351
|
+
entry.automerge === false &&
|
|
352
|
+
Array.isArray(entry.matchUpdateTypes) &&
|
|
353
|
+
entry.matchUpdateTypes.includes("major"));
|
|
357
354
|
pkgRules.forEach((entry, i) => {
|
|
355
|
+
if (i < lastGuardIndex)
|
|
356
|
+
return;
|
|
358
357
|
if (!isRecord(entry))
|
|
359
358
|
return;
|
|
360
359
|
if (entry.automerge !== true)
|
|
@@ -33,7 +33,7 @@ export async function parseStructured(content, fileFormat) {
|
|
|
33
33
|
const DEFAULT_MAX_FILES_WALKED = 2000;
|
|
34
34
|
const DEFAULT_MAX_FILES_MATCHED = 500;
|
|
35
35
|
const DEFAULT_MAX_HITS_PER_FILE = 100;
|
|
36
|
-
const DEFAULT_MATCH_TIMEOUT_MS = 2000;
|
|
36
|
+
export const DEFAULT_MATCH_TIMEOUT_MS = 2000;
|
|
37
37
|
const DEFAULT_MAX_FILE_BYTES = 5 * 1024 * 1024;
|
|
38
38
|
const SKIP_DIRS = new Set([".git", "node_modules"]);
|
|
39
39
|
const TEMPLATE_FIELD_MAP = [
|
|
@@ -114,7 +114,7 @@ function parseRegexPattern(pattern) {
|
|
|
114
114
|
* therefore ADDS its complement rather than excluding — Renovate's
|
|
115
115
|
* `ignorePaths` is the exclusion mechanism. Walk order is preserved.
|
|
116
116
|
*/
|
|
117
|
-
async function matchFilePatterns(allPaths, patterns, matchTimeoutMs, warnings) {
|
|
117
|
+
export async function matchFilePatterns(allPaths, patterns, matchTimeoutMs, warnings) {
|
|
118
118
|
const matched = new Set();
|
|
119
119
|
for (let i = 0; i < patterns.length; i++) {
|
|
120
120
|
const pattern = patterns[i];
|
|
@@ -25,7 +25,7 @@ export const SKIP_HINTS = {
|
|
|
25
25
|
const NOT_FOUND_HINTS = [
|
|
26
26
|
"Check `enabledManagers`: the manager that owns this dependency's file may not be enabled.",
|
|
27
27
|
"Check the manager's `fileMatch` / `managerFilePatterns`: the file holding this dependency may not match the pattern.",
|
|
28
|
-
"Check `ignorePaths`: the file may live under an ignored path (defaults include `**/node_modules/**`, `**/bower_components/**`, …).",
|
|
28
|
+
"Check `ignorePaths`: the file may live under an ignored path (defaults include `**/node_modules/**`, `**/bower_components/**`, …). Run explain_paths with repoPath to see which pattern drops it.",
|
|
29
29
|
'Re-run dry_run with `dryRunMode: "extract"` and search the report to see which files and dependencies Renovate extracted at all.',
|
|
30
30
|
];
|
|
31
31
|
const NO_LOOKUP_HINT = 'A hit carries no `updates` array, so no lookup ran — the report is likely from `dryRunMode: "extract"`. Re-run dry_run with `dryRunMode: "lookup"` (or `"full"`) to see whether updates exist.';
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import JSON5 from "json5";
|
|
1
2
|
import { resolveCredential } from "./credentialResolver.js";
|
|
2
3
|
import { validateEndpoint } from "./endpointValidator.js";
|
|
3
4
|
import { classifyExternalSource, } from "./presetResolver.js";
|
|
@@ -219,7 +220,11 @@ async function fetchJson(url, headers, timeoutMs, presetName, fetchImpl, platfor
|
|
|
219
220
|
};
|
|
220
221
|
}
|
|
221
222
|
try {
|
|
222
|
-
|
|
223
|
+
// Renovate parses preset files as JSONC, falling back to JSON5 (`.json5`
|
|
224
|
+
// straight to JSON5); JSON5 is a superset of JSONC, so one parser covers
|
|
225
|
+
// all of them. It also accepts JSON5 syntax in a `.jsonc` file, which
|
|
226
|
+
// Renovate rejects — matching that would need jsonc-parser here.
|
|
227
|
+
const body = JSON5.parse(text);
|
|
223
228
|
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
|
224
229
|
return {
|
|
225
230
|
ok: false,
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promises as fs } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { isDeepStrictEqual, promisify } from "node:util";
|
|
5
|
+
import { Minimatch } from "minimatch";
|
|
6
|
+
import { OPTIONS } from "../data/options.generated.js";
|
|
7
|
+
import { explainConfig } from "./configExplainer.js";
|
|
8
|
+
import { DEFAULT_MATCH_TIMEOUT_MS, matchFilePatterns } from "./customManagerPreview.js";
|
|
9
|
+
import { isRecord } from "./util.js";
|
|
10
|
+
const execFileP = promisify(execFile);
|
|
11
|
+
export const DEFAULT_MAX_FILES = 50_000;
|
|
12
|
+
const SAMPLE_SIZE = 5;
|
|
13
|
+
const MAX_SUBSTRING_HITS = 50;
|
|
14
|
+
const RENOVATE_DEFAULT = "<renovate default>";
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Renovate's own filters, mirrored from
|
|
17
|
+
// node_modules/renovate/dist/workers/repository/extract/file-match.js
|
|
18
|
+
// (`getIncludedFiles` / `filterIgnoredFiles`). Mirrored rather than
|
|
19
|
+
// worker-imported because they are two one-liners over minimatch (already a
|
|
20
|
+
// direct dep pinned to Renovate's major); test/unit/pathSelection.test.ts runs
|
|
21
|
+
// the real functions as an oracle so the mirror can't drift silently.
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
const mmCache = new Map();
|
|
24
|
+
function glob(pattern) {
|
|
25
|
+
let mm = mmCache.get(pattern);
|
|
26
|
+
if (!mm)
|
|
27
|
+
mmCache.set(pattern, (mm = new Minimatch(pattern, { dot: true })));
|
|
28
|
+
return mm;
|
|
29
|
+
}
|
|
30
|
+
/** includePaths: exact path or case-sensitive glob. */
|
|
31
|
+
function includeHit(file, pattern) {
|
|
32
|
+
if (file === pattern)
|
|
33
|
+
return "exact";
|
|
34
|
+
return glob(pattern).match(file) ? "glob" : null;
|
|
35
|
+
}
|
|
36
|
+
/** ignorePaths: substring OR case-sensitive glob. */
|
|
37
|
+
function ignoreHit(file, pattern) {
|
|
38
|
+
return { substring: file.includes(pattern), glob: glob(pattern).match(file) };
|
|
39
|
+
}
|
|
40
|
+
/** Renovate's `getFilteredFileList`: includePaths first, then ignorePaths. */
|
|
41
|
+
export function filterFileList(files, includePaths, ignorePaths) {
|
|
42
|
+
const included = includePaths.length
|
|
43
|
+
? files.filter((f) => includePaths.some((p) => includeHit(f, p)))
|
|
44
|
+
: files;
|
|
45
|
+
return ignorePaths.length
|
|
46
|
+
? included.filter((f) => !ignorePaths.some((p) => {
|
|
47
|
+
const h = ignoreHit(f, p);
|
|
48
|
+
return h.substring || h.glob;
|
|
49
|
+
}))
|
|
50
|
+
: included;
|
|
51
|
+
}
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// File list
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
/**
|
|
56
|
+
* Renovate's local platform lists files with `git ls-files` and falls back to
|
|
57
|
+
* a `**` glob (dot files included, nothing pruned) — see
|
|
58
|
+
* node_modules/renovate/dist/modules/platform/local/scm.js. Mirror both so
|
|
59
|
+
* `**\/node_modules/**` style ignores are visible, unlike the
|
|
60
|
+
* .gitignore-honouring walk in customManagerPreview.ts.
|
|
61
|
+
*/
|
|
62
|
+
export async function listRepoFiles(repoPath) {
|
|
63
|
+
try {
|
|
64
|
+
const { stdout } = await execFileP("git", ["ls-files", "-z"], {
|
|
65
|
+
cwd: repoPath,
|
|
66
|
+
maxBuffer: 512 * 1024 * 1024,
|
|
67
|
+
});
|
|
68
|
+
return { files: stdout.split("\0").filter(Boolean), source: "git" };
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
const entries = await fs.readdir(repoPath, { recursive: true, withFileTypes: true });
|
|
72
|
+
const files = entries
|
|
73
|
+
.filter((e) => e.isFile() || e.isSymbolicLink())
|
|
74
|
+
.map((e) => path.relative(repoPath, path.join(e.parentPath, e.name)).split(path.sep).join("/"))
|
|
75
|
+
// ponytail: Renovate's glob would include .git/ internals too; they only add noise.
|
|
76
|
+
.filter((f) => !f.startsWith(".git/"))
|
|
77
|
+
.sort();
|
|
78
|
+
return { files, source: "walk" };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function isLeaf(node) {
|
|
82
|
+
return isRecord(node) && "setBy" in node && Array.isArray(node.setBy);
|
|
83
|
+
}
|
|
84
|
+
/** Last contribution whose (array) value contains `el` — works for overwritten and concatenated arrays. */
|
|
85
|
+
function sourceOf(leaf, el) {
|
|
86
|
+
for (let i = leaf.setBy.length - 1; i >= 0; i--) {
|
|
87
|
+
const c = leaf.setBy[i];
|
|
88
|
+
if (Array.isArray(c.value) ? c.value.some((v) => isDeepStrictEqual(v, el)) : isDeepStrictEqual(c.value, el)) {
|
|
89
|
+
return { source: c.source, via: c.via };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return { source: "<unknown>", via: [] };
|
|
93
|
+
}
|
|
94
|
+
function stringList(leaf) {
|
|
95
|
+
return Array.isArray(leaf?.value) ? leaf.value.filter((v) => typeof v === "string") : [];
|
|
96
|
+
}
|
|
97
|
+
// ponytail: naive "what was meant" heuristic; only offered for bare (non-glob) patterns.
|
|
98
|
+
function suggestGlob(pattern) {
|
|
99
|
+
const p = pattern.replace(/\/+$/, "");
|
|
100
|
+
if (p.includes("/"))
|
|
101
|
+
return `${p}/**`;
|
|
102
|
+
if (p.includes("."))
|
|
103
|
+
return `**/${p}`;
|
|
104
|
+
return `**/${p}/**`;
|
|
105
|
+
}
|
|
106
|
+
/** Only the user's own config and external presets can be edited; built-in/default dead globs are just noise. */
|
|
107
|
+
function userOwned(source) {
|
|
108
|
+
return source === "<own>" || (!source.startsWith("<") && source.includes(">"));
|
|
109
|
+
}
|
|
110
|
+
export async function explainPaths(repoPath, config, options = {}) {
|
|
111
|
+
const warnings = [];
|
|
112
|
+
const maxFiles = options.maxFiles ?? DEFAULT_MAX_FILES;
|
|
113
|
+
const explained = await explainConfig(config, options);
|
|
114
|
+
const tree = explained.explanation;
|
|
115
|
+
// Only the merge-fallback warning matters here; the rest are about Handlebars
|
|
116
|
+
// templates, irrelevant to paths (unresolved presets have their own field).
|
|
117
|
+
warnings.push(...explained.warnings.filter((w) => w.preset === "<merge>").map((w) => w.message));
|
|
118
|
+
const includeLeaf = isLeaf(tree.includePaths) ? tree.includePaths : undefined;
|
|
119
|
+
const ignoreLeaf = isLeaf(tree.ignorePaths) ? tree.ignorePaths : undefined;
|
|
120
|
+
const includePaths = stringList(includeLeaf);
|
|
121
|
+
// ignorePaths is non-mergeable with a non-empty default: unset → Renovate's default applies.
|
|
122
|
+
const ignorePaths = ignoreLeaf ? stringList(ignoreLeaf) : [...OPTIONS.ignorePaths.default];
|
|
123
|
+
const attribute = (leaf, p) => leaf ? sourceOf(leaf, p) : { source: RENOVATE_DEFAULT, via: [] };
|
|
124
|
+
for (const [key, node] of Object.entries(tree)) {
|
|
125
|
+
if (isLeaf(node) || !isRecord(node))
|
|
126
|
+
continue;
|
|
127
|
+
for (const field of ["includePaths", "ignorePaths"]) {
|
|
128
|
+
if (field in node) {
|
|
129
|
+
warnings.push(`${key}.${field} is a manager-level override (it replaces the top-level list for the ${key} manager only) and is not evaluated here.`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
const listed = await listRepoFiles(repoPath);
|
|
134
|
+
if (listed.source === "walk") {
|
|
135
|
+
warnings.push("repoPath is not a git work tree (or git is unavailable), so the file list comes from a full directory walk — Renovate's own fallback. In a git repo Renovate scans only `git ls-files` output.");
|
|
136
|
+
}
|
|
137
|
+
const truncated = listed.files.length > maxFiles;
|
|
138
|
+
const files = listed.files.slice(0, maxFiles);
|
|
139
|
+
if (truncated) {
|
|
140
|
+
warnings.push(`The repo lists ${listed.files.length} files; only the first ${maxFiles} were evaluated. Raise maxFiles for exact counts.`);
|
|
141
|
+
}
|
|
142
|
+
const patterns = [];
|
|
143
|
+
// includePaths, against the full list.
|
|
144
|
+
includePaths.forEach((pattern, index) => {
|
|
145
|
+
const matched = files.filter((f) => includeHit(f, pattern));
|
|
146
|
+
patterns.push({
|
|
147
|
+
field: "includePaths",
|
|
148
|
+
index,
|
|
149
|
+
pattern,
|
|
150
|
+
...attribute(includeLeaf, pattern),
|
|
151
|
+
matchKind: matched.length === 0 ? "none" : matched.includes(pattern) ? "exact" : "glob",
|
|
152
|
+
matchedCount: matched.length,
|
|
153
|
+
sampleFiles: matched.slice(0, SAMPLE_SIZE),
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
const included = filterFileList(files, includePaths, []);
|
|
157
|
+
// ignorePaths, against the include-filtered list (Renovate's order).
|
|
158
|
+
const globIgnored = new Set();
|
|
159
|
+
const substringHits = new Map(); // file → patterns that hit it by substring only
|
|
160
|
+
ignorePaths.forEach((pattern, index) => {
|
|
161
|
+
const matched = [];
|
|
162
|
+
let anyGlob = false;
|
|
163
|
+
for (const f of included) {
|
|
164
|
+
const h = ignoreHit(f, pattern);
|
|
165
|
+
if (h.glob) {
|
|
166
|
+
anyGlob = true;
|
|
167
|
+
globIgnored.add(f);
|
|
168
|
+
}
|
|
169
|
+
else if (h.substring) {
|
|
170
|
+
substringHits.set(f, [...(substringHits.get(f) ?? []), pattern]);
|
|
171
|
+
}
|
|
172
|
+
if (h.glob || h.substring)
|
|
173
|
+
matched.push(f);
|
|
174
|
+
}
|
|
175
|
+
patterns.push({
|
|
176
|
+
field: "ignorePaths",
|
|
177
|
+
index,
|
|
178
|
+
pattern,
|
|
179
|
+
...attribute(ignoreLeaf, pattern),
|
|
180
|
+
matchKind: matched.length === 0 ? "none" : anyGlob ? "glob" : "substring",
|
|
181
|
+
matchedCount: matched.length,
|
|
182
|
+
sampleFiles: matched.slice(0, SAMPLE_SIZE),
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
const kept = filterFileList(included, [], ignorePaths);
|
|
186
|
+
const allSubstringOnly = [];
|
|
187
|
+
for (const [file, pats] of substringHits) {
|
|
188
|
+
if (globIgnored.has(file))
|
|
189
|
+
continue;
|
|
190
|
+
for (const pattern of pats) {
|
|
191
|
+
const suggestedGlob = suggestGlob(pattern);
|
|
192
|
+
allSubstringOnly.push({
|
|
193
|
+
file,
|
|
194
|
+
pattern,
|
|
195
|
+
suggestedGlob,
|
|
196
|
+
suggestedGlobMatches: new Minimatch(suggestedGlob, { dot: true }).match(file),
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
// Likely surprises first.
|
|
201
|
+
allSubstringOnly.sort((a, b) => Number(a.suggestedGlobMatches) - Number(b.suggestedGlobMatches));
|
|
202
|
+
if (allSubstringOnly.length > MAX_SUBSTRING_HITS) {
|
|
203
|
+
warnings.push(`${allSubstringOnly.length} substring-only ignorePaths hits; listing the first ${MAX_SUBSTRING_HITS}.`);
|
|
204
|
+
}
|
|
205
|
+
// packageRules[].matchFileNames, against the kept files (matchRegexOrGlob, case-insensitive).
|
|
206
|
+
const matchFileNames = [];
|
|
207
|
+
const rulesLeaf = isLeaf(tree.packageRules) ? tree.packageRules : undefined;
|
|
208
|
+
const rules = Array.isArray(rulesLeaf?.value) ? rulesLeaf.value : [];
|
|
209
|
+
for (const [rule, r] of rules.entries()) {
|
|
210
|
+
if (!isRecord(r) || !Array.isArray(r.matchFileNames))
|
|
211
|
+
continue;
|
|
212
|
+
const { source } = sourceOf(rulesLeaf, r);
|
|
213
|
+
for (const [index, pattern] of r.matchFileNames.entries()) {
|
|
214
|
+
if (typeof pattern !== "string")
|
|
215
|
+
continue;
|
|
216
|
+
let matched = [];
|
|
217
|
+
try {
|
|
218
|
+
matched = await matchFilePatterns(kept, [pattern], DEFAULT_MATCH_TIMEOUT_MS, warnings);
|
|
219
|
+
}
|
|
220
|
+
catch (err) {
|
|
221
|
+
warnings.push(`packageRules[${rule}].matchFileNames[${index}] ${pattern}: ${err.message}`);
|
|
222
|
+
}
|
|
223
|
+
matchFileNames.push({
|
|
224
|
+
rule,
|
|
225
|
+
...(typeof r.description === "string" ? { description: r.description } : {}),
|
|
226
|
+
source,
|
|
227
|
+
index,
|
|
228
|
+
pattern,
|
|
229
|
+
matchedCount: matched.length,
|
|
230
|
+
sampleFiles: matched.slice(0, SAMPLE_SIZE),
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
const deadPatterns = [
|
|
235
|
+
...patterns
|
|
236
|
+
.filter((p) => p.matchedCount === 0 && userOwned(p.source))
|
|
237
|
+
.map(({ field, index, pattern, source }) => ({ field, index, pattern, source })),
|
|
238
|
+
...matchFileNames
|
|
239
|
+
.filter((m) => m.matchedCount === 0 && userOwned(m.source))
|
|
240
|
+
.map(({ rule, index, pattern, source }) => ({
|
|
241
|
+
field: `packageRules[${rule}].matchFileNames`,
|
|
242
|
+
index,
|
|
243
|
+
pattern,
|
|
244
|
+
source,
|
|
245
|
+
})),
|
|
246
|
+
];
|
|
247
|
+
return {
|
|
248
|
+
patterns,
|
|
249
|
+
deadPatterns,
|
|
250
|
+
substringOnlyHits: allSubstringOnly.slice(0, MAX_SUBSTRING_HITS),
|
|
251
|
+
matchFileNames,
|
|
252
|
+
summary: {
|
|
253
|
+
fileListSource: listed.source,
|
|
254
|
+
totalFiles: listed.files.length,
|
|
255
|
+
evaluatedFiles: files.length,
|
|
256
|
+
truncated,
|
|
257
|
+
keptCount: kept.length,
|
|
258
|
+
excludedByIncludePaths: files.length - included.length,
|
|
259
|
+
excludedByIgnorePaths: included.length - kept.length,
|
|
260
|
+
substringOnlyHitCount: allSubstringOnly.length,
|
|
261
|
+
},
|
|
262
|
+
mergeQuality: explained.mergeQuality,
|
|
263
|
+
presetsUnresolved: explained.presetsUnresolved,
|
|
264
|
+
warnings,
|
|
265
|
+
};
|
|
266
|
+
}
|
package/dist/lib/setupCheck.js
CHANGED
package/dist/lib/version.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { fileURLToPath } from "node:url";
|
|
2
|
-
export const SERVER_VERSION = "1.
|
|
2
|
+
export const SERVER_VERSION = "1.8.0"; // x-release-please-version
|
|
3
3
|
// "released" = the server is running from a `node_modules/` install (global
|
|
4
4
|
// `npm i -g`, project-local `npm i`, or `npx renovate-mcp`). Anything else —
|
|
5
5
|
// most commonly an MCP config of `{"command": "node", "args": [".../dist/index.js"]}`
|
|
@@ -33,7 +33,7 @@ export function registerWorkflowPrompts(server) {
|
|
|
33
33
|
? `3. Call suggest_presets with query "${intent}" to get ranked presets and, if coverage is weak, an unvalidated draft config skeleton.`
|
|
34
34
|
: '3. Ask the user what they want Renovate to do, then call suggest_presets with that intent as the query.',
|
|
35
35
|
"4. Call resolve_config and explain_config on the resulting draft/config so you can show what it actually resolves to and which preset set each field.",
|
|
36
|
-
"5. Call validate_config and lint_config against the draft to catch schema errors and Renovate-specific footguns.",
|
|
36
|
+
"5. Call validate_config and lint_config against the draft to catch schema errors and Renovate-specific footguns, and explain_paths with repoPath (plus the draft as configContent) to check which files its includePaths / ignorePaths keep — presets like config:recommended silently ignore test/ and examples/ directories.",
|
|
37
37
|
"6. Call dry_run against the repo to preview the actual updates Renovate would propose.",
|
|
38
38
|
"7. Summarize the plan (presets used, resolved effective config, dry-run results) and ask the user to confirm.",
|
|
39
39
|
"8. Only after the user explicitly confirms, call write_config with repoPath to save. Never call write_config before that confirmation.",
|
|
@@ -105,7 +105,7 @@ export function registerWorkflowPrompts(server) {
|
|
|
105
105
|
intent
|
|
106
106
|
? `4. Call suggest_presets with a query combining the detected ecosystems and the user's intent "${intent}" (e.g. "npm and github-actions monorepo, automerge patch updates, pin digests"), and use the returned draft.config as the starting point.`
|
|
107
107
|
: '4. Ask the user what they want Renovate to do, then call suggest_presets with a query combining the detected ecosystems and that intent (e.g. "npm and github-actions monorepo, automerge patch updates, pin digests"), and use the returned draft.config as the starting point.',
|
|
108
|
-
"5. Call lint_config and validate_config against the draft and fix any findings.",
|
|
108
|
+
"5. Call lint_config and validate_config against the draft and fix any findings. Then call explain_paths with repoPath and the draft as configContent to confirm its ignorePaths (including the ones presets add) do not drop package files detected in step 3.",
|
|
109
109
|
"6. Summarize the draft config and the detected ecosystems, and ask the user to confirm. dry_run has no inline config input, so the draft must be written before a full dry run can use it.",
|
|
110
110
|
"7. Only after the user explicitly confirms, call write_config with repoPath to save. Never call write_config before that confirmation.",
|
|
111
111
|
`8. Call dry_run with repoPath "${repoPath}" (default full mode) to preview the actual updates Renovate would propose with the new config, and report the result.`,
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { configRecord, endpointString, pathString } from "../lib/inputLimits.js";
|
|
3
|
+
import { DEFAULT_MAX_FILES, explainPaths } from "../lib/pathSelection.js";
|
|
4
|
+
import { FETCH_PLATFORMS } from "../lib/presetResolver.js";
|
|
5
|
+
import { assertRepoDir, loadConfigSource, MERGE_PREVIEW_DISCLAIMER, NETWORK_READ_ONLY_ANNOTATIONS, } from "../lib/toolInputs.js";
|
|
6
|
+
const EXPLAIN_PATHS_DISCLAIMER = "Files are listed the way Renovate's local platform does (`git ls-files`, else a full walk) and filtered with a mirror of Renovate's own getIncludedFiles / filterIgnoredFiles; the config is resolved with Renovate's mergeChildConfig. Remote platforms list the base branch's committed tree, so uncommitted files may differ. Built-in managers' own managerFilePatterns are not applied — run dry_run with dryRunMode \"extract\" for the files each manager actually extracts.";
|
|
7
|
+
export function registerExplainPaths(server) {
|
|
8
|
+
server.registerTool("explain_paths", {
|
|
9
|
+
title: "Explain which files Renovate scans",
|
|
10
|
+
annotations: NETWORK_READ_ONLY_ANNOTATIONS,
|
|
11
|
+
description: "Offline: resolve the repo's config (presets included, so hidden ignorePaths like `:ignoreModulesAndTests` from config:recommended show up), list the repo's files the way Renovate does (`git ls-files`), and apply Renovate's includePaths → ignorePaths filter. Returns every pattern with the preset or `<own>` config that set it, how many files it matched and a sample; `deadPatterns` (user-owned patterns matching no file, including packageRules[].matchFileNames entries); `substringOnlyHits` (files dropped only because an ignorePaths entry is a substring of the path, e.g. `test` dropping `src/latest/package.json`, with the glob that was probably meant); per-rule matchFileNames counts against the kept files; and a summary. Pass `configContent` to try a config other than the repo's own. Same `externalPresets` / `endpoint` / `platform` opt-ins as resolve_config.",
|
|
12
|
+
inputSchema: {
|
|
13
|
+
repoPath: pathString("Absolute path to the repository root whose files are evaluated."),
|
|
14
|
+
configContent: configRecord("Inline config to evaluate instead of the repo's own config file.").optional(),
|
|
15
|
+
externalPresets: z
|
|
16
|
+
.boolean()
|
|
17
|
+
.optional()
|
|
18
|
+
.describe("When true, fetch external presets (github>, gitlab>, gitea>, forgejo>) over HTTPS — same as resolve_config. Default false."),
|
|
19
|
+
endpoint: endpointString("API base URL for external preset fetches — same semantics as resolve_config.").optional(),
|
|
20
|
+
platform: z
|
|
21
|
+
.enum(FETCH_PLATFORMS)
|
|
22
|
+
.optional()
|
|
23
|
+
.describe("Platform flavour of `endpoint`; routes `local>` presets — same semantics as resolve_config."),
|
|
24
|
+
maxFiles: z
|
|
25
|
+
.number()
|
|
26
|
+
.int()
|
|
27
|
+
.min(1)
|
|
28
|
+
.max(1_000_000)
|
|
29
|
+
.optional()
|
|
30
|
+
.describe(`Cap on the number of listed files evaluated (default ${DEFAULT_MAX_FILES}); truncation is reported.`),
|
|
31
|
+
},
|
|
32
|
+
}, async ({ repoPath, configContent, externalPresets, endpoint, platform, maxFiles }) => {
|
|
33
|
+
const repoError = await assertRepoDir(repoPath);
|
|
34
|
+
if (repoError)
|
|
35
|
+
return { isError: true, content: [{ type: "text", text: repoError }] };
|
|
36
|
+
const src = configContent !== undefined ? { config: configContent } : await loadConfigSource({ repoPath });
|
|
37
|
+
if ("error" in src)
|
|
38
|
+
return { isError: true, content: [{ type: "text", text: src.error }] };
|
|
39
|
+
const result = await explainPaths(repoPath, src.config, {
|
|
40
|
+
fetchExternal: externalPresets ?? false,
|
|
41
|
+
endpoint,
|
|
42
|
+
platform,
|
|
43
|
+
maxFiles,
|
|
44
|
+
});
|
|
45
|
+
return {
|
|
46
|
+
content: [
|
|
47
|
+
{
|
|
48
|
+
type: "text",
|
|
49
|
+
text: JSON.stringify({
|
|
50
|
+
...("path" in src && src.path ? { path: src.path } : {}),
|
|
51
|
+
...result,
|
|
52
|
+
disclaimer: result.mergeQuality === "faithful" ? EXPLAIN_PATHS_DISCLAIMER : MERGE_PREVIEW_DISCLAIMER,
|
|
53
|
+
}, null, 2),
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
};
|
|
57
|
+
});
|
|
58
|
+
}
|