renovate-mcp 0.12.1 → 0.13.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 +84 -184
- package/dist/index.js +1 -0
- package/dist/lib/endpointProbe.d.ts +20 -0
- package/dist/lib/endpointProbe.js +30 -0
- package/dist/lib/gitRemote.d.ts +28 -0
- package/dist/lib/gitRemote.js +97 -0
- package/dist/lib/setupCheck.d.ts +38 -1
- package/dist/lib/setupCheck.js +201 -2
- package/dist/lib/version.d.ts +1 -1
- package/dist/lib/version.js +1 -1
- package/dist/tools/checkSetup.js +11 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,67 +7,71 @@
|
|
|
7
7
|
|
|
8
8
|
An MCP server for designing [Renovate](https://github.com/renovatebot/renovate) configurations interactively. Point it at a local repo and let an LLM help you read, validate, preview, and save `renovate.json`.
|
|
9
9
|
|
|
10
|
-
##
|
|
10
|
+
## Quick start
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
-
|
|
14
|
-
|
|
15
|
-
- [Install](#install)
|
|
16
|
-
- [Platform setup](#platform-setup)
|
|
17
|
-
- [Example prompts](#example-prompts)
|
|
18
|
-
- [Example session](#example-session)
|
|
19
|
-
- [Development](#development)
|
|
20
|
-
- [Release flow](#release-flow)
|
|
21
|
-
- [Design notes](#design-notes)
|
|
12
|
+
```bash
|
|
13
|
+
curl -fsSL https://raw.githubusercontent.com/tibuntu/renovate-mcp/main/install.sh | bash
|
|
14
|
+
```
|
|
22
15
|
|
|
23
|
-
|
|
16
|
+
Or add this entry manually to your client's `mcpServers` config:
|
|
24
17
|
|
|
25
|
-
|
|
18
|
+
```json
|
|
19
|
+
{
|
|
20
|
+
"mcpServers": {
|
|
21
|
+
"renovate": {
|
|
22
|
+
"command": "npx",
|
|
23
|
+
"args": ["-y", "renovate-mcp"]
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
```
|
|
26
28
|
|
|
27
|
-
|
|
28
|
-
| --- | --- |
|
|
29
|
-
| `check_setup` | Report Renovate CLI + validator availability, versions, and install hints. Also surfaces a `platformContext` block (`RENOVATE_PLATFORM` / `RENOVATE_ENDPOINT` values, token-presence booleans, the platform `dry_run` would pick when its input is unset, and notes about likely misconfigurations) so callers can verify env before invoking `dry_run`. Token values are never echoed — only presence booleans. Compares the running Node version against the bundled Renovate's declared `engines.node` range and emits an actionable hint when they're incompatible — the same condition Renovate would otherwise log only as `Unsupported node environment` during a `dry_run`. Also runs at startup. |
|
|
30
|
-
| `get_version` | Report the renovate-mcp server version and whether it's a released build (running from `node_modules`) or a local/dev build (typically launched via `command: node` against a checkout). |
|
|
31
|
-
| `read_config` | Locate and parse a repo's Renovate config (`renovate.json`, `renovate.json5`, `.renovaterc*`, `package.json#renovate`, …) in priority order. |
|
|
32
|
-
| `resolve_config` | Expand every `extends` preset offline. Opt in to fetching `github>` / `gitlab>` presets over HTTPS with `externalPresets: true`. |
|
|
33
|
-
| `explain_config` | Inverse of `resolve_config`: walk the same preset tree but annotate every leaf field with the chain of presets that touched it. Each leaf is `{ value, setBy }` where `setBy` lists every contribution in merge order — last entry wins for scalars; for arrays each entry adds its own slice. Same offline-by-default behaviour and same `externalPresets` / `endpoint` / `platform` opt-ins as `resolve_config`. |
|
|
34
|
-
| `preview_custom_manager` | Preview a `customManagers` entry (regex or JSONata) against a local repo — for regex, shows file/line hits and extracted dep info; for `customType: "jsonata"` (`fileFormat: "json" \| "yaml" \| "toml"`), parses the file structure and extracts deps from each `matchStrings` JSONata projection. Offline. |
|
|
35
|
-
| `validate_config` | Run `renovate-config-validator` against a file or inline object. |
|
|
36
|
-
| `lint_config` | Semantic lint pass: regex hygiene (`/…/` wrapping), unknown manager names, deprecated keys, automerge/extends/enabled correctness, packageRule completeness, and schedule sanity. Offline; complements `validate_config`. |
|
|
37
|
-
| `dry_run` | Run Renovate with `--dry-run` and return the structured JSON report. Defaults to `--platform=local` against `repoPath`; pass `platform` + `endpoint` + `token` + `repository` to run as a real GitHub/GitLab client (needed when the config extends `local>` presets on a private host). When `platform` is not passed, the tool reads `RENOVATE_PLATFORM` from the MCP server's env before defaulting to `local`; the response echoes `platformSource` (`input` / `env` / `default`) and `effectivePlatform`, and an advisory warning fires when env-derived platform is non-local so a surprising `gitlab`/`github` is never mysterious. Preflight error messages also tag the platform origin. When `token` is not passed, falls back to `RENOVATE_TOKEN` from MCP env, then to `GITLAB_TOKEN` (when `platform=gitlab`) or `GITHUB_TOKEN` (when `platform=github`) — auto-translated to `RENOVATE_TOKEN` for the spawned Renovate CLI, since Renovate itself only reads that one var. `repository` accepts GitHub-style `owner/repo` and GitLab nested-group paths like `group/subgroup/project`. `endpoint`/`token` also flow through in local mode so `gitlab>…` / `github>…` preset shortcuts can be redirected at a self-hosted host without setting up a full remote run. Two optional inputs handle reports that would exceed MCP response-content caps: `reportOutputPath` redirects the structured report to a file (mode 0600) and collapses `summary.report` to `{ reportPath, repoCount, updateCount }`, and `summaryOnly: true` strips per-repo update arrays from the inline payload (only meaningful with `reportOutputPath`). No PRs, no pushes. Emits MCP progress notifications during the run when the caller supplies a `progressToken`. When `token` or any `hostRules[].token` is passed inline, the result `warnings` array carries an advisory steering callers toward env-var auth (see [Secrets](#secrets) under Design notes). RE2 native-fallback noise and `Unsupported node environment` notices are filtered out of `reportErrors` — the former still surfaces under `warnings`, the latter under a separate `environmentWarnings` array — so `ok` reflects whether the run actually failed, not benign degradation. |
|
|
38
|
-
| `dry_run_diff` | Semantic diff between two `dry_run` reports — `added` / `removed` / `changed` proposed updates plus a compact text rendering. Stateless; takes both reports as inputs. Each side accepts either the inline report (raw `{ repositories }` or a full `dry_run` summary with a `report` key) or `{ reportPath: "<absolute path>" }` pointing at a file written by `dry_run`'s `reportOutputPath` — pair the two when reports would otherwise hit the inline truncation cap. Useful when iterating on a config to see exactly what each tweak did. |
|
|
39
|
-
| `migrate_config` | Apply Renovate's built-in config migrations (deprecated key renames like `masterIssue` → `dependencyDashboard`, template-variable rewrites, `packageRules` matcher consolidation, `host-rules` unification, …) and return the migrated config plus a unified diff. Does not write — chain with `write_config` to persist. Runs in an isolated worker thread (see [Design notes](#design-notes)); first call carries a one-time cold-load cost of a few seconds. |
|
|
40
|
-
| `write_config` | Validate, then atomically write a config to disk. When the target file already exists and parses as JSON-with-comments, edits go through a round-trip serializer (`jsonc-parser`) that preserves comments, key order, and trailing trivia; brand-new writes fall back to plain `JSON.stringify`. Refuses to save invalid configs unless `force: true` is paired with `confirmForce: "YES_OVERRIDE_VALIDATION"` — the literal sentinel makes accidental overrides under prompt-injected tool calls harder. Also refuses `.json5` files that use JSON5-only syntax (`reason: "json5-not-jsonc-compatible"`) and corrupted existing files (`reason: "existing-file-unparseable"`); `force: true` is the escape hatch and is a destructive rewrite that skips both validation and round-trip preservation. |
|
|
41
|
-
| `renovate://presets` (resource) | Markdown index of all 1000+ built-in presets grouped by namespace. |
|
|
42
|
-
| `renovate://presets/{namespace}` (resource template) | Markdown listing for a single namespace (e.g. `renovate://presets/config`) — cheaper than the full index. |
|
|
43
|
-
| `renovate://preset/{name}` (resource template) | Expanded JSON body for one preset (e.g. `renovate://preset/config:recommended`). |
|
|
44
|
-
|
|
45
|
-
See [Design notes](#design-notes) for implementation details (timeouts, safety caps, auth scrubbing, merge semantics).
|
|
29
|
+
Restart your client and try the prompt: *"List the namespaces available under `renovate://presets`."* A response listing `config`, `docker`, `npm`, … confirms the server is reachable. Long-form install options are under [Install](#install) below.
|
|
46
30
|
|
|
47
|
-
## What
|
|
31
|
+
## What you can do
|
|
48
32
|
|
|
49
|
-
- **
|
|
50
|
-
-
|
|
51
|
-
-
|
|
52
|
-
-
|
|
33
|
+
- **Read and explain configs** — locate the active `renovate.json*`, expand every `extends` preset offline, and trace which preset set each field.
|
|
34
|
+
- **Preview custom managers** before running Renovate — regex and JSONata, with file/line hits and extracted dep info.
|
|
35
|
+
- **Validate and lint** — schema validation plus a semantic lint pass for Renovate-specific footguns (unwrapped regexes, unknown manager names, deprecated keys).
|
|
36
|
+
- **Dry-run** against a local checkout or a remote GitHub/GitLab — see exactly which PRs Renovate would open.
|
|
37
|
+
- **Save back atomically** — round-trip writes preserve comments and key order in existing JSON-with-comments files.
|
|
53
38
|
|
|
54
|
-
##
|
|
39
|
+
## Tools & resources
|
|
55
40
|
|
|
56
|
-
|
|
41
|
+
Twelve tools and three resource templates. Each tool name below links to its full reference in [`docs/tools.md`](docs/tools.md).
|
|
57
42
|
|
|
58
|
-
|
|
59
|
-
|
|
43
|
+
| Tool | Purpose |
|
|
44
|
+
| --- | --- |
|
|
45
|
+
| [`check_setup`](docs/tools.md#check_setup) | Report Renovate CLI + validator availability, versions, install hints, and a `platformContext` block for env diagnosis. Also runs at startup. Pass an optional `repoPath` to add a `repoContext` block diagnosing the repo's git origin, config endpoint/platform, endpoint reachability, and token coverage. |
|
|
46
|
+
| [`get_version`](docs/tools.md#get_version) | Report the renovate-mcp server version and whether it's a released or local/dev build. |
|
|
47
|
+
| [`read_config`](docs/tools.md#read_config) | Locate and parse a repo's Renovate config in Renovate's own discovery order. |
|
|
48
|
+
| [`resolve_config`](docs/tools.md#resolve_config) | Expand every `extends` preset offline. Opt in to fetching `github>` / `gitlab>` presets over HTTPS. |
|
|
49
|
+
| [`explain_config`](docs/tools.md#explain_config) | Inverse of `resolve_config`: annotate every leaf field with the chain of presets that set it. |
|
|
50
|
+
| [`preview_custom_manager`](docs/tools.md#preview_custom_manager) | Preview a `customManagers` entry (regex or JSONata) against a local repo. Offline. |
|
|
51
|
+
| [`validate_config`](docs/tools.md#validate_config) | Run `renovate-config-validator` against a file or inline object. |
|
|
52
|
+
| [`lint_config`](docs/tools.md#lint_config) | Semantic lint pass for Renovate-specific footguns the schema validator declares valid. Offline. |
|
|
53
|
+
| [`dry_run`](docs/tools.md#dry_run) | Run Renovate with `--dry-run` and return the structured JSON report. Local-by-default; remote with `platform` + `endpoint` + `token` + `repository`. No PRs, no pushes. |
|
|
54
|
+
| [`dry_run_diff`](docs/tools.md#dry_run_diff) | Stateless semantic diff between two `dry_run` reports — added / removed / changed updates. |
|
|
55
|
+
| [`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. |
|
|
56
|
+
| [`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. |
|
|
57
|
+
| [`renovate://presets`](docs/tools.md#renovatepresets) (resource) | Markdown index of all built-in presets grouped by namespace. |
|
|
58
|
+
| [`renovate://presets/{namespace}`](docs/tools.md#renovatepresetsnamespace) (resource) | Markdown listing for a single namespace. |
|
|
59
|
+
| [`renovate://preset/{name}`](docs/tools.md#renovatepresetname) (resource) | Expanded JSON body for one preset. |
|
|
60
|
+
|
|
61
|
+
## Requirements
|
|
62
|
+
|
|
63
|
+
- **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.
|
|
64
|
+
- **Node.js ≥ 24** (aligns with Renovate's own engine requirement).
|
|
60
65
|
|
|
61
66
|
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`, `resolve_config`, `explain_config`, `preview_custom_manager`, `lint_config`) never spawn Renovate at all.
|
|
62
67
|
|
|
63
|
-
**Optional
|
|
68
|
+
**Optional env vars:**
|
|
64
69
|
|
|
65
|
-
- `RENOVATE_BIN` / `RENOVATE_CONFIG_VALIDATOR_BIN` — override the bundled binaries
|
|
70
|
+
- `RENOVATE_BIN` / `RENOVATE_CONFIG_VALIDATOR_BIN` — override the bundled binaries. When set, the override always wins.
|
|
66
71
|
- `RENOVATE_MCP_REQUIRE_CLI=false` — suppress the startup "partial availability" notice when you only intend to use the offline tools.
|
|
67
|
-
- Platform env vars —
|
|
68
|
-
- Private-registry credentials for `dry_run` — whatever Renovate itself would need at lookup time (`COMPOSER_AUTH`, `NPM_TOKEN` / `.npmrc`, Docker registry creds, or a `RENOVATE_HOST_RULES` JSON blob). Alternatively encode these as `hostRules` in the Renovate config, or pass them per-call via the `hostRules` input on `dry_run` (no MCP restart needed). Per-call `hostRules` are appended to whatever the repo's own config declares. Values reach Renovate as JSON through the tool-call transport, so the calling LLM sees them in its context — prefer the env route if that matters. Without any of these, Renovate's lookup often returns 0 updates silently; `dry_run` scans its logs for auth failures and surfaces them under `problems`.
|
|
72
|
+
- Platform + token env vars — see [`docs/platform-setup.md`](docs/platform-setup.md) for the per-platform matrix.
|
|
69
73
|
|
|
70
|
-
>
|
|
74
|
+
> Heads up: MCP servers do **not** inherit your shell env. Set every env var via the `env` key in `.mcp.json` / `claude_desktop_config.json`. See [`docs/security.md`](docs/security.md#where-env-vars-must-live).
|
|
71
75
|
|
|
72
76
|
## Install
|
|
73
77
|
|
|
@@ -88,89 +92,30 @@ npm install
|
|
|
88
92
|
npm run build
|
|
89
93
|
```
|
|
90
94
|
|
|
91
|
-
|
|
95
|
+
### Client config locations
|
|
92
96
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
"renovate": {
|
|
97
|
-
"command": "npx",
|
|
98
|
-
"args": ["-y", "renovate-mcp"]
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
```
|
|
97
|
+
- **Claude Code** — `.mcp.json` (project) or `~/.claude.json` (user).
|
|
98
|
+
- **Claude Desktop** — `~/Library/Application Support/Claude/claude_desktop_config.json`.
|
|
99
|
+
- **Other MCP clients** — any client that can launch a stdio MCP server works; point it at the same `npx -y renovate-mcp` command.
|
|
103
100
|
|
|
104
101
|
For local development, swap to `"command": "node"` with `"args": ["/absolute/path/to/renovate-mcp/dist/index.js"]`.
|
|
105
102
|
|
|
106
|
-
### Claude Code
|
|
107
|
-
|
|
108
|
-
Config lives in `.mcp.json` (project-scoped) or `~/.claude.json` (user-scoped).
|
|
109
|
-
|
|
110
|
-
### Claude Desktop
|
|
111
|
-
|
|
112
|
-
Config lives in `~/Library/Application Support/Claude/claude_desktop_config.json`.
|
|
113
|
-
|
|
114
|
-
### Other MCP clients
|
|
115
|
-
|
|
116
|
-
Any client that can launch a stdio MCP server works — point it at the same command shown above.
|
|
117
|
-
|
|
118
|
-
### Verify the wiring
|
|
119
|
-
|
|
120
|
-
Restart your client and prompt it:
|
|
121
|
-
|
|
122
|
-
> List the namespaces available under `renovate://presets`.
|
|
123
|
-
|
|
124
|
-
A response listing namespaces like `config`, `docker`, `npm`, `helpers`, … confirms the server is reachable and the resource is exposed. If the client reports the tool or resource as unavailable, re-check the config file path and command.
|
|
125
|
-
|
|
126
103
|
## Platform setup
|
|
127
104
|
|
|
128
|
-
|
|
105
|
+
For `dry_run` against a remote platform or `resolve_config` with `externalPresets: true`:
|
|
129
106
|
|
|
130
|
-
| Setup | `RENOVATE_PLATFORM` | `RENOVATE_ENDPOINT` | Token
|
|
107
|
+
| Setup | `RENOVATE_PLATFORM` | `RENOVATE_ENDPOINT` | Token |
|
|
131
108
|
| --- | --- | --- | --- |
|
|
132
|
-
| github.com | `github` | (omit
|
|
109
|
+
| github.com | `github` | (omit) | `RENOVATE_TOKEN` *or* `GITHUB_TOKEN` |
|
|
133
110
|
| GitHub Enterprise | `github` | `https://github.example.com/api/v3/` | `RENOVATE_TOKEN` *or* `GITHUB_TOKEN` |
|
|
134
|
-
| gitlab.com | `gitlab` | (omit
|
|
111
|
+
| gitlab.com | `gitlab` | (omit) | `RENOVATE_TOKEN` *or* `GITLAB_TOKEN` |
|
|
135
112
|
| Self-hosted GitLab | `gitlab` | `https://gitlab.example.com/api/v4/` | `RENOVATE_TOKEN` *or* `GITLAB_TOKEN` |
|
|
136
113
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
- All env vars must be on the MCP server process — set them via the `env` key in `.mcp.json` / `claude_desktop_config.json`, not your shell, since the MCP server runs as a child of the client and does not inherit shell env.
|
|
140
|
-
- `RENOVATE_TOKEN` wins when both it and the platform-specific var are set. `dry_run` and `resolve_config` honour the same precedence; for `dry_run` the platform-specific var is auto-translated to `RENOVATE_TOKEN` for the spawned Renovate CLI (Renovate itself only reads that one var).
|
|
141
|
-
- For repository identifiers, GitLab accepts nested-group paths like `group/subgroup/project`, not just `group/project`.
|
|
142
|
-
- If you only intend to use the offline tools (`read_config`, `resolve_config` without `externalPresets`, `preview_custom_manager`, `lint_config`), you can skip all of the above.
|
|
143
|
-
|
|
144
|
-
### Worked example — self-hosted GitLab
|
|
145
|
-
|
|
146
|
-
```jsonc
|
|
147
|
-
{
|
|
148
|
-
"mcpServers": {
|
|
149
|
-
"renovate": {
|
|
150
|
-
"command": "npx",
|
|
151
|
-
"args": ["-y", "renovate-mcp"],
|
|
152
|
-
"env": {
|
|
153
|
-
"RENOVATE_PLATFORM": "gitlab",
|
|
154
|
-
"RENOVATE_ENDPOINT": "https://gitlab.example.com/api/v4/",
|
|
155
|
-
"GITLAB_TOKEN": "<your token>"
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
```
|
|
161
|
-
|
|
162
|
-
This is enough for both `dry_run` (remote-platform runs) and `resolve_config` (private preset fetches). With this set, `dry_run` against `group/subgroup/project` works without passing `platform` / `endpoint` / `token` per call: `RENOVATE_PLATFORM` and the auto-translated `GITLAB_TOKEN` cover the platform side, and `RENOVATE_ENDPOINT` is inherited naturally by the spawned Renovate CLI. For `resolve_config`, `platform` and `endpoint` are tool *inputs* (not env vars) — pass them when you need `local>` presets routed through your host, since `resolve_config` is in-process and doesn't read `RENOVATE_*` env vars itself.
|
|
163
|
-
|
|
164
|
-
### `local>` presets
|
|
165
|
-
|
|
166
|
-
A config that extends `local>owner/repo:preset` only resolves when there's a platform context to expand it against:
|
|
167
|
-
|
|
168
|
-
- **For `dry_run`** — pass `platform` + `endpoint` + `repository` (token falls back to env). Use `dryRunMode=extract` if you only need manifest extraction; the preset preflight is skipped in that mode.
|
|
169
|
-
- **For `resolve_config`** — pass `platform` + `endpoint` as inputs and the tool rewrites `local>` into `<platform>>` and fetches over HTTPS. Without these inputs, `local>` stays in `presetsUnresolved` with a pointer to the workaround. Run `dry_run` afterwards for full-fidelity merging.
|
|
114
|
+
See [`docs/platform-setup.md`](docs/platform-setup.md) for a worked self-hosted GitLab example, `local>` preset handling, and private-registry credentials.
|
|
170
115
|
|
|
171
116
|
## Example prompts
|
|
172
117
|
|
|
173
|
-
Once the server is wired up, try prompts like these.
|
|
118
|
+
Once the server is wired up, try prompts like these. Written for Claude but work with any MCP-capable client.
|
|
174
119
|
|
|
175
120
|
**Understanding an existing config**
|
|
176
121
|
|
|
@@ -231,78 +176,33 @@ A transcript-style walkthrough: design a Dockerfile custom manager from scratch,
|
|
|
231
176
|
## Development
|
|
232
177
|
|
|
233
178
|
```bash
|
|
234
|
-
npm run dev
|
|
235
|
-
npm run typecheck
|
|
236
|
-
npm run build
|
|
237
|
-
npm start
|
|
238
|
-
npm test
|
|
239
|
-
npm run test:watch
|
|
240
|
-
npm run test:coverage
|
|
241
|
-
npm run generate:presets
|
|
242
|
-
npm run generate:managers
|
|
243
|
-
npm run generate:migrations
|
|
244
|
-
npm run check:snapshot-versions # fail if src/data/*.generated.ts is stale vs installed renovate
|
|
179
|
+
npm run dev # build watch mode
|
|
180
|
+
npm run typecheck # tsc --noEmit
|
|
181
|
+
npm run build # compile to dist/
|
|
182
|
+
npm start # run the built server over stdio
|
|
183
|
+
npm test # vitest run (builds first)
|
|
184
|
+
npm run test:watch # vitest watch mode
|
|
185
|
+
npm run test:coverage # vitest run --coverage
|
|
186
|
+
npm run generate:presets # regenerate src/data/presets.generated.ts
|
|
187
|
+
npm run generate:managers # regenerate src/data/managers.generated.ts
|
|
188
|
+
npm run generate:migrations # regenerate src/data/migrations.generated.ts
|
|
189
|
+
npm run check:snapshot-versions # fail if any src/data/*.generated.ts is stale vs installed renovate
|
|
245
190
|
```
|
|
246
191
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
CI runs `typecheck`, `build`, and `test:coverage` on Node 24 for every PR and push to `main` (see `.github/workflows/ci.yml`), across an OS matrix of `ubuntu-latest` and `macos-latest` with `fail-fast: false` so a platform-specific regression on either side surfaces. Coverage is uploaded as a per-run artifact from the Ubuntu job only (to avoid name collisions); no threshold is enforced yet. The Ubuntu job also re-runs `npm install --package-lock-only` and fails on a non-empty diff, catching drift between `package.json` root metadata (`os`, `engines`, `bin`, dep ranges) and `package-lock.json` that `npm ci` does not validate.
|
|
250
|
-
|
|
251
|
-
A separate scheduled workflow (`.github/workflows/nightly-real-renovate.yml`) runs daily at 04:17 UTC, installs `renovate@latest` on top of `npm ci`, and re-runs the full suite. It's an upstream-drift signal — failures notify the maintainer by email and do not gate per-PR CI.
|
|
252
|
-
|
|
253
|
-
`.github/workflows/dependency-review.yml` runs [`actions/dependency-review-action`](https://github.com/actions/dependency-review-action) on every PR and fails the check when a newly introduced dependency carries a CVE of `high` severity or above. Findings also render inline in the PR's "Files changed" / Conversation tabs.
|
|
254
|
-
|
|
255
|
-
`.github/workflows/claude.yml` is maintainer tooling: it lets the repo owner trigger [`anthropics/claude-code-action`](https://github.com/anthropics/claude-code-action) by mentioning `@claude` in an issue, issue comment, PR review, or PR review comment. It's gated on `sender.login == repository_owner`, so mentions from anyone else are ignored. The workflow needs the `CLAUDE_CODE_OAUTH_TOKEN` secret on the repo; outside contributors and forks do not need any Anthropic credentials to work on this project.
|
|
256
|
-
|
|
257
|
-
## Release flow
|
|
192
|
+
See [`docs/development.md`](docs/development.md) for snapshot-file mechanics, CI matrix, the nightly upstream-drift workflow, dependency review, and integration-test setup.
|
|
258
193
|
|
|
259
|
-
|
|
194
|
+
## Further reading
|
|
260
195
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
196
|
+
- [`docs/tools.md`](docs/tools.md) — full per-tool reference with all inputs, outputs, and edge cases.
|
|
197
|
+
- [`docs/platform-setup.md`](docs/platform-setup.md) — per-platform env vars, worked example, `local>` presets, private-registry credentials.
|
|
198
|
+
- [`docs/security.md`](docs/security.md) — token handling, env-var precedence, endpoint allowlist, redirect/body-cap policy, inline-secrets warning.
|
|
199
|
+
- [`docs/operations.md`](docs/operations.md) — timeouts, caps, large-report escape hatch, `ok` semantics, RE2 / nodeEnv handling, progress notifications.
|
|
200
|
+
- [`docs/architecture.md`](docs/architecture.md) — shell-out vs import, worker isolation, round-trip writer, preset catalogue.
|
|
201
|
+
- [`docs/development.md`](docs/development.md) — snapshot mechanics, CI matrix, nightly drift workflow, integration testing.
|
|
265
202
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
Secrets required on the repo: `RELEASE_PLEASE_TOKEN` (a PAT for release-please). No npm token is needed.
|
|
269
|
-
|
|
270
|
-
## Design notes
|
|
271
|
-
|
|
272
|
-
Scope and non-goals are summarized in [What this is NOT](#what-this-is-not); this section covers implementation decisions behind the scope that *is* supported.
|
|
273
|
-
|
|
274
|
-
### Secrets
|
|
275
|
-
|
|
276
|
-
- **Env-var auth is the preferred path.** `RENOVATE_TOKEN` wins; `GITHUB_TOKEN` / `GITLAB_TOKEN` are platform-specific fallbacks that `dry_run` and `resolve_config` resolve through `src/lib/credentialResolver.ts`. For `dry_run`, the platform-specific var is auto-translated to `RENOVATE_TOKEN` for the spawned Renovate CLI (Renovate itself only reads that one var). See [Platform setup](#platform-setup) for the matrix.
|
|
277
|
-
- **MCP servers do NOT inherit your shell env.** Tokens (and every other env var — `COMPOSER_AUTH`, `RENOVATE_HOST_RULES`, `RENOVATE_PLATFORM`, …) must be set via the `env` key in `claude_desktop_config.json` / `.mcp.json`, not your shell. The MCP server runs as a child of the client; exporting `GITLAB_TOKEN` in your terminal does not reach it.
|
|
278
|
-
- **Inline secrets persist in the tool transcript.** Anything passed as a tool input — `token`, `hostRules[].token`, `hostRules[].password` — is stored in the MCP transcript that the client may share, replay, or feed back into the LLM. As of v0.12, `dry_run` detects inline `token` / `hostRules[].token` on its input and appends a single advisory entry to the result `warnings` array steering callers toward env-var auth. The warning is advisory only — it does not change `isError`, does not block the spawn, and does not alter the report shape. (`password`-only host rules are not yet covered by the warning; the locked v0.12 trigger is `token`-only.)
|
|
279
|
-
- This warning is `dry_run`-only as of v0.12. Other tools that accept inline tokens (e.g. `resolve_config`) follow the same env-var precedence but do not currently emit the warning.
|
|
203
|
+
## What this is NOT
|
|
280
204
|
|
|
281
|
-
-
|
|
282
|
-
-
|
|
283
|
-
- `
|
|
284
|
-
-
|
|
285
|
-
- `preview_custom_manager` honors `.gitignore` (including nested `.gitignore`s and `.git/info/exclude`) when walking the repo, so generated/vendored directories like `dist/`, `.next/`, `target/`, `__pycache__/` don't crowd out real hits against the `maxFilesWalked` cap. `node_modules/` and `.git/` are always skipped as a safety net even when no `.gitignore` is present.
|
|
286
|
-
- `preview_custom_manager` exposes two separate safety caps so the warning text can name which one tripped: `maxFilesWalked` (default 2000) bounds the directory walk before any `fileMatch` testing, and `maxFilesMatched` (default 500) bounds the result set after `fileMatch` is applied. Previously a single `maxFilesScanned` conflated the two, leaving the user unable to tell whether to narrow `fileMatch` or widen the walk.
|
|
287
|
-
- `preview_custom_manager` runs every user-supplied regex on a `worker_threads` worker with a wall-clock budget per operation (default 2 s, configurable via `matchTimeoutMs`). Catastrophic backtracking — e.g. `^(a+)+b$` against `aaaa…c`, or `(.*)*=` against a modestly sized file — would otherwise pin the MCP server's event loop indefinitely. On timeout the worker is terminated and a warning is appended identifying which `fileMatch[i]` or `matchStrings[i]` was aborted, so the user can simplify the pattern or raise the budget.
|
|
288
|
-
- `preview_custom_manager` also stats each matched file before reading it and skips anything larger than `maxFileBytes` (default 5 MiB) with a warning, so a stray lockfile, generated artifact, or SQL dump caught by a loose `fileMatch` can't OOM the server. `maxHitsPerFile` already bounds output size; this guards input size.
|
|
289
|
-
- `preview_custom_manager` supports two `customType` values: `regex` (the original path) and `jsonata` (added in v0.13). For `customType: "jsonata"`, `fileFormat` is required and must be one of `json`, `yaml`, or `toml` — the file is parsed structurally up front (via `JSON.parse`, the `yaml` package, or `smol-toml`), then each `matchStrings` entry is evaluated as a JSONata expression against the parsed value. Each expression must return either an array of objects whose keys map to dep fields (`depName`, `currentValue`, `datasource`, `versioning`, …) OR a single bare object that is auto-wrapped to a one-element array — this mirrors Renovate's own `QueryResultZod` schema. Non-string primitive values in the returned objects are stringified for `{{groupName}}` template substitution (e.g. `currentValue: 1.2` → `"1.2"`); `null` / `undefined` and nested objects/arrays are dropped from the substitution bag. The `{{groupName}}` template-substitution gap is shared with the regex path — full Handlebars helpers and conditionals are not implemented. JSONata expressions evaluate inside the same `worker_threads` worker as user regexes and share the `matchTimeoutMs` budget (default 2 s, configurable) — a pathological expression that doesn't return in time is killed by `worker.terminate()`, identical to the regex safety posture. Run `dry_run` afterwards for full-fidelity confirmation.
|
|
290
|
-
- `resolve_config` expands `extends` against a committed snapshot of Renovate's built-in presets (`src/data/presets.generated.ts`). External `github>` / `gitlab>` fetching is opt-in, uses each platform's contents API with a 10 s timeout, and caches results per call. The `endpoint` input swaps in a custom API base for GHE / self-hosted GitLab; `platform` additionally rewrites `local>` presets to be fetched against that endpoint.
|
|
291
|
-
- External preset fetches refuse to follow HTTP redirects (`redirect: "manual"`) and never attach `Authorization: Bearer …` / `PRIVATE-TOKEN: …` headers to non-`https://` URLs. Following redirects with custom auth headers can leak `RENOVATE_TOKEN` / `GITHUB_TOKEN` / `GITLAB_TOKEN` cross-host (undici only strips the standard `Authorization` header on cross-*origin* redirects, not `PRIVATE-TOKEN`); rejecting them outright avoids the leak path and matches the fact that the GitHub/GitLab content APIs don't normally redirect for valid presets.
|
|
292
|
-
- External preset fetches cap the response body at 1 MB. A `Content-Length` over the cap is rejected up front; chunked responses without a declared length are streamed into a bounded buffer that aborts the read once the running total passes the cap. Auth-failure bodies (401/403) are read with an 8 KB cap so a misconfigured endpoint returning a giant HTML error page can't OOM the long-lived MCP process. Preset bodies are tiny in practice, so the cap is well above any legitimate value.
|
|
293
|
-
- The `endpoint` input on `resolve_config` / `explain_config` / `dry_run` is validated before any token-bearing request is built or forwarded to the Renovate child as `--endpoint=`. Anything that isn't an `https://` URL with a non-empty public host is refused: non-https schemes (`http:`, `file:`, `data:`, …), userinfo (`user:pass@host`), and any RFC 1918 / loopback / link-local literal (`localhost`, `127.0.0.0/8`, `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`, `169.254.0.0/16` (incl. cloud-metadata), `0.0.0.0/8`, `::1`, `::`, `::ffff:*`, `fc00::/7`, `fe80::/10`). Self-hosted GitHub Enterprise / GitLab on public-DNS https URLs is unaffected. The check is string-level (no DNS) — the goal is to make a prompt-injected `endpoint` value unable to coerce a token-bearing request to an attacker-controlled or internal-only address.
|
|
294
|
-
- `resolve_config` merges preset bodies with a close approximation of Renovate's own `mergeChildConfig` — arrays concat, objects recursively merge, scalars overwrite — not a bit-identical port. Rule-specific semantics for `hostRules`, `regexManagers` / `customManagers`, and certain boolean flags aren't modeled here. Every response carries `mergeQuality: "preview"` plus a human-readable `disclaimer` so callers can't miss the limitation; run `dry_run` for authoritative output.
|
|
295
|
-
- When `resolve_config` encounters template tokens outside its supported subset, it records a structured entry in `warnings`: under-argument cases (`{{arg2}}` referenced when only one arg was passed) substitute an empty string, while non-positional tokens (`{{packageRules}}`, Handlebars helpers like `{{#if …}}`) pass through verbatim.
|
|
296
|
-
- `explain_config` reuses the same expansion machinery as `resolve_config` (`parsePreset`, `loadPresetBody`, `applyArgs`, `recordTemplateWarnings`) so the two can't drift — if `resolve_config` resolves a preset, `explain_config` does too. The annotated layer threads each preset body's leaves through a parallel `mergeAnnotated` that preserves contributors instead of dropping them, then pins each contribution with the source name (literal `extends` entry, or `<own>` for the user's input config) and a `via` chain naming every parent preset traversed to reach it. Arrays accumulate every contribution in `setBy` and concat their `value`s; scalars list every contribution in merge order with the last as the winner. `merge_quality: "preview"` carries the same disclaimer as `resolve_config` — Renovate's rule-specific semantics for `hostRules`, `regexManagers`, and certain boolean flags aren't modelled.
|
|
297
|
-
- `dry_run` uses `--report-type=file` so we get a structured JSON report instead of scraping stdout. The report path is pre-created with mode 0600 before Renovate is spawned (Renovate overwrites in place with `O_TRUNC`, preserving the mode), so the on-disk JSON is never world-readable while the run is in flight — Renovate's `problems` array can carry host-rule values that we don't want exposed under the default `umask`. The file is unlinked in a `finally` block. When a `hostRules` input is passed it's written to a mode-0600 temp file in `os.tmpdir()`, handed to Renovate via the `RENOVATE_CONFIG_FILE` env var (the CLI has no `--config-file` flag), and deleted in a `finally` block. Token/password values — including the platform `token` input — are scrubbed from the detected `problems` list and the `logTail` fallback before returning.
|
|
298
|
-
- `dry_run` defaults to `--platform=local` so no host token is required, but that mode can't resolve `local>` presets (they have no platform context to expand against) and silently hides non-default-host GitHub/GitLab setups. When the caller doesn't pass a `platform` input, the tool first falls back to `RENOVATE_PLATFORM` from the MCP server's env (if it's one of `local`/`github`/`gitlab`) before defaulting to `local` — without that fallback, the wrapper would unconditionally pass `--platform=local` and silently override an env var the user had set in their `mcp.json`. Passing `platform` + `endpoint` + `token` + `repository` (or relying on the `RENOVATE_PLATFORM` / `RENOVATE_ENDPOINT` env vars) switches the run to a real platform client so the preset-fetch path works end-to-end; `--dry-run` is still set, so no PRs are opened. The repository is passed to Renovate as a positional argument because the CLI has no `--repository` flag. `endpoint` and `token` are also forwarded in the default `platform=local` mode so `gitlab>…` / `github>…` preset shortcuts (which are otherwise hardcoded to gitlab.com/github.com) can be redirected at a self-hosted host. As a guard against the silent-failure mode, `dry_run` preflight-checks the repo's config: if it extends any `local>…` preset while the effective platform is `local`, the tool fails fast with specific remediation rather than spawning a Renovate run that would opaquely report `config-validation`. The preflight is skipped for `dryRunMode=extract` so manifest-only extraction can still be attempted.
|
|
299
|
-
- `dry_run` returns a top-level `ok` boolean that is `false` whenever the CLI exited non-zero OR the structured report contains a validation/error-level problem (`level >= 40`, `message === "config-validation"`, or a non-empty `validationError` field). Renovate frequently writes exit-code 0 alongside report-level failures — trusting the exit code alone hides runs that did nothing. Two specific kinds of in-report problems are *filtered out* of `reportErrors` because they describe benign degradation rather than a failed run: the bundled RE2 native module failing to load (Renovate falls back to JS `RegExp` and keeps running — still surfaced under `warnings`) and Renovate's own `Unsupported node environment` notice (the run still produces a usable report — surfaced under a separate `environmentWarnings[]` array). For the nodeEnv case, `ok` also stays `true` even when Renovate exits non-zero, because the report is intact. Classification lives in `src/lib/runtimeWarnings.ts` (`classifyReportProblem`).
|
|
300
|
-
- **Platform-source precedence is visible to callers.** Resolution is `platform` input → `RENOVATE_PLATFORM` env → `local`, and the response echoes which step won via `platformSource: "input" | "env" | "default"` and `effectivePlatform`. Both preflight error messages (missing `repository`, missing token for a remote platform) carry an origin tag so a surprising `gitlab` value can be traced without reading source. When `platform` is unset and the env fallback yields a non-local platform, an advisory entry is appended to `warnings` reminding the caller they can pass `platform: 'local'` to override.
|
|
301
|
-
- **Large-report escape hatch.** MCP harnesses truncate tool responses at modest sizes (≈75 KB on Claude Desktop), and a real Renovate report easily exceeds that. `dry_run` accepts `reportOutputPath` (absolute path; mode-0600 write of the full report) which collapses `summary.report` to `{ reportPath, repoCount, updateCount }`, plus `summaryOnly: true` for further inline trimming. `dry_run_diff` accepts each input as either an inline report or `{ reportPath: … }`, so the iterative workflow becomes: `dry_run({ reportOutputPath: "/tmp/before.json" })`, tweak config, `dry_run({ reportOutputPath: "/tmp/after.json" })`, then `dry_run_diff({ before: { reportPath }, after: { reportPath } })`. `warnings` / `problems` / `reportErrors` / `environmentWarnings` always stay inline — those are the actionable bits.
|
|
302
|
-
- `dry_run` emits MCP progress notifications only when the caller's `tools/call` includes `_meta.progressToken` — no-op otherwise, so legacy clients see zero overhead. A 5-second heartbeat ticks while the child runs; each tick's message is best-effort enriched with the latest Renovate JSON-log `msg` seen on stdout. Notifications are also emitted at start and completion. We deliberately don't couple to Renovate's log schema beyond reading `msg`, since that schema isn't a stable API.
|
|
303
|
-
- `dry_run_diff` is stateless on purpose: both reports are passed as inputs, no per-repo state is kept on the server. Updates are keyed by `(manager, packageFile, depName)` so a version bump on the same dep shows up once under `changed` rather than twice as `removed + added`. Compared per identity: `newValue`, `newVersion`, `updateType`, `branchName`, `groupName`, `schedule`. Either input can be the raw Renovate report, the full `dry_run` summary (with the `report` key), or a `{ reportPath: "<absolute path>" }` pointer at a file on disk (the tool reads + JSON-parses it) — the inline and path forms can be mixed. The path form is the round-trip with `dry_run`'s `reportOutputPath`.
|
|
304
|
-
- `write_config` writes to a temp file, validates, then atomically renames — so a failed validation never leaves a broken config on disk. The `force: true` escape hatch is gated behind a separate `confirmForce: "YES_OVERRIDE_VALIDATION"` literal: a single boolean is too easy for a prompt-injected agent to flip, but emitting an unusual sentinel string is a deliberate step that's harder to navigate accidentally.
|
|
305
|
-
- **Round-trip writes.** When the target file already exists and parses as JSON-with-comments, `write_config` performs a surgical edit through `src/lib/configWriter.ts` (backed by `jsonc-parser`, the same library VS Code uses to edit `settings.json`): comments, key order, trailing commas, blank-line groupings, and any unrelated trivia are preserved — only the keys the caller actually changed are rewritten. Brand-new file writes (no prior file at the target path) fall back to `JSON.stringify(config, null, 2) + "\n"` — byte-identical to pre-Phase-4 behavior. `.json5` files that lean on JSON5-only syntax (unquoted keys, single-quoted strings, hex literals, etc.) are refused with `reason: "json5-not-jsonc-compatible"`, and corrupted/unparseable existing files are refused with `reason: "existing-file-unparseable"`; both refusals hint at `force: true` as the documented escape hatch. `force: true` (with `confirmForce`) is intentionally destructive — it skips both validation AND the round-trip path and rewrites the file with a clean `JSON.stringify` rendering. The `bytes` field on a successful response is the full file size after the round-trip edit, not the size of the diff. The implementation lives in `src/lib/configWriter.ts` and is the single place `jsonc-parser` is used; the existing `json5` dependency stays for parse-only consumers (`migrate_config`, `lint_config`, `configLocations.ts`) and is not migrated.
|
|
306
|
-
- `migrate_config` is the only tool that calls into Renovate as a library rather than shelling out to it — Renovate ships no `renovate-config-migrate` CLI, only a library export. To keep the main MCP server process decoupled from Renovate's internal API surface (and to avoid the ~20-30 MB resident cost of `getOptions()` eagerly loading the manager registry), the tool spawns a `node:worker_threads` worker on demand that imports `renovate/dist/config/migration.js` inside the worker. The main process never imports `renovate`. First call carries a one-time cold-load latency (a few seconds) for the worker's ESM graph; the worker is terminated after each call. See `docs/adr/0001-worker-isolated-renovate-migration.md` for the full rationale. The tool returns the migrated config plus a unified diff but never writes — chain with `write_config` to persist.
|
|
307
|
-
- **`check_setup` fast-paths the bundled binary probe.** Each Renovate CLI binary is `node node_modules/renovate/dist/<cli>.js`, and a cold `--version` spawn loads Renovate's full ESM graph (~2 s per binary). Two of those at every MCP session start used to dominate cold-start latency and could blow past clients' `initialize` timeout. When the binary resolves via the bundled path (the default install), `check_setup` now reads the version directly from `node_modules/renovate/package.json` — no spawn — bringing the probe to a few milliseconds. The `RENOVATE_BIN` / `RENOVATE_CONFIG_VALIDATOR_BIN` env overrides keep the spawn-based check (a user pointing those at a custom binary wants real proof of life). Side effect: runtime warnings derived from `--version` stderr (e.g. RE2 dlopen failure) no longer appear in the startup banner for the bundled path; they still fire on the first actual tool call through `renovateCli.ts:run`, so users see the warning on first use rather than at session start.
|
|
308
|
-
- `check_setup` and the shell-out tools (`validate_config`, `write_config`, `dry_run`) detect Renovate runtime conditions that degrade the tool silently — currently the bundled `re2` native module failing to load (typically after a Node major-version upgrade leaves a prebuilt `re2.node` compiled against the old ABI). When the WARN appears in Renovate's stderr, the tool response carries a `warnings: [{ kind: "re2-unusable", … }]` entry alongside its normal output and `check_setup` aggregates the same warning into `SetupStatus.warnings` plus the startup `instructions` banner. Renovate keeps running on JavaScript `RegExp` so calls don't fail, but regex-heavy operations (custom managers, validation, lookups) are noticeably slower; the fix hint is to reinstall renovate-mcp (which reinstalls the bundled `renovate` and rebuilds its native deps), or run `npm rebuild re2` inside the renovate-mcp install's `node_modules/renovate`. This is passive detection only — the server doesn't rebuild anything itself.
|
|
205
|
+
- **Not a Renovate replacement.** This server doesn't open PRs, run scheduled updates, or execute in CI — it's a design-time companion for a local `renovate.json`. Use the real Renovate for the actual dependency-update pipeline.
|
|
206
|
+
- **`resolve_config` is preview-quality.** Preset expansion runs against a committed snapshot, and template substitution implements only positional `{{argN}}` placeholders — non-positional tokens and Handlebars helpers are flagged in `warnings` and pass through verbatim. For authoritative output, run `dry_run`.
|
|
207
|
+
- **`preview_custom_manager` is a subset of Renovate's custom managers.** It covers `customType: "regex"` (with `matchStringsStrategy` of `any` / `combination` / `recursive`) and `customType: "jsonata"` (with `fileFormat: "json" | "yaml" | "toml"`). Template substitution is `{{groupName}}` only — full Handlebars helpers/conditionals are not implemented. Other custom types (e.g. `html`) are out of scope. Use it for fast iteration; confirm with `dry_run`. Full coverage matrix in [`docs/tools.md#preview_custom_manager`](docs/tools.md#preview_custom_manager).
|
|
208
|
+
- **`validate_config` / `dry_run` aren't exercised end-to-end in CI.** The bundled Renovate is available (it's a runtime dep), but the integration tests use fake binaries via `RENOVATE_BIN` / `RENOVATE_CONFIG_VALIDATOR_BIN` env overrides for determinism and speed. Run the tools locally against a real config to validate behaviour.
|
package/dist/index.js
CHANGED
|
@@ -41,6 +41,7 @@ const BASE_INSTRUCTIONS = [
|
|
|
41
41
|
" 9. migrate_config — apply Renovate's built-in migrations (deprecated keys → current schema) and return the migrated config",
|
|
42
42
|
" 10. write_config — save the agreed-upon config (validates first)",
|
|
43
43
|
"",
|
|
44
|
+
"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.",
|
|
44
45
|
"If any tool fails unexpectedly, call check_setup to diagnose CLI availability.",
|
|
45
46
|
"Built-in preset reference: renovate://presets (namespace index), renovate://presets/{namespace} (one namespace), renovate://preset/{name} (one preset's expanded JSON).",
|
|
46
47
|
].join("\n");
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Best-effort reachability check for an `endpoint` URL. Used by `check_setup`
|
|
3
|
+
* (when invoked with a `repoPath`) to surface unreachable hosts as a hint
|
|
4
|
+
* *before* the user runs `dry_run` and discovers the failure later.
|
|
5
|
+
*
|
|
6
|
+
* Security invariant: every URL goes through `validateEndpoint` first — same
|
|
7
|
+
* allowlist (`https://`-only, no userinfo, no RFC1918 / loopback / link-local
|
|
8
|
+
* / cloud-metadata addresses) used by `dry_run`, `resolve_config`, and
|
|
9
|
+
* `externalPresetFetcher`. The probe must not become a side channel that
|
|
10
|
+
* bypasses that policy. No credentials are sent on the probe — it's a bare
|
|
11
|
+
* HEAD / fallback GET with no Authorization header.
|
|
12
|
+
*/
|
|
13
|
+
export interface EndpointProbeResult {
|
|
14
|
+
url: string;
|
|
15
|
+
reachable: boolean;
|
|
16
|
+
status?: number;
|
|
17
|
+
error?: string;
|
|
18
|
+
skipped?: "endpoint-blocked";
|
|
19
|
+
}
|
|
20
|
+
export declare function probeEndpoint(url: string, timeoutMs?: number): Promise<EndpointProbeResult>;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { EndpointValidationError, validateEndpoint } from "./endpointValidator.js";
|
|
2
|
+
export async function probeEndpoint(url, timeoutMs = 3000) {
|
|
3
|
+
try {
|
|
4
|
+
validateEndpoint(url);
|
|
5
|
+
}
|
|
6
|
+
catch (err) {
|
|
7
|
+
const reason = err instanceof EndpointValidationError ? err.message : String(err);
|
|
8
|
+
return { url, reachable: false, skipped: "endpoint-blocked", error: reason };
|
|
9
|
+
}
|
|
10
|
+
const ac = new AbortController();
|
|
11
|
+
const timer = setTimeout(() => ac.abort(), timeoutMs);
|
|
12
|
+
try {
|
|
13
|
+
let response = await fetch(url, { method: "HEAD", signal: ac.signal });
|
|
14
|
+
if (response.status === 405) {
|
|
15
|
+
response = await fetch(url, { method: "GET", signal: ac.signal });
|
|
16
|
+
}
|
|
17
|
+
return { url, reachable: response.ok || response.status < 500, status: response.status };
|
|
18
|
+
}
|
|
19
|
+
catch (err) {
|
|
20
|
+
const message = err instanceof Error && err.name === "AbortError"
|
|
21
|
+
? `timed out after ${timeoutMs}ms`
|
|
22
|
+
: err instanceof Error
|
|
23
|
+
? err.message
|
|
24
|
+
: String(err);
|
|
25
|
+
return { url, reachable: false, error: message };
|
|
26
|
+
}
|
|
27
|
+
finally {
|
|
28
|
+
clearTimeout(timer);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parsed origin remote URL. Both SSH (`git@host:owner/repo.git`) and HTTPS
|
|
3
|
+
* (`https://host/owner/repo.git`) forms are supported; nested GitLab subgroup
|
|
4
|
+
* paths are preserved verbatim in `owner` (the leading path segments) so
|
|
5
|
+
* `parseRemoteUrl("git@gitlab.example.com:group/sub/repo.git")` yields
|
|
6
|
+
* `{ host: "gitlab.example.com", owner: "group/sub", repo: "repo" }`.
|
|
7
|
+
*/
|
|
8
|
+
export interface ParsedRemote {
|
|
9
|
+
host: string;
|
|
10
|
+
scheme: "ssh" | "https" | "git";
|
|
11
|
+
owner?: string;
|
|
12
|
+
repo?: string;
|
|
13
|
+
}
|
|
14
|
+
export declare function parseRemoteUrl(url: string): ParsedRemote | null;
|
|
15
|
+
/**
|
|
16
|
+
* Read the `[remote "origin"] url = …` field from a repo's `.git/config`
|
|
17
|
+
* without spawning `git`. Returns `null` for non-git directories, missing
|
|
18
|
+
* config, missing origin remote, or unreadable files (no exceptions thrown).
|
|
19
|
+
*/
|
|
20
|
+
export declare function readOriginRemote(repoPath: string): Promise<string | null>;
|
|
21
|
+
export declare function parseGitConfigOriginUrl(content: string): string | null;
|
|
22
|
+
export type RemoteClassification = "github" | "gitlab" | "self-hosted" | "unknown";
|
|
23
|
+
export type SelfHostedFlavor = "github" | "gitlab" | "unknown";
|
|
24
|
+
export interface ClassifiedRemote {
|
|
25
|
+
classified: RemoteClassification;
|
|
26
|
+
flavor?: SelfHostedFlavor;
|
|
27
|
+
}
|
|
28
|
+
export declare function classifyRemoteHost(host: string): ClassifiedRemote;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
const SSH_RE = /^(?:ssh:\/\/)?(?:[^@]+@)?([^:/]+)[:/]+(.+?)(?:\.git)?\/?$/;
|
|
4
|
+
export function parseRemoteUrl(url) {
|
|
5
|
+
if (!url || typeof url !== "string")
|
|
6
|
+
return null;
|
|
7
|
+
const trimmed = url.trim();
|
|
8
|
+
if (!trimmed)
|
|
9
|
+
return null;
|
|
10
|
+
if (/^(https?|git|ssh):\/\//i.test(trimmed)) {
|
|
11
|
+
let u;
|
|
12
|
+
try {
|
|
13
|
+
u = new URL(trimmed);
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
const scheme = u.protocol === "git:" ? "git" : u.protocol === "ssh:" ? "ssh" : "https";
|
|
19
|
+
const pathname = u.pathname.replace(/^\/+/, "").replace(/\/+$/, "").replace(/\.git$/, "");
|
|
20
|
+
const segments = pathname.split("/").filter(Boolean);
|
|
21
|
+
const repo = segments.length ? segments[segments.length - 1] : undefined;
|
|
22
|
+
const owner = segments.length > 1 ? segments.slice(0, -1).join("/") : undefined;
|
|
23
|
+
return {
|
|
24
|
+
host: u.hostname.toLowerCase(),
|
|
25
|
+
scheme,
|
|
26
|
+
owner,
|
|
27
|
+
repo,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
const m = SSH_RE.exec(trimmed);
|
|
31
|
+
if (!m)
|
|
32
|
+
return null;
|
|
33
|
+
const host = m[1].toLowerCase();
|
|
34
|
+
const rest = m[2].replace(/\.git$/, "").replace(/\/+$/, "");
|
|
35
|
+
const segments = rest.split("/").filter(Boolean);
|
|
36
|
+
if (segments.length === 0)
|
|
37
|
+
return null;
|
|
38
|
+
const repo = segments[segments.length - 1];
|
|
39
|
+
const owner = segments.length > 1 ? segments.slice(0, -1).join("/") : undefined;
|
|
40
|
+
return { host, scheme: "ssh", owner, repo };
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Read the `[remote "origin"] url = …` field from a repo's `.git/config`
|
|
44
|
+
* without spawning `git`. Returns `null` for non-git directories, missing
|
|
45
|
+
* config, missing origin remote, or unreadable files (no exceptions thrown).
|
|
46
|
+
*/
|
|
47
|
+
export async function readOriginRemote(repoPath) {
|
|
48
|
+
const configPath = path.join(repoPath, ".git", "config");
|
|
49
|
+
let raw;
|
|
50
|
+
try {
|
|
51
|
+
raw = await fs.readFile(configPath, "utf8");
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
return parseGitConfigOriginUrl(raw);
|
|
57
|
+
}
|
|
58
|
+
export function parseGitConfigOriginUrl(content) {
|
|
59
|
+
const lines = content.split(/\r?\n/);
|
|
60
|
+
let inOrigin = false;
|
|
61
|
+
for (const rawLine of lines) {
|
|
62
|
+
const line = rawLine.trim();
|
|
63
|
+
if (!line || line.startsWith("#") || line.startsWith(";"))
|
|
64
|
+
continue;
|
|
65
|
+
if (line.startsWith("[")) {
|
|
66
|
+
inOrigin = /^\[remote\s+"origin"\]\s*$/.test(line);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (!inOrigin)
|
|
70
|
+
continue;
|
|
71
|
+
const eq = line.indexOf("=");
|
|
72
|
+
if (eq < 0)
|
|
73
|
+
continue;
|
|
74
|
+
const key = line.slice(0, eq).trim().toLowerCase();
|
|
75
|
+
if (key !== "url")
|
|
76
|
+
continue;
|
|
77
|
+
const value = line.slice(eq + 1).trim();
|
|
78
|
+
return value || null;
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
export function classifyRemoteHost(host) {
|
|
83
|
+
const h = host.toLowerCase();
|
|
84
|
+
if (h === "github.com" || h === "www.github.com")
|
|
85
|
+
return { classified: "github" };
|
|
86
|
+
if (h === "gitlab.com" || h === "www.gitlab.com")
|
|
87
|
+
return { classified: "gitlab" };
|
|
88
|
+
// Heuristics for self-hosted: hostname prefix/suffix conventions.
|
|
89
|
+
let flavor = "unknown";
|
|
90
|
+
if (/(^|\.)gitlab\./.test(h) || h.startsWith("gitlab-") || h.startsWith("gitlab.")) {
|
|
91
|
+
flavor = "gitlab";
|
|
92
|
+
}
|
|
93
|
+
else if (/(^|\.)github\./.test(h) || h.startsWith("github-") || h.startsWith("github.")) {
|
|
94
|
+
flavor = "github";
|
|
95
|
+
}
|
|
96
|
+
return { classified: "self-hosted", flavor };
|
|
97
|
+
}
|
package/dist/lib/setupCheck.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { type EndpointProbeResult } from "./endpointProbe.js";
|
|
2
|
+
import { type ClassifiedRemote, type ParsedRemote } from "./gitRemote.js";
|
|
1
3
|
import { type RuntimeWarning } from "./runtimeWarnings.js";
|
|
2
4
|
export type RenovateBinary = "renovate" | "renovate-config-validator";
|
|
3
5
|
export interface BinaryStatus {
|
|
@@ -41,6 +43,35 @@ export interface PlatformContext {
|
|
|
41
43
|
/** Cross-checks that are mechanical to compute but easy for callers to miss. */
|
|
42
44
|
notes: string[];
|
|
43
45
|
}
|
|
46
|
+
export type EffectivePlatform = "github" | "gitlab" | "local" | "unknown";
|
|
47
|
+
export type EffectivePlatformSource = "remote" | "config" | "env" | "default";
|
|
48
|
+
export interface RepoContext {
|
|
49
|
+
repoPath: string;
|
|
50
|
+
remote: (ParsedRemote & ClassifiedRemote & {
|
|
51
|
+
url: string;
|
|
52
|
+
}) | null;
|
|
53
|
+
configFile: {
|
|
54
|
+
path: string;
|
|
55
|
+
format: string;
|
|
56
|
+
} | null;
|
|
57
|
+
configEndpoint?: string;
|
|
58
|
+
configPlatform?: "github" | "gitlab" | "local";
|
|
59
|
+
effectivePlatform: EffectivePlatform;
|
|
60
|
+
effectivePlatformSource: EffectivePlatformSource;
|
|
61
|
+
endpointProbe?: EndpointProbeResult & {
|
|
62
|
+
derivedFrom: "config" | "env" | "default";
|
|
63
|
+
};
|
|
64
|
+
inconsistencies: string[];
|
|
65
|
+
}
|
|
66
|
+
export interface CheckSetupOptions {
|
|
67
|
+
repoPath?: string;
|
|
68
|
+
/**
|
|
69
|
+
* Override the network probe (for tests). When set, the helper is called
|
|
70
|
+
* instead of `probeEndpoint` for any reachability check. Production callers
|
|
71
|
+
* should leave this unset.
|
|
72
|
+
*/
|
|
73
|
+
probe?: (url: string) => Promise<EndpointProbeResult>;
|
|
74
|
+
}
|
|
44
75
|
export interface SetupStatus {
|
|
45
76
|
node: string;
|
|
46
77
|
renovate: BinaryStatus;
|
|
@@ -56,6 +87,12 @@ export interface SetupStatus {
|
|
|
56
87
|
* when nothing was detected.
|
|
57
88
|
*/
|
|
58
89
|
warnings: RuntimeWarning[];
|
|
90
|
+
/**
|
|
91
|
+
* Repo-aware diagnosis. Populated only when `checkSetup` is called with a
|
|
92
|
+
* `repoPath`. Omitted entirely otherwise so the startup-time invocation in
|
|
93
|
+
* `src/index.ts` (no repoPath) keeps its existing output shape.
|
|
94
|
+
*/
|
|
95
|
+
repoContext?: RepoContext;
|
|
59
96
|
}
|
|
60
97
|
/**
|
|
61
98
|
* Tiny semver-range matcher tuned for the shapes Renovate's `engines.node`
|
|
@@ -73,7 +110,7 @@ export declare function versionSatisfiesRange(version: string, range: string): b
|
|
|
73
110
|
* LLM sees the mismatch before any `dry_run` is attempted.
|
|
74
111
|
*/
|
|
75
112
|
export declare function checkRenovateEnginesMatch(nodeVersion: string): string | null;
|
|
76
|
-
export declare function checkSetup(): Promise<SetupStatus>;
|
|
113
|
+
export declare function checkSetup(options?: CheckSetupOptions): Promise<SetupStatus>;
|
|
77
114
|
/**
|
|
78
115
|
* Reads platform-related env (RENOVATE_PLATFORM/_ENDPOINT/_TOKEN, plus
|
|
79
116
|
* GITHUB_TOKEN / GITLAB_TOKEN) and reports what the `dry_run` tool would
|
package/dist/lib/setupCheck.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
2
|
import fs from "node:fs";
|
|
3
|
+
import { locateConfig } from "./configLocations.js";
|
|
4
|
+
import { resolveCredential } from "./credentialResolver.js";
|
|
5
|
+
import { probeEndpoint } from "./endpointProbe.js";
|
|
6
|
+
import { classifyRemoteHost, parseRemoteUrl, readOriginRemote, } from "./gitRemote.js";
|
|
3
7
|
import { run, resolveRenovateTool } from "./renovateCli.js";
|
|
4
8
|
import { dedupeRuntimeWarnings } from "./runtimeWarnings.js";
|
|
5
9
|
const requireFromHere = createRequire(import.meta.url);
|
|
@@ -164,7 +168,7 @@ async function checkBinary(tool) {
|
|
|
164
168
|
};
|
|
165
169
|
}
|
|
166
170
|
}
|
|
167
|
-
export async function checkSetup() {
|
|
171
|
+
export async function checkSetup(options = {}) {
|
|
168
172
|
const [renovate, renovateConfigValidator] = await Promise.all([
|
|
169
173
|
checkBinary("renovate"),
|
|
170
174
|
checkBinary("renovate-config-validator"),
|
|
@@ -190,17 +194,171 @@ export async function checkSetup() {
|
|
|
190
194
|
...(renovate.runtimeWarnings ?? []),
|
|
191
195
|
...(renovateConfigValidator.runtimeWarnings ?? []),
|
|
192
196
|
]);
|
|
197
|
+
const platformContext = inspectPlatformContext(process.env);
|
|
198
|
+
let repoContext;
|
|
199
|
+
if (options.repoPath) {
|
|
200
|
+
repoContext = await gatherRepoContext(options.repoPath, platformContext, options.probe);
|
|
201
|
+
appendRepoHints(repoContext, hints, platformContext);
|
|
202
|
+
}
|
|
193
203
|
return {
|
|
194
204
|
node: process.version,
|
|
195
205
|
renovate,
|
|
196
206
|
renovateConfigValidator,
|
|
197
207
|
envOverrides,
|
|
198
|
-
platformContext
|
|
208
|
+
platformContext,
|
|
199
209
|
ok: renovate.found && renovateConfigValidator.found,
|
|
200
210
|
hints,
|
|
201
211
|
warnings,
|
|
212
|
+
...(repoContext ? { repoContext } : {}),
|
|
202
213
|
};
|
|
203
214
|
}
|
|
215
|
+
async function gatherRepoContext(repoPath, platformContext, probe) {
|
|
216
|
+
const inconsistencies = [];
|
|
217
|
+
const remoteUrl = await readOriginRemote(repoPath);
|
|
218
|
+
let remote = null;
|
|
219
|
+
if (remoteUrl) {
|
|
220
|
+
const parsed = parseRemoteUrl(remoteUrl);
|
|
221
|
+
if (parsed) {
|
|
222
|
+
const classification = classifyRemoteHost(parsed.host);
|
|
223
|
+
remote = { url: remoteUrl, ...parsed, ...classification };
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
let configFile = null;
|
|
227
|
+
let configEndpoint;
|
|
228
|
+
let configPlatform;
|
|
229
|
+
try {
|
|
230
|
+
const located = await locateConfig(repoPath);
|
|
231
|
+
if (located) {
|
|
232
|
+
configFile = { path: located.relPath, format: located.format };
|
|
233
|
+
const cfgEndpoint = located.config.endpoint;
|
|
234
|
+
if (typeof cfgEndpoint === "string" && cfgEndpoint.length > 0) {
|
|
235
|
+
configEndpoint = cfgEndpoint;
|
|
236
|
+
}
|
|
237
|
+
const cfgPlatform = located.config.platform;
|
|
238
|
+
if (cfgPlatform === "github" || cfgPlatform === "gitlab" || cfgPlatform === "local") {
|
|
239
|
+
configPlatform = cfgPlatform;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
// Parsing errors are surfaced by read_config; for setup diagnosis we
|
|
245
|
+
// intentionally swallow them rather than failing the whole tool.
|
|
246
|
+
}
|
|
247
|
+
const { platform: effectivePlatform, source: effectivePlatformSource } = derivePlatform(remote, configPlatform, platformContext.renovatePlatform);
|
|
248
|
+
if (remote && configEndpoint) {
|
|
249
|
+
const cfgHost = safeHostFromUrl(configEndpoint);
|
|
250
|
+
if (cfgHost && cfgHost !== remote.host) {
|
|
251
|
+
inconsistencies.push(`Remote origin (\`${remote.host}\`) and config \`endpoint\` (\`${cfgHost}\`) point at different hosts. Renovate will use the config endpoint.`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
if (remote && platformContext.renovatePlatform) {
|
|
255
|
+
const remoteSays = remote.classified === "github" || remote.classified === "gitlab"
|
|
256
|
+
? remote.classified
|
|
257
|
+
: remote.flavor;
|
|
258
|
+
if (remoteSays
|
|
259
|
+
&& remoteSays !== platformContext.renovatePlatform
|
|
260
|
+
&& platformContext.renovatePlatform !== "local") {
|
|
261
|
+
inconsistencies.push(`Remote origin classifies as \`${remoteSays}\` but \`RENOVATE_PLATFORM=${platformContext.renovatePlatform}\`. \`dry_run\` will use the env value when its \`platform\` input is unset.`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
const probeUrlInfo = resolveProbeUrl(remote, configEndpoint, platformContext.renovateEndpoint);
|
|
265
|
+
let endpointProbe;
|
|
266
|
+
if (probeUrlInfo) {
|
|
267
|
+
const fn = probe ?? probeEndpoint;
|
|
268
|
+
const result = await fn(probeUrlInfo.url);
|
|
269
|
+
endpointProbe = { ...result, derivedFrom: probeUrlInfo.derivedFrom };
|
|
270
|
+
}
|
|
271
|
+
return {
|
|
272
|
+
repoPath,
|
|
273
|
+
remote,
|
|
274
|
+
configFile,
|
|
275
|
+
configEndpoint,
|
|
276
|
+
configPlatform,
|
|
277
|
+
effectivePlatform,
|
|
278
|
+
effectivePlatformSource,
|
|
279
|
+
endpointProbe,
|
|
280
|
+
inconsistencies,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
function derivePlatform(remote, configPlatform, envPlatform) {
|
|
284
|
+
if (remote) {
|
|
285
|
+
if (remote.classified === "github")
|
|
286
|
+
return { platform: "github", source: "remote" };
|
|
287
|
+
if (remote.classified === "gitlab")
|
|
288
|
+
return { platform: "gitlab", source: "remote" };
|
|
289
|
+
if (remote.classified === "self-hosted" && remote.flavor && remote.flavor !== "unknown") {
|
|
290
|
+
return { platform: remote.flavor, source: "remote" };
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
if (configPlatform)
|
|
294
|
+
return { platform: configPlatform, source: "config" };
|
|
295
|
+
if (envPlatform === "github" || envPlatform === "gitlab" || envPlatform === "local") {
|
|
296
|
+
return { platform: envPlatform, source: "env" };
|
|
297
|
+
}
|
|
298
|
+
if (remote?.classified === "self-hosted") {
|
|
299
|
+
return { platform: "unknown", source: "remote" };
|
|
300
|
+
}
|
|
301
|
+
return { platform: "local", source: "default" };
|
|
302
|
+
}
|
|
303
|
+
function resolveProbeUrl(remote, configEndpoint, envEndpoint) {
|
|
304
|
+
if (configEndpoint)
|
|
305
|
+
return { url: configEndpoint, derivedFrom: "config" };
|
|
306
|
+
if (envEndpoint)
|
|
307
|
+
return { url: envEndpoint, derivedFrom: "env" };
|
|
308
|
+
if (!remote)
|
|
309
|
+
return null;
|
|
310
|
+
if (remote.classified === "github") {
|
|
311
|
+
return { url: "https://api.github.com", derivedFrom: "default" };
|
|
312
|
+
}
|
|
313
|
+
if (remote.classified === "gitlab") {
|
|
314
|
+
return { url: "https://gitlab.com/api/v4/version", derivedFrom: "default" };
|
|
315
|
+
}
|
|
316
|
+
// self-hosted with no configured endpoint: don't guess a path on someone's
|
|
317
|
+
// private host.
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
function safeHostFromUrl(url) {
|
|
321
|
+
try {
|
|
322
|
+
return new URL(url).hostname.toLowerCase();
|
|
323
|
+
}
|
|
324
|
+
catch {
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
function appendRepoHints(ctx, hints, platformContext) {
|
|
329
|
+
const platform = ctx.effectivePlatform;
|
|
330
|
+
if (platform === "github") {
|
|
331
|
+
const cred = resolveCredential(["RENOVATE_TOKEN", "GITHUB_TOKEN"]);
|
|
332
|
+
if (!cred.envVar) {
|
|
333
|
+
hints.push("This repo's origin is on GitHub. Set `GITHUB_TOKEN` (a read-only PAT is enough — used for rate-limit headroom on public deps) in the MCP server's env, or `dry_run` will skip your github-actions dependencies with `skipReason: github-token-required`.");
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
else if (platform === "gitlab") {
|
|
337
|
+
const cred = resolveCredential(["RENOVATE_TOKEN", "GITLAB_TOKEN"]);
|
|
338
|
+
if (!cred.envVar) {
|
|
339
|
+
hints.push("This repo's origin is on GitLab. Set `GITLAB_TOKEN` (or `RENOVATE_TOKEN`) in the MCP server's env, or private-preset / private-registry lookups will fail.");
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
if (ctx.remote?.classified === "self-hosted"
|
|
343
|
+
&& !ctx.configEndpoint
|
|
344
|
+
&& !platformContext.renovateEndpoint) {
|
|
345
|
+
hints.push(`Self-hosted host \`${ctx.remote.host}\` detected from origin, but no \`endpoint\` is configured. Set \`endpoint\` in renovate.json or \`RENOVATE_ENDPOINT\` in the MCP server's env — see docs/platform-setup.md.`);
|
|
346
|
+
if (ctx.remote.flavor === "unknown") {
|
|
347
|
+
hints.push(`Could not infer GitHub vs GitLab from hostname \`${ctx.remote.host}\`. Set \`RENOVATE_PLATFORM=github\` or \`RENOVATE_PLATFORM=gitlab\` explicitly.`);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
if (ctx.endpointProbe && !ctx.endpointProbe.reachable) {
|
|
351
|
+
if (ctx.endpointProbe.skipped === "endpoint-blocked") {
|
|
352
|
+
hints.push(`Skipped reachability probe of \`${ctx.endpointProbe.url}\`: ${ctx.endpointProbe.error ?? "blocked by endpoint allowlist"}. Use a public-DNS https URL for \`endpoint\` / \`RENOVATE_ENDPOINT\` — see docs/security.md#endpoint-validation.`);
|
|
353
|
+
}
|
|
354
|
+
else {
|
|
355
|
+
hints.push(`Could not reach \`${ctx.endpointProbe.url}\`: ${ctx.endpointProbe.error ?? "no response"}. If you're behind a VPN/proxy, \`dry_run\` will fail with the same network error.`);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
for (const item of ctx.inconsistencies) {
|
|
359
|
+
hints.push(item);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
204
362
|
/**
|
|
205
363
|
* Reads platform-related env (RENOVATE_PLATFORM/_ENDPOINT/_TOKEN, plus
|
|
206
364
|
* GITHUB_TOKEN / GITLAB_TOKEN) and reports what the `dry_run` tool would
|
|
@@ -337,6 +495,47 @@ export function describeSetup(status) {
|
|
|
337
495
|
for (const n of ctx.notes)
|
|
338
496
|
lines.push(` - ${n}`);
|
|
339
497
|
}
|
|
498
|
+
if (status.repoContext) {
|
|
499
|
+
const rc = status.repoContext;
|
|
500
|
+
lines.push("");
|
|
501
|
+
lines.push(`Repo context (${rc.repoPath}):`);
|
|
502
|
+
if (rc.remote) {
|
|
503
|
+
const flavor = rc.remote.flavor ? ` (${rc.remote.flavor})` : "";
|
|
504
|
+
lines.push(` origin: ${rc.remote.url}`);
|
|
505
|
+
lines.push(` host: ${rc.remote.host} → ${rc.remote.classified}${flavor}`);
|
|
506
|
+
}
|
|
507
|
+
else {
|
|
508
|
+
lines.push(" origin: (no git remote found)");
|
|
509
|
+
}
|
|
510
|
+
if (rc.configFile) {
|
|
511
|
+
lines.push(` config: ${rc.configFile.path} (${rc.configFile.format})`);
|
|
512
|
+
}
|
|
513
|
+
else {
|
|
514
|
+
lines.push(" config: (no renovate config found)");
|
|
515
|
+
}
|
|
516
|
+
if (rc.configEndpoint)
|
|
517
|
+
lines.push(` config.endpoint: ${rc.configEndpoint}`);
|
|
518
|
+
if (rc.configPlatform)
|
|
519
|
+
lines.push(` config.platform: ${rc.configPlatform}`);
|
|
520
|
+
lines.push(` effective platform: ${rc.effectivePlatform} (from ${rc.effectivePlatformSource})`);
|
|
521
|
+
if (rc.endpointProbe) {
|
|
522
|
+
if (rc.endpointProbe.skipped) {
|
|
523
|
+
lines.push(` endpoint probe (${rc.endpointProbe.derivedFrom}): SKIPPED — ${rc.endpointProbe.skipped}`);
|
|
524
|
+
}
|
|
525
|
+
else if (rc.endpointProbe.reachable) {
|
|
526
|
+
const status = rc.endpointProbe.status ?? "?";
|
|
527
|
+
lines.push(` endpoint probe (${rc.endpointProbe.derivedFrom}): ${rc.endpointProbe.url} → ${status}`);
|
|
528
|
+
}
|
|
529
|
+
else {
|
|
530
|
+
lines.push(` endpoint probe (${rc.endpointProbe.derivedFrom}): ${rc.endpointProbe.url} → UNREACHABLE (${rc.endpointProbe.error ?? "no response"})`);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
if (rc.inconsistencies.length > 0) {
|
|
534
|
+
lines.push(" Inconsistencies:");
|
|
535
|
+
for (const i of rc.inconsistencies)
|
|
536
|
+
lines.push(` - ${i}`);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
340
539
|
if (status.hints.length > 0) {
|
|
341
540
|
lines.push("");
|
|
342
541
|
lines.push("Hints:");
|
package/dist/lib/version.d.ts
CHANGED
package/dist/lib/version.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { fileURLToPath } from "node:url";
|
|
2
|
-
export const SERVER_VERSION = "0.
|
|
2
|
+
export const SERVER_VERSION = "0.13.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"]}`
|
package/dist/tools/checkSetup.js
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
1
2
|
import { checkSetup, describeSetup } from "../lib/setupCheck.js";
|
|
2
3
|
export function registerCheckSetup(server) {
|
|
3
4
|
server.registerTool("check_setup", {
|
|
4
5
|
title: "Check Renovate MCP setup",
|
|
5
|
-
description: "Report whether the Renovate CLI and config validator are reachable, their versions, and any env overrides in effect. Call this first when other tools fail with a spawn error or unexpected validation output.",
|
|
6
|
-
inputSchema: {
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
description: "Report whether the Renovate CLI and config validator are reachable, their versions, and any env overrides in effect. Pass an optional `repoPath` to also diagnose the target repo's environment: parses the `.git/config` origin remote, reads any `endpoint`/`platform` in the repo's renovate config, probes endpoint reachability (using the same `https://`-only public-host allowlist as `dry_run`), and cross-references all three with token presence — surfaces actionable hints like 'set GITHUB_TOKEN before running dry_run'. The probe sends no credentials. Call this first when other tools fail with a spawn error or unexpected validation output.",
|
|
7
|
+
inputSchema: {
|
|
8
|
+
repoPath: z
|
|
9
|
+
.string()
|
|
10
|
+
.max(4096)
|
|
11
|
+
.optional()
|
|
12
|
+
.describe("Absolute path to a repository root. When set, the tool also returns a `repoContext` block diagnosing the repo's git origin, config endpoint/platform, endpoint reachability, and token coverage."),
|
|
13
|
+
},
|
|
14
|
+
}, async ({ repoPath }) => {
|
|
15
|
+
const status = await checkSetup(repoPath ? { repoPath } : {});
|
|
9
16
|
const summary = describeSetup(status);
|
|
10
17
|
const payload = JSON.stringify(status, null, 2);
|
|
11
18
|
return {
|