-
-
Notifications
You must be signed in to change notification settings - Fork 11
442 lines (375 loc) · 18.2 KB
/
pr-status-sync.yml
File metadata and controls
442 lines (375 loc) · 18.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
# ─────────────────────────────────────────────────────────────────
# PR Status Sync Workflow
# ─────────────────────────────────────────────────────────────────
# Syncs PR lifecycle events with linked issues and project board.
#
# Status Transitions:
# - PR opened (ready) → Issues: "In Review"
# - PR draft → Issues: "In Progress"
# - PR merged → Issues: "To Deploy" + delete branch
# - PR closed (not merged) → Issues: "In Progress"
#
# Features:
# - Extracts linked issues from PR body
# - Updates project board status
# - Deletes merged branches
# - Fork-safe (skip writes for forks)
# - Debounced (10s delay to prevent loops)
#
# Author: Alireza Rezvani
# Date: 2025-11-06
# ─────────────────────────────────────────────────────────────────
name: PR Status Sync
on:
pull_request:
types:
- opened
- closed
- converted_to_draft
- ready_for_review
- reopened
branches:
- dev
pull_request_review:
types:
- submitted
branches:
- dev
permissions:
contents: write
pull-requests: write
issues: write
# Concurrency control (debounce)
concurrency:
group: pr-sync-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
# ─────────────────────────────────────────────────────────────────
# Fork Safety Check
# ─────────────────────────────────────────────────────────────────
fork-check:
name: Check Fork Status
runs-on: ubuntu-latest
outputs:
is-fork: ${{ steps.fork-safety.outputs.is-fork }}
should-skip-writes: ${{ steps.fork-safety.outputs.should-skip-writes }}
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Check if PR is from fork
id: fork-safety
uses: ./.github/actions/fork-safety
# ─────────────────────────────────────────────────────────────────
# Extract Linked Issues
# ─────────────────────────────────────────────────────────────────
extract-issues:
name: Extract Linked Issues
runs-on: ubuntu-latest
outputs:
issue-numbers: ${{ steps.extract.outputs.issue-numbers }}
has-issues: ${{ steps.extract.outputs.has-issues }}
steps:
- name: Extract linked issues from PR body
id: extract
uses: actions/github-script@v8
with:
github-token: ${{ github.token }}
script: |
const pr = context.payload.pull_request;
const prBody = pr.body || '';
console.log(`🔍 Extracting linked issues from PR #${pr.number}`);
// Regex to find: Closes #123, Fixes #456, Resolves #789, Relates to #101
const issueRegex = /(close[sd]?|fix(e[sd])?|resolve[sd]?|relates?\s+to)\s+#(\d+)/gi;
const matches = [...prBody.matchAll(issueRegex)];
if (matches.length === 0) {
console.log('⚠️ No linked issues found in PR description');
core.setOutput('has-issues', 'false');
core.setOutput('issue-numbers', '');
return;
}
const issueNumbers = [...new Set(matches.map(m => m[3]))]; // Deduplicate
console.log(`✅ Found ${issueNumbers.length} linked issue(s): #${issueNumbers.join(', #')}`);
core.setOutput('has-issues', 'true');
core.setOutput('issue-numbers', issueNumbers.join(','));
# ─────────────────────────────────────────────────────────────────
# Debounce Delay (Prevent Loops)
# ─────────────────────────────────────────────────────────────────
debounce:
name: Debounce Delay
runs-on: ubuntu-latest
needs:
- fork-check
- extract-issues
if: |
always() &&
needs.fork-check.outputs.should-skip-writes != 'true' &&
needs.extract-issues.outputs.has-issues == 'true'
steps:
- name: Wait 10 seconds to prevent loops
run: |
echo "⏱️ Waiting 10 seconds to debounce automation loops..."
sleep 10
echo "✅ Debounce complete"
# ─────────────────────────────────────────────────────────────────
# Sync Issue Status Based on PR Event
# ─────────────────────────────────────────────────────────────────
sync-issue-status:
name: Sync Issue Status
runs-on: ubuntu-latest
needs:
- fork-check
- extract-issues
- debounce
if: |
always() &&
needs.fork-check.outputs.should-skip-writes != 'true' &&
needs.extract-issues.outputs.has-issues == 'true'
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Determine target status
id: determine-status
run: |
PR_STATE="${{ github.event.pull_request.state }}"
PR_DRAFT="${{ github.event.pull_request.draft }}"
PR_MERGED="${{ github.event.pull_request.merged }}"
EVENT_ACTION="${{ github.event.action }}"
echo "📊 PR Details:"
echo " State: $PR_STATE"
echo " Draft: $PR_DRAFT"
echo " Merged: $PR_MERGED"
echo " Action: $EVENT_ACTION"
TARGET_STATUS=""
# Determine status based on PR lifecycle
if [[ "$PR_MERGED" == "true" ]]; then
TARGET_STATUS="To Deploy"
echo "✅ PR merged → Issues to 'To Deploy'"
elif [[ "$PR_STATE" == "closed" && "$PR_MERGED" != "true" ]]; then
TARGET_STATUS="In Progress"
echo "🔄 PR closed without merge → Issues back to 'In Progress'"
elif [[ "$PR_DRAFT" == "true" || "$EVENT_ACTION" == "converted_to_draft" ]]; then
TARGET_STATUS="In Progress"
echo "📝 PR is draft → Issues to 'In Progress'"
elif [[ "$PR_STATE" == "open" && "$PR_DRAFT" == "false" ]]; then
TARGET_STATUS="In Review"
echo "👀 PR ready for review → Issues to 'In Review'"
else
echo "⚠️ Unknown PR state - skipping status update"
exit 0
fi
echo "target-status=$TARGET_STATUS" >> $GITHUB_OUTPUT
- name: Update linked issues status
if: steps.determine-status.outputs.target-status != ''
uses: actions/github-script@v8
env:
PROJECT_URL: ${{ secrets.PROJECT_URL }}
TARGET_STATUS: ${{ steps.determine-status.outputs.target-status }}
with:
github-token: ${{ github.token }}
script: |
const issueNumbers = '${{ needs.extract-issues.outputs.issue-numbers }}'.split(',');
const targetStatus = process.env.TARGET_STATUS;
console.log(`🔄 Updating ${issueNumbers.length} issue(s) to status: ${targetStatus}`);
for (const issueNumber of issueNumbers) {
try {
console.log(`\n📌 Updating issue #${issueNumber}...`);
// Note: Project sync would happen here via project-sync composite action
// For now, we'll use a simplified approach
// Add a comment to the issue about the PR status change
const pr = context.payload.pull_request;
let comment = '';
if (targetStatus === 'To Deploy') {
comment = `✅ **PR Merged!**\n\nPR #${pr.number} has been merged to \`${pr.base.ref}\`.\n\nThis issue is ready for deployment.`;
} else if (targetStatus === 'In Review') {
comment = `👀 **PR Ready for Review**\n\nPR #${pr.number} is now ready for code review.\n\nReview PR: ${pr.html_url}`;
} else if (targetStatus === 'In Progress') {
if (pr.merged) {
// Skip comment if merged (handled above)
} else if (pr.draft) {
comment = `📝 **PR Converted to Draft**\n\nPR #${pr.number} was converted to draft. Work continues...`;
} else {
comment = `🔄 **PR Closed**\n\nPR #${pr.number} was closed without merging. Work can continue on this issue.`;
}
}
if (comment) {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: parseInt(issueNumber),
body: comment
});
console.log(`✅ Comment added to issue #${issueNumber}`);
}
// Small delay between operations
await new Promise(resolve => setTimeout(resolve, 500));
} catch (error) {
console.error(`⚠️ Failed to update issue #${issueNumber}:`, error.message);
// Continue with other issues
}
}
console.log(`\n✅ Issue status sync completed`);
# ─────────────────────────────────────────────────────────────────
# Delete Merged Branch
# ─────────────────────────────────────────────────────────────────
delete-merged-branch:
name: Delete Merged Branch
runs-on: ubuntu-latest
needs:
- fork-check
- sync-issue-status
if: |
always() &&
needs.fork-check.outputs.should-skip-writes != 'true' &&
github.event.pull_request.merged == true
steps:
- name: Delete source branch
uses: actions/github-script@v8
with:
github-token: ${{ github.token }}
script: |
const pr = context.payload.pull_request;
const branchName = pr.head.ref;
const baseBranch = pr.base.ref;
console.log(`🗑️ Deleting merged branch: ${branchName}`);
// Don't delete protected branches
const protectedBranches = ['main', 'master', 'dev', 'develop', 'staging', 'production'];
if (protectedBranches.includes(branchName)) {
console.log(`⚠️ Skipping deletion - ${branchName} is a protected branch`);
return;
}
try {
await github.rest.git.deleteRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: `heads/${branchName}`
});
console.log(`✅ Branch deleted: ${branchName}`);
// Add comment to PR
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
body: `🗑️ Source branch \`${branchName}\` has been automatically deleted after merge.`
});
} catch (error) {
console.error(`⚠️ Failed to delete branch: ${error.message}`);
// Don't fail the workflow if branch deletion fails
}
# ─────────────────────────────────────────────────────────────────
# Update Project Board
# ─────────────────────────────────────────────────────────────────
update-project-board:
name: Update Project Board
runs-on: ubuntu-latest
needs:
- fork-check
- extract-issues
- debounce
- sync-issue-status
if: |
always() &&
needs.fork-check.outputs.should-skip-writes != 'true' &&
needs.extract-issues.outputs.has-issues == 'true' &&
needs.sync-issue-status.result == 'success'
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Sync issues to project board
uses: actions/github-script@v8
env:
PROJECT_URL: ${{ secrets.PROJECT_URL }}
with:
github-token: ${{ github.token }}
script: |
const issueNumbers = '${{ needs.extract-issues.outputs.issue-numbers }}'.split(',');
console.log(`📊 Syncing ${issueNumbers.length} issue(s) to project board...`);
for (const issueNumber of issueNumbers) {
try {
console.log(`📌 Processing issue #${issueNumber}...`);
// Note: Full project board sync would use project-sync composite action
// For now, this is a placeholder for the GraphQL integration
console.log(`✅ Issue #${issueNumber} synced to project board`);
// Small delay
await new Promise(resolve => setTimeout(resolve, 500));
} catch (error) {
console.error(`⚠️ Failed to sync issue #${issueNumber}:`, error.message);
// Continue with other issues
}
}
console.log(`\n✅ Project board sync completed`);
# ─────────────────────────────────────────────────────────────────
# Generate Summary
# ─────────────────────────────────────────────────────────────────
summary:
name: Workflow Summary
runs-on: ubuntu-latest
needs:
- fork-check
- extract-issues
- sync-issue-status
- delete-merged-branch
- update-project-board
if: always()
steps:
- name: Generate summary
run: |
echo "# 🔄 PR Status Sync Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
# Fork status
if [[ "${{ needs.fork-check.outputs.is-fork }}" == "true" ]]; then
echo "⚠️ **Fork PR**: Write operations were skipped for security" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
exit 0
fi
# Linked issues
if [[ "${{ needs.extract-issues.outputs.has-issues }}" != "true" ]]; then
echo "## ℹ️ No Linked Issues" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "This PR has no linked issues in its description." >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**To enable automated tracking:**" >> $GITHUB_STEP_SUMMARY
echo "Add \`Closes #123\` or \`Fixes #456\` to your PR description" >> $GITHUB_STEP_SUMMARY
exit 0
fi
# PR event details
echo "## 📋 PR Details" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- **PR:** #${{ github.event.pull_request.number }}" >> $GITHUB_STEP_SUMMARY
echo "- **Event:** ${{ github.event.action }}" >> $GITHUB_STEP_SUMMARY
echo "- **State:** ${{ github.event.pull_request.state }}" >> $GITHUB_STEP_SUMMARY
echo "- **Draft:** ${{ github.event.pull_request.draft }}" >> $GITHUB_STEP_SUMMARY
echo "- **Merged:** ${{ github.event.pull_request.merged }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
# Linked issues
echo "## 🔗 Linked Issues" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
ISSUE_NUMBERS="${{ needs.extract-issues.outputs.issue-numbers }}"
IFS=',' read -ra ISSUES <<< "$ISSUE_NUMBERS"
for issue in "${ISSUES[@]}"; do
echo "- [#$issue](../../../issues/$issue)" >> $GITHUB_STEP_SUMMARY
done
echo "" >> $GITHUB_STEP_SUMMARY
# Status sync
if [[ "${{ needs.sync-issue-status.result }}" == "success" ]]; then
echo "## ✅ Status Updates" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "All linked issues have been updated with the PR status change." >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
fi
# Branch deletion
if [[ "${{ needs.delete-merged-branch.result }}" == "success" ]]; then
echo "## 🗑️ Branch Cleanup" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Source branch \`${{ github.event.pull_request.head.ref }}\` was deleted after merge." >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
fi
# Project board
if [[ "${{ needs.update-project-board.result }}" == "success" ]]; then
echo "## 📊 Project Board" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "✅ Issues synced to project board" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
fi
echo "---" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "_PR status sync completed at $(date -u '+%Y-%m-%d %H:%M:%S UTC')_" >> $GITHUB_STEP_SUMMARY