Skip to content

Commit 88572c4

Browse files
Support mixing remote and local configs in EXTENDS
EXTENDS accepts both absolute URLs and relative file paths to config files to inherit from. Stop assuming that all relative file paths are relative to the current workspace. This assumption does not hold when parsing a config inherited from a different repository. This situation arises most simply when A inherits from B via an absolute URL, and B inherits from C via a relative file path. Both inherited config files, B and C, are in a different repository than A. Make a best effort to infer the URL of the repository root, and use that to correctly resolve relative file paths within that repository recursively.
1 parent 4363d26 commit 88572c4

File tree

3 files changed

+72
-12
lines changed

3 files changed

+72
-12
lines changed

.github/linters/.cspell.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -501,6 +501,7 @@
501501
"cmidrule",
502502
"codacy",
503503
"codebases",
504+
"codeberg",
504505
"codeclimate",
505506
"codecov",
506507
"codenarcargs",
@@ -680,6 +681,8 @@
680681
"gijsreyn",
681682
"gitattributes",
682683
"gitblame",
684+
"gitea",
685+
"gitee",
683686
"gitlab",
684687
"gitleaks",
685688
"gitmodified",
@@ -733,6 +736,7 @@
733736
"htmlhint",
734737
"htmlhintrc",
735738
"htmlout",
739+
"huggingface",
736740
"hyhs",
737741
"idiv",
738742
"ighe",
@@ -777,6 +781,7 @@
777781
"joereynolds",
778782
"jscoverage",
779783
"jscpd",
784+
"jsdelivr",
780785
"jsonify",
781786
"jsonlint",
782787
"jsonlintrc",
@@ -1003,6 +1008,7 @@
10031008
"packagename",
10041009
"pagebreak",
10051010
"pageref",
1011+
"pagure",
10061012
"pandoc",
10071013
"parallelization",
10081014
"paren",
@@ -1016,6 +1022,7 @@
10161022
"perlcriticrc",
10171023
"pgfpicture",
10181024
"phar",
1025+
"phcdn",
10191026
"phive",
10201027
"phpcs",
10211028
"phplint",
@@ -1132,6 +1139,7 @@
11321139
"returncode",
11331140
"returnrules",
11341141
"rexec",
1142+
"rhodecode",
11351143
"risd",
11361144
"rmfamily",
11371145
"rockspec",

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ Note: Can be used with `oxsecurity/megalinter@beta` in your GitHub Action mega-l
2222
- Upgrade create-pull-request and create-or-update-comment GitHub Actions
2323
- Increase auto-update-linters GitHub Action timeout
2424
- Upgrade base Docker image to python:3.11.3-alpine3.17
25+
- Fix a config inheritance bug that prevented extending a remote config that
26+
extends a local config by @Kurt-von-Laven
27+
([#2371](https://github.com/oxsecurity/megalinter/issues/2371)).
2528

2629
- Documentation
2730

megalinter/config.py

Lines changed: 61 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,22 @@
44
import os
55
import shlex
66
import tempfile
7+
from collections.abc import Mapping, Sequence
8+
from pathlib import Path, PurePath
9+
from typing import AnyStr, cast
10+
from urllib.parse import ParseResult, urlparse, urlunparse
711

812
import requests
913
import yaml
1014

1115
CONFIG_DATA = None
1216
CONFIG_SOURCE = None
1317

18+
JsonValue = (
19+
None | bool | int | float | str | Sequence["JsonValue"] | Mapping[str, "JsonValue"]
20+
)
21+
JsonObject = dict[str, JsonValue]
22+
1423

1524
def init_config(workspace=None):
1625
global CONFIG_DATA, CONFIG_SOURCE
@@ -72,7 +81,7 @@ def init_config(workspace=None):
7281
)
7382
# manage EXTENDS in configuration
7483
if "EXTENDS" in runtime_config:
75-
combined_config = {}
84+
combined_config: JsonObject = {}
7685
CONFIG_SOURCE = combine_config(
7786
workspace, runtime_config, combined_config, CONFIG_SOURCE
7887
)
@@ -82,22 +91,32 @@ def init_config(workspace=None):
8291
set_config(runtime_config)
8392

8493

85-
def combine_config(workspace, config, combined_config, config_source):
86-
extends = config["EXTENDS"]
94+
def combine_config(
95+
workspace: str | None,
96+
config: JsonObject,
97+
combined_config: JsonObject,
98+
config_source: str,
99+
child_uri: ParseResult | None = None,
100+
) -> str:
101+
workspace_path = Path(workspace) if workspace else None
102+
parsed_uri: ParseResult | None = None
103+
extends = cast(str | Sequence[str], config["EXTENDS"])
87104
if isinstance(extends, str):
88105
extends = extends.split(",")
89106
for extends_item in extends:
90107
if extends_item.startswith("http"):
91-
r = requests.get(extends_item, allow_redirects=True)
92-
assert (
93-
r.status_code == 200
94-
), f"Unable to retrieve EXTENDS config file {extends_item}"
95-
extends_config_data = yaml.safe_load(r.content)
108+
parsed_uri = urlparse(extends_item)
109+
extends_config_data = download_config(extends_item)
96110
else:
97-
with open(
98-
workspace + os.path.sep + extends_item, "r", encoding="utf-8"
99-
) as f:
100-
extends_config_data = yaml.safe_load(f)
111+
path = PurePath(extends_item)
112+
if child_uri:
113+
parsed_uri = resolve_uri(child_uri, path)
114+
uri = urlunparse(parsed_uri)
115+
extends_config_data = download_config(uri)
116+
else:
117+
resolved_path = workspace_path / path if workspace_path else Path(path)
118+
with resolved_path.open("r", encoding="utf-8") as f:
119+
extends_config_data = yaml.safe_load(f)
101120
combined_config.update(extends_config_data)
102121
config_source += f"\n[config] - extends from: {extends_item}"
103122
if "EXTENDS" in extends_config_data:
@@ -106,11 +125,41 @@ def combine_config(workspace, config, combined_config, config_source):
106125
extends_config_data,
107126
combined_config,
108127
config_source,
128+
parsed_uri,
109129
)
110130
combined_config.update(config)
111131
return config_source
112132

113133

134+
def download_config(uri: AnyStr) -> JsonObject:
135+
r = requests.get(uri, allow_redirects=True)
136+
assert r.status_code == 200, f"Unable to retrieve EXTENDS config file {uri!r}"
137+
return yaml.safe_load(r.content)
138+
139+
140+
def resolve_uri(child_uri: ParseResult, relative_config_path: PurePath) -> ParseResult:
141+
match child_uri.netloc:
142+
case "cdn.jsdelivr.net" | "git.launchpad.net":
143+
repo_root_index = 3
144+
case "code.rhodecode.com" | "git.savannah.gnu.org" | "raw.githubusercontent.com" | "repo.or.cz":
145+
repo_root_index = 4
146+
case "bitbucket.org" | "git.sr.ht" | "gitee.com" | "pagure.io":
147+
repo_root_index = 5
148+
case "codeberg.org" | "gitea.com" | "gitlab.com" | "huggingface.co" | "p.phcdn.net" | "sourceforge.net":
149+
repo_root_index = 6
150+
case _:
151+
message = (
152+
f"Unsupported Git repo hosting service: {child_uri.netloc}. "
153+
"Request support be added to MegaLinter, or use absolute URLs "
154+
"with EXTENDS in inherited configs rather than relative paths."
155+
)
156+
raise ValueError(message)
157+
child_path = PurePath(child_uri.path)
158+
repo_root_path = child_path.parts[:repo_root_index]
159+
path = PurePath(*repo_root_path, str(relative_config_path))
160+
return child_uri._replace(path=str(path))
161+
162+
114163
def get_config():
115164
global CONFIG_DATA
116165
if CONFIG_DATA is not None:

0 commit comments

Comments
 (0)