From c7dec64b2dd4f94a0c1f39a37c6c0b3cbbb9646b Mon Sep 17 00:00:00 2001 From: Henry Park Date: Thu, 12 Mar 2026 15:33:35 -0700 Subject: [PATCH] feat(ci): include commit history in staging promotion PRs (#952) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(ci): include commit history in staging promotion PRs and merge commits Promotion PRs from staging->main previously had opaque bodies showing only the batch SHA range. Now they enumerate all non-merge commits in each batch as a flat markdown list, visible both in the PR body and embedded in the merge commit message via --subject/--body flags. Co-Authored-By: Claude Opus 4.6 * fix(ci): use unique delimiter for commit_summary output Replace hardcoded COMMIT_SUMMARY_DELIM with a uuidgen-based delimiter to prevent theoretical collisions with commit message content. Co-Authored-By: Claude Opus 4.6 * fix(ci): use heredoc for PR body to avoid GFM code-block rendering The inline --body string had 10 leading spaces per line (from YAML indentation), which GitHub-flavored Markdown renders as a code block. Move the body into a heredoc variable so content starts at column 0. Co-Authored-By: Claude Opus 4.6 * fix(ci): truncate commit list at 50 and include PR number in merge subject - Cap commit enumeration at 50 entries with a truncation note to avoid blowing past GitHub PR body/merge message limits on large batches. - Prefix merge commit subject with #PR_NUMBER for traceability in git log. Co-Authored-By: Claude Opus 4.6 * fix(ci): address review — shell expansion, body-file, uuidgen 1. Replace heredoc with string concatenation to prevent shell expansion of commit messages containing $, backticks, or backslashes 2. Use --body-file for merge commit body for robustness 3. Replace uuidgen with date +%s for portability Addresses: https://github.com/nearai/ironclaw/pull/952#pullrequestreview-3938725460 Co-Authored-By: Claude Opus 4.6 --------- Co-authored-by: Claude Opus 4.6 --- .github/workflows/staging-ci.yml | 66 ++++++++++++++++++++++++-------- 1 file changed, 51 insertions(+), 15 deletions(-) diff --git a/.github/workflows/staging-ci.yml b/.github/workflows/staging-ci.yml index f89fa05a..ba0b8f91 100644 --- a/.github/workflows/staging-ci.yml +++ b/.github/workflows/staging-ci.yml @@ -108,6 +108,7 @@ jobs: outputs: pr_number: ${{ steps.create-pr.outputs.pr_number }} promotion_branch: ${{ steps.branch.outputs.branch }} + commit_summary: ${{ steps.create-pr.outputs.commit_summary }} steps: - uses: actions/checkout@v6 with: @@ -186,30 +187,59 @@ jobs: BRANCH="${{ steps.branch.outputs.branch }}" BASE="${{ steps.find-base.outputs.base }}" + # Enumerate commits in this batch (exclude merge commits, cap at 50) + MAX_COMMITS=50 + COMMIT_LIST=$(git log --oneline --no-merges --reverse "${RANGE}" 2>/dev/null || echo "") + if [ -n "$COMMIT_LIST" ]; then + COMMIT_COUNT=$(echo "$COMMIT_LIST" | wc -l | tr -d ' ') + if [ "$COMMIT_COUNT" -gt "$MAX_COMMITS" ]; then + COMMIT_MD=$(echo "$COMMIT_LIST" | head -n "$MAX_COMMITS" | sed 's/^/- /') + COMMIT_MD="${COMMIT_MD} +- ... and $((COMMIT_COUNT - MAX_COMMITS)) more (see compare view)" + else + COMMIT_MD=$(echo "$COMMIT_LIST" | sed 's/^/- /') + fi + else + COMMIT_COUNT=0 + COMMIT_MD="- (no non-merge commits in range)" + fi + + # Build PR body via concatenation to avoid heredoc shell expansion + # (commit messages in COMMIT_MD may contain $, backticks, or backslashes) + PR_BODY="## Auto-promotion from staging CI" + PR_BODY+=$'\n\n'"**Batch range:** \`${RANGE}\`" + PR_BODY+=$'\n'"**Promotion branch:** \`${BRANCH}\`" + PR_BODY+=$'\n'"**Base:** \`${BASE}\`" + PR_BODY+=$'\n'"**Triggered by:** Staging CI batch at ${TIMESTAMP}" + PR_BODY+=$'\n\n'"### Commits in this batch (${COMMIT_COUNT}):" + PR_BODY+=$'\n'"${COMMIT_MD}" + PR_BODY+=$'\n\n'"Waiting for gates:" + PR_BODY+=$'\n'"- Tests: pending" + PR_BODY+=$'\n'"- E2E: pending" + PR_BODY+=$'\n'"- Claude Code review: pending (will post comments on this PR)" + PR_BODY+=$'\n\n'"---" + PR_BODY+=$'\n'"*Auto-created by staging-ci workflow*" + PR_URL=$(gh pr create \ --base "$BASE" \ --head "$BRANCH" \ --title "chore: promote staging to ${BASE} (${TIMESTAMP})" \ - --body "## Auto-promotion from staging CI - - **Batch range:** \`${RANGE}\` - **Promotion branch:** \`${BRANCH}\` - **Base:** \`${BASE}\` - **Triggered by:** Staging CI batch at ${TIMESTAMP} - - Waiting for gates: - - Tests: pending - - E2E: pending - - Claude Code review: pending (will post comments on this PR) - - --- - *Auto-created by staging-ci workflow*" \ + --body "$PR_BODY" \ --label "staging-promotion") PR_NUM=$(echo "$PR_URL" | grep -oE '[0-9]+$') echo "pr_number=${PR_NUM}" >> "$GITHUB_OUTPUT" echo "Created promotion PR #${PR_NUM}" + # Output commit summary for use in merge commit message + DELIM="COMMIT_SUMMARY_EOF_$(date +%s)" + { + echo "commit_summary<<${DELIM}" + echo "Commits in this batch (${COMMIT_COUNT}):" + echo "${COMMIT_MD}" + echo "${DELIM}" + } >> "$GITHUB_OUTPUT" + # ── Gate: wait for review, process findings, merge or block ───── gate: name: Staging Gate @@ -419,12 +449,18 @@ jobs: env: GH_TOKEN: ${{ steps.token.outputs.token }} PR_NUMBER: ${{ needs.create-promotion-pr.outputs.pr_number }} + COMMIT_SUMMARY: ${{ needs.create-promotion-pr.outputs.commit_summary }} run: | if [ -n "$PR_NUMBER" ]; then BASE=$(gh pr view "$PR_NUMBER" --json baseRefName --jq '.baseRefName') if [ "$BASE" = "main" ]; then echo "Merging promotion PR #${PR_NUMBER} (targets main)" - gh pr merge "$PR_NUMBER" --merge + TITLE=$(gh pr view "$PR_NUMBER" --json title --jq '.title') + if [ -n "$COMMIT_SUMMARY" ]; then + gh pr merge "$PR_NUMBER" --merge --subject "#${PR_NUMBER} $TITLE" --body-file <(printf '%s' "$COMMIT_SUMMARY") + else + gh pr merge "$PR_NUMBER" --merge + fi echo "merged=true" >> "$GITHUB_OUTPUT" else echo "PR #${PR_NUMBER} targets '${BASE}' (not main) — leaving open for chain resolution"