Skip to content

Commit 61df57c

Browse files
CopilotyCodeTech
andauthored
ci: fix changelog auto-update workflow for concurrent stacked PR runs (#46)
* ci: add race-safe push logic to changelog workflow * ci: hardcode BASE_BRANCH to master in changelog workflow * ci: introduce CHANGELOG_BASE_BRANCH env var as single source of truth * ci: replace git pull with git reset --hard in checkout changelog branch step * ci: add workflow_dispatch trigger with PR number input * fix: harden changelog workflow input and push guards * fix: improve changelog push retry failure handling * fix: simplify changelog push flow and inline PR payload * fix: safely pass PR payload through workflow steps * fix: quote workflow payload env and set push upstream --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: yCodeTech <31927084+yCodeTech@users.noreply.github.com>
1 parent 5ec5c07 commit 61df57c

1 file changed

Lines changed: 91 additions & 14 deletions

File tree

.github/workflows/changelog-ci.yml

Lines changed: 91 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,25 @@ on:
44
pull_request:
55
types:
66
- closed
7+
workflow_dispatch:
8+
inputs:
9+
pr_number:
10+
description: "PR number to add to the changelog (must be a merged PR)"
11+
required: true
12+
type: string
713

814
permissions:
915
contents: write
1016
pull-requests: write
1117
actions: read
1218

19+
env:
20+
CHANGELOG_BASE_BRANCH: master
21+
1322
jobs:
1423
update_changelog:
1524
name: Update Changelog
16-
if: github.event.pull_request.merged == true
25+
if: github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch'
1726
runs-on: ubuntu-latest
1827

1928
steps:
@@ -25,16 +34,56 @@ jobs:
2534
.github/scripts/update-changelog.mjs
2635
sparse-checkout-cone-mode: false
2736

37+
- name: Resolve PR data
38+
id: resolve-pr
39+
uses: actions/github-script@v7
40+
with:
41+
script: |
42+
let pr;
43+
44+
if (context.eventName === 'workflow_dispatch') {
45+
const rawPrNumber = (context.payload.inputs?.pr_number ?? '').trim();
46+
if (!/^\d+$/.test(rawPrNumber) || Number(rawPrNumber) <= 0) {
47+
core.setFailed(`Invalid workflow input "pr_number": "${rawPrNumber}". Please provide a positive integer PR number.`);
48+
return;
49+
}
50+
51+
const prNumber = Number(rawPrNumber);
52+
console.log(`🔍 Fetching PR #${prNumber} from API...`);
53+
54+
const { data } = await github.rest.pulls.get({
55+
owner: context.repo.owner,
56+
repo: context.repo.repo,
57+
pull_number: prNumber,
58+
});
59+
60+
if (!data.merged) {
61+
core.setFailed(`PR #${prNumber} has not been merged. Only merged PRs can be added to the changelog.`);
62+
return;
63+
}
64+
65+
pr = data;
66+
console.log(`✅ Resolved PR #${prNumber}: ${pr.title}`);
67+
} else {
68+
pr = context.payload.pull_request;
69+
}
70+
71+
const prJson = JSON.stringify(pr);
72+
core.setOutput('pr-json-base64', Buffer.from(prJson, 'utf8').toString('base64'));
73+
2874
- name: Check if PR should be excluded
2975
id: check-exclusions
3076
uses: actions/github-script@v7
3177
with:
3278
script: |
3379
const { default: checkExclusions } = await import('${{ github.workspace }}/.github/scripts/check-changelog-exclusions.mjs');
80+
const pr = JSON.parse(Buffer.from(process.env.RESOLVED_PR_BASE64, 'base64').toString('utf8'));
3481
return await checkExclusions({
35-
pr: context.payload.pull_request,
82+
pr,
3683
core
3784
});
85+
env:
86+
RESOLVED_PR_BASE64: "${{ steps.resolve-pr.outputs.pr-json-base64 }}"
3887

3988
- name: Changelog update skipped
4089
if: steps.check-exclusions.outputs.should-skip == 'true'
@@ -56,7 +105,7 @@ jobs:
56105
if: steps.check-exclusions.outputs.should-skip == 'false'
57106
id: check-existing-changelog-pr
58107
run: |
59-
BASE_BRANCH="${{ github.event.pull_request.base.ref }}"
108+
BASE_BRANCH="${{ env.CHANGELOG_BASE_BRANCH }}"
60109
61110
# Find existing changelog PR
62111
CHANGELOG_PR=$(gh pr list --base "$BASE_BRANCH" --state open --json number,title,headRefName,body --jq '.[] | select(.title | test("^docs: Update changelog"; "i"))')
@@ -99,12 +148,12 @@ jobs:
99148
100149
if [ "${{ steps.check-existing-changelog-pr.outputs.changelog-pr-exists }}" = "true" ]; then
101150
echo "🔄 Checking out existing branch: $BRANCH_NAME"
102-
git fetch origin $BRANCH_NAME
103-
git checkout $BRANCH_NAME
104-
git pull origin $BRANCH_NAME
151+
git fetch origin "$BRANCH_NAME"
152+
git checkout "$BRANCH_NAME"
153+
git reset --hard "origin/$BRANCH_NAME"
105154
else
106155
echo "✨ Creating new branch: $BRANCH_NAME"
107-
git checkout -b $BRANCH_NAME
156+
git checkout -b "$BRANCH_NAME"
108157
fi
109158
110159
- name: Update Changelog
@@ -114,10 +163,13 @@ jobs:
114163
with:
115164
script: |
116165
const { default: updateChangelog } = await import('/tmp/update-changelog.mjs');
166+
const pr = JSON.parse(Buffer.from(process.env.RESOLVED_PR_BASE64, 'base64').toString('utf8'));
117167
return await updateChangelog({
118-
pr: context.payload.pull_request,
168+
pr,
119169
core
120170
});
171+
env:
172+
RESOLVED_PR_BASE64: "${{ steps.resolve-pr.outputs.pr-json-base64 }}"
121173

122174
- name: Prettify Changelog
123175
id: prettify-changelog
@@ -133,23 +185,48 @@ jobs:
133185
git diff CHANGELOG.md
134186
135187
- name: Commit and push changes
188+
id: commit-push
136189
if: steps.check-exclusions.outputs.should-skip == 'false' && steps.update-changelog.outputs.changelog-updated == 'true'
137190
run: |
191+
set -euo pipefail
138192
BRANCH_NAME="${{ steps.check-existing-changelog-pr.outputs.changelog-pr-branch-name }}"
193+
echo "pushed=false" >> "$GITHUB_OUTPUT"
139194
140195
git add CHANGELOG.md
196+
197+
# Avoid failing when there's nothing new to commit
198+
if git diff --cached --quiet; then
199+
echo "No changelog changes to commit."
200+
exit 0
201+
fi
202+
141203
git commit -m "docs: update changelog for PR #${{ steps.update-changelog.outputs.pr-number }}"
142204
143-
if [ "${{ steps.check-existing-changelog-pr.outputs.changelog-pr-exists }}" = "true" ]; then
144-
git push origin $BRANCH_NAME
145-
else
146-
git push -u origin $BRANCH_NAME
205+
# Push and recover from race by rebasing once and retrying
206+
if ! git push -u origin "$BRANCH_NAME"; then
207+
echo "Push rejected, retrying after rebase..."
208+
if git ls-remote --exit-code --heads origin "$BRANCH_NAME" >/dev/null 2>&1; then
209+
git fetch origin "$BRANCH_NAME"
210+
if ! git rebase "origin/$BRANCH_NAME"; then
211+
echo "Rebase failed while retrying push to $BRANCH_NAME." >&2
212+
git rebase --abort || true
213+
exit 1
214+
fi
215+
else
216+
echo "Remote branch $BRANCH_NAME not found during retry; retrying push without rebase."
217+
fi
218+
if ! git push -u origin "$BRANCH_NAME"; then
219+
echo "Failed to push changelog changes after retry." >&2
220+
exit 1
221+
fi
147222
fi
148223
224+
echo "pushed=true" >> "$GITHUB_OUTPUT"
225+
149226
- name: Create or update changelog PR
150-
if: steps.check-exclusions.outputs.should-skip == 'false' && steps.update-changelog.outputs.changelog-updated == 'true'
227+
if: steps.check-exclusions.outputs.should-skip == 'false' && steps.update-changelog.outputs.changelog-updated == 'true' && steps.commit-push.outputs.pushed == 'true'
151228
run: |
152-
BASE_BRANCH="${{ github.event.pull_request.base.ref }}"
229+
BASE_BRANCH="${{ env.CHANGELOG_BASE_BRANCH }}"
153230
BRANCH_NAME="${{ steps.check-existing-changelog-pr.outputs.changelog-pr-branch-name }}"
154231
PR_NUMBER="${{ steps.update-changelog.outputs.pr-number }}"
155232
CHANGELOG_PR_EXISTS="${{ steps.check-existing-changelog-pr.outputs.changelog-pr-exists }}"

0 commit comments

Comments
 (0)