diff --git a/.docfx/docfx.json b/.docfx/docfx.json
index 3098c98..39d08ad 100644
--- a/.docfx/docfx.json
+++ b/.docfx/docfx.json
@@ -45,7 +45,7 @@
],
"globalMetadata": {
"_appTitle": "Extensions for Asp.Versioning by Codebelt",
- "_appFooter": "Generated by DocFX. Copyright 2024-2026 Geekle. All rights reserved.",
+ "_appFooter": "Generated by DocFX. Copyright 2024-2026 Geekle. All rights reserved.",
"_appLogoPath": "images/50x50.png",
"_appFaviconPath": "images/favicon.ico",
"_googleAnalyticsTagId": "G-G15R2J7GBR",
diff --git a/.github/dispatch-targets.json b/.github/dispatch-targets.json
new file mode 100644
index 0000000..6fadab5
--- /dev/null
+++ b/.github/dispatch-targets.json
@@ -0,0 +1 @@
+[ "swashbuckle-aspnetcore" ]
diff --git a/.github/scripts/bump-nuget.py b/.github/scripts/bump-nuget.py
new file mode 100644
index 0000000..65a8b21
--- /dev/null
+++ b/.github/scripts/bump-nuget.py
@@ -0,0 +1,133 @@
+#!/usr/bin/env python3
+"""
+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
+import os
+import sys
+from typing import Dict, List
+
+TRIGGER_SOURCE = os.environ.get("TRIGGER_SOURCE", "")
+TRIGGER_VERSION = os.environ.get("TRIGGER_VERSION", "")
+
+# 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"],
+ "newtonsoft-json": [
+ "Codebelt.Extensions.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.Mvc.Formatters.Text.Yaml",
+ ],
+ "globalization": ["Codebelt.Extensions.Globalization"],
+ "asp-versioning": ["Codebelt.Extensions.Asp.Versioning"],
+ "swashbuckle-aspnetcore": ["Codebelt.Extensions.Swashbuckle"],
+ "savvyio": ["Savvyio."],
+ "shared-kernel": [],
+}
+
+
+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, [])
+ return any(package_name.startswith(prefix) for prefix in prefixes)
+
+
+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)
+
+ # Strip 'v' prefix if present in version
+ target_version = TRIGGER_VERSION.lstrip("v")
+
+ print(f"Trigger: {TRIGGER_SOURCE} @ {target_version}")
+ print(f"Only updating packages from: {TRIGGER_SOURCE}")
+ print()
+
+ 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
new file mode 100644
index 0000000..ea92ea5
--- /dev/null
+++ b/.github/workflows/service-update.yml
@@ -0,0 +1,139 @@
+name: Service Update
+
+on:
+ repository_dispatch:
+ types: [codebelt-service-update]
+ workflow_dispatch:
+ inputs:
+ source_repo:
+ description: 'Triggering source repo name (e.g. cuemon)'
+ required: false
+ default: ''
+ source_version:
+ description: 'Version released by source (e.g. 10.3.0)'
+ required: false
+ default: ''
+ dry_run:
+ type: boolean
+ description: 'Dry run — show changes but do not commit or open PR'
+ default: false
+
+permissions:
+ contents: write
+ pull-requests: write
+
+jobs:
+ service-update:
+ runs-on: ubuntu-24.04
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Resolve trigger inputs
+ id: trigger
+ run: |
+ SOURCE="${{ github.event.client_payload.source_repo || github.event.inputs.source_repo }}"
+ VERSION="${{ github.event.client_payload.source_version || github.event.inputs.source_version }}"
+ echo "source=$SOURCE" >> $GITHUB_OUTPUT
+ echo "version=$VERSION" >> $GITHUB_OUTPUT
+
+ - name: Determine new version for this repo
+ id: newver
+ run: |
+ CURRENT=$(grep -oP '(?<=## \[)[\d.]+(?=\])' CHANGELOG.md | head -1)
+ NEW=$(echo "$CURRENT" | awk -F. '{printf "%s.%s.%d", $1, $2, $3+1}')
+ BRANCH="v${NEW}/service-update"
+ echo "current=$CURRENT" >> $GITHUB_OUTPUT
+ echo "new=$NEW" >> $GITHUB_OUTPUT
+ echo "branch=$BRANCH" >> $GITHUB_OUTPUT
+
+ - name: Generate codebelt-aicia token
+ id: app-token
+ uses: actions/create-github-app-token@v1
+ with:
+ app-id: ${{ vars.CODEBELT_AICIA_APP_ID }}
+ private-key: ${{ secrets.CODEBELT_AICIA_PRIVATE_KEY }}
+ owner: codebeltnet
+
+ - name: Bump NuGet packages
+ run: python3 .github/scripts/bump-nuget.py
+ env:
+ TRIGGER_SOURCE: ${{ steps.trigger.outputs.source }}
+ TRIGGER_VERSION: ${{ steps.trigger.outputs.version }}
+
+ - name: Update PackageReleaseNotes.txt
+ run: |
+ NEW="${{ steps.newver.outputs.new }}"
+ for f in .nuget/*/PackageReleaseNotes.txt; do
+ [ -f "$f" ] || continue
+ TFM=$(grep -m1 "^Availability:" "$f" | sed 's/Availability: //' || echo ".NET 10, .NET 9 and .NET Standard 2.0")
+ ENTRY="Version: ${NEW}\nAvailability: ${TFM}\n \n# ALM\n- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs)\n \n"
+ { printf "$ENTRY"; cat "$f"; } > "$f.tmp" && mv "$f.tmp" "$f"
+ done
+
+ - name: Update CHANGELOG.md
+ run: |
+ python3 - <<'EOF'
+ import os, re
+ from datetime import date
+ new_ver = os.environ['NEW_VERSION']
+ today = date.today().isoformat()
+ entry = f"## [{new_ver}] - {today}\n\nThis is a service update that focuses on package dependencies.\n\n"
+ with open("CHANGELOG.md") as f:
+ content = f.read()
+ idx = content.find("## [")
+ content = (content[:idx] + entry + content[idx:]) if idx != -1 else (content + entry)
+ with open("CHANGELOG.md", "w") as f:
+ f.write(content)
+ print(f"CHANGELOG updated for v{new_ver}")
+ EOF
+ env:
+ NEW_VERSION: ${{ steps.newver.outputs.new }}
+
+ # 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' }}
+ run: git diff
+
+ - name: Create branch and open PR
+ if: ${{ github.event.inputs.dry_run != 'true' }}
+ env:
+ GH_TOKEN: ${{ steps.app-token.outputs.token }}
+ run: |
+ NEW="${{ steps.newver.outputs.new }}"
+ BRANCH="${{ steps.newver.outputs.branch }}"
+ SOURCE="${{ steps.trigger.outputs.source }}"
+ SRC_VER="${{ steps.trigger.outputs.version }}"
+
+ git config user.name "codebelt-aicia[bot]"
+ git config user.email "codebelt-aicia[bot]@users.noreply.github.com"
+ git checkout -b "$BRANCH"
+ git add -A
+ git diff --cached --quiet && echo "Nothing changed - skipping PR." && exit 0
+ git commit -m "V${NEW}/service update"
+ git push origin "$BRANCH"
+
+ 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 "- 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 "" >> 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
+ echo "Triggered by: ${SOURCE} @ ${SRC_VER}" >> pr_body.txt
+ else
+ echo "Triggered by: manual workflow dispatch" >> pr_body.txt
+ fi
+
+ gh pr create --title "V${NEW}/service update" --body-file pr_body.txt --base main --head "$BRANCH" --assignee gimlichael
diff --git a/.github/workflows/trigger-downstream.yml b/.github/workflows/trigger-downstream.yml
new file mode 100644
index 0000000..29eb29c
--- /dev/null
+++ b/.github/workflows/trigger-downstream.yml
@@ -0,0 +1,78 @@
+name: Trigger Downstream Service Updates
+
+on:
+ release:
+ types: [published]
+
+jobs:
+ dispatch:
+ if: github.event.release.prerelease == false
+ runs-on: ubuntu-24.04
+ permissions:
+ contents: read
+
+ steps:
+ - name: Checkout (to read dispatch-targets.json)
+ uses: actions/checkout@v4
+
+ - name: Check for dispatch targets
+ id: check
+ run: |
+ if [ ! -f .github/dispatch-targets.json ]; then
+ echo "No dispatch-targets.json found, skipping."
+ echo "has_targets=false" >> $GITHUB_OUTPUT
+ exit 0
+ fi
+ COUNT=$(python3 -c "import json; print(len(json.load(open('.github/dispatch-targets.json'))))")
+ echo "has_targets=$([ $COUNT -gt 0 ] && echo true || echo false)" >> $GITHUB_OUTPUT
+
+ - name: Extract version from release tag
+ if: steps.check.outputs.has_targets == 'true'
+ id: version
+ run: |
+ VERSION="${{ github.event.release.tag_name }}"
+ VERSION="${VERSION#v}"
+ echo "version=$VERSION" >> $GITHUB_OUTPUT
+
+ - name: Generate codebelt-aicia token
+ if: steps.check.outputs.has_targets == 'true'
+ id: app-token
+ uses: actions/create-github-app-token@v1
+ with:
+ app-id: ${{ vars.CODEBELT_AICIA_APP_ID }}
+ private-key: ${{ secrets.CODEBELT_AICIA_PRIVATE_KEY }}
+ owner: codebeltnet
+
+ - name: Dispatch to downstream repos
+ if: steps.check.outputs.has_targets == 'true'
+ run: |
+ python3 - <<'EOF'
+ import json, urllib.request, os, sys
+
+ targets = json.load(open('.github/dispatch-targets.json'))
+ token = os.environ['GH_TOKEN']
+ version = os.environ['VERSION']
+ source = os.environ['SOURCE_REPO']
+
+ for repo in targets:
+ url = f'https://api.github.com/repos/codebeltnet/{repo}/dispatches'
+ payload = json.dumps({
+ 'event_type': 'codebelt-service-update',
+ 'client_payload': {
+ 'source_repo': source,
+ 'source_version': version
+ }
+ }).encode()
+ req = urllib.request.Request(url, data=payload, method='POST', headers={
+ 'Authorization': f'Bearer {token}',
+ 'Accept': 'application/vnd.github+json',
+ 'Content-Type': 'application/json',
+ 'X-GitHub-Api-Version': '2022-11-28'
+ })
+ with urllib.request.urlopen(req) as r:
+ print(f'✓ Dispatched to {repo}: HTTP {r.status}')
+ EOF
+ env:
+ GH_TOKEN: ${{ steps.app-token.outputs.token }}
+ VERSION: ${{ steps.version.outputs.version }}
+ SOURCE_REPO: ${{ github.event.repository.name }}
diff --git a/.nuget/Codebelt.Extensions.Asp.Versioning/PackageReleaseNotes.txt b/.nuget/Codebelt.Extensions.Asp.Versioning/PackageReleaseNotes.txt
index 20e2c41..cca8f81 100644
--- a/.nuget/Codebelt.Extensions.Asp.Versioning/PackageReleaseNotes.txt
+++ b/.nuget/Codebelt.Extensions.Asp.Versioning/PackageReleaseNotes.txt
@@ -1,16 +1,16 @@
-Version 10.0.2
+Version: 10.0.2
Availability: .NET 10 and .NET 9
# ALM
- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs)
-Version 10.0.1
+Version: 10.0.1
Availability: .NET 10 and .NET 9
# ALM
- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs)
-Version 10.0.0
+Version: 10.0.0
Availability: .NET 10 and .NET 9
# ALM
@@ -18,80 +18,80 @@ Availability: .NET 10 and .NET 9
- REMOVED Support for TFM .NET 8 (LTS)
- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs)
-Version 9.0.8
+Version: 9.0.8
Availability: .NET 9 and .NET 8
# ALM
- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs)
-Version 9.0.7
+Version: 9.0.7
Availability: .NET 9 and .NET 8
# ALM
- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs)
-Version 9.0.6
+Version: 9.0.6
Availability: .NET 9 and .NET 8
# ALM
- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs)
-Version 9.0.5
+Version: 9.0.5
Availability: .NET 9 and .NET 8
# ALM
- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs)
-Version 9.0.4
+Version: 9.0.4
Availability: .NET 9 and .NET 8
# ALM
- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs)
-Version 9.0.3
+Version: 9.0.3
Availability: .NET 9 and .NET 8
# ALM
- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs)
-Version 9.0.2
+Version: 9.0.2
Availability: .NET 9 and .NET 8
# ALM
- CHANGED Dependencies to latest and greatest with respect to TFMs
-Version 9.0.1
+Version: 9.0.1
Availability: .NET 9 and .NET 8
# ALM
- CHANGED Dependencies to latest and greatest with respect to TFMs
-Version 9.0.0
+Version: 9.0.0
Availability: .NET 9 and .NET 8
# ALM
- CHANGED Dependencies to latest and greatest with respect to TFMs
- REMOVED Support for TFM .NET 6 (LTS)
-Version 8.4.0
+Version: 8.4.0
Availability: .NET 8 and .NET 6
# ALM
- CHANGED Dependencies to latest and greatest with respect to TFMs
-Version 8.3.2
+Version: 8.3.2
Availability: .NET 8 and .NET 6
# ALM
- REMOVED Support for TFM .NET 7 (STS)
-Version 8.3.0
+Version: 8.3.0
Availability: .NET 8, .NET 7 and .NET 6
# ALM
- CHANGED Dependencies to latest and greatest with respect to TFMs
-Version 8.2.0
+Version: 8.2.0
Availability: .NET 8, .NET 7 and .NET 6
# ALM