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 @@ -48,7 +48,7 @@
],
"globalMetadata": {
"_appTitle": "Bootstrapper 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><script async src=\"https://context7.com/widget.js\" data-library=\"/codebeltnet/bootstrapper\"></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.

Adding an external JavaScript widget from context7.com directly in the footer. Ensure that this third-party script is trusted and that loading it doesn't pose security risks. Consider verifying that the script uses Subresource Integrity (SRI) hashes or that context7.com has appropriate security measures in place. Additionally, the async attribute is good for performance, but consider whether the script should have defer instead if it needs to maintain document order execution.

Suggested change
"_appFooter": "<span>Generated by <strong>DocFX</strong>. Copyright 2024-2026 Geekle. All rights reserved.</span><script async src=\"https://context7.com/widget.js\" data-library=\"/codebeltnet/bootstrapper\"></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-56S43SYNH9",
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 @@
[ ]
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 JSON array contains a space between the brackets. While this is valid JSON, it's unconventional and could cause issues with parsing. Standard JSON convention for empty arrays is to have no whitespace between brackets.

Suggested change
[ ]
[]

Copilot uses AI. Check for mistakes.
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}")
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
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)
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 line return 0 if changes else 0 always returns 0 regardless of whether changes were made. While the comment explains this is intentional (not an error), this could be simplified to just return 0 for better clarity. The conditional serves no functional purpose.

Suggested change
return 0 if changes else 0 # Return 0 even if no changes (not an error)
return 0 # Return 0 even if no changes (not an error)

Copilot uses AI. Check for mistakes.


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)
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 version extraction regex (?<=## \[)[\d.]+(?=\]) assumes that the latest version in CHANGELOG.md is always in the format ## [x.y.z]. If the CHANGELOG.md format changes or if there are any non-version entries in square brackets before the actual version (e.g., ## [Unreleased]), this will extract the wrong value. Consider making the regex more specific or adding validation to ensure the extracted value is a valid semantic version.

Suggested change
CURRENT=$(grep -oP '(?<=## \[)[\d.]+(?=\])' CHANGELOG.md | head -1)
CURRENT=$(grep -oP '^## \[\K[0-9]+\.[0-9]+\.[0-9]+(?=\])' CHANGELOG.md | head -1)

Copilot uses AI. Check for mistakes.
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
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'
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 uses a hardcoded organization name 'codebeltnet' in the URL construction (line 58). If the repository structure or organization changes, this will break. Consider using environment variables or dynamic resolution from the GitHub context to make this more maintainable.

Copilot uses AI. Check for mistakes.
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:
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 script uses urllib.request.urlopen without a timeout parameter. This means the request could hang indefinitely if the GitHub API is slow to respond. Consider adding a timeout parameter to prevent the workflow from hanging, for example: urllib.request.urlopen(req, timeout=30).

Suggested change
with urllib.request.urlopen(req) as r:
with urllib.request.urlopen(req, timeout=30) as r:

Copilot uses AI. Check for mistakes.
print(f'✓ Dispatched to {repo}: HTTP {r.status}')
EOF
Comment on lines +49 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 inline Python script has no error handling for failed HTTP requests. If a dispatch to a downstream repository fails (e.g., due to authentication issues, network problems, or the repository not existing), the script will crash and stop processing remaining repositories. Consider wrapping the urlopen call in a try-except block to log the error and continue processing other repositories.

Copilot uses AI. Check for mistakes.
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
VERSION: ${{ steps.version.outputs.version }}
SOURCE_REPO: ${{ github.event.repository.name }}
Loading
Loading