ci(nexus): fix GITHUB_OUTPUT delimiter and auto-generate changelogs#2254
Conversation
json.dump() produces no trailing newline, so the closing delimiter was appended to the last JSON character instead of being on a line by itself. GitHub Actions requires the delimiter to start at column 0. Fix: use printf '\n%s\n' so there is always a newline before the closing delimiter regardless of whether the JSON ends with one. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add get_feature_changelog() to feature_version_audit.py: collects feat/fix/perf/breaking commits for each feature dir + src/Features/ since the previous stable release tag, formatted as bullet points - build_nexus_upload_matrix() now accepts base_ref and populates a 'changelog' field on each feature row; core row is left empty as a placeholder (filled by the workflow from the GitHub release body) - upload-nexus.yaml: after running the audit script, fetch the GitHub release body via gh api (list endpoint, jq-JSON-encoded for safety) and inject it into the core matrix row before the matrix is split - upload-to-nexus job uses matrix.changelog with fallback to inputs.changelog (manual workflow_dispatch override) then '' Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
📝 WalkthroughWalkthroughThe changes enhance the Nexus matrix export workflow by adding commit-derived changelog generation. The GitHub workflow now fetches release body information using the GitHub API and injects it into the matrix configuration, while a Python tool extracts relevant commits from feature directories to generate user-facing changelogs during matrix building. Changes
Sequence DiagramsequenceDiagram
participant Workflow as Upload Workflow
participant GH as GitHub API
participant Python as Feature Version Audit
participant Git as Git Repository
participant Matrix as Nexus Matrix JSON
Workflow->>GH: Fetch release body for RELEASE_TAG (with GH_TOKEN)
GH-->>Workflow: Release body/changelog
Workflow->>Matrix: Inject changelog into core row
Workflow->>Python: Call build_nexus_upload_matrix(base_ref)
Python->>Git: git log for each feature dir (base_ref..HEAD)
Git-->>Python: Commit messages
Python->>Python: Filter feat/fix/perf/breaking-change commits
Python->>Python: Deduplicate and format as bullets
Python-->>Matrix: Populate changelog field per feature
Workflow->>Workflow: Pass matrix (with changelog) to downstream
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~22 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 actionlint (1.7.12).github/workflows/upload-nexus.yamlcould not read ".github/workflows/upload-nexus.yaml": open .github/workflows/upload-nexus.yaml: no such file or directory 🔧 YAMLlint (1.38.0).github/workflows/upload-nexus.yaml[Errno 2] No such file or directory: '.github/workflows/upload-nexus.yaml' 🔧 Checkov (3.2.525).github/workflows/upload-nexus.yaml2026-05-01 21:48:52,570 [MainThread ] [ERROR] Template file not found: .github/workflows/upload-nexus.yaml ... [truncated 9182 characters] ... File "/usr/local/lib/python3.11/dist-packages/checkov/common/runners/runner_registry.py", line 839, in _parallel_run Review rate limit: 9/10 reviews remaining, refill in 6 minutes. Comment |
|
No actionable suggestions for changed features. |
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 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/workflows/upload-nexus.yaml:
- Around line 151-153: Replace the paginated releases list call with the
releases-by-tag endpoint so the body is fetched server-side for the specific
tag: change the gh api invocation that currently calls
"repos/$GITHUB_REPOSITORY/releases" and uses --jq 'select(.tag_name ==
"$RELEASE_TAG") | .body' to call the tag endpoint for the given $RELEASE_TAG
(i.e., "repos/$GITHUB_REPOSITORY/releases/tags/$RELEASE_TAG") and write its
.body to core-body.json, preserving the existing error handling/redirect
behavior so older releases are not lost.
- Line 280: The changelog precedence currently uses "changelog: ${{
matrix.changelog || inputs.changelog || '' }}" which makes matrix.changelog win
over the manual workflow_dispatch input; swap the order so the manual input
overrides the matrix value by setting the expression to use inputs.changelog
first and fall back to matrix.changelog, e.g. change the expression so
inputs.changelog is evaluated before matrix.changelog while keeping the final
fallback of an empty string; update the line referencing matrix.changelog and
inputs.changelog accordingly.
In `@tools/feature_version_audit.py`:
- Around line 965-968: The code is directly constructing feature_dir as
FEATURES_DIR / name, bypassing the file-name fuzzy matching logic earlier;
modify the block that sets feature_dir (before calling get_feature_changelog) to
use the existing fuzzy resolver (the function used earlier in this file to map
display names to folder names—e.g., the resolve/find_feature_dir_by_name or
fuzzy matcher utility) to obtain the actual folder path and only fall back to
FEATURES_DIR / name if the matcher returns None; then pass that resolved
feature_dir into get_feature_changelog so folder-level commits like
"ScreenSpaceGI -> screen space gi" are detected.
- Around line 339-355: The git log call currently uses "--pretty=%s" so only
commit subjects are seen and FOOTER "BREAKING CHANGE:" entries are missed;
update the subprocess.check_output invocation (the raw variable creation) to
request commit body text as well (e.g., use "--pretty=%B" or a format that
includes subject and body like "%s%n%n%b") so RE_COMMIT_BREAKING can match
footers, keep the same decoding/error handling and downstream dedup logic that
iterates over raw and tests
RE_COMMIT_FEAT/RE_COMMIT_FIX/RE_COMMIT_PERF/RE_COMMIT_BREAKING.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: fe4e5af0-d936-4465-8fd0-2fdc533a6775
📒 Files selected for processing (2)
.github/workflows/upload-nexus.yamltools/feature_version_audit.py
| gh api "repos/$GITHUB_REPOSITORY/releases" \ | ||
| --jq "[.[] | select(.tag_name == \"$RELEASE_TAG\") | .body][0] // \"\"" \ | ||
| 2>/dev/null > core-body.json || echo '""' > core-body.json |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, locate and examine the workflow file
find . -name "upload-nexus.yaml" -type fRepository: community-shaders/skyrim-community-shaders
Length of output: 124
🏁 Script executed:
# Once found, examine the relevant section of the workflow file
cat -n .github/workflows/upload-nexus.yaml | sed -n '145,160p'Repository: community-shaders/skyrim-community-shaders
Length of output: 1093
🌐 Web query:
GitHub REST API releases endpoint pagination default page size
💡 Result:
The default page size for the GitHub REST API releases endpoint (e.g., GET /repos/{owner}/{repo}/releases) is 30 items per page. This can be overridden using the per_page parameter (max 100).
Citations:
- 1: https://docs.github.com/rest/releases/releases
- 2: https://docs.github.com/en/rest/releases/releases
- 3: https://docs.github.com/de/rest/releases/releases?apiVersion=2022-11-28
🌐 Web query:
GitHub API "releases/tags/{tag}" endpoint documentation
💡 Result:
The GitHub REST API endpoint for retrieving a release by tag name is GET /repos/{owner}/{repo}/releases/tags/{tag}. This endpoint gets a published release with the specified tag name. Parameters: - owner (string, required): The account owner of the repository. The name is not case sensitive. - repo (string, required): The name of the repository without the .git extension. The name is not case sensitive. - tag (string, required): The name of the tag. Headers: - accept (string): application/vnd.github+json is recommended. HTTP response status codes: - 200: OK - 404: Resource not found Example curl request: curl -L -X GET https://api.github.com/repos/OWNER/REPO/releases/tags/TAG Response schema (Status: 200): Same as the Create a release endpoint. Official documentation: https://docs.github.com/rest/releases/releases#Get-a-release-by-tag-name
Citations:
- 1: https://docs.github.com/rest/releases/releases
- 2: https://docs.github.com/en/rest/releases/releases
🏁 Script executed:
# Check how many releases are in this repository
gh release list --repo community-shaders/skyrim-community-shaders --limit 1000 2>/dev/null | wc -lRepository: community-shaders/skyrim-community-shaders
Length of output: 89
Fetch the release body from the tag endpoint instead of the releases list.
This repository has 56 releases, which exceeds the GitHub API's default page size of 30. The current code filters the paginated /releases response client-side, causing tags on pages 2+ to fall off and resolve to "", which leaves the core Nexus changelog blank for older releases.
Suggested fix
- gh api "repos/$GITHUB_REPOSITORY/releases" \
- --jq "[.[] | select(.tag_name == \"$RELEASE_TAG\") | .body][0] // \"\"" \
+ gh api "repos/$GITHUB_REPOSITORY/releases/tags/$RELEASE_TAG" \
+ --jq '.body // ""' \
2>/dev/null > core-body.json || echo '""' > core-body.json📝 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 api "repos/$GITHUB_REPOSITORY/releases" \ | |
| --jq "[.[] | select(.tag_name == \"$RELEASE_TAG\") | .body][0] // \"\"" \ | |
| 2>/dev/null > core-body.json || echo '""' > core-body.json | |
| gh api "repos/$GITHUB_REPOSITORY/releases/tags/$RELEASE_TAG" \ | |
| --jq '.body // ""' \ | |
| 2>/dev/null > core-body.json || echo '""' > core-body.json |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In @.github/workflows/upload-nexus.yaml around lines 151 - 153, Replace the
paginated releases list call with the releases-by-tag endpoint so the body is
fetched server-side for the specific tag: change the gh api invocation that
currently calls "repos/$GITHUB_REPOSITORY/releases" and uses --jq
'select(.tag_name == "$RELEASE_TAG") | .body' to call the tag endpoint for the
given $RELEASE_TAG (i.e., "repos/$GITHUB_REPOSITORY/releases/tags/$RELEASE_TAG")
and write its .body to core-body.json, preserving the existing error
handling/redirect behavior so older releases are not lost.
| mod_version: ${{ matrix.mod_version || needs.prepare-nexus-matrix.outputs.version }} | ||
| mod_filename: ${{ matrix.mod_filename }} | ||
| changelog: ${{ inputs.changelog || '' }} | ||
| changelog: ${{ matrix.changelog || inputs.changelog || '' }} |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n .github/workflows/upload-nexus.yaml | sed -n '270,290p'Repository: community-shaders/skyrim-community-shaders
Length of output: 1323
🏁 Script executed:
# Check the broader context to understand the intent
cat -n .github/workflows/upload-nexus.yaml | sed -n '250,300p'Repository: community-shaders/skyrim-community-shaders
Length of output: 3359
🏁 Script executed:
# Look for any documentation or comments about the changelog input precedence
rg -i "changelog.*input\|override\|precedence" .github/workflows/upload-nexus.yaml -B 3 -A 3Repository: community-shaders/skyrim-community-shaders
Length of output: 68
🏁 Script executed:
# Check the workflow inputs definition to understand what inputs.changelog is
rg -i "workflow_dispatch\|inputs:" .github/workflows/upload-nexus.yaml -A 20 | head -50Repository: community-shaders/skyrim-community-shaders
Length of output: 68
🏁 Script executed:
# Find workflow_dispatch and inputs definition
rg -i "on:\|workflow_dispatch\|inputs:" .github/workflows/upload-nexus.yaml -A 30 | head -80Repository: community-shaders/skyrim-community-shaders
Length of output: 68
🏁 Script executed:
# Check git log for recent commits mentioning changelog or precedence
git log --oneline --all -20 -- .github/workflows/upload-nexus.yamlRepository: community-shaders/skyrim-community-shaders
Length of output: 455
🏁 Script executed:
# Look for any PR or discussion context about this
git log --format="%B" --all -5 -- .github/workflows/upload-nexus.yaml | head -100Repository: community-shaders/skyrim-community-shaders
Length of output: 1269
🌐 Web query:
GitHub Actions expression syntax || operator precedence behavior
💡 Result:
GitHub Actions expression syntax follows standard operator precedence where parentheses have the highest precedence, followed by indexing and property dereference, unary !, comparison operators (< <= > >= == !=), logical AND (&&), and logical OR (||) with the lowest precedence. This means && binds tighter than ||, so expressions like 'condition1 || condition2 && condition3' are parsed as 'condition1 || (condition2 && condition3)'. The || operator performs logical OR with short-circuit evaluation: it evaluates the left operand first; if truthy, returns it without evaluating the right; if falsy, evaluates and returns the right. This enables ternary-like patterns: '${{ condition && 'true_value' || 'false_value' }}', but requires the 'true_value' to be truthy (non-empty string, non-falsy), as empty strings are falsy. Falsy values (false, 0, -0, "", '', null) coerce to false; others to true. Comparisons are loose, case-insensitive for strings. Examples: - Always true due to precedence/logic: (branch != 'prod') || (branch != 'staging') — use && for "neither". - Ternary: github.ref == 'refs/heads/main' && 'prod' || 'dev' — works if 'prod' truthy.
Citations:
- 1: https://docs.github.com/en/actions/reference/workflows-and-actions/expressions
- 2: https://docs.github.com/actions/reference/evaluate-expressions-in-workflows-and-actions
- 3: Conditional operator or function for expression syntax actions/runner#409
- 4: https://help.github.com/en/enterprise-server@3.4/actions/learn-github-actions/expressions
- 5: Or Condition not working in if actions/runner#1197
🌐 Web query:
GitHub Actions expression || operator "stops at first truthy value" how does it work
💡 Result:
The || (logical OR) operator in GitHub Actions expressions performs short-circuit evaluation, stopping at the first truthy value and returning it without evaluating subsequent operands. This behavior is demonstrated and relied upon in official documentation examples. Key evidence: - Official docs use the idiom condition && 'true_value' || 'false_value' as a ternary-like operator. They explicitly state: "It is important to note that the first value after the && condition must be truthy otherwise the value after the || will always be returned." This requires short-circuiting: if the condition is truthy, && returns the truthy 'true_value' (skipping ||), else && returns falsy, triggering || to return 'false_value'. - Operator precedence follows standard rules where && has higher precedence than || (as listed in docs with ! before comparisons before && before ||), and evaluation is left-to-right. - Community usage in GitHub issues (e.g., actions/runner#409) confirms this pattern works as expected for conditional value selection. Truthy/falsy coercion: falsy values include false, 0, '', null (coerced to false in conditionals per docs); all others are truthy. Example: env: MY_ENV_VAR: ${{ github.ref == 'refs/heads/main' && 'value_for_main_branch' || 'value_for_other_branches' }} If condition true (truthy), returns 'value_for_main_branch'; else 'value_for_other_branches'. Without short-circuit, it would always evaluate to the last operand. For boolean contexts like if:, the result is coerced to boolean (truthy/non-falsy -> true). No official docs explicitly state "short-circuit", but the documented behavior and examples confirm it functions identically to JavaScript logical operators (which GitHub expressions emulate).
Citations:
- 1: https://help.github.com/en/enterprise-server@3.7/actions/learn-github-actions/expressions
- 2: https://docs.github.com/en/actions/reference/workflows-and-actions/expressions
- 3: https://docs.github.com/actions/reference/evaluate-expressions-in-workflows-and-actions
- 4: https://docs.github.com/en/actions/concepts/workflows-and-actions/expressions
- 5: Conditional operator or function for expression syntax actions/runner#409
- 6: https://docs.github.com/en/actions/reference/evaluate-expressions-in-workflows-and-actions
Swap the changelog precedence so manual input can override matrix-computed values.
The || operator stops at the first truthy value. With the current order, any non-empty matrix.changelog prevents inputs.changelog from being used, contradicting the documented intent that workflow_dispatch.inputs.changelog provides a "manual override."
Suggested fix
- changelog: ${{ matrix.changelog || inputs.changelog || '' }}
+ changelog: ${{ inputs.changelog || matrix.changelog || '' }}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In @.github/workflows/upload-nexus.yaml at line 280, The changelog precedence
currently uses "changelog: ${{ matrix.changelog || inputs.changelog || '' }}"
which makes matrix.changelog win over the manual workflow_dispatch input; swap
the order so the manual input overrides the matrix value by setting the
expression to use inputs.changelog first and fall back to matrix.changelog, e.g.
change the expression so inputs.changelog is evaluated before matrix.changelog
while keeping the final fallback of an empty string; update the line referencing
matrix.changelog and inputs.changelog accordingly.
| raw = subprocess.check_output( | ||
| ["git", "log", f"{base_ref}..{HEAD_REF}", "--pretty=%s", "--"] + paths, | ||
| cwd=str(PROJECT_ROOT), | ||
| stderr=subprocess.DEVNULL, | ||
| ).decode("utf-8", errors="replace") | ||
| except Exception: | ||
| return "" | ||
| seen = set() | ||
| lines = [] | ||
| for msg in raw.splitlines(): | ||
| msg = msg.strip() | ||
| if not msg or msg in seen: | ||
| continue | ||
| seen.add(msg) | ||
| if (RE_COMMIT_FEAT.match(msg) or RE_COMMIT_FIX.match(msg) or | ||
| RE_COMMIT_PERF.match(msg) or RE_COMMIT_BREAKING.search(msg)): | ||
| lines.append(f"- {msg}") |
There was a problem hiding this comment.
Include footer-based breaking changes in the changelog.
Using git log --pretty=%s means this filter only sees the subject line, so commits that mark a breaking change via a BREAKING CHANGE: footer are silently dropped even though the function says it includes them.
Suggested fix
- raw = subprocess.check_output(
- ["git", "log", f"{base_ref}..{HEAD_REF}", "--pretty=%s", "--"] + paths,
+ raw = subprocess.check_output(
+ ["git", "log", f"{base_ref}..{HEAD_REF}", "--pretty=%B%x1e", "--"] + paths,
cwd=str(PROJECT_ROOT),
stderr=subprocess.DEVNULL,
).decode("utf-8", errors="replace")
@@
- for msg in raw.splitlines():
- msg = msg.strip()
- if not msg or msg in seen:
+ for entry in raw.split("\x1e"):
+ msg = entry.strip()
+ if not msg:
continue
- seen.add(msg)
+ subject = msg.splitlines()[0].strip()
+ if not subject or subject in seen:
+ continue
+ seen.add(subject)
if (RE_COMMIT_FEAT.match(msg) or RE_COMMIT_FIX.match(msg) or
RE_COMMIT_PERF.match(msg) or RE_COMMIT_BREAKING.search(msg)):
- lines.append(f"- {msg}")
+ lines.append(f"- {subject}")🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@tools/feature_version_audit.py` around lines 339 - 355, The git log call
currently uses "--pretty=%s" so only commit subjects are seen and FOOTER
"BREAKING CHANGE:" entries are missed; update the subprocess.check_output
invocation (the raw variable creation) to request commit body text as well
(e.g., use "--pretty=%B" or a format that includes subject and body like
"%s%n%n%b") so RE_COMMIT_BREAKING can match footers, keep the same
decoding/error handling and downstream dedup logic that iterates over raw and
tests RE_COMMIT_FEAT/RE_COMMIT_FIX/RE_COMMIT_PERF/RE_COMMIT_BREAKING.
| if base_ref: | ||
| feature_dir = FEATURES_DIR / name | ||
| changelog = get_feature_changelog(feature_dir, info, base_ref) | ||
| if changelog: |
There was a problem hiding this comment.
Resolve the feature folder with the existing fuzzy matcher.
Line 966 rebuilds the path as FEATURES_DIR / name, which skips the ScreenSpaceGI -> screen space gi fallback already implemented earlier in this file. For those features, folder-level commits are missed and the generated Nexus changelog is incomplete.
Suggested fix
if mod_version:
row['mod_version'] = mod_version
if base_ref:
- feature_dir = FEATURES_DIR / name
+ feature_dir = find_feature_dir(name) or (FEATURES_DIR / name)
changelog = get_feature_changelog(feature_dir, info, base_ref)
if changelog:
row['changelog'] = changelog📝 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.
| if base_ref: | |
| feature_dir = FEATURES_DIR / name | |
| changelog = get_feature_changelog(feature_dir, info, base_ref) | |
| if changelog: | |
| if base_ref: | |
| feature_dir = find_feature_dir(name) or (FEATURES_DIR / name) | |
| changelog = get_feature_changelog(feature_dir, info, base_ref) | |
| if changelog: |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@tools/feature_version_audit.py` around lines 965 - 968, The code is directly
constructing feature_dir as FEATURES_DIR / name, bypassing the file-name fuzzy
matching logic earlier; modify the block that sets feature_dir (before calling
get_feature_changelog) to use the existing fuzzy resolver (the function used
earlier in this file to map display names to folder names—e.g., the
resolve/find_feature_dir_by_name or fuzzy matcher utility) to obtain the actual
folder path and only fall back to FEATURES_DIR / name if the matcher returns
None; then pass that resolved feature_dir into get_feature_changelog so
folder-level commits like "ScreenSpaceGI -> screen space gi" are detected.
|
✅ A pre-release build is available for this PR: |
…shaders#2254) Adapted from PR community-shaders#2254 onto this branch's Nexus upload workflow.
…shaders#2254) Adapted from PR community-shaders#2254 onto this branch's Nexus upload workflow.
…shaders#2254) Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Summary
json.dump()produces no trailing newline, causing the GITHUB_OUTPUT multiline delimiter to land on the same line as the last JSON character instead of its own line. GitHub Actions requires the delimiter at column 0. Fixed by usingprintf '\n%s\n'for both closing delimiters.feature_version_audit.pynow adds achangelogfield to each feature row in the upload matrix, built fromfeat/fix/perf/breaking commits touching that feature's files since the previous stable taggh apilist endpoint, JSON-encoded for safe multiline handling) injected into the matrix before the split stepupload-to-nexusjob usesmatrix.changelog || inputs.changelog || ''so manualworkflow_dispatchwith an explicit changelog still works as overrideTest plan
prepare-nexus-matrixsucceeds (matrix output parses correctly)changeloginput: confirm the explicit value is used whenmatrix.changelogis absent🤖 Generated with Claude Code
Summary by CodeRabbit