diff --git a/.github/scripts/bump-nuget.py b/.github/scripts/bump-nuget.py index 375f41a..65a8b21 100644 --- a/.github/scripts/bump-nuget.py +++ b/.github/scripts/bump-nuget.py @@ -1,116 +1,133 @@ #!/usr/bin/env python3 """ -Bumps PackageVersion entries in Directory.Packages.props. -- For packages published by the triggering source repo: uses the known TRIGGER_VERSION. -- For all other packages: queries NuGet API for latest stable version. -- Preserves all XML formatting via regex (no ElementTree rewriting). +Simplified package bumping for Codebelt service updates (Option B). + +Only updates packages published by the triggering source repo. +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 + - Microsoft.Extensions.Hosting: 9.0.13 → UNCHANGED (not a Codebelt package) + - BenchmarkDotNet: 0.15.8 → UNCHANGED (not a Codebelt package) """ -import re, os, urllib.request, json, sys +import re +import os +import sys from typing import Dict, List TRIGGER_SOURCE = os.environ.get("TRIGGER_SOURCE", "") TRIGGER_VERSION = os.environ.get("TRIGGER_VERSION", "") -# Maps source repo name → NuGet package ID prefixes published by that repo. -# Keep this aligned with what each repo actually publishes. +# Map of source repos to their package ID prefixes SOURCE_PACKAGE_MAP: Dict[str, List[str]] = { - "cuemon": [ - "Cuemon.", - ], - "xunit": [ - "Codebelt.Extensions.Xunit", - ], - "benchmarkdotnet": [ - "Codebelt.Extensions.BenchmarkDotNet", - ], - "bootstrapper": [ - "Codebelt.Bootstrapper", - ], + "cuemon": ["Cuemon."], + "xunit": ["Codebelt.Extensions.Xunit"], + "benchmarkdotnet": ["Codebelt.Extensions.BenchmarkDotNet"], + "bootstrapper": ["Codebelt.Bootstrapper"], "newtonsoft-json": [ "Codebelt.Extensions.Newtonsoft.Json", "Codebelt.Extensions.AspNetCore.Mvc.Formatters.Newtonsoft", ], - "aws-signature-v4": [ - "Codebelt.Extensions.AspNetCore.Authentication.AwsSignature", - ], - "unitify": [ - "Codebelt.Unitify", - ], + "aws-signature-v4": ["Codebelt.Extensions.AspNetCore.Authentication.AwsSignature"], + "unitify": ["Codebelt.Unitify"], "yamldotnet": [ "Codebelt.Extensions.YamlDotNet", "Codebelt.Extensions.AspNetCore.Mvc.Formatters.Text.Yaml", ], - "globalization": [ - "Codebelt.Extensions.Globalization", - ], - "asp-versioning": [ - "Codebelt.Extensions.Asp.Versioning", - ], - "swashbuckle-aspnetcore": [ - "Codebelt.Extensions.Swashbuckle", - ], - "savvyio": [ - "Savvyio.", - ], + "globalization": ["Codebelt.Extensions.Globalization"], + "asp-versioning": ["Codebelt.Extensions.Asp.Versioning"], + "swashbuckle-aspnetcore": ["Codebelt.Extensions.Swashbuckle"], + "savvyio": ["Savvyio."], "shared-kernel": [], } -def nuget_latest(pkg: str) -> str | None: - """Query NuGet flat container API for the latest stable version.""" - try: - url = f"https://api.nuget.org/v3-flatcontainer/{pkg.lower()}/index.json" - with urllib.request.urlopen(url, timeout=15) as r: - versions = json.loads(r.read())["versions"] - stable = [v for v in versions if not re.search(r"-", v)] - return stable[-1] if stable else None - except Exception as e: - print(f" SKIP {pkg}: {e}", file=sys.stderr) - return None - - -def triggered_version(pkg: str) -> str | None: - """Return TRIGGER_VERSION if pkg is published by the triggering source repo.""" - if not TRIGGER_VERSION or not TRIGGER_SOURCE: - return None +def is_triggered_package(package_name: str) -> bool: + """Check if package is published by the triggering source repo.""" + if not TRIGGER_SOURCE: + return False prefixes = SOURCE_PACKAGE_MAP.get(TRIGGER_SOURCE, []) - if any(pkg.startswith(p) for p in prefixes): - return TRIGGER_VERSION - return None - - -with open("Directory.Packages.props", "r") as f: - content = f.read() + return any(package_name.startswith(prefix) for prefix in prefixes) -changes = [] +def main(): + if not TRIGGER_SOURCE or not TRIGGER_VERSION: + print( + "Error: TRIGGER_SOURCE and TRIGGER_VERSION environment variables required" + ) + print(f" TRIGGER_SOURCE={TRIGGER_SOURCE}") + print(f" TRIGGER_VERSION={TRIGGER_VERSION}") + sys.exit(1) -def replace_version(m: re.Match) -> str: - pkg = m.group(1) - current = m.group(2) + # Strip 'v' prefix if present in version + target_version = TRIGGER_VERSION.lstrip("v") - new_ver = triggered_version(pkg) or nuget_latest(pkg) or current + print(f"Trigger: {TRIGGER_SOURCE} @ {target_version}") + print(f"Only updating packages from: {TRIGGER_SOURCE}") + print() - if new_ver != current: - changes.append(f" {pkg}: {current} → {new_ver}") - return m.group(0).replace(f'Version="{current}"', f'Version="{new_ver}"') - - -pattern = re.compile( - r']*\bInclude="([^"]+)")' - r'(?=[^>]*\bVersion="([^"]+)")' - r'[^>]*>', - re.DOTALL, -) -new_content = pattern.sub(replace_version, content) - -if changes: - print(f"Bumped {len(changes)} package(s):") - print("\n".join(changes)) -else: - print("All packages already at target versions.") - -with open("Directory.Packages.props", "w") as f: - f.write(new_content) + try: + with open("Directory.Packages.props", "r") as f: + content = f.read() + except FileNotFoundError: + print("Error: Directory.Packages.props not found") + sys.exit(1) + + changes = [] + skipped_third_party = [] + + 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})") + 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}"' + ) + + return m.group(0) + + # Match PackageVersion elements (handles multiline) + pattern = re.compile( + r"]*\bInclude="([^"]+)")' + r'(?=[^>]*\bVersion="([^"]+)")' + r"[^>]*>", + re.DOTALL, + ) + new_content = pattern.sub(replace_version, content) + + # Show results + if changes: + print(f"Updated {len(changes)} package(s) from {TRIGGER_SOURCE}:") + print("\n".join(changes)) + else: + print(f"No packages from {TRIGGER_SOURCE} needed updating.") + + if skipped_third_party: + print() + print(f"Skipped {len(skipped_third_party)} third-party package(s):") + print("\n".join(skipped_third_party[:5])) # Show first 5 + if len(skipped_third_party) > 5: + print(f" ... and {len(skipped_third_party) - 5} more") + + with open("Directory.Packages.props", "w") as f: + f.write(new_content) + + return 0 if changes else 0 # Return 0 even if no changes (not an error) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.github/workflows/service-update.yml b/.github/workflows/service-update.yml index e2e5202..d08f940 100644 --- a/.github/workflows/service-update.yml +++ b/.github/workflows/service-update.yml @@ -93,33 +93,9 @@ jobs: env: NEW_VERSION: ${{ steps.newver.outputs.new }} - - name: Bump test runner Docker tag - run: | - [ -f testenvironments.json ] || exit 0 - LATEST=$(curl -sf "https://hub.docker.com/v2/repositories/codebeltnet/ubuntu-testrunner/tags/?page_size=10&ordering=last_updated" \ - | python3 -c " - import sys, json - tags = [t['name'] for t in json.load(sys.stdin)['results'] if 'latest' not in t['name']] - print(tags[0] if tags else '') - ") - [ -n "$LATEST" ] && \ - sed -i "s|codebeltnet/ubuntu-testrunner:[^\"]*|codebeltnet/ubuntu-testrunner:${LATEST}|g" \ - testenvironments.json && echo "Bumped to $LATEST" - - - name: Bump NGINX Alpine version - run: | - [ -f .docfx/Dockerfile.docfx ] || exit 0 - LATEST=$(curl -sf "https://hub.docker.com/v2/repositories/library/nginx/tags/?page_size=50&name=alpine&ordering=last_updated" \ - | python3 -c " - import sys, json, re - data = json.load(sys.stdin) - tags = [t['name'] for t in data['results'] if re.match(r'^\d+\.\d+\.\d+-alpine$', t['name'])] - if tags: - print(sorted(tags, key=lambda v: list(map(int, v.replace('-alpine','').split('.'))))[-1]) - ") - [ -n "$LATEST" ] && \ - sed -i "s|NGINX_VERSION=.*|NGINX_VERSION=${LATEST}|g" \ - .docfx/Dockerfile.docfx && echo "Bumped NGINX to $LATEST" + # Note: Docker image bumps removed in favor of manual updates + # The automated selection was picking wrong variants (e.g., mono-* instead of standard) + # TODO: Move to hosted service for smarter image selection - name: Show diff (dry run) if: ${{ github.event.inputs.dry_run == 'true' }} @@ -146,11 +122,12 @@ jobs: echo "This is a service update that focuses on package dependencies." > pr_body.txt echo "" >> pr_body.txt echo "Automated changes:" >> pr_body.txt - echo "- NuGet package versions bumped to latest compatible" >> pr_body.txt + echo "- Codebelt/Cuemon package versions bumped to latest compatible" >> pr_body.txt echo "- PackageReleaseNotes.txt updated for v${NEW}" >> pr_body.txt echo "- CHANGELOG.md entry added for v${NEW}" >> pr_body.txt - echo "- Docker test runner tag bumped (if applicable)" >> pr_body.txt - echo "- NGINX Alpine version bumped (if applicable)" >> pr_body.txt + echo "" >> pr_body.txt + echo "Note: Third-party packages (Microsoft.Extensions.*, BenchmarkDotNet, etc.) are not auto-updated." >> pr_body.txt + echo "Use Dependabot or manual updates for those." >> pr_body.txt echo "" >> pr_body.txt echo "Generated by codebelt-aicia" >> pr_body.txt if [ -n "$SOURCE" ] && [ -n "$SRC_VER" ]; then