Skip to content

Commit 235479d

Browse files
committed
feat(deps): add version discrepancy detection and composite action
- Add composite action for dependency extraction setup to reduce duplication - Implement version discrepancy detection to identify dependencies pinned at different versions across the repo - Output GitHub Actions warning annotations for CI visibility - Add normalize_dependency_name() to handle common naming variations - Add detect_version_discrepancies() to identify version conflicts - Update workflows to use the new composite action - Display discrepancy warnings in extraction summary with actionable tips Addresses nv-tusharma's feedback about using composite actions. Adds warning system as requested to detect version conflicts that could cause runtime issues, build failures, or security vulnerabilities. Related: DYN-1235 Signed-off-by: Dan Gil <[email protected]>
1 parent 419c3b1 commit 235479d

File tree

4 files changed

+197
-10
lines changed

4 files changed

+197
-10
lines changed
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
name: 'Dependency Extraction Setup'
17+
description: 'Set up Python environment and install dependencies for dependency extraction'
18+
inputs:
19+
python-version:
20+
description: 'Python version to use'
21+
required: false
22+
default: '3.12'
23+
24+
runs:
25+
using: "composite"
26+
steps:
27+
- name: Set up Python
28+
uses: actions/setup-python@v5
29+
with:
30+
python-version: ${{ inputs.python-version }}
31+
32+
- name: Install dependencies
33+
shell: bash
34+
run: pip install pyyaml
35+

.github/workflows/dependency-extraction-nightly.yml

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,11 @@ jobs:
3535
with:
3636
fetch-depth: 0 # Need history for comparison
3737

38-
- name: Set up Python
39-
uses: actions/setup-python@v5
38+
- name: Setup dependency extraction environment
39+
uses: ./.github/actions/dependency-extraction-setup
4040
with:
4141
python-version: '3.12'
4242

43-
- name: Install dependencies
44-
run: pip install pyyaml
45-
4643
- name: Run dependency extraction
4744
run: |
4845
TIMESTAMP=$(date +%Y%m%d_%H%M)

.github/workflows/dependency-extraction-release.yml

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,11 @@ jobs:
4040
with:
4141
fetch-depth: 0
4242

43-
- name: Set up Python
44-
uses: actions/setup-python@v5
43+
- name: Setup dependency extraction environment
44+
uses: ./.github/actions/dependency-extraction-setup
4545
with:
4646
python-version: '3.12'
4747

48-
- name: Install dependencies
49-
run: pip install pyyaml
50-
5148
- name: Extract version from branch or input
5249
id: version
5350
run: |

.github/workflows/extract_dependency_versions.py

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1833,6 +1833,123 @@ def write_unversioned_report(self, output_path: Path) -> None:
18331833

18341834
print(f"✓ Written {len(unversioned)} unversioned dependencies to {output_path}")
18351835

1836+
def normalize_dependency_name(self, name: str) -> str:
1837+
"""
1838+
Normalize dependency names to detect the same dependency referred to differently.
1839+
1840+
Examples:
1841+
- torch, pytorch, PyTorch -> pytorch
1842+
- tensorflow, TensorFlow -> tensorflow
1843+
- numpy, NumPy -> numpy
1844+
1845+
Note: This is intentionally conservative to avoid false positives.
1846+
Only normalizes well-known dependencies with common naming variations.
1847+
"""
1848+
# Convert to lowercase for comparison
1849+
name_lower = name.lower()
1850+
1851+
# Common normalization rules (ordered by specificity to avoid false matches)
1852+
normalizations = {
1853+
"tensorrt-llm": "tensorrt-llm",
1854+
"trtllm": "tensorrt-llm",
1855+
"tensorrt": "tensorrt",
1856+
"pytorch": "pytorch",
1857+
"torch": "pytorch",
1858+
"tensorflow": "tensorflow",
1859+
"cuda": "cuda",
1860+
"cudnn": "cudnn",
1861+
"nccl": "nccl",
1862+
"nixl": "nixl",
1863+
}
1864+
1865+
# Check if name matches any normalization rules (exact or starts with)
1866+
for key, normalized in normalizations.items():
1867+
if name_lower == key or name_lower.startswith(key + " "):
1868+
return normalized
1869+
1870+
# Default: return the lowercase name unchanged
1871+
# This avoids false positives from overly broad matching
1872+
return name_lower.strip()
1873+
1874+
def detect_version_discrepancies(self) -> List[Dict[str, any]]:
1875+
"""
1876+
Detect dependencies that appear multiple times with different versions.
1877+
1878+
Returns:
1879+
List of dictionaries containing discrepancy information:
1880+
- dependency_name: The normalized dependency name
1881+
- instances: List of {version, source_file, component} for each occurrence
1882+
"""
1883+
# Group dependencies by normalized name
1884+
dependency_groups = {}
1885+
1886+
for dep in self.dependencies:
1887+
normalized_name = self.normalize_dependency_name(dep["Dependency Name"])
1888+
1889+
# Skip unversioned dependencies for discrepancy detection
1890+
if dep["Version"] in ["unspecified", "N/A", "", "latest"]:
1891+
continue
1892+
1893+
if normalized_name not in dependency_groups:
1894+
dependency_groups[normalized_name] = []
1895+
1896+
dependency_groups[normalized_name].append({
1897+
"original_name": dep["Dependency Name"],
1898+
"version": dep["Version"],
1899+
"source_file": dep["Source File"],
1900+
"component": dep["Component"],
1901+
"category": dep["Category"],
1902+
"critical": dep["Critical"] == "Yes",
1903+
})
1904+
1905+
# Detect discrepancies: same normalized name with different versions
1906+
discrepancies = []
1907+
1908+
for normalized_name, instances in dependency_groups.items():
1909+
# Get unique versions
1910+
versions = set(inst["version"] for inst in instances)
1911+
1912+
# If multiple versions exist, it's a discrepancy
1913+
if len(versions) > 1:
1914+
discrepancies.append({
1915+
"normalized_name": normalized_name,
1916+
"versions": sorted(versions),
1917+
"instances": instances,
1918+
"is_critical": any(inst["critical"] for inst in instances),
1919+
})
1920+
1921+
return discrepancies
1922+
1923+
def _output_github_warnings(self, discrepancies: List[Dict[str, any]]) -> None:
1924+
"""
1925+
Output GitHub Actions warning annotations for version discrepancies.
1926+
1927+
This uses the GitHub Actions workflow command format:
1928+
::warning file={file},line={line}::{message}
1929+
1930+
See: https://docs.github.com/en/actions/reference/workflow-commands-for-github-actions
1931+
"""
1932+
for disc in discrepancies:
1933+
normalized_name = disc["normalized_name"]
1934+
versions = disc["versions"]
1935+
is_critical = disc["is_critical"]
1936+
instances = disc["instances"]
1937+
1938+
# Create a concise message for the annotation
1939+
critical_prefix = "[CRITICAL] " if is_critical else ""
1940+
versions_str = ", ".join(versions)
1941+
1942+
# Output a warning for each source file where the dependency appears
1943+
for inst in instances:
1944+
message = (
1945+
f"{critical_prefix}Version discrepancy detected for '{normalized_name}': "
1946+
f"found {inst['version']} here, but also appears as {versions_str} elsewhere"
1947+
)
1948+
1949+
# Output GitHub Actions warning annotation
1950+
# Format: ::warning file={name}::{message}
1951+
print(f"::warning file={inst['source_file']}::{message}")
1952+
18361953
def print_summary(self) -> None:
18371954
"""Print comprehensive summary statistics."""
18381955
components = {}
@@ -1919,6 +2036,47 @@ def print_summary(self) -> None:
19192036
else:
19202037
print("\n✓ All dependencies have version specifiers")
19212038

2039+
# Check for version discrepancies
2040+
discrepancies = self.detect_version_discrepancies()
2041+
if discrepancies:
2042+
print(
2043+
f"\n⚠️ WARNING: Found {len(discrepancies)} dependencies with version discrepancies!"
2044+
)
2045+
print("\nDependencies pinned at different versions across the repo:")
2046+
2047+
for disc in discrepancies[:10]: # Show first 10
2048+
critical_flag = " [CRITICAL]" if disc["is_critical"] else ""
2049+
print(f"\n{disc['normalized_name']}{critical_flag}")
2050+
print(f" Versions found: {', '.join(disc['versions'])}")
2051+
print(f" Locations:")
2052+
2053+
for inst in disc["instances"][:5]: # Show first 5 instances
2054+
print(
2055+
f" - {inst['version']:15s} in {inst['component']:10s} "
2056+
f"({inst['source_file']})"
2057+
)
2058+
2059+
if len(disc["instances"]) > 5:
2060+
print(f" ... and {len(disc['instances']) - 5} more locations")
2061+
2062+
if len(discrepancies) > 10:
2063+
print(f"\n ... and {len(discrepancies) - 10} more discrepancies")
2064+
2065+
print("\n 💡 Tip: Version discrepancies can cause:")
2066+
print(" - Runtime conflicts and crashes")
2067+
print(" - Unexpected behavior differences between components")
2068+
print(" - Build failures due to incompatible APIs")
2069+
print(" - Security vulnerabilities if older versions are used")
2070+
print(
2071+
"\n Consider standardizing versions across the repo or documenting why "
2072+
"differences are necessary."
2073+
)
2074+
2075+
# Output GitHub Actions warnings for CI visibility
2076+
self._output_github_warnings(discrepancies)
2077+
else:
2078+
print("\n✓ No version discrepancies detected")
2079+
19222080
# Check against baseline and warn if exceeded
19232081
if total_deps > self.baseline_count:
19242082
increase = total_deps - self.baseline_count

0 commit comments

Comments
 (0)