Skip to content
Open
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
121 changes: 84 additions & 37 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
name: Build and Release Executable

# TODO(definitive-release-workflow): Keep this release workflow aligned with
# opencode-agent-variants where practical. The intended shared shape is:
# centralized config, branch/channel release policy, generated release notes,
# author resolution, community contributor detection, and project-specific
# packaging steps. Do not extract into a separate shared workflow unless more
# projects need it; keep the two workflows readable sibling implementations.

# ╔═══════════════════════════════════════════════════════════════════════════════════════╗
# ║ CONFIGURATION SECTION ║
# ║ Edit the values below to customize build triggers, release contents, and behavior. ║
Expand Down Expand Up @@ -107,8 +114,10 @@ on:
paths:
- 'src/proxy_app/**' # Main application source code
- 'src/rotator_library/**' # Key rotation library
Comment on lines 114 to 116
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Restore workflow triggers

The push.paths filter now only matches source directories, so a commit that changes only this release workflow or .github/cliff.toml on dev/main will not run the build-and-release workflow. That means release workflow fixes and changelog-template changes can merge without exercising the release path they changed, and the updated release notes logic will not run until an unrelated source change is pushed.

Suggested change
paths:
- 'src/proxy_app/**' # Main application source code
- 'src/rotator_library/**' # Key rotation library
paths:
- 'src/proxy_app/**' # Main application source code
- 'src/rotator_library/**' # Key rotation library
- '.github/workflows/build.yml' # This workflow file
- '.github/cliff.toml' # Changelog configuration

- '.github/workflows/build.yml' # This workflow file
- 'cliff.toml' # Changelog configuration

concurrency:
group: build-release-${{ github.ref }}
cancel-in-progress: false

# ════════════════════════════════════════════════════════════════════════════════════════════
# JOBS
Expand Down Expand Up @@ -234,6 +243,21 @@ jobs:
BRANCH_NAME=${{ github.ref_name }}
DATE_STAMP_NEW=$(date +'%Y%m%d')
DATE_STAMP_OLD=$(date +'%Y.%m.%d')

EXISTING_TAG=$(gh release list --repo "${{ github.repository }}" --limit 1000 --json tagName,targetCommitish \
--jq ".[] | select(.tagName | startswith(\"$BRANCH_NAME/build-\")) | select(.targetCommitish == \"${{ github.sha }}\") | .tagName" \
| head -n 1)
Comment on lines +247 to +249
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The --limit 1000 fetches up to 1000 releases to search for an existing tag targeting this SHA. If the repo ever exceeds 1000 releases (unlikely with pruning, but possible if pruning is disabled or during heavy development), this check could silently miss an existing release and create a duplicate.

Consider adding a targeted check via gh release view with the expected tag prefix, or document the 1000-release assumption with a brief comment for future maintainers.


if [ -n "$EXISTING_TAG" ]; then
echo "Release already exists for this commit: $EXISTING_TAG"
echo "release_title=Build ($BRANCH_NAME): ${EXISTING_TAG#*/build-}" >> $GITHUB_OUTPUT
echo "release_tag=$EXISTING_TAG" >> $GITHUB_OUTPUT
echo "archive_version_part=${EXISTING_TAG%%/*}-${EXISTING_TAG#*/build-}" >> $GITHUB_OUTPUT
echo "version=${EXISTING_TAG#*/build-}" >> $GITHUB_OUTPUT
echo "timestamp=$(date -u +'%Y-%m-%d %H:%M:%S UTC')" >> $GITHUB_OUTPUT
echo "should_release=false" >> $GITHUB_OUTPUT
exit 0
fi
Comment on lines +247 to +260
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The idempotency check correctly prevents duplicate releases, but the build job (3 OS matrix runners, lines 127–214) still compiles all binaries before the release job discovers the duplicate and skips. For repos where duplicate triggers are common, this wastes significant runner time.

Consider adding a lightweight check job before build that performs this same existing-tag lookup and gates the build job with if: needs.check.outputs.should_build == true. This would short-circuit the expensive cross-platform compilation when no release is needed.

Comment on lines +247 to +260
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

What JSON fields does gh release list --json support, and does it include targetCommitish?

💡 Result:

The gh release list command supports the --json flag, which allows you to output specific fields for each release in the repository [1][2]. According to the official GitHub CLI documentation, the following fields are supported [1][2]: createdAt, isDraft, isImmutable, isLatest, isPrerelease, name, publishedAt, tagName The field targetCommitish is not included in the supported fields for gh release list [1][2]. While targetCommitish is available as a JSON field in other commands like gh release view [3], it is currently not an option for the list subcommand [1][2]. If you need to access targetCommitish for releases, you would typically need to use gh release view for individual releases or query the GitHub API directly [4][3].

Citations:


Fix unsupported targetCommitish in gh release list --json

gh release list --json only supports createdAt, isDraft, isImmutable, isLatest, isPrerelease, name, publishedAt, tagName and does not include targetCommitish, so this command will error and abort the “Generate Build Version” step.

🐛 Proposed fix using local tags
-        EXISTING_TAG=$(gh release list --repo "${{ github.repository }}" --limit 1000 --json tagName,targetCommitish \
-          --jq ".[] | select(.tagName | startswith(\"$BRANCH_NAME/build-\")) | select(.targetCommitish == \"${{ github.sha }}\") | .tagName" \
-          | head -n 1)
+        EXISTING_TAG=$(git tag --list "$BRANCH_NAME/build-*" --points-at "${{ github.sha }}" | head -n 1)
🧰 Tools
🪛 zizmor (1.25.2)

[warning] 247-247: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)


[warning] 248-248: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/build.yml around lines 247 - 260, The gh release list call
uses an unsupported field targetCommitish; update the logic that sets
EXISTING_TAG to only request supported fields (e.g., tagName) and then, for each
tagName that starts with "$BRANCH_NAME/build-", resolve that tag's commit and
compare it to "${{ github.sha }}" to decide if a release for this commit already
exists; specifically, keep the EXISTING_TAG variable and BRANCH_NAME usage but
replace the single gh release list --json ... --jq pipeline with a two-step
approach: (1) call gh release list --json tagName (or use gh api to list
releases) and filter tagName values that startwith "$BRANCH_NAME/build-", and
(2) for each candidate tagName use git (git rev-list --max-count=1 <tag> or git
rev-parse refs/tags/<tag>) or gh api (repos/:owner/:repo/git/ref/tags/:tag) to
get the tag's target commit SHA and compare to "${{ github.sha }}" to set
EXISTING_TAG when equal so the rest of the existing early-exit code
(release_title, release_tag, archive_version_part, version, timestamp,
should_release) can remain unchanged.


# Find the number of releases already created today for this branch, matching either old or new format.
# We use grep -E for an OR condition and wrap it to prevent failures when no matches are found.
Expand All @@ -251,14 +275,17 @@ jobs:
echo "archive_version_part=$BRANCH_NAME-$VERSION" >> $GITHUB_OUTPUT
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "timestamp=$(date -u +'%Y-%m-%d %H:%M:%S UTC')" >> $GITHUB_OUTPUT
echo "should_release=true" >> $GITHUB_OUTPUT

- name: Download build artifacts
if: steps.version.outputs.should_release == 'true'
uses: actions/download-artifact@v4
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# List action references not pinned to a 40-char commit SHA
rg -nP 'uses:\s*\S+@(?![0-9a-f]{40}\b)\S+' -g '*.yml' -g '*.yaml' .github

Repository: Mirrowel/LLM-API-Key-Proxy

Length of output: 1990


🏁 Script executed:

#!/bin/bash
# List action references not pinned to a 40-char commit SHA
rg -nP 'uses:\s*\S+@(?![0-9a-f]{40}\b)\S+' -g '*.yml' -g '*.yaml' .github

Repository: Mirrowel/LLM-API-Key-Proxy

Length of output: 1990


🏁 Script executed:

#!/bin/bash
# List action references not pinned to a 40-char commit SHA
rg -nP 'uses:\s*\S+@(?![0-9a-f]{40}\b)\S+' -g '*.yml' -g '*.yaml' .github

Repository: Mirrowel/LLM-API-Key-Proxy

Length of output: 1990


🏁 Script executed:

#!/bin/bash
# List action references not pinned to a 40-char commit SHA
rg -nP 'uses:\s*\S+@(?![0-9a-f]{40}\b)\S+' -g '*.yml' -g '*.yaml' .github

Repository: Mirrowel/LLM-API-Key-Proxy

Length of output: 1990


🏁 Script executed:

#!/bin/bash
# List action references not pinned to a 40-char commit SHA
rg -nP 'uses:\s*\S+@(?![0-9a-f]{40}\b)\S+' -g '*.yml' -g '*.yaml' .github

Repository: Mirrowel/LLM-API-Key-Proxy

Length of output: 1990


🏁 Script executed:

#!/bin/bash
# List action references not pinned to a 40-char commit SHA
rg -nP 'uses:\s*\S+@(?![0-9a-f]{40}\b)\S+' -g '*.yml' -g '*.yaml' .github

Repository: Mirrowel/LLM-API-Key-Proxy

Length of output: 1990


Pin zizmor-flagged uses: actions to commit SHAs

.github/workflows/build.yml:282 uses actions/download-artifact@v4 (mutable tag), which zizmor reports as an unpinned-uses error. Other unpinned uses: refs found:

  • .github/workflows/issue-comment.yml:29 actions/checkout@v4
  • .github/workflows/issue-comment.yml:56 actions/checkout@v4
  • .github/workflows/pr-review.yml:98 actions/checkout@v4
  • .github/workflows/pr-review.yml:527 actions/checkout@v4
  • .github/workflows/docker-build.yml:38 actions/checkout@v5
  • .github/workflows/docker-build.yml:43 docker/setup-qemu-action@v3
  • .github/workflows/docker-build.yml:46 docker/setup-buildx-action@v3
  • .github/workflows/docker-build.yml:50 docker/login-action@v3
  • .github/workflows/docker-build.yml:90 docker/metadata-action@v5
  • .github/workflows/docker-build.yml:101 docker/build-push-action@v6
  • .github/workflows/docker-build.yml:113 actions/attest-build-provenance@v3
  • .github/workflows/docker-build.yml:127 actions/delete-package-versions@v5
  • .github/workflows/cleanup.yml:34 actions/checkout@v4
  • .github/workflows/compliance-check.yml:217 actions/checkout@v4
  • .github/workflows/compliance-check.yml:376 actions/checkout@v4
  • .github/workflows/bot-reply.yml:79 actions/checkout@v4
  • .github/workflows/bot-reply.yml:536 actions/checkout@v4
  • .github/workflows/bot-reply.yml:599 actions/checkout@v4
  • .github/workflows/build.yml:134 actions/checkout@v5
  • .github/workflows/build.yml:137 astral-sh/setup-uv@v4
  • .github/workflows/build.yml:169 actions/cache@v4
  • .github/workflows/build.yml:211 actions/upload-artifact@v4
  • .github/workflows/build.yml:224 actions/checkout@v5
  • .github/workflows/build.yml:282 actions/download-artifact@v4
  • .github/actions/bot-setup/action.yml:34 actions/create-github-app-token@v1
  • .github/actions/bot-setup/action.yml:159 astral-sh/setup-uv@v4

Replace each owner/repo@v* with owner/repo@<40-char-commit-sha> (keep the version as a trailing comment) to satisfy the policy and avoid gating.

🧰 Tools
🪛 zizmor (1.25.2)

[error] 282-282: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/build.yml at line 282, The workflow uses mutable action
tags like actions/download-artifact@v4 (and many others such as
actions/checkout@v4, actions/checkout@v5, docker/build-push-action@v6, etc.
listed by the review), which must be replaced with immutable 40-character commit
SHAs; update each `uses: owner/repo@v*` entry (including the one referenced as
actions/download-artifact@v4) to `owner/repo@<40-char-commit-sha>` and add the
original tag as a trailing comment (e.g., # v4) so the version is documented
while ensuring the action is pinned to a specific commit SHA.

with:
path: release-assets
pattern: proxy-app-build-*-${{ steps.get_sha.outputs.sha }}

- name: Archive release files
if: steps.version.outputs.should_release == 'true'
id: archive
shell: bash
run: |
Expand Down Expand Up @@ -321,6 +348,7 @@ jobs:
echo "ASSET_PATHS=$ASSET_PATHS" >> $GITHUB_OUTPUT

- name: Install git-cliff
if: steps.version.outputs.should_release == 'true'
shell: bash
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Expand All @@ -338,6 +366,7 @@ jobs:
sudo mv git-cliff-*/git-cliff /usr/local/bin/

- name: Prepare git-cliff config
if: steps.version.outputs.should_release == 'true'
shell: bash
run: |
# Inject the GitHub repo URL into your template
Expand All @@ -346,6 +375,7 @@ jobs:
head -20 .github/cliff.toml

- name: Generate Changelog
if: steps.version.outputs.should_release == 'true'
id: changelog
shell: bash
env:
Expand Down Expand Up @@ -474,6 +504,7 @@ jobs:
fi

- name: Resolve GitHub Usernames in Changelog
if: steps.version.outputs.should_release == 'true'
id: resolve_usernames
shell: bash
env:
Expand Down Expand Up @@ -652,39 +683,55 @@ jobs:
if [ -n "$PREV_TAG" ]; then
echo "🔍 Layer PR: Generating Community Contributions section..."

# Get all merge commits in the range
MERGE_COMMITS=$(git log "$PREV_TAG".."$CURRENT_SHA" --oneline --grep="Merge pull request" 2>/dev/null || true)

if [ -n "$MERGE_COMMITS" ]; then
OWNER="${{ github.repository_owner }}"
REPO_NAME="${{ github.event.repository.name }}"
Comment on lines +686 to +687
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Move GitHub context expansions into env: to clear the template-injection findings.

zizmor flags lines 662, 663 (error), and 695 as template-injection because the ${{ … }} values are interpolated directly into the script body. Real exploitability here is low (github.repository_owner, github.event.repository.name, and github.repository cannot contain shell metacharacters), but routing them through the step env: block is the standard hardening and resolves the gating static-analysis error on Line 663.

🔒 Proposed hardening via step env

Add to the step's env: block (near Line 486):

      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        OWNER: ${{ github.repository_owner }}
        REPO_NAME: ${{ github.event.repository.name }}
        REPO_FULL: ${{ github.repository }}

Then reference the env vars instead of interpolating:

-          OWNER="${{ github.repository_owner }}"
-          REPO_NAME="${{ github.event.repository.name }}"
           PR_NUMBERS=""
-              PR_INFO=$(gh api "repos/${{ github.repository }}/pulls/$PR_NUM" \
+              PR_INFO=$(gh api "repos/$REPO_FULL/pulls/$PR_NUM" \
                 --jq '{title: .title, author: .user.login, url: .html_url}' 2>/dev/null || echo "{}")
🧰 Tools
🪛 zizmor (1.25.2)

[warning] 662-662: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)


[error] 663-663: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/build.yml around lines 662 - 663, Move direct GitHub
context expansions out of the script body and into the step-level env block:
define environment variables (e.g., OWNER, REPO_NAME, REPO_FULL) in the step's
env: using the GitHub contexts (${ { github.repository_owner } }, ${ {
github.event.repository.name } }, ${ { github.repository } }) and then update
any shell script references that currently interpolate those contexts (OWNER and
REPO_NAME usages in the script) to read from the environment variables instead;
this removes template-injection instances flagged by static analysis while
keeping the same values available to the step.

PR_NUMBERS=""

# Layer PR-1: parse PR numbers from commit subjects. This keeps the old
# merge-commit behavior and also catches squash commits that include #123.
SUBJECT_PRS=$(git log "$PREV_TAG".."$CURRENT_SHA" --format='%s' 2>/dev/null | grep -oE 'Merge pull request #[0-9]+|#[0-9]+' | grep -oE '[0-9]+' || true)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Issue numbers become PRs

This scans every commit subject for any #123 token, not just merge commits or squash-merge PR suffixes. When a release range contains a normal commit like fix: handle timeout #42, this code treats 42 as a pull request number. If PR 42 exists, the release notes can add an unrelated Community Contributions entry.

Suggested change
SUBJECT_PRS=$(git log "$PREV_TAG".."$CURRENT_SHA" --format='%s' 2>/dev/null | grep -oE 'Merge pull request #[0-9]+|#[0-9]+' | grep -oE '[0-9]+' || true)
SUBJECT_PRS=$(git log "$PREV_TAG".."$CURRENT_SHA" --format='%s' 2>/dev/null | grep -oE '^Merge pull request #[0-9]+|\(#[0-9]+\)$' | grep -oE '[0-9]+' || true)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This regex will match any #123 occurrence in commit subjects — including false positives like issue references (fixes #123, related to #456). Consider tightening to only match explicit PR patterns:

Suggested change
SUBJECT_PRS=$(git log "$PREV_TAG".."$CURRENT_SHA" --format='%s' 2>/dev/null | grep -oE 'Merge pull request #[0-9]+|#[0-9]+' | grep -oE '[0-9]+' || true)
SUBJECT_PRS=$(git log "$PREV_TAG".."$CURRENT_SHA" --format='%s' 2>/dev/null | grep -oE 'Merge pull request #[0-9]+|\([^)]*#[0-9]+[^)]*\)' | grep -oE '[0-9]+' || true)

This limits matching to Merge pull request #N subjects and (#N) parenthesized references, which are the conventional squash/merge PR formats.

PR_NUMBERS=$(printf '%s\n%s\n' "$PR_NUMBERS" "$SUBJECT_PRS")

# Layer PR-2: GraphQL associatedPullRequests catches squash/rebase flows
# even when the commit subject does not include a PR number.
COMMIT_SHAS=$(git rev-list "$PREV_TAG".."$CURRENT_SHA" 2>/dev/null || true)
while read -r SHA; do
if [ -z "$SHA" ]; then
continue
fi
ASSOCIATED_PRS=$(gh api graphql \
-f owner="$OWNER" \
-f name="$REPO_NAME" \
-f oid="$SHA" \
-f query='query($owner:String!, $name:String!, $oid:GitObjectID!) { repository(owner:$owner, name:$name) { object(oid:$oid) { ... on Commit { associatedPullRequests(first: 10) { nodes { number } } } } } }' \
--jq '.data.repository.object.associatedPullRequests.nodes[].number' 2>/dev/null || true)
PR_NUMBERS=$(printf '%s\n%s\n' "$PR_NUMBERS" "$ASSOCIATED_PRS")
Comment on lines +697 to +708
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Per-commit API loop

This makes one GraphQL request for every commit between the previous tag and the current SHA. On a large release range, such as a parent-tag fallback or a long-lived branch, the release job can spend a long time in this loop or hit GitHub API limits. Since errors are redirected and swallowed, failed requests silently drop associated PRs from the Community Contributions section.

done <<< "$COMMIT_SHAS"
Comment on lines +697 to +709
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This issues a GraphQL API call per commit in the range. For releases with many commits (20-50+), this adds significant runtime. Two options to consider:

  1. Batch via GraphQL aliases — query multiple OIDs in a single request using c0: ... on Commit { ... }, c1: ... aliases.
  2. Early exit — break out of the loop once no new PR numbers are being discovered (most PRs are found in the first few commits of a range).

Neither is blocking, but for a CI workflow that may run on every release, the N API calls could become noticeable.

Comment on lines +695 to +709
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Per-commit GraphQL lookup is O(commits) API calls — watch rate limits on large ranges.

This loop issues one gh api graphql call per commit. Combined with the existing per-commit username resolution (Layer B-2) and the per-email search/users calls in "Generate Build Metadata", a large range — especially the parent-branch fallback path where the range can span many commits — can generate hundreds of API calls and risk secondary rate limiting plus long runtimes. Consider batching the associatedPullRequests lookups into a single GraphQL request using aliased object(oid:) fields (chunked), which keeps the robustness benefit while collapsing N calls into a few.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/build.yml around lines 671 - 685, The current loop issues
a gh api graphql call per commit (COMMIT_SHAS -> while read -> ASSOCIATED_PRS),
which can exhaust rate limits; replace it by batching commits into chunks and
issuing a single gh api graphql call per chunk that builds a GraphQL query with
aliased object(oid: "<sha>") fields (e.g., alias1: object(oid: "...") { ... on
Commit { associatedPullRequests { nodes { number } } } } ) for all SHAs in the
chunk, then extract all returned numbers and append them into PR_NUMBERS;
implement chunking (e.g., 50–100 SHAs per request) and fall back gracefully on
partial failures (continue on errors) to preserve robustness while drastically
reducing the number of API calls.


PR_NUMBERS=$(printf '%s\n' "$PR_NUMBERS" | grep -E '^[0-9]+$' | sort -n | uniq || true)

if [ -n "$PR_NUMBERS" ]; then
PR_SECTION=""

while IFS= read -r commit_line; do
if [ -n "$commit_line" ]; then
# Extract PR number from "Merge pull request #XX from ..."
PR_NUM=$(echo "$commit_line" | grep -oE '#[0-9]+' | head -1 | tr -d '#')

if [ -n "$PR_NUM" ]; then
# Fetch PR info from GitHub API
PR_INFO=$(gh api "repos/${{ github.repository }}/pulls/$PR_NUM" \
--jq '{title: .title, author: .user.login}' 2>/dev/null || echo "{}")

PR_TITLE=$(echo "$PR_INFO" | jq -r '.title // empty')
PR_AUTHOR=$(echo "$PR_INFO" | jq -r '.author // empty')

if [ -n "$PR_TITLE" ] && [ -n "$PR_AUTHOR" ]; then
PR_URL="https://github.com/${{ github.repository }}/pull/$PR_NUM"
PR_SECTION="${PR_SECTION}- ${PR_TITLE} ([#${PR_NUM}](${PR_URL})) by @${PR_AUTHOR}"$'\n'
PR_COUNT=$((PR_COUNT + 1))
echo " ✅ PR #$PR_NUM: $PR_TITLE by @$PR_AUTHOR"
else
echo " ⚠️ PR #$PR_NUM: Could not fetch info"
fi
fi
while read -r PR_NUM; do
if [ -z "$PR_NUM" ]; then
continue
fi
done <<< "$MERGE_COMMITS"

PR_INFO=$(gh api "repos/${{ github.repository }}/pulls/$PR_NUM" \
--jq '{title: .title, author: .user.login, url: .html_url}' 2>/dev/null || echo "{}")
PR_TITLE=$(echo "$PR_INFO" | jq -r '.title // empty')
PR_AUTHOR=$(echo "$PR_INFO" | jq -r '.author // empty')
PR_URL=$(echo "$PR_INFO" | jq -r '.url // empty')

if [ -n "$PR_TITLE" ] && [ -n "$PR_AUTHOR" ] && [ -n "$PR_URL" ]; then
PR_SECTION="${PR_SECTION}- ${PR_TITLE} ([#${PR_NUM}](${PR_URL})) by @${PR_AUTHOR}"$'\n'
PR_COUNT=$((PR_COUNT + 1))
echo " ✅ PR #$PR_NUM: $PR_TITLE by @$PR_AUTHOR"
else
echo " ⚠️ PR #$PR_NUM: Could not fetch info"
fi
done <<< "$PR_NUMBERS"

if [ "$PR_COUNT" -gt 0 ]; then
# Append PR section to changelog
{
echo ""
echo "### 💜 Community Contributions"
Expand All @@ -696,7 +743,7 @@ jobs:
echo " ✅ Added $PR_COUNT PRs to Community Contributions section"
fi
else
echo " ℹ️ No merge commits found in range"
echo " ℹ️ No associated PRs found in range"
fi
else
echo "⚠️ Layer PR: Skipped (no previous tag available)"
Expand Down Expand Up @@ -732,6 +779,7 @@ jobs:
fi

- name: Debug artifact contents
if: steps.version.outputs.should_release == 'true'
shell: bash
run: |
echo "🔍 Debugging artifact contents..."
Expand All @@ -748,6 +796,7 @@ jobs:
find release-assets -type f 2>/dev/null || echo "No files found in release-assets"

- name: Generate Build Metadata
if: steps.version.outputs.should_release == 'true'
id: metadata
shell: bash
env:
Expand Down Expand Up @@ -810,6 +859,7 @@ jobs:
echo " - Contributors: $CONTRIBUTORS_LIST"

- name: Create Release
if: steps.version.outputs.should_release == 'true'
shell: bash
run: |
# Prepare changelog content - prefer resolved version if available
Expand Down Expand Up @@ -921,10 +971,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Prune Old Releases
if: always() # Run even if release creation failed (optional, but safer to run only on success usually. Let's stick to default behavior which is success)
# Actually, if release creation failed, we probably don't want to prune.
# But wait, the user might want to prune even if the new release fails? No, usually we prune to make space for the new one or clean up after.
# Let's stick to running only on success of previous steps.
if: steps.version.outputs.should_release == 'true'
shell: bash
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Expand Down
Loading