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
2 changes: 1 addition & 1 deletion .docfx/Dockerfile.docfx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
FROM --platform=$BUILDPLATFORM nginx:${NGINX_VERSION} AS base
RUN rm -rf /usr/share/nginx/html/*

FROM --platform=$BUILDPLATFORM codebeltnet/docfx:2.78.4 AS build
FROM --platform=$BUILDPLATFORM codebeltnet/docfx:2.78.5 AS build

ADD [".", "docfx"]

Expand Down
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)
Comment on lines +89 to +90
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Do not fall back to prerelease when “latest stable” is required.

Current logic can select a prerelease when no stable version exists, which conflicts with the function contract and PR objective.

Suggested fix
-        stable = [v for v in versions if "-" not in v]
-        result = stable[-1] if stable else (versions[-1] if versions else None)
+        stable = [v for v in versions if "-" not in v]
+        result = stable[-1] if stable else None
+        if versions and not stable:
+            print(
+                f"  Warning: No stable release found for {package_name}; keeping current version."
+            )
🤖 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 89 - 90, The current selection
logic for latest stable (variables "stable", "versions", and "result")
incorrectly falls back to a prerelease when no stable versions exist; change the
assignment so "result" is the latest stable if any (stable[-1]) and otherwise is
None (do not return versions[-1] or any prerelease), and update any callers of
the function to handle a None return if necessary.

except Exception as exc:
print(f" Warning: Could not fetch latest version for {package_name}: {exc}")
result = None

_nuget_version_cache[package_name] = result
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()
Comment on lines 109 to 113
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

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

If TRIGGER_SOURCE is misspelled/unknown (not a key in SOURCE_PACKAGE_MAP), is_triggered_package never matches and the script will not pin any packages to TRIGGER_VERSION; instead it will treat all matching prefixes as “other Codebelt packages” and bump them to latest-from-NuGet. This can silently produce the wrong service update. Consider validating TRIGGER_SOURCE against SOURCE_PACKAGE_MAP (fail fast with a list of allowed values) before proceeding.

Copilot uses AI. Check for mistakes.

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 @@ -5,7 +5,7 @@
<ItemGroup>
<PackageVersion Include="Cuemon.Extensions.IO" Version="10.3.0" />
<PackageVersion Include="Codebelt.Extensions.Xunit.App" Version="11.0.6" />
<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="Savvyio.Domain" Version="5.0.3" />
<PackageVersion Include="Savvyio.Extensions.Newtonsoft.Json" Version="5.0.3" />
Expand Down
Loading