Files
buzz/.github/workflows/sync-release-changelog.yml
Will Pfleger 3c2edfeb8a feat(release): add GitHub App token, git-log release notes, and Buildkite automation
The sync-release-changelog workflow now uses a GitHub App token instead
of GITHUB_TOKEN so pushes to the release PR branch re-trigger CI (the
repo's ruleset requires 5 status checks to pass). Also hardens PR
discovery with --limit 100 --base main, and serializes concurrent runs
with cancel-in-progress: false to prevent non-fast-forward push races.

release.yml generates GitHub release notes from git log at release time
instead of reading from CHANGELOG.md, closing the merge-before-sync
race where someone merges the release PR before the changelog sync
finishes.

auto-tag-on-release-pr-merge.yml now triggers the internal
sprout-releases Buildkite pipeline via REST API after pushing the tag,
passing version metadata so the pipeline's input step is skipped.
2026-06-03 15:29:09 -04:00

166 lines
6.0 KiB
YAML

name: Sync Release PR Changelog
on:
push:
branches: [main]
# Serialize successive main pushes — each run generates the correct changelog.
concurrency:
group: sync-release-changelog
cancel-in-progress: false
permissions:
contents: write
pull-requests: write
jobs:
sync:
name: Sync changelog on open release PR
runs-on: ubuntu-latest
steps:
- name: Find open release PR
id: find-pr
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
# Filter to same-repo PRs only (exclude forks) with version-bump/<semver> branches.
PRS=$(gh pr list \
--repo block/sprout \
--state open \
--base main \
--limit 100 \
--json number,headRefName,headRepository \
--jq '[.[] |
select(.headRepository.nameWithOwner == "block/sprout") |
select(.headRefName | test("^version-bump/[0-9]+\\.[0-9]+\\.[0-9]+(-[0-9A-Za-z.-]+)?$"))
]')
COUNT=$(echo "$PRS" | jq 'length')
if [[ "$COUNT" -eq 0 ]]; then
echo "No open release PR found — nothing to sync"
echo "found=false" >> "$GITHUB_OUTPUT"
exit 0
fi
if [[ "$COUNT" -gt 1 ]]; then
echo "::error::Multiple open release PRs found — cannot auto-sync. Resolve manually."
echo "$PRS" | jq -r '.[].headRefName'
exit 1
fi
BRANCH=$(echo "$PRS" | jq -r '.[0].headRefName')
NUMBER=$(echo "$PRS" | jq -r '.[0].number')
VERSION="${BRANCH#version-bump/}"
echo "found=true" >> "$GITHUB_OUTPUT"
echo "branch=$BRANCH" >> "$GITHUB_OUTPUT"
echo "number=$NUMBER" >> "$GITHUB_OUTPUT"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "Found release PR #${NUMBER} (${BRANCH})"
- name: Generate GitHub App token
if: steps.find-pr.outputs.found == 'true'
id: app-token
uses: actions/create-github-app-token@v3
with:
client-id: ${{ vars.RELEASE_APP_CLIENT_ID }}
private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
- name: Check out release PR branch
if: steps.find-pr.outputs.found == 'true'
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: ${{ steps.find-pr.outputs.branch }}
fetch-depth: 0
fetch-tags: true
token: ${{ steps.app-token.outputs.token }}
- name: Regenerate changelog section
if: steps.find-pr.outputs.found == 'true'
env:
VERSION: ${{ steps.find-pr.outputs.version }}
PR_NUMBER: ${{ steps.find-pr.outputs.number }}
GH_TOKEN: ${{ steps.app-token.outputs.token }}
run: |
set -euo pipefail
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git config user.name "github-actions[bot]"
# Refresh origin/main so the log range reflects what's actually on main now.
git fetch origin main
# Most recent release tag reachable from origin/main (i.e., the previous release).
# Using origin/main — not HEAD — so the release commit on this PR branch is excluded.
LAST_TAG=$(git describe --tags --abbrev=0 --match 'v[0-9]*' origin/main 2>/dev/null || echo "")
if [[ -z "$LAST_TAG" ]]; then
NEW_ENTRIES="Initial release"
else
# Entries since the last tag up to current main tip, excluding merge commits.
# Upper bound is origin/main, NOT HEAD, so release/sync commits on this PR
# branch never appear in the changelog.
NEW_ENTRIES=$(git log "${LAST_TAG}..origin/main" --oneline --no-merges || echo "")
fi
echo "Changelog range: ${LAST_TAG:-<beginning>}..origin/main"
echo "New entries:"
echo "${NEW_ENTRIES:- (none)}"
# Locate the ## v<VERSION> section in CHANGELOG.md.
START=$(grep -n "^## v${VERSION}$" CHANGELOG.md | head -1 | cut -d: -f1)
if [[ -z "$START" ]]; then
echo "::error::Section '## v${VERSION}' not found in CHANGELOG.md — cannot sync"
exit 1
fi
# Line number of the next ## v section after our section (may be empty if last).
NEXT=$(awk -v start="$START" 'NR > start && /^## v[0-9]/{print NR; exit}' CHANGELOG.md)
TMPFILE=$(mktemp)
# Lines before the version section (the "# Changelog" header block).
if [[ "$START" -gt 1 ]]; then
head -n $((START - 1)) CHANGELOG.md >> "$TMPFILE"
fi
# Write the refreshed version section.
printf '## v%s\n\n' "$VERSION" >> "$TMPFILE"
if [[ -n "$NEW_ENTRIES" ]]; then
printf '%s\n' "$NEW_ENTRIES" >> "$TMPFILE"
else
printf 'No changes.\n' >> "$TMPFILE"
fi
printf '\n' >> "$TMPFILE"
# Append the remainder of the changelog (previous releases).
if [[ -n "$NEXT" ]]; then
tail -n "+${NEXT}" CHANGELOG.md >> "$TMPFILE"
fi
mv "$TMPFILE" CHANGELOG.md
# Skip commit if nothing changed.
if git diff --quiet CHANGELOG.md; then
echo "Changelog already up to date — nothing to commit"
exit 0
fi
git add CHANGELOG.md
git commit -m "chore(release): sync changelog for v${VERSION}"
git push origin "HEAD:refs/heads/version-bump/${VERSION}"
# Update the PR body with the refreshed commit list.
if [[ -n "$LAST_TAG" ]]; then
PR_HEADER="### Changes since ${LAST_TAG}:"$'\n\n'"${NEW_ENTRIES}"
else
PR_HEADER="Initial release."
fi
PR_BODY="## Release v${VERSION}"$'\n\n'"${PR_HEADER}"$'\n\n'"**To release:** merge this PR. The tag and build will happen automatically."
gh pr edit "$PR_NUMBER" --body "$PR_BODY"
echo "Changelog synced and PR #${PR_NUMBER} body updated"