Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 64 additions & 17 deletions .github/scripts/bump-nuget.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,30 @@
#!/usr/bin/env python3
"""
Simplified package bumping for Codebelt service updates (Option B).
Package bumping for Codebelt service updates.

Only updates packages published by the triggering source repo.
Updates packages published by the triggering source repo to the specified version.
Additionally fetches the latest stable version from NuGet for all other Codebelt-related
packages and updates them as well.
Does NOT update Microsoft.Extensions.*, BenchmarkDotNet, or other third-party packages.
Does NOT parse TFM conditions - only bumps Codebelt/Cuemon/Savvyio packages to the triggering version.

Usage:
TRIGGER_SOURCE=cuemon TRIGGER_VERSION=10.3.0 python3 bump-nuget.py

Behavior:
- If TRIGGER_SOURCE is "cuemon" and TRIGGER_VERSION is "10.3.0":
- Cuemon.Core: 10.2.1 → 10.3.0
- Cuemon.Extensions.IO: 10.2.1 → 10.3.0
- Cuemon.Core: 10.2.1 → 10.3.0 (triggered source, set to given version)
- Cuemon.Extensions.IO: 10.2.1 → 10.3.0 (triggered source, set to given version)
- Codebelt.Extensions.BenchmarkDotNet.*: 1.2.3 → <latest from NuGet> (other Codebelt)
- Microsoft.Extensions.Hosting: 9.0.13 → UNCHANGED (not a Codebelt package)
- BenchmarkDotNet: 0.15.8 → UNCHANGED (not a Codebelt package)
"""

import json
import re
import os
import sys
from typing import Dict, List
import urllib.request
from typing import Dict, List, Optional

TRIGGER_SOURCE = os.environ.get("TRIGGER_SOURCE", "")
TRIGGER_VERSION = os.environ.get("TRIGGER_VERSION", "")
Expand All @@ -31,21 +35,24 @@
"xunit": ["Codebelt.Extensions.Xunit"],
"benchmarkdotnet": ["Codebelt.Extensions.BenchmarkDotNet"],
"bootstrapper": ["Codebelt.Bootstrapper"],
"carter": ["Codebelt.Extensions.Carter"],
"newtonsoft-json": [
"Codebelt.Extensions.Newtonsoft.Json",
"Codebelt.Extensions.AspNetCore.Newtonsoft.Json",
"Codebelt.Extensions.AspNetCore.Mvc.Formatters.Newtonsoft",
],
"aws-signature-v4": ["Codebelt.Extensions.AspNetCore.Authentication.AwsSignature"],
"unitify": ["Codebelt.Unitify"],
"yamldotnet": [
"Codebelt.Extensions.YamlDotNet",
"Codebelt.Extensions.AspNetCore.Text.Yaml",
"Codebelt.Extensions.AspNetCore.Mvc.Formatters.Text.Yaml",
],
"globalization": ["Codebelt.Extensions.Globalization"],
"asp-versioning": ["Codebelt.Extensions.Asp.Versioning"],
"swashbuckle-aspnetcore": ["Codebelt.Extensions.Swashbuckle"],
"savvyio": ["Savvyio."],
"shared-kernel": [],
"shared-kernel": ["Codebelt.SharedKernel"],
}


Expand All @@ -57,6 +64,38 @@ def is_triggered_package(package_name: str) -> bool:
return any(package_name.startswith(prefix) for prefix in prefixes)


def is_codebelt_package(package_name: str) -> bool:
"""Check if package belongs to any Codebelt repo (regardless of trigger source)."""
for repo_prefixes in SOURCE_PACKAGE_MAP.values():
if any(package_name.startswith(prefix) for prefix in repo_prefixes if prefix):
return True
return False


_nuget_version_cache: Dict[str, Optional[str]] = {}


def get_latest_nuget_version(package_name: str) -> Optional[str]:
"""Fetch the latest stable version of a package from NuGet."""
if package_name in _nuget_version_cache:
return _nuget_version_cache[package_name]

url = f"https://api.nuget.org/v3-flatcontainer/{package_name.lower()}/index.json"
try:
with urllib.request.urlopen(url, timeout=15) as response:
data = json.loads(response.read())
versions = data.get("versions", [])
# Stable versions have no hyphen (no pre-release suffix)
stable = [v for v in versions if "-" not in v]
result = stable[-1] if stable else (versions[-1] if versions else None)
except Exception as exc:
print(f" Warning: Could not fetch latest version for {package_name}: {exc}")
result = None
Comment on lines +91 to +93

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

NuGet lookup failures are currently treated as successful runs.

On Lines 91–93, lookup errors are downgraded to warnings; on Line 176, the script always exits with success. This can hide failed package resolution and let CI pass with stale versions.

Proposed fix
 _nuget_version_cache: Dict[str, Optional[str]] = {}
+_nuget_lookup_failures: List[str] = []

 def get_latest_nuget_version(package_name: str) -> Optional[str]:
@@
     except Exception as exc:
         print(f"  Warning: Could not fetch latest version for {package_name}: {exc}")
+        _nuget_lookup_failures.append(package_name)
         result = None
@@
-    return 0 if changes else 0  # Return 0 even if no changes (not an error)
+    if _nuget_lookup_failures:
+        print()
+        print(
+            f"Error: Failed to resolve NuGet versions for {len(set(_nuget_lookup_failures))} package(s)."
+        )
+        return 1
+    return 0

Also applies to: 176-176

🧰 Tools
🪛 Ruff (0.15.2)

[warning] 91-91: Do not catch blind exception: Exception

(BLE001)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/scripts/bump-nuget.py around lines 91 - 93, The except block that
catches Exception and sets result = None for package_name swallows lookup
failures; update it to record the failure (e.g., set a failure flag or append
package_name to a failures list) or re-raise after logging so the error isn't
treated as success, and ensure the top-level exit logic (currently always
exiting successfully) checks that failure flag/list and calls sys.exit(1) when
any NuGet lookups failed; update the code paths around result, package_name, and
the fetch function (e.g., fetch_latest_version) so a failed lookup causes a
non-zero exit instead of silently passing.


_nuget_version_cache[package_name] = result
Comment on lines +91 to +95

Copilot AI Feb 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On NuGet lookup failure, the exception path sets result=None and the caller silently keeps the existing package version. This can lead to confusing runs (e.g., no updates applied even though an update exists). Consider tracking lookup failures and surfacing them in the summary (or optionally failing the run for unresolved Codebelt packages).

Copilot uses AI. Check for mistakes.
return result


def main():
if not TRIGGER_SOURCE or not TRIGGER_VERSION:
print(
Expand All @@ -70,7 +109,7 @@ def main():
target_version = TRIGGER_VERSION.lstrip("v")

print(f"Trigger: {TRIGGER_SOURCE} @ {target_version}")
print(f"Only updating packages from: {TRIGGER_SOURCE}")
print(f"Triggered packages set to {target_version}; other Codebelt packages fetched from NuGet.")
print()

try:
Expand All @@ -87,16 +126,24 @@ def replace_version(m: re.Match) -> str:
pkg = m.group(1)
current = m.group(2)

if not is_triggered_package(pkg):
skipped_third_party.append(f" {pkg} (skipped - not from {TRIGGER_SOURCE})")
if is_triggered_package(pkg):
if target_version != current:
changes.append(f" {pkg}: {current} → {target_version}")
return m.group(0).replace(
f'Version="{current}"', f'Version="{target_version}"'
)
return m.group(0)

if target_version != current:
changes.append(f" {pkg}: {current} → {target_version}")
return m.group(0).replace(
f'Version="{current}"', f'Version="{target_version}"'
)
if is_codebelt_package(pkg):
latest = get_latest_nuget_version(pkg)
if latest and latest != current:
changes.append(f" {pkg}: {current} → {latest} (latest from NuGet)")
return m.group(0).replace(
f'Version="{current}"', f'Version="{latest}"'
)
return m.group(0)

skipped_third_party.append(f" {pkg} (skipped - not a Codebelt package)")
return m.group(0)

# Match PackageVersion elements (handles multiline)
Expand All @@ -111,10 +158,10 @@ def replace_version(m: re.Match) -> str:

# Show results
if changes:
print(f"Updated {len(changes)} package(s) from {TRIGGER_SOURCE}:")
print(f"Updated {len(changes)} package(s):")
print("\n".join(changes))
else:
print(f"No packages from {TRIGGER_SOURCE} needed updating.")
print("No Codebelt packages needed updating.")

if skipped_third_party:
print()
Expand Down
2 changes: 1 addition & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
<PackageVersion Include="Cuemon.Extensions.Core" Version="10.3.0" />
<PackageVersion Include="Cuemon.Extensions.IO" Version="10.3.0" />
<PackageVersion Include="Cuemon.IO" Version="10.3.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.3.0" />
<PackageVersion Include="MinVer" Version="7.0.0" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.4" />
<PackageVersion Include="coverlet.collector" Version="8.0.0" />
Expand Down
Loading