Skip to content

Commit 2d05f60

Browse files
committed
Skip symlinked env discovery inputs
1 parent 1bed4ee commit 2d05f60

2 files changed

Lines changed: 79 additions & 6 deletions

File tree

pithos/env_discovery.py

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ def _env_example_hints(repo: Path, errors: list[str]) -> list[EnvHint]:
201201
hints: list[EnvHint] = []
202202
for name in ENV_EXAMPLE_NAMES:
203203
path = repo / name
204-
if not path.exists():
204+
if _has_symlink_part(path, repo) or not path.exists():
205205
continue
206206
rel = _rel(path, repo)
207207
try:
@@ -220,10 +220,12 @@ def _env_example_hints(repo: Path, errors: list[str]) -> list[EnvHint]:
220220

221221
def _github_workflow_hints(repo: Path, errors: list[str]) -> list[EnvHint]:
222222
workflows = repo / ".github" / "workflows"
223-
if not workflows.is_dir():
223+
if _has_symlink_part(workflows, repo) or not workflows.is_dir():
224224
return []
225225
hints: list[EnvHint] = []
226226
for path in sorted([*workflows.glob("*.yml"), *workflows.glob("*.yaml")]):
227+
if _has_symlink_part(path, repo):
228+
continue
227229
rel = _rel(path, repo)
228230
try:
229231
text = path.read_text(encoding="utf-8", errors="replace")
@@ -287,11 +289,12 @@ def _source_hints(repo: Path, errors: list[str]) -> tuple[list[EnvHint], int, in
287289

288290
def _source_paths(repo: Path) -> Iterable[Path]:
289291
for path in repo.rglob("*"):
290-
if not path.is_file() or path.suffix not in SOURCE_EXTENSIONS:
292+
rel_parts = path.relative_to(repo).parts
293+
if any(part in SKIP_DIRS or part.startswith(".git") for part in rel_parts):
291294
continue
292-
if any(
293-
part in SKIP_DIRS or part.startswith(".git") for part in path.relative_to(repo).parts
294-
):
295+
if _has_symlink_part(path, repo):
296+
continue
297+
if not path.is_file() or path.suffix not in SOURCE_EXTENSIONS:
295298
continue
296299
yield path
297300

@@ -326,6 +329,19 @@ def _dedupe_hints(hints: list[EnvHint]) -> list[EnvHint]:
326329
return out
327330

328331

332+
def _has_symlink_part(path: Path, repo: Path) -> bool:
333+
try:
334+
rel_parts = path.relative_to(repo).parts
335+
except ValueError:
336+
return True
337+
current = repo
338+
for part in rel_parts:
339+
current = current / part
340+
if current.is_symlink():
341+
return True
342+
return False
343+
344+
329345
def _rel(path: Path, repo: Path) -> str:
330346
try:
331347
return path.relative_to(repo).as_posix()

tests/test_env_discovery.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
# SPDX-License-Identifier: Apache-2.0
33
"""Coverage for deterministic runtime env discovery."""
44

5+
import pytest
6+
57
from pithos.env_discovery import collect_env_hints, env_names_from_examples
68

79

@@ -77,3 +79,58 @@ def test_collect_env_hints_ignores_skipped_directories(tmp_path):
7779
report = collect_env_hints(repo)
7880

7981
assert "PITHOS_TEST_DEPENDENCY_SECRET" not in report.required_names()
82+
83+
84+
def test_collect_env_hints_ignores_symlinked_inputs(tmp_path):
85+
repo = tmp_path / "app"
86+
repo.mkdir()
87+
outside = tmp_path / "outside"
88+
outside.mkdir()
89+
outside_env = outside / "external.env"
90+
outside_env.write_text("PITHOS_TEST_OUTSIDE_EXAMPLE=value\n", encoding="utf-8")
91+
outside_workflow = outside / "workflow.yml"
92+
outside_workflow.write_text(
93+
"env:\n PITHOS_TEST_OUTSIDE_WORKFLOW: ${{ secrets.PITHOS_TEST_OUTSIDE_WORKFLOW }}\n",
94+
encoding="utf-8",
95+
)
96+
outside_source = outside / "external.py"
97+
outside_source.write_text(
98+
'import os\nsecret = os.getenv("PITHOS_TEST_OUTSIDE_SOURCE")\n',
99+
encoding="utf-8",
100+
)
101+
try:
102+
(repo / ".env.example").symlink_to(outside_env)
103+
workflows = repo / ".github" / "workflows"
104+
workflows.mkdir(parents=True)
105+
(workflows / "ci.yml").symlink_to(outside_workflow)
106+
(repo / "linked.py").symlink_to(outside_source)
107+
except OSError as e:
108+
pytest.skip(f"symlinks unavailable: {e}")
109+
110+
report = collect_env_hints(repo)
111+
112+
assert "PITHOS_TEST_OUTSIDE_EXAMPLE" not in report.observed_names()
113+
assert "PITHOS_TEST_OUTSIDE_WORKFLOW" not in report.observed_names()
114+
assert "PITHOS_TEST_OUTSIDE_SOURCE" not in report.observed_names()
115+
assert env_names_from_examples(repo) == []
116+
117+
118+
def test_collect_env_hints_ignores_symlinked_workflow_directory(tmp_path):
119+
repo = tmp_path / "app"
120+
repo.mkdir()
121+
outside_workflows = tmp_path / "outside-workflows"
122+
outside_workflows.mkdir()
123+
(outside_workflows / "ci.yml").write_text(
124+
"env:\n PITHOS_TEST_OUTSIDE_WORKFLOW_DIR: value\n",
125+
encoding="utf-8",
126+
)
127+
try:
128+
github_dir = repo / ".github"
129+
github_dir.mkdir()
130+
(github_dir / "workflows").symlink_to(outside_workflows, target_is_directory=True)
131+
except OSError as e:
132+
pytest.skip(f"symlinks unavailable: {e}")
133+
134+
report = collect_env_hints(repo)
135+
136+
assert "PITHOS_TEST_OUTSIDE_WORKFLOW_DIR" not in report.observed_names()

0 commit comments

Comments
 (0)