Skip to content

Commit 54e8fa2

Browse files
authored
Merge pull request #157 from NVIDIA-NeMo/fix/spdx-check-local-artifacts
fix(release): use Git excludes for SPDX scan
2 parents 498d5b0 + 7930836 commit 54e8fa2

2 files changed

Lines changed: 73 additions & 30 deletions

File tree

scripts/check_license_headers.py

Lines changed: 24 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -6,50 +6,44 @@
66
``lint`` job runs it on every merge request. Empty ``__init__.py`` package
77
markers (0 bytes) are exempt — they hold no copyrightable content.
88
9-
Walks the filesystem (no ``git`` dependency, so it runs in the minimal CI lint
10-
image), skipping virtualenvs, caches, build output, and vendored trees.
9+
Uses Git's file and exclude handling as the source of truth: tracked Python
10+
files and untracked, nonignored Python files are checked. Files excluded by
11+
``.gitignore``, ``.git/info/exclude``, or the user's global Git excludes are
12+
not repository source and are not scanned.
1113
"""
1214

1315
from __future__ import annotations
1416

17+
import os
18+
import subprocess
1519
import sys
1620
from pathlib import Path
1721

1822
REQUIRED = "SPDX-License-Identifier"
1923
# Only the first few lines are checked (header sits above any docstring, after
2024
# an optional shebang/coding line).
2125
HEAD_LINES = 5
22-
# Directory names to skip anywhere in the tree.
23-
SKIP_DIRS = {
24-
".git",
25-
".venv",
26-
"venv",
27-
"__pycache__",
28-
"node_modules",
29-
"dist",
30-
"build",
31-
".uv-cache",
32-
".mypy_cache",
33-
".pytest_cache",
34-
".ruff_cache",
35-
"site-packages",
36-
".scratch",
37-
"htmlcov",
38-
# Local git worktrees are separate checkouts of other branches. Scanning
39-
# them makes this check fail on someone else's in-progress work — and on
40-
# vendored trees that branch happens to contain. CI never has them, so the
41-
# failure only ever hits developers running the check locally.
42-
".worktrees",
43-
}
4426

4527

4628
def source_python_files(root: Path) -> list[Path]:
47-
files: list[Path] = []
48-
for path in root.rglob("*.py"):
49-
if any(part in SKIP_DIRS for part in path.relative_to(root).parts):
50-
continue
51-
files.append(path)
52-
return files
29+
"""Return tracked and untracked, nonignored Python files under *root*."""
30+
result = subprocess.run(
31+
[
32+
"git",
33+
"ls-files",
34+
"--cached",
35+
"--others",
36+
"--exclude-standard",
37+
"-z",
38+
"--",
39+
"*.py",
40+
],
41+
cwd=root,
42+
check=True,
43+
capture_output=True,
44+
)
45+
relative_paths = (os.fsdecode(raw) for raw in result.stdout.split(b"\0") if raw)
46+
return sorted(root / relative for relative in relative_paths)
5347

5448

5549
def main() -> int:
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
"""Tests for the repository SPDX header scanner."""
4+
5+
from __future__ import annotations
6+
7+
import subprocess
8+
from pathlib import Path
9+
10+
from scripts.check_license_headers import source_python_files
11+
12+
13+
def _python_file(path: Path) -> Path:
14+
path.parent.mkdir(parents=True, exist_ok=True)
15+
path.write_text("print('test')\n")
16+
return path
17+
18+
19+
def _git(root: Path, *args: str) -> None:
20+
subprocess.run(["git", *args], cwd=root, check=True, capture_output=True)
21+
22+
23+
def test_source_files_follow_standard_git_excludes(tmp_path: Path) -> None:
24+
_git(tmp_path, "init")
25+
(tmp_path / ".gitignore").write_text("/tmp/\n")
26+
with (tmp_path / ".git" / "info" / "exclude").open("a") as exclude:
27+
exclude.write("\n.codex/\n")
28+
29+
tracked_source = _python_file(tmp_path / "src" / "tracked.py")
30+
untracked_source = _python_file(tmp_path / "src" / "new.py")
31+
ignored_by_gitignore = _python_file(tmp_path / "tmp" / "generated.py")
32+
ignored_by_info_exclude = _python_file(tmp_path / ".codex" / "external.py")
33+
_git(tmp_path, "add", ".gitignore", "src/tracked.py")
34+
35+
found = set(source_python_files(tmp_path))
36+
37+
assert tracked_source in found
38+
assert untracked_source in found
39+
assert ignored_by_gitignore not in found
40+
assert ignored_by_info_exclude not in found
41+
42+
43+
def test_source_files_include_tracked_files_that_match_ignore_rules(tmp_path: Path) -> None:
44+
_git(tmp_path, "init")
45+
tracked_source = _python_file(tmp_path / "generated" / "tracked.py")
46+
_git(tmp_path, "add", "generated/tracked.py")
47+
(tmp_path / ".gitignore").write_text("/generated/\n")
48+
49+
assert tracked_source in source_python_files(tmp_path)

0 commit comments

Comments
 (0)