renovate-mcp 1.8.1 → 2.0.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 +11 -9
- package/dist/data/presets.generated.js +116 -0
- package/dist/lib/credentialResolver.js +1 -15
- package/dist/lib/externalPresetFetcher.js +55 -27
- package/dist/lib/gitRemote.js +28 -5
- package/dist/lib/hostRulesConfig.js +18 -13
- package/dist/lib/hostTokens.js +229 -0
- package/dist/lib/inputLimits.js +4 -0
- package/dist/lib/presetFlattenWorker.js +174 -0
- package/dist/lib/presetFlattenWorkerImpl.js +79 -0
- package/dist/lib/presetResolver.js +180 -12
- package/dist/lib/renovateWorker.js +3 -4
- package/dist/lib/repoCopy.js +143 -0
- package/dist/lib/setupCheck.js +83 -60
- package/dist/lib/toolInputs.js +11 -0
- package/dist/lib/version.js +1 -1
- package/dist/tools/annotateDryRun.js +6 -3
- package/dist/tools/dryRun.js +243 -116
- package/dist/tools/explainConfig.js +6 -3
- package/dist/tools/explainPaths.js +6 -3
- package/dist/tools/resolveConfig.js +8 -5
- package/dist/tools/resolveConfigDiff.js +7 -4
- package/dist/tools/testPackageRules.js +6 -3
- package/dist/tools/writeConfig.js +34 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -45,7 +45,7 @@ Nineteen tools, seven resources, and four workflow prompts. Each tool name below
|
|
|
45
45
|
| [`preview_custom_manager`](docs/tools.md#preview_custom_manager) | Preview a `customManagers` entry (regex or JSONata) against a local repo. Offline. |
|
|
46
46
|
| [`validate_config`](docs/tools.md#validate_config) | Run `renovate-config-validator` against a file or inline object. |
|
|
47
47
|
| [`lint_config`](docs/tools.md#lint_config) | Semantic lint pass for Renovate-specific footguns the schema validator declares valid. Offline. |
|
|
48
|
-
| [`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. |
|
|
48
|
+
| [`dry_run`](docs/tools.md#dry_run) | Run Renovate with `--dry-run` and return the structured JSON report. Local-by-default, flattening `local>` presets with Renovate's own resolver into a temporary copy first; remote with `platform` + `endpoint` + `token` + `repository`. No PRs, no pushes. |
|
|
49
49
|
| [`dry_run_diff`](docs/tools.md#dry_run_diff) | Stateless semantic diff between two `dry_run` reports — added / removed / changed updates. |
|
|
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. |
|
|
@@ -81,7 +81,8 @@ Renovate ships bundled — the `renovate` package is a runtime dependency, so `v
|
|
|
81
81
|
|
|
82
82
|
- `RENOVATE_BIN` / `RENOVATE_CONFIG_VALIDATOR_BIN` — override the bundled binaries. When set, the override always wins.
|
|
83
83
|
- `RENOVATE_MCP_REQUIRE_CLI=false` — suppress the startup "partial availability" notice when you only intend to use the offline tools.
|
|
84
|
-
- Platform + token env vars — see [`docs/platform-setup.md`](docs/platform-setup.md) for the per-platform matrix.
|
|
84
|
+
- Platform + token env vars — see [`docs/platform-setup.md`](docs/platform-setup.md) for the per-platform matrix. Preset fetching and `dry_run` bind every token to its own host; `RENOVATE_TOKEN` only covers the home host named by `RENOVATE_PLATFORM` / `RENOVATE_ENDPOINT`. A `platform` / `endpoint` passed on a call routes requests but never moves a token.
|
|
85
|
+
- `RENOVATE_MCP_HOST_TOKENS` — JSON array of `{matchHost, hostType, tokenEnv}` naming the env var that holds the token for an extra host (never the value itself). See [`docs/security.md`](docs/security.md#token-resolution).
|
|
85
86
|
- `GITHUB_COM_TOKEN` — authenticates github.com *datasource* lookups (release notes, `github-tags` / `github-releases` / `github-actions`). Distinct from the platform token and never auto-derived from it; set it when github.com-hosted deps are resolved under the default `dry_run` `platform: local`. See [`docs/security.md`](docs/security.md#github_com_token--a-separate-role).
|
|
86
87
|
|
|
87
88
|
> 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).
|
|
@@ -139,8 +140,9 @@ The offline tools need nothing. Add env vars only for what you use:
|
|
|
139
140
|
| Local `dry_run` on a repo with github.com dependencies | `GITHUB_COM_TOKEN` (read-only PAT, avoids anonymous rate limits) |
|
|
140
141
|
| `dry_run` against GitHub, or private `github>` presets | `RENOVATE_PLATFORM=github` + `GITHUB_TOKEN` |
|
|
141
142
|
| `dry_run` against GitLab, or private `gitlab>` presets | `RENOVATE_PLATFORM=gitlab` + `GITLAB_TOKEN` |
|
|
143
|
+
| Private presets on another host | `RENOVATE_MCP_HOST_TOKENS` + the variable it names |
|
|
142
144
|
|
|
143
|
-
Self-hosted GitLab and GitHub Enterprise also need `RENOVATE_ENDPOINT` — see [Platform setup](#platform-setup).
|
|
145
|
+
Self-hosted GitLab and GitHub Enterprise also need `RENOVATE_ENDPOINT` — see [Platform setup](#platform-setup). Each token is only sent to its own platform's host.
|
|
144
146
|
|
|
145
147
|
In Claude Code, re-register with `-e`:
|
|
146
148
|
|
|
@@ -169,11 +171,11 @@ For `dry_run` against a remote platform or `resolve_config` with `externalPreset
|
|
|
169
171
|
| GitHub Enterprise | `github` | `https://github.example.com/api/v3/` | `RENOVATE_TOKEN` *or* `GITHUB_TOKEN` |
|
|
170
172
|
| gitlab.com | `gitlab` | (omit) | `RENOVATE_TOKEN` *or* `GITLAB_TOKEN` |
|
|
171
173
|
| Self-hosted GitLab | `gitlab` | `https://gitlab.example.com/api/v4/` | `RENOVATE_TOKEN` *or* `GITLAB_TOKEN` |
|
|
172
|
-
| gitea.com / Forgejo (code.forgejo.org) | — | (omit) | `
|
|
173
|
-
| Self-hosted Gitea / Forgejo |
|
|
174
|
-
| Bitbucket Cloud | — | (omit) | `
|
|
174
|
+
| gitea.com / Forgejo (code.forgejo.org) | — | (omit) | `GITEA_TOKEN` / `FORGEJO_TOKEN` |
|
|
175
|
+
| Self-hosted Gitea / Forgejo | `gitea` / `forgejo` | `https://forgejo.example.com/api/v1` | `RENOVATE_TOKEN` *or* `GITEA_TOKEN` / `FORGEJO_TOKEN` |
|
|
176
|
+
| Bitbucket Cloud | — | (omit) | `BITBUCKET_TOKEN` |
|
|
175
177
|
|
|
176
|
-
The Gitea / Forgejo / Bitbucket Cloud rows apply to the `resolve_config` family only: `dry_run` runs Renovate as `local` / `github` / `gitlab` and
|
|
178
|
+
The Gitea / Forgejo / Bitbucket Cloud rows apply to the `resolve_config` family only: `dry_run` runs Renovate as `local` / `github` / `gitlab` and only passes those tokens on as host rules for their hosts. `platform` (`gitea` / `forgejo` / `bitbucket`) and `endpoint` can also be passed as tool inputs, but they only route `local>` presets: a token goes only to its platform's public host or to the env home (`RENOVATE_PLATFORM` at `RENOVATE_ENDPOINT`, where `RENOVATE_TOKEN` comes first), so an endpoint passed only per call needs a `RENOVATE_MCP_HOST_TOKENS` entry. `endpoint` routes `local>` presets only, as in Renovate.
|
|
177
179
|
|
|
178
180
|
See [`docs/platform-setup.md`](docs/platform-setup.md) for a worked self-hosted GitLab example, `local>` preset handling, and private-registry credentials.
|
|
179
181
|
|
|
@@ -196,8 +198,8 @@ Once the server is wired up, try prompts like these. Written for Claude but work
|
|
|
196
198
|
|
|
197
199
|
**Self-hosted GitLab / GitHub Enterprise** (env set per [Platform setup](#platform-setup))
|
|
198
200
|
|
|
199
|
-
- "Resolve my config with external presets enabled
|
|
200
|
-
- "
|
|
201
|
+
- "Resolve my config with external presets enabled. Its `local>platform/renovate-presets` presets live on our self-hosted GitLab at `https://gitlab.example.com/api/v4`."
|
|
202
|
+
- "Resolve my config with external presets enabled. Its `local>acme/renovate-config//base` preset lives on our GitHub Enterprise at `https://github.acme.corp/api/v3`."
|
|
201
203
|
- "Dry-run `infrastructure/kubernetes/our-platform` (a nested-group GitLab project) so Renovate can fetch the `local>` presets the config extends."
|
|
202
204
|
|
|
203
205
|
**Authoring a custom manager (regex or JSONata)**
|
|
@@ -22049,4 +22049,120 @@ export const PRESETS = {
|
|
|
22049
22049
|
}
|
|
22050
22050
|
}
|
|
22051
22051
|
};
|
|
22052
|
+
/**
|
|
22053
|
+
* Renovate's `removedPresets` map: renamed built-in presets map to their new
|
|
22054
|
+
* name, removed ones map to null (Renovate resolves them to an empty config).
|
|
22055
|
+
*/
|
|
22056
|
+
export const REMOVED_PRESETS = {
|
|
22057
|
+
":app": "config:js-app",
|
|
22058
|
+
":autodetectPinVersions": null,
|
|
22059
|
+
":autodetectRangeStrategy": null,
|
|
22060
|
+
":automergeBranchMergeCommit": ":automergeBranch",
|
|
22061
|
+
":automergeBranchPush": ":automergeBranch",
|
|
22062
|
+
":base": "config:recommended",
|
|
22063
|
+
":disableLockFiles": ":skipArtifactsUpdate",
|
|
22064
|
+
":enableGradleLite": null,
|
|
22065
|
+
":js-app": "config:js-app",
|
|
22066
|
+
":library": "config:js-lib",
|
|
22067
|
+
":masterIssue": ":dependencyDashboard",
|
|
22068
|
+
":masterIssueApproval": ":dependencyDashboardApproval",
|
|
22069
|
+
":switchToGradleLite": null,
|
|
22070
|
+
":unpublishSafe": "npm:unpublishSafe",
|
|
22071
|
+
"compatibility:additionalBranchPrefix": null,
|
|
22072
|
+
"config:application": "config:js-app",
|
|
22073
|
+
"config:base": "config:recommended",
|
|
22074
|
+
"config:base-js": "config:recommended",
|
|
22075
|
+
"config:library": "config:js-lib",
|
|
22076
|
+
"default:app": "config:js-app",
|
|
22077
|
+
"default:automergeBranchMergeCommit": ":automergeBranch",
|
|
22078
|
+
"default:automergeBranchPush": ":automergeBranch",
|
|
22079
|
+
"default:base": "config:recommended",
|
|
22080
|
+
"default:js-app": "config:js-app",
|
|
22081
|
+
"default:library": "config:js-lib",
|
|
22082
|
+
"default:onlyNpm": null,
|
|
22083
|
+
"default:unpublishSafe": "npm:unpublishSafe",
|
|
22084
|
+
"github>whitesource/merge-confidence:beta": "mergeConfidence:all-badges",
|
|
22085
|
+
"group:FontsourceMonorepo": "group:fontsourceMonorepo",
|
|
22086
|
+
"group:HangfireMonorepo": "group:hangfireMonorepo",
|
|
22087
|
+
"group:HotChocolateMonorepo": "group:hotchocolateMonorepo",
|
|
22088
|
+
"group:MassTransitMonorepo": "group:masstransitMonorepo",
|
|
22089
|
+
"group:OpenTelemetryDotnetMonorepo": "group:opentelemetry-dotnetMonorepo",
|
|
22090
|
+
"group:SteeltoeMonorepo": "group:steeltoeMonorepo",
|
|
22091
|
+
"group:SwashbuckleMonorepo": "group:swashbuckle-aspnetcoreMonorepo",
|
|
22092
|
+
"group:System.IO.AbstractionsMonorepo": "group:system.io.abstractionsMonorepo",
|
|
22093
|
+
"group:angular1Monorepo": "group:angularjsMonorepo",
|
|
22094
|
+
"group:angularcliMonorepo": "group:angular-cliMonorepo",
|
|
22095
|
+
"group:arcus background-jobsMonorepo": "group:arcus.background-jobsMonorepo",
|
|
22096
|
+
"group:arcus event-gridMonorepo": "group:arcus.event-gridMonorepo",
|
|
22097
|
+
"group:arcus messagingMonorepo": "group:arcus.messagingMonorepo",
|
|
22098
|
+
"group:arcus observabilityMonorepo": "group:arcus.observabilityMonorepo",
|
|
22099
|
+
"group:arcus securityMonorepo": "group:arcus.securityMonorepo",
|
|
22100
|
+
"group:arcus webapiMonorepo": "group:arcus.webapiMonorepo",
|
|
22101
|
+
"group:aspnet AspNetWebStackMonorepo": "group:aspnet aspnetwebstackMonorepo",
|
|
22102
|
+
"group:aspnet ExtensionsMonorepo": "group:aspnet extensionsMonorepo",
|
|
22103
|
+
"group:hamcrestMonorepo": "group:javahamcrestMonorepo",
|
|
22104
|
+
"group:infrastructureMonorepo": "group:infrastructure-uiMonorepo",
|
|
22105
|
+
"group:jsTestMonMajor": "group:jsTestNonMajor",
|
|
22106
|
+
"group:junit5Monorepo": "group:junit-frameworkMonorepo",
|
|
22107
|
+
"group:k8s-ioMonorepo": "group:kubernetesMonorepo",
|
|
22108
|
+
"group:kubernetes": "group:kubernetesMonorepo",
|
|
22109
|
+
"group:linguiMonorepo": "group:linguijsMonorepo",
|
|
22110
|
+
"group:materialMonorepo": "group:material-components-webMonorepo",
|
|
22111
|
+
"group:muiMonorepo": "group:material-uiMonorepo",
|
|
22112
|
+
"group:nrwlMonorepo": "group:nxMonorepo",
|
|
22113
|
+
"group:openfeignMonorepo": "group:feignMonorepo",
|
|
22114
|
+
"group:opentelemetryMonorepo": "group:opentelemetry-jsMonorepo",
|
|
22115
|
+
"group:picassoMonorepo": "group:picassojsMonorepo",
|
|
22116
|
+
"group:reactrouterMonorepo": "group:react-routerMonorepo",
|
|
22117
|
+
"group:sentryMonorepo": "group:sentry-javascriptMonorepo",
|
|
22118
|
+
"group:strykerMonorepo": "group:stryker-jsMonorepo",
|
|
22119
|
+
"helpers:oddIsUnstable": null,
|
|
22120
|
+
"helpers:oddIsUnstablePackages": null,
|
|
22121
|
+
"monorepo:Fontsource": "monorepo:fontsource",
|
|
22122
|
+
"monorepo:Hangfire": "monorepo:hangfire",
|
|
22123
|
+
"monorepo:HotChocolate": "monorepo:hotchocolate",
|
|
22124
|
+
"monorepo:MassTransit": "monorepo:masstransit",
|
|
22125
|
+
"monorepo:OpenTelemetryDotnet": "monorepo:opentelemetry-dotnet",
|
|
22126
|
+
"monorepo:Steeltoe": "monorepo:steeltoe",
|
|
22127
|
+
"monorepo:Swashbuckle": "monorepo:swashbuckle-aspnetcore",
|
|
22128
|
+
"monorepo:System.IO.Abstractions": "monorepo:system.io.abstractions",
|
|
22129
|
+
"monorepo:angular1": "monorepo:angularjs",
|
|
22130
|
+
"monorepo:angularcli": "monorepo:angular-cli",
|
|
22131
|
+
"monorepo:arcus background-jobs": "monorepo:arcus.background-jobs",
|
|
22132
|
+
"monorepo:arcus event-grid": "monorepo:arcus.event-grid",
|
|
22133
|
+
"monorepo:arcus messaging": "monorepo:arcus.messaging",
|
|
22134
|
+
"monorepo:arcus observability": "monorepo:arcus.observability",
|
|
22135
|
+
"monorepo:arcus security": "monorepo:arcus.security",
|
|
22136
|
+
"monorepo:arcus webapi": "monorepo:arcus.webapi",
|
|
22137
|
+
"monorepo:aspnet AspNetWebStack": "monorepo:aspnet aspnetwebstack",
|
|
22138
|
+
"monorepo:aspnet Extensions": "monorepo:aspnet extensions",
|
|
22139
|
+
"monorepo:hamcrest": "monorepo:javahamcrest",
|
|
22140
|
+
"monorepo:infrastructure": "monorepo:infrastructure-ui",
|
|
22141
|
+
"monorepo:junit5": "monorepo:junit-framework",
|
|
22142
|
+
"monorepo:k8s-io": "monorepo:kubernetes",
|
|
22143
|
+
"monorepo:lingui": "monorepo:linguijs",
|
|
22144
|
+
"monorepo:material": "monorepo:material-components-web",
|
|
22145
|
+
"monorepo:mui": "monorepo:material-ui",
|
|
22146
|
+
"monorepo:nrwl": "monorepo:nx",
|
|
22147
|
+
"monorepo:openfeign": "monorepo:feign",
|
|
22148
|
+
"monorepo:opentelemetry": "monorepo:opentelemetry-js",
|
|
22149
|
+
"monorepo:picasso": "monorepo:picassojs",
|
|
22150
|
+
"monorepo:reactrouter": "monorepo:react-router",
|
|
22151
|
+
"monorepo:sentry": "monorepo:sentry-javascript",
|
|
22152
|
+
"monorepo:stryker": "monorepo:stryker-js",
|
|
22153
|
+
"npm:unpublishSafe": "security:minimumReleaseAgeNpm",
|
|
22154
|
+
"regexManagers:azurePipelinesVersions": "customManagers:azurePipelinesVersions",
|
|
22155
|
+
"regexManagers:biomeVersions": "customManagers:biomeVersions",
|
|
22156
|
+
"regexManagers:bitbucketPipelinesVersions": "customManagers:bitbucketPipelinesVersions",
|
|
22157
|
+
"regexManagers:dockerfileVersions": "customManagers:dockerfileVersions",
|
|
22158
|
+
"regexManagers:githubActionsVersions": "customManagers:githubActionsVersions",
|
|
22159
|
+
"regexManagers:gitlabPipelineVersions": "customManagers:gitlabPipelineVersions",
|
|
22160
|
+
"regexManagers:helmChartYamlAppVersions": "customManagers:helmChartYamlAppVersions",
|
|
22161
|
+
"regexManagers:mavenPropertyVersions": "customManagers:mavenPropertyVersions",
|
|
22162
|
+
"regexManagers:tfvarsVersions": "customManagers:tfvarsVersions",
|
|
22163
|
+
"regexManagers:tsconfigNodeVersions": "customManagers:tsconfigNodeVersions",
|
|
22164
|
+
"replacements:messageFormat-{{package}}-to-@messageformat/{{package}}": "replacements:messageFormat-to-scoped",
|
|
22165
|
+
"replacements:ojdbc10-to-ojdbc11": "replacements:ojdbc-to-ojdbc11",
|
|
22166
|
+
"workarounds:reduceRepologyServerLoad": null
|
|
22167
|
+
};
|
|
22052
22168
|
export const PRESET_NAMES = Object.freeze(Object.keys(PRESETS));
|
|
@@ -1,15 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* Resolve a credential by checking each env var name in order and returning
|
|
3
|
-
* the first one that's set. Used by both `resolve_config` (for `github>` /
|
|
4
|
-
* `gitlab>` preset fetches) and `dry_run` (when picking which token to export
|
|
5
|
-
* as `RENOVATE_TOKEN` to the spawned Renovate CLI). Keeping the precedence in
|
|
6
|
-
* one place ensures the two tools cannot drift out of sync.
|
|
7
|
-
*/
|
|
8
|
-
export function resolveCredential(vars) {
|
|
9
|
-
for (const envVar of vars) {
|
|
10
|
-
const value = process.env[envVar];
|
|
11
|
-
if (value)
|
|
12
|
-
return { envVar, token: value, triedVars: vars };
|
|
13
|
-
}
|
|
14
|
-
return { envVar: null, token: undefined, triedVars: vars };
|
|
15
|
-
}
|
|
1
|
+
export {};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import JSON5 from "json5";
|
|
2
|
-
import { resolveCredential } from "./credentialResolver.js";
|
|
3
2
|
import { validateEndpoint } from "./endpointValidator.js";
|
|
3
|
+
import { selectToken } from "./hostTokens.js";
|
|
4
4
|
import { classifyExternalSource, } from "./presetResolver.js";
|
|
5
5
|
const DEFAULT_TIMEOUT_MS = 10_000;
|
|
6
6
|
const DEFAULT_API_BASE = {
|
|
@@ -45,30 +45,46 @@ function dispatch(parsed, options) {
|
|
|
45
45
|
return Promise.resolve({ ok: false, reason: blocked });
|
|
46
46
|
endpoint = trimTrailingSlash(options.endpoint);
|
|
47
47
|
}
|
|
48
|
-
|
|
48
|
+
// `classifyExternalSource` refused local / npm above, so only the hosted
|
|
49
|
+
// platforms remain and the switch is exhaustive.
|
|
50
|
+
const source = parsed.source;
|
|
51
|
+
switch (source) {
|
|
49
52
|
case "github":
|
|
50
53
|
return fetchGitHub(parsed, timeoutMs, fetchImpl, endpoint);
|
|
51
54
|
case "gitlab":
|
|
52
55
|
return fetchGitLab(parsed, timeoutMs, fetchImpl, endpoint);
|
|
53
56
|
case "gitea":
|
|
54
57
|
case "forgejo":
|
|
55
|
-
return fetchGitea(parsed, timeoutMs, fetchImpl, endpoint,
|
|
58
|
+
return fetchGitea(parsed, timeoutMs, fetchImpl, endpoint, source);
|
|
56
59
|
case "bitbucket":
|
|
57
60
|
return fetchBitbucket(parsed, timeoutMs, fetchImpl, endpoint);
|
|
58
|
-
default:
|
|
59
|
-
// Unreachable: `classifyExternalSource` already refused local / npm.
|
|
60
|
-
return Promise.resolve({ ok: false, reason: `Unknown preset source: ${parsed.source}` });
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
|
+
/** GitHub, Gitea and Bitbucket address repositories as exactly two path segments (nested groups are a GitLab thing). */
|
|
64
|
+
const OWNER_REPO_RE = /^[^/]+\/[^/]+$/;
|
|
65
|
+
/** GitLab projects live under a namespace that may itself be nested (`group/sub/project`). */
|
|
66
|
+
const GITLAB_PROJECT_RE = /^[^/]+(\/[^/]+)+$/;
|
|
67
|
+
/**
|
|
68
|
+
* Rejects empty, `.` and `..` segments. URL parsing collapses dot segments
|
|
69
|
+
* (also when spelled `%2e`), and `encodeURIComponent` leaves `.` alone, so
|
|
70
|
+
* without this a preset string could steer a token-bearing request to any
|
|
71
|
+
* path on the API host.
|
|
72
|
+
*/
|
|
73
|
+
function isSafePath(path) {
|
|
74
|
+
return path.split("/").every((seg) => seg !== "" && seg !== "." && seg !== "..");
|
|
75
|
+
}
|
|
63
76
|
async function fetchGitHub(parsed, timeoutMs, fetchImpl, endpoint) {
|
|
64
|
-
|
|
77
|
+
const file = presetFileName(parsed);
|
|
78
|
+
if (!parsed.repoPath ||
|
|
79
|
+
!OWNER_REPO_RE.test(parsed.repoPath) ||
|
|
80
|
+
!isSafePath(parsed.repoPath) ||
|
|
81
|
+
!isSafePath(file)) {
|
|
65
82
|
return { ok: false, reason: `Invalid github preset: ${parsed.original}` };
|
|
66
83
|
}
|
|
67
|
-
const file = presetFileName(parsed);
|
|
68
84
|
const ref = parsed.ref ?? "HEAD";
|
|
69
85
|
const apiBase = endpoint ?? DEFAULT_API_BASE.github;
|
|
70
|
-
const url = `${apiBase}/repos/${parsed.repoPath}/contents/${encodeFilePath(file)}?ref=${encodeURIComponent(ref)}`;
|
|
71
|
-
const credential =
|
|
86
|
+
const url = `${apiBase}/repos/${encodeFilePath(parsed.repoPath)}/contents/${encodeFilePath(file)}?ref=${encodeURIComponent(ref)}`;
|
|
87
|
+
const credential = selectToken(url, "github");
|
|
72
88
|
const headers = {
|
|
73
89
|
Accept: "application/vnd.github.raw",
|
|
74
90
|
"User-Agent": "renovate-mcp",
|
|
@@ -79,36 +95,37 @@ async function fetchGitHub(parsed, timeoutMs, fetchImpl, endpoint) {
|
|
|
79
95
|
return fetchJson(url, headers, timeoutMs, parsed.original, fetchImpl, "github", sendAuth ? credential : suppressCredential(credential));
|
|
80
96
|
}
|
|
81
97
|
async function fetchGitLab(parsed, timeoutMs, fetchImpl, endpoint) {
|
|
82
|
-
|
|
98
|
+
const file = presetFileName(parsed);
|
|
99
|
+
if (!parsed.repoPath ||
|
|
100
|
+
!GITLAB_PROJECT_RE.test(parsed.repoPath) ||
|
|
101
|
+
!isSafePath(parsed.repoPath) ||
|
|
102
|
+
!isSafePath(file)) {
|
|
83
103
|
return { ok: false, reason: `Invalid gitlab preset: ${parsed.original}` };
|
|
84
104
|
}
|
|
85
|
-
const file = presetFileName(parsed);
|
|
86
105
|
const ref = parsed.ref ?? "HEAD";
|
|
87
106
|
const apiBase = endpoint ?? DEFAULT_API_BASE.gitlab;
|
|
88
107
|
const url = `${apiBase}/projects/${encodeURIComponent(parsed.repoPath)}/repository/files/${encodeURIComponent(file)}/raw?ref=${encodeURIComponent(ref)}`;
|
|
89
|
-
const credential =
|
|
108
|
+
const credential = selectToken(url, "gitlab");
|
|
90
109
|
const headers = { "User-Agent": "renovate-mcp" };
|
|
91
110
|
const sendAuth = Boolean(credential.token) && isHttpsUrl(url);
|
|
92
111
|
if (sendAuth)
|
|
93
112
|
headers["PRIVATE-TOKEN"] = credential.token;
|
|
94
113
|
return fetchJson(url, headers, timeoutMs, parsed.original, fetchImpl, "gitlab", sendAuth ? credential : suppressCredential(credential));
|
|
95
114
|
}
|
|
96
|
-
/** Gitea and Bitbucket address repositories as exactly two path segments (nested groups are a GitLab thing). */
|
|
97
|
-
const OWNER_REPO_RE = /^[^/]+\/[^/]+$/;
|
|
98
115
|
/** Gitea's raw-contents API, which Forgejo serves unchanged; the two differ only in default host and token var. */
|
|
99
116
|
async function fetchGitea(parsed, timeoutMs, fetchImpl, endpoint, platform) {
|
|
100
|
-
|
|
117
|
+
const file = presetFileName(parsed);
|
|
118
|
+
if (!parsed.repoPath ||
|
|
119
|
+
!OWNER_REPO_RE.test(parsed.repoPath) ||
|
|
120
|
+
!isSafePath(parsed.repoPath) ||
|
|
121
|
+
!isSafePath(file)) {
|
|
101
122
|
return { ok: false, reason: `Invalid ${platform} preset: ${parsed.original}` };
|
|
102
123
|
}
|
|
103
|
-
const file = presetFileName(parsed);
|
|
104
124
|
const apiBase = endpoint ?? DEFAULT_API_BASE[platform];
|
|
105
125
|
// No `ref` means the repository's default branch, as in Renovate.
|
|
106
126
|
const refQuery = parsed.ref ? `?ref=${encodeURIComponent(parsed.ref)}` : "";
|
|
107
127
|
const url = `${apiBase}/repos/${encodeFilePath(parsed.repoPath)}/raw/${encodeFilePath(file)}${refQuery}`;
|
|
108
|
-
const credential =
|
|
109
|
-
"RENOVATE_TOKEN",
|
|
110
|
-
platform === "gitea" ? "GITEA_TOKEN" : "FORGEJO_TOKEN",
|
|
111
|
-
]);
|
|
128
|
+
const credential = selectToken(url, platform);
|
|
112
129
|
const headers = { "User-Agent": "renovate-mcp" };
|
|
113
130
|
const sendAuth = Boolean(credential.token) && isHttpsUrl(url);
|
|
114
131
|
if (sendAuth)
|
|
@@ -124,14 +141,23 @@ async function fetchGitea(parsed, timeoutMs, fetchImpl, endpoint, platform) {
|
|
|
124
141
|
* if anyone needs it.
|
|
125
142
|
*/
|
|
126
143
|
async function fetchBitbucket(parsed, timeoutMs, fetchImpl, endpoint) {
|
|
127
|
-
|
|
144
|
+
const file = presetFileName(parsed);
|
|
145
|
+
// The ref is a single encoded path segment here (a query parameter elsewhere), so only an
|
|
146
|
+
// empty or dot ref can shape the path.
|
|
147
|
+
const rawRef = parsed.ref ?? "HEAD";
|
|
148
|
+
if (!parsed.repoPath ||
|
|
149
|
+
!OWNER_REPO_RE.test(parsed.repoPath) ||
|
|
150
|
+
!isSafePath(parsed.repoPath) ||
|
|
151
|
+
!isSafePath(file) ||
|
|
152
|
+
rawRef === "" ||
|
|
153
|
+
rawRef === "." ||
|
|
154
|
+
rawRef === "..") {
|
|
128
155
|
return { ok: false, reason: `Invalid bitbucket preset: ${parsed.original}` };
|
|
129
156
|
}
|
|
130
|
-
const file = presetFileName(parsed);
|
|
131
157
|
const apiBase = endpoint ?? DEFAULT_API_BASE.bitbucket;
|
|
132
|
-
const ref = encodeURIComponent(
|
|
158
|
+
const ref = encodeURIComponent(rawRef);
|
|
133
159
|
const url = `${apiBase}/2.0/repositories/${encodeFilePath(parsed.repoPath)}/src/${ref}/${encodeFilePath(file)}`;
|
|
134
|
-
const credential =
|
|
160
|
+
const credential = selectToken(url, "bitbucket");
|
|
135
161
|
const headers = { "User-Agent": "renovate-mcp" };
|
|
136
162
|
const sendAuth = Boolean(credential.token) && isHttpsUrl(url);
|
|
137
163
|
if (sendAuth)
|
|
@@ -309,7 +335,7 @@ async function discardBody(res) {
|
|
|
309
335
|
function formatAuthFailure(status, presetName, url, credential, body) {
|
|
310
336
|
const lines = [`HTTP ${status} when fetching ${presetName}`];
|
|
311
337
|
lines.push(` URL: ${url}`);
|
|
312
|
-
lines.push(` Credential: ${formatCredential(credential)}`);
|
|
338
|
+
lines.push(` Credential: ${formatCredential(credential, url)}`);
|
|
313
339
|
const trimmed = body.trim();
|
|
314
340
|
if (trimmed) {
|
|
315
341
|
const snippet = trimmed.length > AUTH_BODY_MAX
|
|
@@ -319,9 +345,11 @@ function formatAuthFailure(status, presetName, url, credential, body) {
|
|
|
319
345
|
}
|
|
320
346
|
return lines.join("\n");
|
|
321
347
|
}
|
|
322
|
-
function formatCredential(credential) {
|
|
348
|
+
function formatCredential(credential, url) {
|
|
323
349
|
if (credential.envVar)
|
|
324
350
|
return `${credential.envVar} (present)`;
|
|
351
|
+
if (credential.triedVars.length === 0)
|
|
352
|
+
return `none (no token is bound to ${new URL(url).hostname})`;
|
|
325
353
|
return `none (tried ${credential.triedVars.join(", ")})`;
|
|
326
354
|
}
|
|
327
355
|
const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
|
package/dist/lib/gitRemote.js
CHANGED
|
@@ -40,15 +40,38 @@ export function parseRemoteUrl(url) {
|
|
|
40
40
|
return { host, scheme: "ssh", owner, repo };
|
|
41
41
|
}
|
|
42
42
|
/**
|
|
43
|
-
* Read the `[remote "origin"] url = …` field from a repo's
|
|
44
|
-
*
|
|
45
|
-
*
|
|
43
|
+
* Read the `[remote "origin"] url = …` field from a repo's git config without
|
|
44
|
+
* spawning `git`. A `.git` file (worktree, submodule) is followed through its
|
|
45
|
+
* `gitdir:` line, and a worktree's `commondir` to the shared config. The
|
|
46
|
+
* gitdir must look like one git writes (`…/.git`, `…/.git/worktrees/<name>`
|
|
47
|
+
* or `…/.git/modules/…`), and a worktree's common dir must be a `…/.git`;
|
|
48
|
+
* any other target is no origin, so a crafted `.git` file cannot point the
|
|
49
|
+
* origin at an arbitrary config. Returns `null` for
|
|
50
|
+
* non-git directories, missing config, missing origin remote, or unreadable
|
|
51
|
+
* files (no exceptions thrown).
|
|
46
52
|
*/
|
|
53
|
+
/** A `.git` dir, a worktree's `.git/worktrees/<name>`, or a submodule's `.git/modules/<path>`. */
|
|
54
|
+
const GITDIR_SHAPE = /\/\.git(?:\/worktrees\/[^/]+|\/modules\/.+)?$/;
|
|
47
55
|
export async function readOriginRemote(repoPath) {
|
|
48
|
-
const
|
|
56
|
+
const dotGit = path.join(repoPath, ".git");
|
|
57
|
+
// Reading a `.git` directory fails (EISDIR): then it is the git dir itself.
|
|
58
|
+
const pointer = await fs.readFile(dotGit, "utf8").catch(() => "");
|
|
59
|
+
const gitdirLine = /^gitdir:\s*(.+)$/m.exec(pointer);
|
|
60
|
+
let gitDir = dotGit;
|
|
61
|
+
if (gitdirLine) {
|
|
62
|
+
gitDir = path.resolve(repoPath, gitdirLine[1].trim());
|
|
63
|
+
if (!GITDIR_SHAPE.test(gitDir.split(path.sep).join("/")))
|
|
64
|
+
return null;
|
|
65
|
+
const commonDir = await fs.readFile(path.join(gitDir, "commondir"), "utf8").catch(() => null);
|
|
66
|
+
if (commonDir) {
|
|
67
|
+
gitDir = path.resolve(gitDir, commonDir.trim());
|
|
68
|
+
if (!/\/\.git$/.test(gitDir.split(path.sep).join("/")))
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
49
72
|
let raw;
|
|
50
73
|
try {
|
|
51
|
-
raw = await fs.readFile(
|
|
74
|
+
raw = await fs.readFile(path.join(gitDir, "config"), "utf8");
|
|
52
75
|
}
|
|
53
76
|
catch {
|
|
54
77
|
return null;
|
|
@@ -1,25 +1,30 @@
|
|
|
1
1
|
import { promises as fs } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
|
-
import { randomUUID } from "node:crypto";
|
|
5
4
|
/**
|
|
6
|
-
* Write a throwaway Renovate config containing just `{ hostRules: [...] }`
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
5
|
+
* Write a throwaway Renovate config containing just `{ hostRules: [...] }` and
|
|
6
|
+
* return its absolute path. The file sits alone in a fresh `mkdtemp`
|
|
7
|
+
* directory (mode 0700) under `os.tmpdir()` and is created with `wx` and mode
|
|
8
|
+
* 0600: it holds env tokens on most runs, so no other local user may read it
|
|
9
|
+
* or plant a file at the path first. The caller removes it with
|
|
10
|
+
* `removeHostRulesConfig` (in a `finally` block) once Renovate has consumed it.
|
|
10
11
|
*
|
|
11
|
-
* The caller points Renovate at this file via the `RENOVATE_CONFIG_FILE`
|
|
12
|
-
* var
|
|
13
|
-
* Renovate
|
|
14
|
-
*
|
|
15
|
-
*
|
|
12
|
+
* The caller points Renovate at this file via the `RENOVATE_CONFIG_FILE` (or
|
|
13
|
+
* `RENOVATE_ADDITIONAL_CONFIG_FILE`) env var, not a CLI flag — `--config-file`
|
|
14
|
+
* is not a real Renovate option. Renovate concatenates its rules with any
|
|
15
|
+
* other `hostRules`, so callers can combine per-invocation credentials with
|
|
16
|
+
* whatever the repo itself already declares.
|
|
16
17
|
*/
|
|
17
18
|
export async function writeHostRulesConfig(hostRules) {
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
await fs.writeFile(tmpPath,
|
|
19
|
+
const dir = await fs.mkdtemp(path.join(tmpdir(), "renovate-mcp-hostrules-"));
|
|
20
|
+
const tmpPath = path.join(dir, "config.json");
|
|
21
|
+
await fs.writeFile(tmpPath, JSON.stringify({ hostRules }, null, 2), { flag: "wx", mode: 0o600 });
|
|
21
22
|
return tmpPath;
|
|
22
23
|
}
|
|
24
|
+
/** Remove a file written by `writeHostRulesConfig` together with its directory. */
|
|
25
|
+
export async function removeHostRulesConfig(filePath) {
|
|
26
|
+
await fs.rm(path.dirname(filePath), { recursive: true, force: true });
|
|
27
|
+
}
|
|
23
28
|
const SECRET_KEYS = ["token", "password"];
|
|
24
29
|
/**
|
|
25
30
|
* Pull the token/password values out of the provided hostRules so callers can
|