Skip to content

Commit 722f599

Browse files
authored
Docfx/context7 chat (#18)
✨ update app footer to include Context7 widget script 🔧 fix formatting of version entries in release notes ✨ add workflows for service updates and downstream dispatching
1 parent 45b00a5 commit 722f599

6 files changed

Lines changed: 367 additions & 16 deletions

File tree

.docfx/docfx.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
],
4646
"globalMetadata": {
4747
"_appTitle": "Extensions for Globalization by Codebelt",
48-
"_appFooter": "<span>Generated by <strong>DocFX</strong>. Copyright 2024-2026 Geekle. All rights reserved.</span>",
48+
"_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>",
4949
"_appLogoPath": "images/50x50.png",
5050
"_appFaviconPath": "images/favicon.ico",
5151
"_googleAnalyticsTagId": "G-R07CSX4Z91",

.github/dispatch-targets.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[ ]

.github/scripts/bump-nuget.py

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Simplified package bumping for Codebelt service updates (Option B).
4+
5+
Only updates packages published by the triggering source repo.
6+
Does NOT update Microsoft.Extensions.*, BenchmarkDotNet, or other third-party packages.
7+
Does NOT parse TFM conditions - only bumps Codebelt/Cuemon/Savvyio packages to the triggering version.
8+
9+
Usage:
10+
TRIGGER_SOURCE=cuemon TRIGGER_VERSION=10.3.0 python3 bump-nuget.py
11+
12+
Behavior:
13+
- If TRIGGER_SOURCE is "cuemon" and TRIGGER_VERSION is "10.3.0":
14+
- Cuemon.Core: 10.2.1 → 10.3.0
15+
- Cuemon.Extensions.IO: 10.2.1 → 10.3.0
16+
- Microsoft.Extensions.Hosting: 9.0.13 → UNCHANGED (not a Codebelt package)
17+
- BenchmarkDotNet: 0.15.8 → UNCHANGED (not a Codebelt package)
18+
"""
19+
20+
import re
21+
import os
22+
import sys
23+
from typing import Dict, List
24+
25+
TRIGGER_SOURCE = os.environ.get("TRIGGER_SOURCE", "")
26+
TRIGGER_VERSION = os.environ.get("TRIGGER_VERSION", "")
27+
28+
# Map of source repos to their package ID prefixes
29+
SOURCE_PACKAGE_MAP: Dict[str, List[str]] = {
30+
"cuemon": ["Cuemon."],
31+
"xunit": ["Codebelt.Extensions.Xunit"],
32+
"benchmarkdotnet": ["Codebelt.Extensions.BenchmarkDotNet"],
33+
"bootstrapper": ["Codebelt.Bootstrapper"],
34+
"newtonsoft-json": [
35+
"Codebelt.Extensions.Newtonsoft.Json",
36+
"Codebelt.Extensions.AspNetCore.Mvc.Formatters.Newtonsoft",
37+
],
38+
"aws-signature-v4": ["Codebelt.Extensions.AspNetCore.Authentication.AwsSignature"],
39+
"unitify": ["Codebelt.Unitify"],
40+
"yamldotnet": [
41+
"Codebelt.Extensions.YamlDotNet",
42+
"Codebelt.Extensions.AspNetCore.Mvc.Formatters.Text.Yaml",
43+
],
44+
"globalization": ["Codebelt.Extensions.Globalization"],
45+
"asp-versioning": ["Codebelt.Extensions.Asp.Versioning"],
46+
"swashbuckle-aspnetcore": ["Codebelt.Extensions.Swashbuckle"],
47+
"savvyio": ["Savvyio."],
48+
"shared-kernel": [],
49+
}
50+
51+
52+
def is_triggered_package(package_name: str) -> bool:
53+
"""Check if package is published by the triggering source repo."""
54+
if not TRIGGER_SOURCE:
55+
return False
56+
prefixes = SOURCE_PACKAGE_MAP.get(TRIGGER_SOURCE, [])
57+
return any(package_name.startswith(prefix) for prefix in prefixes)
58+
59+
60+
def main():
61+
if not TRIGGER_SOURCE or not TRIGGER_VERSION:
62+
print(
63+
"Error: TRIGGER_SOURCE and TRIGGER_VERSION environment variables required"
64+
)
65+
print(f" TRIGGER_SOURCE={TRIGGER_SOURCE}")
66+
print(f" TRIGGER_VERSION={TRIGGER_VERSION}")
67+
sys.exit(1)
68+
69+
# Strip 'v' prefix if present in version
70+
target_version = TRIGGER_VERSION.lstrip("v")
71+
72+
print(f"Trigger: {TRIGGER_SOURCE} @ {target_version}")
73+
print(f"Only updating packages from: {TRIGGER_SOURCE}")
74+
print()
75+
76+
try:
77+
with open("Directory.Packages.props", "r") as f:
78+
content = f.read()
79+
except FileNotFoundError:
80+
print("Error: Directory.Packages.props not found")
81+
sys.exit(1)
82+
83+
changes = []
84+
skipped_third_party = []
85+
86+
def replace_version(m: re.Match) -> str:
87+
pkg = m.group(1)
88+
current = m.group(2)
89+
90+
if not is_triggered_package(pkg):
91+
skipped_third_party.append(f" {pkg} (skipped - not from {TRIGGER_SOURCE})")
92+
return m.group(0)
93+
94+
if target_version != current:
95+
changes.append(f" {pkg}: {current}{target_version}")
96+
return m.group(0).replace(
97+
f'Version="{current}"', f'Version="{target_version}"'
98+
)
99+
100+
return m.group(0)
101+
102+
# Match PackageVersion elements (handles multiline)
103+
pattern = re.compile(
104+
r"<PackageVersion\b"
105+
r'(?=[^>]*\bInclude="([^"]+)")'
106+
r'(?=[^>]*\bVersion="([^"]+)")'
107+
r"[^>]*>",
108+
re.DOTALL,
109+
)
110+
new_content = pattern.sub(replace_version, content)
111+
112+
# Show results
113+
if changes:
114+
print(f"Updated {len(changes)} package(s) from {TRIGGER_SOURCE}:")
115+
print("\n".join(changes))
116+
else:
117+
print(f"No packages from {TRIGGER_SOURCE} needed updating.")
118+
119+
if skipped_third_party:
120+
print()
121+
print(f"Skipped {len(skipped_third_party)} third-party package(s):")
122+
print("\n".join(skipped_third_party[:5])) # Show first 5
123+
if len(skipped_third_party) > 5:
124+
print(f" ... and {len(skipped_third_party) - 5} more")
125+
126+
with open("Directory.Packages.props", "w") as f:
127+
f.write(new_content)
128+
129+
return 0 if changes else 0 # Return 0 even if no changes (not an error)
130+
131+
132+
if __name__ == "__main__":
133+
sys.exit(main())
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
name: Service Update
2+
3+
on:
4+
repository_dispatch:
5+
types: [codebelt-service-update]
6+
workflow_dispatch:
7+
inputs:
8+
source_repo:
9+
description: 'Triggering source repo name (e.g. cuemon)'
10+
required: false
11+
default: ''
12+
source_version:
13+
description: 'Version released by source (e.g. 10.3.0)'
14+
required: false
15+
default: ''
16+
dry_run:
17+
type: boolean
18+
description: 'Dry run — show changes but do not commit or open PR'
19+
default: false
20+
21+
permissions:
22+
contents: write
23+
pull-requests: write
24+
25+
jobs:
26+
service-update:
27+
runs-on: ubuntu-24.04
28+
29+
steps:
30+
- name: Checkout
31+
uses: actions/checkout@v4
32+
with:
33+
fetch-depth: 0
34+
35+
- name: Resolve trigger inputs
36+
id: trigger
37+
run: |
38+
SOURCE="${{ github.event.client_payload.source_repo || github.event.inputs.source_repo }}"
39+
VERSION="${{ github.event.client_payload.source_version || github.event.inputs.source_version }}"
40+
echo "source=$SOURCE" >> $GITHUB_OUTPUT
41+
echo "version=$VERSION" >> $GITHUB_OUTPUT
42+
43+
- name: Determine new version for this repo
44+
id: newver
45+
run: |
46+
CURRENT=$(grep -oP '(?<=## \[)[\d.]+(?=\])' CHANGELOG.md | head -1)
47+
NEW=$(echo "$CURRENT" | awk -F. '{printf "%s.%s.%d", $1, $2, $3+1}')
48+
BRANCH="v${NEW}/service-update"
49+
echo "current=$CURRENT" >> $GITHUB_OUTPUT
50+
echo "new=$NEW" >> $GITHUB_OUTPUT
51+
echo "branch=$BRANCH" >> $GITHUB_OUTPUT
52+
53+
- name: Generate codebelt-aicia token
54+
id: app-token
55+
uses: actions/create-github-app-token@v1
56+
with:
57+
app-id: ${{ vars.CODEBELT_AICIA_APP_ID }}
58+
private-key: ${{ secrets.CODEBELT_AICIA_PRIVATE_KEY }}
59+
owner: codebeltnet
60+
61+
- name: Bump NuGet packages
62+
run: python3 .github/scripts/bump-nuget.py
63+
env:
64+
TRIGGER_SOURCE: ${{ steps.trigger.outputs.source }}
65+
TRIGGER_VERSION: ${{ steps.trigger.outputs.version }}
66+
67+
- name: Update PackageReleaseNotes.txt
68+
run: |
69+
NEW="${{ steps.newver.outputs.new }}"
70+
for f in .nuget/*/PackageReleaseNotes.txt; do
71+
[ -f "$f" ] || continue
72+
TFM=$(grep -m1 "^Availability:" "$f" | sed 's/Availability: //' || echo ".NET 10, .NET 9 and .NET Standard 2.0")
73+
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"
74+
{ printf "$ENTRY"; cat "$f"; } > "$f.tmp" && mv "$f.tmp" "$f"
75+
done
76+
77+
- name: Update CHANGELOG.md
78+
run: |
79+
python3 - <<'EOF'
80+
import os, re
81+
from datetime import date
82+
new_ver = os.environ['NEW_VERSION']
83+
today = date.today().isoformat()
84+
entry = f"## [{new_ver}] - {today}\n\nThis is a service update that focuses on package dependencies.\n\n"
85+
with open("CHANGELOG.md") as f:
86+
content = f.read()
87+
idx = content.find("## [")
88+
content = (content[:idx] + entry + content[idx:]) if idx != -1 else (content + entry)
89+
with open("CHANGELOG.md", "w") as f:
90+
f.write(content)
91+
print(f"CHANGELOG updated for v{new_ver}")
92+
EOF
93+
env:
94+
NEW_VERSION: ${{ steps.newver.outputs.new }}
95+
96+
# Note: Docker image bumps removed in favor of manual updates
97+
# The automated selection was picking wrong variants (e.g., mono-* instead of standard)
98+
# TODO: Move to hosted service for smarter image selection
99+
100+
- name: Show diff (dry run)
101+
if: ${{ github.event.inputs.dry_run == 'true' }}
102+
run: git diff
103+
104+
- name: Create branch and open PR
105+
if: ${{ github.event.inputs.dry_run != 'true' }}
106+
env:
107+
GH_TOKEN: ${{ steps.app-token.outputs.token }}
108+
run: |
109+
NEW="${{ steps.newver.outputs.new }}"
110+
BRANCH="${{ steps.newver.outputs.branch }}"
111+
SOURCE="${{ steps.trigger.outputs.source }}"
112+
SRC_VER="${{ steps.trigger.outputs.version }}"
113+
114+
git config user.name "codebelt-aicia[bot]"
115+
git config user.email "codebelt-aicia[bot]@users.noreply.github.com"
116+
git checkout -b "$BRANCH"
117+
git add -A
118+
git diff --cached --quiet && echo "Nothing changed - skipping PR." && exit 0
119+
git commit -m "V${NEW}/service update"
120+
git push origin "$BRANCH"
121+
122+
echo "This is a service update that focuses on package dependencies." > pr_body.txt
123+
echo "" >> pr_body.txt
124+
echo "Automated changes:" >> pr_body.txt
125+
echo "- Codebelt/Cuemon package versions bumped to latest compatible" >> pr_body.txt
126+
echo "- PackageReleaseNotes.txt updated for v${NEW}" >> pr_body.txt
127+
echo "- CHANGELOG.md entry added for v${NEW}" >> pr_body.txt
128+
echo "" >> pr_body.txt
129+
echo "Note: Third-party packages (Microsoft.Extensions.*, BenchmarkDotNet, etc.) are not auto-updated." >> pr_body.txt
130+
echo "Use Dependabot or manual updates for those." >> pr_body.txt
131+
echo "" >> pr_body.txt
132+
echo "Generated by codebelt-aicia" >> pr_body.txt
133+
if [ -n "$SOURCE" ] && [ -n "$SRC_VER" ]; then
134+
echo "Triggered by: ${SOURCE} @ ${SRC_VER}" >> pr_body.txt
135+
else
136+
echo "Triggered by: manual workflow dispatch" >> pr_body.txt
137+
fi
138+
139+
gh pr create --title "V${NEW}/service update" --body-file pr_body.txt --base main --head "$BRANCH" --assignee gimlichael
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
name: Trigger Downstream Service Updates
2+
3+
on:
4+
release:
5+
types: [published]
6+
7+
jobs:
8+
dispatch:
9+
if: github.event.release.prerelease == false
10+
runs-on: ubuntu-24.04
11+
permissions:
12+
contents: read
13+
14+
steps:
15+
- name: Checkout (to read dispatch-targets.json)
16+
uses: actions/checkout@v4
17+
18+
- name: Check for dispatch targets
19+
id: check
20+
run: |
21+
if [ ! -f .github/dispatch-targets.json ]; then
22+
echo "No dispatch-targets.json found, skipping."
23+
echo "has_targets=false" >> $GITHUB_OUTPUT
24+
exit 0
25+
fi
26+
COUNT=$(python3 -c "import json; print(len(json.load(open('.github/dispatch-targets.json'))))")
27+
echo "has_targets=$([ $COUNT -gt 0 ] && echo true || echo false)" >> $GITHUB_OUTPUT
28+
29+
- name: Extract version from release tag
30+
if: steps.check.outputs.has_targets == 'true'
31+
id: version
32+
run: |
33+
VERSION="${{ github.event.release.tag_name }}"
34+
VERSION="${VERSION#v}"
35+
echo "version=$VERSION" >> $GITHUB_OUTPUT
36+
37+
- name: Generate codebelt-aicia token
38+
if: steps.check.outputs.has_targets == 'true'
39+
id: app-token
40+
uses: actions/create-github-app-token@v1
41+
with:
42+
app-id: ${{ vars.CODEBELT_AICIA_APP_ID }}
43+
private-key: ${{ secrets.CODEBELT_AICIA_PRIVATE_KEY }}
44+
owner: codebeltnet
45+
46+
- name: Dispatch to downstream repos
47+
if: steps.check.outputs.has_targets == 'true'
48+
run: |
49+
python3 - <<'EOF'
50+
import json, urllib.request, os, sys
51+
52+
targets = json.load(open('.github/dispatch-targets.json'))
53+
token = os.environ['GH_TOKEN']
54+
version = os.environ['VERSION']
55+
source = os.environ['SOURCE_REPO']
56+
57+
for repo in targets:
58+
url = f'https://api.github.com/repos/codebeltnet/{repo}/dispatches'
59+
payload = json.dumps({
60+
'event_type': 'codebelt-service-update',
61+
'client_payload': {
62+
'source_repo': source,
63+
'source_version': version
64+
}
65+
}).encode()
66+
req = urllib.request.Request(url, data=payload, method='POST', headers={
67+
'Authorization': f'Bearer {token}',
68+
'Accept': 'application/vnd.github+json',
69+
'Content-Type': 'application/json',
70+
'X-GitHub-Api-Version': '2022-11-28'
71+
})
72+
with urllib.request.urlopen(req) as r:
73+
print(f'✓ Dispatched to {repo}: HTTP {r.status}')
74+
EOF
75+
env:
76+
GH_TOKEN: ${{ steps.app-token.outputs.token }}
77+
VERSION: ${{ steps.version.outputs.version }}
78+
SOURCE_REPO: ${{ github.event.repository.name }}

0 commit comments

Comments
 (0)