From 28e45258c3fb716c35cdd778f7ca72e65675428c Mon Sep 17 00:00:00 2001 From: Ralf Juengling Date: Tue, 3 Nov 2026 15:37:51 -0800 Subject: [PATCH 1/8] toolshed: fix copyright-year check to work outside staged-file workflows Replace is_staged() with last_modified_year(), which consults git status for working-tree changes and git log -1 for committed files. The old is_staged() gate used git diff --staged as a proxy for 'was this file changed recently', which only holds during a git commit. Under pre-commit run --all-files or CI nothing is staged, so the gate short-circuited to 'return True' and the year was never checked. Also change fetch-depth from 1 to 0 in the pre-commit CI workflow. git log -1 needs enough history to find when each file was last touched; a depth-1 shallow clone returns empty output for files not changed in the tip commit, causing silent false passes on stale copyright years. Fixes #2273. --- .github/workflows/pre-commit.yml | 6 +++++- toolshed/check_spdx.py | 22 ++++++++++++++++------ 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index 40c4636eb2a..7db8d993e13 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -58,10 +58,14 @@ jobs: # PR workflows run from the merge revision; check out that same revision # so their helper scripts and configuration stay in sync. + # Full history is required so that check_spdx.py can call `git log -1` + # to determine when each file was last modified (used for copyright-year + # enforcement). A shallow clone would return no history for files not + # touched in the tip commit, causing false passes on stale years. - name: Checkout repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - fetch-depth: 1 + fetch-depth: 0 persist-credentials: false - name: Set up Python diff --git a/toolshed/check_spdx.py b/toolshed/check_spdx.py index 1be8a565db7..e85ee24c8b8 100644 --- a/toolshed/check_spdx.py +++ b/toolshed/check_spdx.py @@ -71,14 +71,24 @@ def load_spdx_ignore(): CURRENT_YEAR = str(datetime.datetime.now(tz=datetime.UTC).year) -def is_staged(filepath): - # If the file is staged, we need to update it to the current year - process = subprocess.run( # noqa: S603 - ["git", "diff", "--staged", "--", filepath], # noqa: S607 +def last_modified_year(filepath): + # Staged or unstaged working-tree changes mean the file is being modified now. + status = subprocess.run( # noqa: S603 + ["git", "status", "--porcelain", "--", filepath], # noqa: S607 capture_output=True, text=True, ) - return process.stdout.strip() != "" + if status.stdout.strip(): + return int(CURRENT_YEAR) + # Find the most recent commit that touched this file. + log = subprocess.run( # noqa: S603 + ["git", "log", "-1", "--format=%cd", "--date=format:%Y", "--", filepath], # noqa: S607 + capture_output=True, + text=True, + ) + year_str = log.stdout.strip() + # Fall back to current year for brand-new untracked files with no git history. + return int(year_str) if year_str else int(CURRENT_YEAR) def normalize_repo_path(filepath): @@ -165,7 +175,7 @@ def validate_or_fix_copyright(filepath, blob, fix): else: start_year = end_year = years - if not is_staged(filepath) or int(end_year) >= int(CURRENT_YEAR): + if int(end_year) >= last_modified_year(filepath): return True, blob print(f"OUTDATED copyright {years!r} (expected {CURRENT_YEAR!r}) in {filepath!r}") From 09ab7f6338b188d52c86da00390fd3470156bdd0 Mon Sep 17 00:00:00 2001 From: Ralf Juengling Date: Tue, 3 Nov 2026 15:37:52 -0800 Subject: [PATCH 2/8] toolshed: fix 20 stale copyright years surfaced by the year-check fix Running check_spdx.py --fix --all-files after the previous commit updated files last committed in 2026 that still had a 2025 (or earlier) copyright end year. These were invisible to the hook before because is_staged() always returned False in the --all-files invocation mode. --- .github/actions/install_unix_deps/action.yml | 2 +- .github/copy-pr-bot.yaml | 2 +- ci/tools/guess_latest.sh | 2 +- cuda_core/DESCRIPTION.rst | 2 +- cuda_core/cuda/core/_memory/_graph_memory_resource.pxd | 2 +- cuda_core/cuda/core/_utils/__init__.py | 2 +- cuda_core/docs/source/_templates/autosummary/cyclass.rst | 2 +- cuda_core/docs/source/release.rst | 2 +- cuda_core/docs/source/release/0.3.0-notes.rst | 2 +- cuda_core/docs/source/release/0.4.0-notes.rst | 2 +- cuda_core/docs/source/release/0.5.1-notes.rst | 2 +- cuda_core/tests/cython/build_tests.sh | 2 +- cuda_core/tests/helpers/layout.py | 2 +- cuda_core/tests/test_context.py | 2 +- cuda_pathfinder/cuda/pathfinder/_utils/env_vars.py | 2 +- cuda_pathfinder/cuda/pathfinder/_utils/find_sub_dirs.py | 2 +- cuda_pathfinder/cuda/pathfinder/_utils/platform_aware.py | 2 +- cuda_pathfinder/tests/conftest.py | 2 +- cuda_pathfinder/tests/test_utils_env_vars.py | 2 +- cuda_pathfinder/tests/test_utils_find_sub_dirs.py | 2 +- 20 files changed, 20 insertions(+), 20 deletions(-) diff --git a/.github/actions/install_unix_deps/action.yml b/.github/actions/install_unix_deps/action.yml index b0a1831abf2..bd44fe9f52b 100644 --- a/.github/actions/install_unix_deps/action.yml +++ b/.github/actions/install_unix_deps/action.yml @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # SPDX-License-Identifier: Apache-2.0 diff --git a/.github/copy-pr-bot.yaml b/.github/copy-pr-bot.yaml index e2fc4a5f4a2..1c328793a11 100644 --- a/.github/copy-pr-bot.yaml +++ b/.github/copy-pr-bot.yaml @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # Configuration file for `copy-pr-bot` GitHub App diff --git a/ci/tools/guess_latest.sh b/ci/tools/guess_latest.sh index 8a0a13034ca..f9d383f2094 100644 --- a/ci/tools/guess_latest.sh +++ b/ci/tools/guess_latest.sh @@ -1,5 +1,5 @@ #!/bin/bash -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # SPDX-License-Identifier: Apache-2.0 diff --git a/cuda_core/DESCRIPTION.rst b/cuda_core/DESCRIPTION.rst index 0d725a2520a..9759aed8731 100644 --- a/cuda_core/DESCRIPTION.rst +++ b/cuda_core/DESCRIPTION.rst @@ -1,4 +1,4 @@ -.. SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +.. SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. .. SPDX-License-Identifier: Apache-2.0 ******************************************************* diff --git a/cuda_core/cuda/core/_memory/_graph_memory_resource.pxd b/cuda_core/cuda/core/_memory/_graph_memory_resource.pxd index 492aa23cd3d..d28c638c04f 100644 --- a/cuda_core/cuda/core/_memory/_graph_memory_resource.pxd +++ b/cuda_core/cuda/core/_memory/_graph_memory_resource.pxd @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # SPDX-License-Identifier: Apache-2.0 diff --git a/cuda_core/cuda/core/_utils/__init__.py b/cuda_core/cuda/core/_utils/__init__.py index 79599c77db0..644dbb92e75 100644 --- a/cuda_core/cuda/core/_utils/__init__.py +++ b/cuda_core/cuda/core/_utils/__init__.py @@ -1,3 +1,3 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # SPDX-License-Identifier: Apache-2.0 diff --git a/cuda_core/docs/source/_templates/autosummary/cyclass.rst b/cuda_core/docs/source/_templates/autosummary/cyclass.rst index 81d045a6b0a..99b74ca0658 100644 --- a/cuda_core/docs/source/_templates/autosummary/cyclass.rst +++ b/cuda_core/docs/source/_templates/autosummary/cyclass.rst @@ -1,4 +1,4 @@ -.. SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +.. SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. .. SPDX-License-Identifier: Apache-2.0 {{ fullname | escape | underline}} diff --git a/cuda_core/docs/source/release.rst b/cuda_core/docs/source/release.rst index f7c9f19c776..b8726b4d661 100644 --- a/cuda_core/docs/source/release.rst +++ b/cuda_core/docs/source/release.rst @@ -1,4 +1,4 @@ -.. SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +.. SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. .. SPDX-License-Identifier: Apache-2.0 Release Notes diff --git a/cuda_core/docs/source/release/0.3.0-notes.rst b/cuda_core/docs/source/release/0.3.0-notes.rst index d59f912743d..057664a56ff 100644 --- a/cuda_core/docs/source/release/0.3.0-notes.rst +++ b/cuda_core/docs/source/release/0.3.0-notes.rst @@ -1,4 +1,4 @@ -.. SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +.. SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. .. SPDX-License-Identifier: Apache-2.0 .. currentmodule:: cuda.core.experimental diff --git a/cuda_core/docs/source/release/0.4.0-notes.rst b/cuda_core/docs/source/release/0.4.0-notes.rst index cbe3d432400..f7d69dc5d58 100644 --- a/cuda_core/docs/source/release/0.4.0-notes.rst +++ b/cuda_core/docs/source/release/0.4.0-notes.rst @@ -1,4 +1,4 @@ -.. SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +.. SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. .. SPDX-License-Identifier: Apache-2.0 .. currentmodule:: cuda.core.experimental diff --git a/cuda_core/docs/source/release/0.5.1-notes.rst b/cuda_core/docs/source/release/0.5.1-notes.rst index a52b88d1380..538b838a3ca 100644 --- a/cuda_core/docs/source/release/0.5.1-notes.rst +++ b/cuda_core/docs/source/release/0.5.1-notes.rst @@ -1,4 +1,4 @@ -.. SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +.. SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. .. SPDX-License-Identifier: Apache-2.0 .. currentmodule:: cuda.core diff --git a/cuda_core/tests/cython/build_tests.sh b/cuda_core/tests/cython/build_tests.sh index 7ee65c50d68..72403baf571 100755 --- a/cuda_core/tests/cython/build_tests.sh +++ b/cuda_core/tests/cython/build_tests.sh @@ -1,7 +1,7 @@ #!/bin/bash set -eo pipefail -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 UNAME=$(uname) diff --git a/cuda_core/tests/helpers/layout.py b/cuda_core/tests/helpers/layout.py index e4102f4d45a..3e2aa6d2241 100644 --- a/cuda_core/tests/helpers/layout.py +++ b/cuda_core/tests/helpers/layout.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # SPDX-License-Identifier: Apache-2.0 diff --git a/cuda_core/tests/test_context.py b/cuda_core/tests/test_context.py index 66f7582ea7e..53182520e24 100644 --- a/cuda_core/tests/test_context.py +++ b/cuda_core/tests/test_context.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 import pytest diff --git a/cuda_pathfinder/cuda/pathfinder/_utils/env_vars.py b/cuda_pathfinder/cuda/pathfinder/_utils/env_vars.py index 12198ac9f7f..b8c3267d541 100644 --- a/cuda_pathfinder/cuda/pathfinder/_utils/env_vars.py +++ b/cuda_pathfinder/cuda/pathfinder/_utils/env_vars.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 """Centralized CUDA environment variable handling. diff --git a/cuda_pathfinder/cuda/pathfinder/_utils/find_sub_dirs.py b/cuda_pathfinder/cuda/pathfinder/_utils/find_sub_dirs.py index ebb7b13f488..55705ba6393 100644 --- a/cuda_pathfinder/cuda/pathfinder/_utils/find_sub_dirs.py +++ b/cuda_pathfinder/cuda/pathfinder/_utils/find_sub_dirs.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 import functools diff --git a/cuda_pathfinder/cuda/pathfinder/_utils/platform_aware.py b/cuda_pathfinder/cuda/pathfinder/_utils/platform_aware.py index af0610a6cdb..be55b6ff9c3 100644 --- a/cuda_pathfinder/cuda/pathfinder/_utils/platform_aware.py +++ b/cuda_pathfinder/cuda/pathfinder/_utils/platform_aware.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 import sys diff --git a/cuda_pathfinder/tests/conftest.py b/cuda_pathfinder/tests/conftest.py index e8a5e11b391..d88e22b1959 100644 --- a/cuda_pathfinder/tests/conftest.py +++ b/cuda_pathfinder/tests/conftest.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 diff --git a/cuda_pathfinder/tests/test_utils_env_vars.py b/cuda_pathfinder/tests/test_utils_env_vars.py index 99a55324fc0..e675f243b4a 100644 --- a/cuda_pathfinder/tests/test_utils_env_vars.py +++ b/cuda_pathfinder/tests/test_utils_env_vars.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 import os diff --git a/cuda_pathfinder/tests/test_utils_find_sub_dirs.py b/cuda_pathfinder/tests/test_utils_find_sub_dirs.py index 56dab23dc42..7714e898649 100644 --- a/cuda_pathfinder/tests/test_utils_find_sub_dirs.py +++ b/cuda_pathfinder/tests/test_utils_find_sub_dirs.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 from pathlib import Path From dd0ac2f89a610986bdaaa180bbbbc89ae9b10913 Mon Sep 17 00:00:00 2001 From: Ralf Juengling Date: Tue, 3 Nov 2026 15:47:42 -0800 Subject: [PATCH 3/8] toolshed: make check_spdx --fix use the last-modified year and add tests --fix wrote the current year even for files whose last change was in an earlier year, which would re-date untouched files whenever the year rolled over. Use the same last_modified_year() the check compares against, and report it in the OUTDATED message. Add ci/tools/tests/test_check_spdx.py, which builds temporary git repositories with controlled commit dates and covers committed, staged, unstaged and untracked files, plus --fix. Install pathspec in the nightly CI-tools job so the tests run there instead of skipping. --- .github/workflows/ci-nightly.yml | 2 +- ci/tools/tests/test_check_spdx.py | 170 ++++++++++++++++++++++++++++++ toolshed/check_spdx.py | 7 +- 3 files changed, 175 insertions(+), 4 deletions(-) create mode 100644 ci/tools/tests/test_check_spdx.py diff --git a/.github/workflows/ci-nightly.yml b/.github/workflows/ci-nightly.yml index 0abfdd626cb..75425ef0627 100644 --- a/.github/workflows/ci-nightly.yml +++ b/.github/workflows/ci-nightly.yml @@ -65,7 +65,7 @@ jobs: - name: Run CI tools unit tests run: | - python -m pip install pytest pyyaml==6.0.3 + python -m pip install pytest pyyaml==6.0.3 pathspec==0.12.1 # Standalone CI tool tests; skip repo-root conftest.py (imports cuda.pathfinder). python -m pytest -v --noconftest ci/tools/tests diff --git a/ci/tools/tests/test_check_spdx.py b/ci/tools/tests/test_check_spdx.py new file mode 100644 index 00000000000..17cca9743b5 --- /dev/null +++ b/ci/tools/tests/test_check_spdx.py @@ -0,0 +1,170 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for the copyright-year rule in toolshed/check_spdx.py. + +The rule must give the same answer however the hook is invoked (``git commit``, +``pre-commit run --all-files``, CI), so each test builds a throwaway git repository +with controlled commit dates and runs the script against it. Years are derived from +the current year so the tests do not expire. + + python -m pytest ci/tools/tests/test_check_spdx.py -v --noconftest +""" + +from __future__ import annotations + +import datetime +import os +import shutil +import subprocess +import sys +from pathlib import Path + +import pytest + +pytest.importorskip("pathspec", reason="check_spdx.py requires pathspec") + +REPO = Path(__file__).resolve().parents[3] +CHECKER = REPO / "toolshed" / "check_spdx.py" +LICENSE = REPO / "LICENSE" + +CURRENT_YEAR = datetime.datetime.now(tz=datetime.UTC).year +SAMPLE = "toolshed/sample.py" + +CONTENT = ( + "# SPDX-FileCopyrightText: Copyright (c) {years} NVIDIA CORPORATION & AFFILIATES. All rights reserved.\n" + "# SPDX-License-Identifier: Apache-2.0\n" + "\n" + "pass\n" +) + + +def git(repo, *args, year=None): + env = dict(os.environ) + env.update( + GIT_AUTHOR_NAME="Test", + GIT_AUTHOR_EMAIL="test@example.com", + GIT_COMMITTER_NAME="Test", + GIT_COMMITTER_EMAIL="test@example.com", + ) + if year is not None: + date = f"{year}-06-15T12:00:00+0000" + env.update(GIT_AUTHOR_DATE=date, GIT_COMMITTER_DATE=date) + subprocess.run( # noqa: S603 + ["git", "-c", "commit.gpgsign=false", *args], # noqa: S607 + cwd=repo, + env=env, + check=True, + capture_output=True, + ) + + +def commit(repo, relpath, years, *, year): + path = repo / relpath + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(CONTENT.format(years=years), encoding="utf-8") + git(repo, "add", relpath) + git(repo, "commit", "-m", f"add {relpath}", year=year) + return path + + +def run_checker(repo, *args): + # The script reads LICENSE and runs git relative to the working directory, + # as pre-commit does from the repository root. + return subprocess.run( # noqa: S603 + [sys.executable, str(CHECKER), *args, SAMPLE], + cwd=repo, + capture_output=True, + text=True, + check=False, + ) + + +@pytest.fixture +def repo(tmp_path): + git(tmp_path, "init") + shutil.copy(LICENSE, tmp_path / "LICENSE") + git(tmp_path, "add", "LICENSE") + git(tmp_path, "commit", "-m", "init", year=CURRENT_YEAR - 5) + return tmp_path + + +@pytest.mark.agent_authored(model="claude-sonnet-5.5") +@pytest.mark.parametrize( + ("commit_age", "copyright_age", "passes"), + [ + pytest.param(1, 1, True, id="untouched-since-last-year"), + pytest.param(0, 0, True, id="committed-this-year-current-copyright"), + pytest.param(1, 0, True, id="copyright-ahead-of-commit"), + # The bug in #2273: committed this year, nothing staged, stale copyright. + pytest.param(0, 1, False, id="committed-this-year-stale-copyright"), + pytest.param(2, 3, False, id="stale-relative-to-its-own-last-commit"), + ], +) +def test_committed_file_is_checked_against_its_last_commit_year(repo, commit_age, copyright_age, passes): + commit(repo, SAMPLE, str(CURRENT_YEAR - copyright_age), year=CURRENT_YEAR - commit_age) + + result = run_checker(repo) + + assert (result.returncode == 0) is passes, result.stdout + result.stderr + assert ("OUTDATED" in result.stdout) is (not passes) + + +@pytest.mark.agent_authored(model="claude-sonnet-5.5") +def test_unstaged_edit_requires_current_year(repo): + path = commit(repo, SAMPLE, str(CURRENT_YEAR - 1), year=CURRENT_YEAR - 1) + path.write_text(path.read_text(encoding="utf-8") + "# edited\n", encoding="utf-8") + + result = run_checker(repo) + + assert result.returncode == 1, result.stdout + result.stderr + assert f"(expected {CURRENT_YEAR})" in result.stdout + + +@pytest.mark.agent_authored(model="claude-sonnet-5.5") +def test_staged_edit_requires_current_year(repo): + path = commit(repo, SAMPLE, str(CURRENT_YEAR - 1), year=CURRENT_YEAR - 1) + path.write_text(path.read_text(encoding="utf-8") + "# edited\n", encoding="utf-8") + git(repo, "add", SAMPLE) + + result = run_checker(repo) + + assert result.returncode == 1, result.stdout + result.stderr + assert f"(expected {CURRENT_YEAR})" in result.stdout + + +@pytest.mark.agent_authored(model="claude-sonnet-5.5") +def test_untracked_file_requires_current_year(repo): + path = repo / SAMPLE + path.parent.mkdir(parents=True) + path.write_text(CONTENT.format(years=CURRENT_YEAR - 1), encoding="utf-8") + + result = run_checker(repo) + + assert result.returncode == 1, result.stdout + result.stderr + assert f"(expected {CURRENT_YEAR})" in result.stdout + + +@pytest.mark.agent_authored(model="claude-sonnet-5.5") +def test_fix_writes_last_commit_year_not_current_year(repo): + path = commit(repo, SAMPLE, str(CURRENT_YEAR - 3), year=CURRENT_YEAR - 2) + + result = run_checker(repo, "--fix") + + # --fix reports that it changed something, as the hook always has. + assert result.returncode == 1, result.stdout + result.stderr + assert f"Copyright (c) {CURRENT_YEAR - 3}-{CURRENT_YEAR - 2} " in path.read_text(encoding="utf-8") + + +@pytest.mark.agent_authored(model="claude-sonnet-5.5") +def test_fix_writes_current_year_for_modified_file(repo): + path = commit(repo, SAMPLE, str(CURRENT_YEAR - 3), year=CURRENT_YEAR - 2) + path.write_text(path.read_text(encoding="utf-8") + "# edited\n", encoding="utf-8") + + result = run_checker(repo, "--fix") + + assert result.returncode == 1, result.stdout + result.stderr + assert f"Copyright (c) {CURRENT_YEAR - 3}-{CURRENT_YEAR} " in path.read_text(encoding="utf-8") + + # The rewrite is stable: a second run accepts the file. + assert run_checker(repo).returncode == 0 diff --git a/toolshed/check_spdx.py b/toolshed/check_spdx.py index e85ee24c8b8..7e1f1af44d8 100644 --- a/toolshed/check_spdx.py +++ b/toolshed/check_spdx.py @@ -175,14 +175,15 @@ def validate_or_fix_copyright(filepath, blob, fix): else: start_year = end_year = years - if int(end_year) >= last_modified_year(filepath): + modified_year = last_modified_year(filepath) + if int(end_year) >= modified_year: return True, blob - print(f"OUTDATED copyright {years!r} (expected {CURRENT_YEAR!r}) in {filepath!r}") + print(f"OUTDATED copyright {years!r} (expected {modified_year!r}) in {filepath!r}") if not fix: return False, blob - new_years = f"{start_year}-{CURRENT_YEAR}" + new_years = f"{start_year}-{modified_year}" return ( False, re.sub( From ff2962b6925e6b16f42a10193e5e5044bdad30ec Mon Sep 17 00:00:00 2001 From: Ralf Juengling Date: Tue, 3 Nov 2026 15:50:30 -0800 Subject: [PATCH 4/8] ci/tools/tests: trim check_spdx tests to the cases that pin distinct behavior Drop cases that exercise the same git-status branch or restate another test: unstaged and untracked edits, the current-copyright happy path, copyright-ahead-of-commit, and the stale-relative-to-last-commit case already pinned by the --fix test. Fold the staged-edit check and the --fix rewrite of a modified file into one test. --- ci/tools/tests/test_check_spdx.py | 57 +++++++------------------------ 1 file changed, 13 insertions(+), 44 deletions(-) diff --git a/ci/tools/tests/test_check_spdx.py b/ci/tools/tests/test_check_spdx.py index 17cca9743b5..7b22c95d523 100644 --- a/ci/tools/tests/test_check_spdx.py +++ b/ci/tools/tests/test_check_spdx.py @@ -93,12 +93,10 @@ def repo(tmp_path): @pytest.mark.parametrize( ("commit_age", "copyright_age", "passes"), [ + # Not modified this year, so last year's copyright is still correct. pytest.param(1, 1, True, id="untouched-since-last-year"), - pytest.param(0, 0, True, id="committed-this-year-current-copyright"), - pytest.param(1, 0, True, id="copyright-ahead-of-commit"), # The bug in #2273: committed this year, nothing staged, stale copyright. pytest.param(0, 1, False, id="committed-this-year-stale-copyright"), - pytest.param(2, 3, False, id="stale-relative-to-its-own-last-commit"), ], ) def test_committed_file_is_checked_against_its_last_commit_year(repo, commit_age, copyright_age, passes): @@ -111,38 +109,23 @@ def test_committed_file_is_checked_against_its_last_commit_year(repo, commit_age @pytest.mark.agent_authored(model="claude-sonnet-5.5") -def test_unstaged_edit_requires_current_year(repo): - path = commit(repo, SAMPLE, str(CURRENT_YEAR - 1), year=CURRENT_YEAR - 1) - path.write_text(path.read_text(encoding="utf-8") + "# edited\n", encoding="utf-8") - - result = run_checker(repo) - - assert result.returncode == 1, result.stdout + result.stderr - assert f"(expected {CURRENT_YEAR})" in result.stdout - - -@pytest.mark.agent_authored(model="claude-sonnet-5.5") -def test_staged_edit_requires_current_year(repo): - path = commit(repo, SAMPLE, str(CURRENT_YEAR - 1), year=CURRENT_YEAR - 1) +def test_staged_edit_requires_current_year_and_fix_writes_it(repo): + # The `git commit` path: the edited file is staged when the hook runs. + path = commit(repo, SAMPLE, str(CURRENT_YEAR - 3), year=CURRENT_YEAR - 2) path.write_text(path.read_text(encoding="utf-8") + "# edited\n", encoding="utf-8") git(repo, "add", SAMPLE) - result = run_checker(repo) - - assert result.returncode == 1, result.stdout + result.stderr - assert f"(expected {CURRENT_YEAR})" in result.stdout - + check = run_checker(repo) + assert check.returncode == 1, check.stdout + check.stderr + assert f"(expected {CURRENT_YEAR})" in check.stdout -@pytest.mark.agent_authored(model="claude-sonnet-5.5") -def test_untracked_file_requires_current_year(repo): - path = repo / SAMPLE - path.parent.mkdir(parents=True) - path.write_text(CONTENT.format(years=CURRENT_YEAR - 1), encoding="utf-8") - - result = run_checker(repo) + # --fix reports that it changed something, as the hook always has. + fix = run_checker(repo, "--fix") + assert fix.returncode == 1, fix.stdout + fix.stderr + assert f"Copyright (c) {CURRENT_YEAR - 3}-{CURRENT_YEAR} " in path.read_text(encoding="utf-8") - assert result.returncode == 1, result.stdout + result.stderr - assert f"(expected {CURRENT_YEAR})" in result.stdout + # The rewrite is stable: a second run accepts the file. + assert run_checker(repo).returncode == 0 @pytest.mark.agent_authored(model="claude-sonnet-5.5") @@ -154,17 +137,3 @@ def test_fix_writes_last_commit_year_not_current_year(repo): # --fix reports that it changed something, as the hook always has. assert result.returncode == 1, result.stdout + result.stderr assert f"Copyright (c) {CURRENT_YEAR - 3}-{CURRENT_YEAR - 2} " in path.read_text(encoding="utf-8") - - -@pytest.mark.agent_authored(model="claude-sonnet-5.5") -def test_fix_writes_current_year_for_modified_file(repo): - path = commit(repo, SAMPLE, str(CURRENT_YEAR - 3), year=CURRENT_YEAR - 2) - path.write_text(path.read_text(encoding="utf-8") + "# edited\n", encoding="utf-8") - - result = run_checker(repo, "--fix") - - assert result.returncode == 1, result.stdout + result.stderr - assert f"Copyright (c) {CURRENT_YEAR - 3}-{CURRENT_YEAR} " in path.read_text(encoding="utf-8") - - # The rewrite is stable: a second run accepts the file. - assert run_checker(repo).returncode == 0 From 6b27ed4382badd5b63fded84aa35704f886fd1a1 Mon Sep 17 00:00:00 2001 From: Ralf Juengling Date: Tue, 3 Nov 2026 15:57:58 -0800 Subject: [PATCH 5/8] toolshed: make check_spdx --fix always write the current year Writing the last-commit year for a clean, stale file is undone by committing the fix, since that commit is itself a change made this year (the cycle described in #1514). Revert to writing the current year and restore the original OUTDATED message. Replace the last-commit-year test with one that fixes a clean stale file, commits the result, and checks that it is accepted. --- ci/tools/tests/test_check_spdx.py | 19 +++++++++++++------ toolshed/check_spdx.py | 9 +++++---- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/ci/tools/tests/test_check_spdx.py b/ci/tools/tests/test_check_spdx.py index 7b22c95d523..3f281dea13f 100644 --- a/ci/tools/tests/test_check_spdx.py +++ b/ci/tools/tests/test_check_spdx.py @@ -117,7 +117,7 @@ def test_staged_edit_requires_current_year_and_fix_writes_it(repo): check = run_checker(repo) assert check.returncode == 1, check.stdout + check.stderr - assert f"(expected {CURRENT_YEAR})" in check.stdout + assert f"(expected '{CURRENT_YEAR}')" in check.stdout # --fix reports that it changed something, as the hook always has. fix = run_checker(repo, "--fix") @@ -129,11 +129,18 @@ def test_staged_edit_requires_current_year_and_fix_writes_it(repo): @pytest.mark.agent_authored(model="claude-sonnet-5.5") -def test_fix_writes_last_commit_year_not_current_year(repo): +def test_fix_of_clean_stale_file_survives_being_committed(repo): + # Stale relative to an old commit, with nothing staged (for example a manual + # `pre-commit run --all-files`). path = commit(repo, SAMPLE, str(CURRENT_YEAR - 3), year=CURRENT_YEAR - 2) - result = run_checker(repo, "--fix") + fix = run_checker(repo, "--fix") + assert fix.returncode == 1, fix.stdout + fix.stderr + # Not the older last-commit year: committing the fix is a change made this year. + assert f"Copyright (c) {CURRENT_YEAR - 3}-{CURRENT_YEAR} " in path.read_text(encoding="utf-8") - # --fix reports that it changed something, as the hook always has. - assert result.returncode == 1, result.stdout + result.stderr - assert f"Copyright (c) {CURRENT_YEAR - 3}-{CURRENT_YEAR - 2} " in path.read_text(encoding="utf-8") + git(repo, "add", SAMPLE) + git(repo, "commit", "-m", "fix copyright", year=CURRENT_YEAR) + + # The fix must not demand another fix once it is part of this year's history. + assert run_checker(repo).returncode == 0 diff --git a/toolshed/check_spdx.py b/toolshed/check_spdx.py index 7e1f1af44d8..25ef50fe4c8 100644 --- a/toolshed/check_spdx.py +++ b/toolshed/check_spdx.py @@ -175,15 +175,16 @@ def validate_or_fix_copyright(filepath, blob, fix): else: start_year = end_year = years - modified_year = last_modified_year(filepath) - if int(end_year) >= modified_year: + if int(end_year) >= last_modified_year(filepath): return True, blob - print(f"OUTDATED copyright {years!r} (expected {modified_year!r}) in {filepath!r}") + print(f"OUTDATED copyright {years!r} (expected {CURRENT_YEAR!r}) in {filepath!r}") if not fix: return False, blob - new_years = f"{start_year}-{modified_year}" + # Always write the current year: committing the fix is itself a change made + # this year, so an older year would be stale again immediately. + new_years = f"{start_year}-{CURRENT_YEAR}" return ( False, re.sub( From 8f6000ac953ee2c413a116a6b98b1e9dcef46500 Mon Sep 17 00:00:00 2001 From: Ralf Juengling Date: Tue, 3 Nov 2026 16:06:54 -0800 Subject: [PATCH 6/8] ci: use a blobless full-history checkout for pre-commit check_spdx.py needs commit history to find when each file last changed. Correct the comment: in a shallow clone every file appears to be introduced by the tip commit, so each would be judged against the tip commit's year (not a false pass, as the comment previously claimed). Add filter: blob:none so the job fetches all commits and trees but not historical file contents, which a path-limited git log does not read. --- .github/workflows/pre-commit.yml | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index 7db8d993e13..b8efab0f610 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -58,14 +58,17 @@ jobs: # PR workflows run from the merge revision; check out that same revision # so their helper scripts and configuration stay in sync. - # Full history is required so that check_spdx.py can call `git log -1` - # to determine when each file was last modified (used for copyright-year - # enforcement). A shallow clone would return no history for files not - # touched in the tip commit, causing false passes on stale years. + # check_spdx.py runs `git log -1 -- ` to find the year each file last + # changed (copyright-year enforcement), so it needs the full commit history. + # In a shallow clone every file looks like it was introduced by the tip + # commit, so each would be judged against the tip commit's year. + # blob:none keeps all commits and trees but skips historical file contents, + # which a path-limited `git log` does not read. - name: Checkout repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 + filter: blob:none persist-credentials: false - name: Set up Python From 0f8ff10f3e7191cca67f760654c033ba7c5debb4 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Tue, 3 Nov 2026 23:19:19 -0800 Subject: [PATCH 7/8] toolshed: make SPDX history lookups independent of Git display settings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit — codex GPT-6 on behalf of Ralf — Read committer timestamps and convert them explicitly to UTC so clean files use the same year boundary as staged changes. Disable signature display so signed commits cannot contaminate the timestamp parsed by the checker. Cover both New Year timezone directions and signed history with regression tests. All 312 CI-tool tests and the full pre-commit suite pass. Signed-off-by: Ralf W. Grosse-Kunstleve --- ci/tools/tests/test_check_spdx.py | 49 +++++++++++++++++++++++++++++++ toolshed/check_spdx.py | 8 ++--- 2 files changed, 53 insertions(+), 4 deletions(-) diff --git a/ci/tools/tests/test_check_spdx.py b/ci/tools/tests/test_check_spdx.py index 3f281dea13f..b5e7afd6fa8 100644 --- a/ci/tools/tests/test_check_spdx.py +++ b/ci/tools/tests/test_check_spdx.py @@ -144,3 +144,52 @@ def test_fix_of_clean_stale_file_survives_being_committed(repo): # The fix must not demand another fix once it is part of this year's history. assert run_checker(repo).returncode == 0 + + +@pytest.mark.parametrize( + ("date", "passes"), + [ + pytest.param(f"{CURRENT_YEAR - 1}-12-31T23:30:00-0800", False, id="utc-year-after-recorded-year"), + pytest.param(f"{CURRENT_YEAR}-01-01T00:30:00+1400", True, id="utc-year-before-recorded-year"), + ], +) +@pytest.mark.agent_authored(model="gpt-6") +def test_committed_file_uses_utc_year_across_new_year(repo, monkeypatch, date, passes): + monkeypatch.setenv("GIT_AUTHOR_DATE", date) + monkeypatch.setenv("GIT_COMMITTER_DATE", date) + commit(repo, SAMPLE, str(CURRENT_YEAR - 1), year=None) + + result = run_checker(repo) + + assert (result.returncode == 0) is passes, result.stdout + result.stderr + assert ("OUTDATED" in result.stdout) is (not passes) + + +@pytest.mark.agent_authored(model="gpt-6") +def test_signed_last_commit_with_show_signature_enabled(repo): + ssh_keygen = shutil.which("ssh-keygen") + if ssh_keygen is None: + pytest.skip("ssh-keygen is required to create a signed commit") + + signing_key = repo / "signing_key" + subprocess.run( # noqa: S603 + [ssh_keygen, "-q", "-t", "ed25519", "-N", "", "-f", str(signing_key)], + check=True, + capture_output=True, + ) + allowed_signers = repo / "allowed_signers" + allowed_signers.write_text( + "test@example.com " + signing_key.with_suffix(".pub").read_text(encoding="utf-8"), encoding="utf-8" + ) + git(repo, "config", "gpg.format", "ssh") + git(repo, "config", "user.signingkey", str(signing_key)) + git(repo, "config", "gpg.ssh.allowedSignersFile", str(allowed_signers)) + git(repo, "config", "log.showSignature", "true") + path = commit(repo, SAMPLE, str(CURRENT_YEAR), year=CURRENT_YEAR) + path.write_text(path.read_text(encoding="utf-8") + "# signed edit\n", encoding="utf-8") + git(repo, "add", SAMPLE) + git(repo, "-c", "commit.gpgsign=true", "commit", "-m", "signed edit", year=CURRENT_YEAR) + + result = run_checker(repo) + + assert result.returncode == 0, result.stdout + result.stderr diff --git a/toolshed/check_spdx.py b/toolshed/check_spdx.py index 25ef50fe4c8..03733ba533f 100644 --- a/toolshed/check_spdx.py +++ b/toolshed/check_spdx.py @@ -80,15 +80,15 @@ def last_modified_year(filepath): ) if status.stdout.strip(): return int(CURRENT_YEAR) - # Find the most recent commit that touched this file. + # Use the last commit's UTC year, matching the year for working-tree changes. log = subprocess.run( # noqa: S603 - ["git", "log", "-1", "--format=%cd", "--date=format:%Y", "--", filepath], # noqa: S607 + ["git", "log", "-1", "--no-show-signature", "--format=%ct", "--", filepath], # noqa: S607 capture_output=True, text=True, ) - year_str = log.stdout.strip() + timestamp = log.stdout.strip() # Fall back to current year for brand-new untracked files with no git history. - return int(year_str) if year_str else int(CURRENT_YEAR) + return datetime.datetime.fromtimestamp(int(timestamp), tz=datetime.UTC).year if timestamp else int(CURRENT_YEAR) def normalize_repo_path(filepath): From 93b8f25c251716be391ded8bc02373b949229a6f Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Wed, 4 Nov 2026 01:06:12 -0800 Subject: [PATCH 8/8] test(cuda.core): serialize pool trimming around the driver race MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit — codex GPT-6 on behalf of Ralf — Mark only test_mempool_trim thread-unsafe, as proposed in gh-3350. Concurrent legacy-default-stream pool teardown and context synchronization reproduce a native driver crash without cuda.core; this test hit that failure twice in the Windows A100 CI configuration. Keep all parameter cases and assertions enabled while avoiding the known concurrent lifecycle. Full pre-commit validation passes. Signed-off-by: Ralf W. Grosse-Kunstleve --- cuda_core/tests/test_memory.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cuda_core/tests/test_memory.py b/cuda_core/tests/test_memory.py index a2afff99469..7eb92608d1c 100644 --- a/cuda_core/tests/test_memory.py +++ b/cuda_core/tests/test_memory.py @@ -2946,6 +2946,7 @@ def test_mempool_watermark_reset(memory_resource_factory, kind): mr.close() +@pytest.mark.thread_unsafe(reason="CUDA legacy-default-stream mempool teardown/sync race (gh-3350)") @pytest.mark.agent_authored(model="claude-opus-5-5") def test_mempool_trim(memory_resource_factory): device = Device()