Skip to content
Open
Changes from 1 commit
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
85 changes: 54 additions & 31 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 @@ -652,39 +659,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 +719,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
Loading