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/docfx.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
],
"globalMetadata": {
"_appTitle": "Extensions for Globalization by Codebelt",
"_appFooter": "<span>Generated by <strong>DocFX</strong>. Copyright 2024-2026 Geekle. All rights reserved.</span>",
"_appFooter": "<span>Generated by <strong>DocFX</strong>. Copyright 2024-2026 Geekle. All rights reserved.</span>\n<script src=\"https://context7.com/widget.js\" data-library=\"/codebeltnet/globalization\" data-color=\"#059669\" data-position=\"bottom-right\" data-placeholder=\"Ask about Codebelt Globalization\"></script>",
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

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

The footer now injects a third-party script from https://context7.com/widget.js into the generated docs. This increases supply-chain/XSS risk for the published site; consider hosting/pinning the script (or adding Subresource Integrity + a documented allowlist/CSP) so the build output isn’t executing unpinned remote code.

Suggested change
"_appFooter": "<span>Generated by <strong>DocFX</strong>. Copyright 2024-2026 Geekle. All rights reserved.</span>\n<script src=\"https://context7.com/widget.js\" data-library=\"/codebeltnet/globalization\" data-color=\"#059669\" data-position=\"bottom-right\" data-placeholder=\"Ask about Codebelt Globalization\"></script>",
"_appFooter": "<span>Generated by <strong>DocFX</strong>. Copyright 2024-2026 Geekle. All rights reserved.</span>",

Copilot uses AI. Check for mistakes.
"_appLogoPath": "images/50x50.png",
"_appFaviconPath": "images/favicon.ico",
"_googleAnalyticsTagId": "G-R07CSX4Z91",
Expand Down
1 change: 1 addition & 0 deletions .github/dispatch-targets.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[ ]
133 changes: 133 additions & 0 deletions .github/scripts/bump-nuget.py
Original file line number Diff line number Diff line change
@@ -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}")
Comment on lines +61 to +65
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

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

After validating TRIGGER_SOURCE/TRIGGER_VERSION, the script does not validate that TRIGGER_SOURCE is a supported key in SOURCE_PACKAGE_MAP. If an unknown source is provided, the run will succeed but update nothing, and the workflow may still open a “service update” PR with only changelog/release note changes. Consider failing fast when TRIGGER_SOURCE is not in the map (or when it maps to an empty prefix list) to avoid generating misleading PRs.

Copilot uses AI. Check for mistakes.
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"<PackageVersion\b"
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
Comment on lines +119 to +122
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

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

skipped_third_party is populated for every package that doesn’t match the triggering source, including other internal Codebelt packages (not just third-party ones). This makes the summary output misleading ("Skipped ... third-party package(s)"). Consider renaming the variable/message to something like “skipped non-target packages” or explicitly distinguishing third-party vs other internal packages if that matters.

Copilot uses AI. Check for mistakes.
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())
139 changes: 139 additions & 0 deletions .github/workflows/service-update.yml
Original file line number Diff line number Diff line change
@@ -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 }}
Comment on lines +61 to +65
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

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

The workflow_dispatch inputs source_repo / source_version default to empty, but the Bump NuGet packages step will fail when either is empty (the script exits 1). Consider making the inputs required for manual runs, or guard this step (and later PR creation) with a check that resolved source/version are non-empty.

Copilot uses AI. Check for mistakes.

- 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"
Comment on lines +72 to +74
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

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

The TFM=$(grep ... | sed ... || echo ...) fallback won’t work as intended because sed will exit 0 even when grep finds nothing, so the || echo ... branch won’t run. Also, printf "$ENTRY" treats $ENTRY as a format string, so any % in the content can be misinterpreted. Prefer an explicit check for the Availability line and print the entry with a safe format (e.g., printf '%b' ... / printf '%s' ...) to avoid formatting surprises.

Suggested change
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"
AVAIL_LINE=$(grep -m1 "^Availability:" "$f" || true)
if [ -n "$AVAIL_LINE" ]; then
TFM="${AVAIL_LINE#Availability: }"
else
TFM=".NET 10, .NET 9 and .NET Standard 2.0"
fi
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 '%b' "$ENTRY"; cat "$f"; } > "$f.tmp" && mv "$f.tmp" "$f"

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

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

PackageReleaseNotes.txt currently starts with a UTF-8 BOM; prepending content via { printf ...; cat "$f"; } will push the BOM into the middle of the file (as part of the old first line), which can show up as a stray character in some consumers. Consider preserving/removing the BOM explicitly (e.g., rewrite using a tool that keeps BOM at the beginning, or strip it before concatenation).

Suggested change
{ printf "$ENTRY"; cat "$f"; } > "$f.tmp" && mv "$f.tmp" "$f"
{
printf "$ENTRY"
python3 - << 'EOF' < "$f"
import sys
data = sys.stdin.read()
if data.startswith('\ufeff'):
data = data[1:]
sys.stdout.write(data)
EOF
} > "$f.tmp" && mv "$f.tmp" "$f"

Copilot uses AI. Check for mistakes.
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
78 changes: 78 additions & 0 deletions .github/workflows/trigger-downstream.yml
Original file line number Diff line number Diff line change
@@ -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'
})
Comment on lines +66 to +71
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

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

GitHub’s REST API expects a User-Agent header on requests; the urllib.request.Request(..., headers={...}) here omits it, which can cause the dispatch call to be rejected. Add an explicit User-Agent (and optionally handle HTTPError to surface per-repo failures more clearly).

Copilot uses AI. Check for mistakes.
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 }}
Loading
Loading