Skip to content

Commit 680da5e

Browse files
committed
Helper script to help manage GH PRs and issues
1 parent b1ac8d6 commit 680da5e

1 file changed

Lines changed: 328 additions & 0 deletions

File tree

scripts/manage_prs.sh

Lines changed: 328 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,328 @@
1+
#!/bin/bash
2+
#
3+
# DBDiff Pull Request Manager
4+
#
5+
# Usage: ./scripts/manage_prs.sh [--apply] [--auto-close] [--resolve-conflicts]
6+
#
7+
# Dependencies: gh (GitHub CLI), jq
8+
#
9+
# This script is designed to be defensive and robust. It will:
10+
# 1. Iterate through open PRs.
11+
# 2. Attempt to update branches (handling API errors gracefully).
12+
# 3. Check statuses (handling confusing JSON structures safely).
13+
# 4. Merge if conditions are met.
14+
# 5. Provide a detailed summary at the end.
15+
16+
set -o pipefail
17+
18+
# -----------------------------------------------------------------------------
19+
# Globals & State
20+
# -----------------------------------------------------------------------------
21+
DRY_RUN=true
22+
AUTO_CLOSE=false
23+
RESOLVE_CONFLICTS=false
24+
TOTAL_PRS=0
25+
UPDATED_PRS=0
26+
MERGED_PRS=0
27+
SKIPPED_PRS=0
28+
FAILED_PRS=0
29+
ERRORS=()
30+
31+
# -----------------------------------------------------------------------------
32+
# Helper Functions
33+
# -----------------------------------------------------------------------------
34+
log() {
35+
echo -e "[\033[0;34mINFO\033[0m] $1"
36+
}
37+
38+
warn() {
39+
echo -e "[\033[0;33mWARN\033[0m] $1"
40+
}
41+
42+
error_log() {
43+
echo -e "[\033[0;31mERROR\033[0m] $1"
44+
ERRORS+=("$1")
45+
}
46+
47+
print_summary() {
48+
echo ""
49+
echo "========================================================"
50+
echo " EXECUTION SUMMARY "
51+
echo "========================================================"
52+
printf "Total PRs Found: %d\n" "$TOTAL_PRS"
53+
printf "Branches Updated: %d\n" "$UPDATED_PRS"
54+
printf "PRs Merged: %d\n" "$MERGED_PRS"
55+
printf "Skipped (No Action): %d\n" "$SKIPPED_PRS"
56+
printf "Failed/Errors: %d\n" "$FAILED_PRS"
57+
echo "========================================================"
58+
59+
if [ ${#ERRORS[@]} -gt 0 ]; then
60+
echo "Errors & Warnings:"
61+
for err in "${ERRORS[@]}"; do
62+
echo " - $err"
63+
done
64+
echo "========================================================"
65+
fi
66+
}
67+
68+
trap 'print_summary' EXIT
69+
70+
# -----------------------------------------------------------------------------
71+
# Dependency Checks
72+
# -----------------------------------------------------------------------------
73+
if ! command -v gh >/dev/null 2>&1; then
74+
echo "Error: 'gh' is not installed. Please install it."
75+
exit 1
76+
fi
77+
78+
if ! command -v jq >/dev/null 2>&1; then
79+
echo "Error: 'jq' is not installed. Please install it."
80+
exit 1
81+
fi
82+
83+
if ! gh auth status >/dev/null 2>&1; then
84+
echo "Error: You must be logged in to GitHub CLI. Run 'gh auth login' first."
85+
exit 1
86+
fi
87+
88+
# -----------------------------------------------------------------------------
89+
# Argument Parsing
90+
# -----------------------------------------------------------------------------
91+
while [[ "$#" -gt 0 ]]; do
92+
case $1 in
93+
--apply) DRY_RUN=false ;;
94+
--auto-close) AUTO_CLOSE=true ;;
95+
--resolve-conflicts) RESOLVE_CONFLICTS=true ;;
96+
*) echo "Unknown parameter: $1"; exit 1 ;;
97+
esac
98+
shift
99+
done
100+
101+
if [ "$DRY_RUN" = true ]; then
102+
log "RUNNING IN DRY-RUN MODE (Default). Use --apply to execute changes."
103+
else
104+
warn "RUNNING IN APPLY MODE. Changes WILL be made!"
105+
fi
106+
107+
# -----------------------------------------------------------------------------
108+
# Main Execution
109+
# -----------------------------------------------------------------------------
110+
111+
REPO=$(gh repo view --json nameWithOwner -q .nameWithOwner)
112+
log "Target Repository: $REPO"
113+
114+
log "Fetching open PRs..."
115+
# Fetch PRs safely
116+
if ! PRS_JSON=$(gh pr list --state open --limit 100 --json number 2>/dev/null); then
117+
error_log "Failed to fetch PR list check your network or token."
118+
exit 1
119+
fi
120+
121+
TOTAL_PRS=$(echo "$PRS_JSON" | jq '. | length')
122+
123+
if [ "$TOTAL_PRS" -eq 0 ]; then
124+
log "No open pull requests found."
125+
exit 0
126+
fi
127+
128+
log "Found $TOTAL_PRS open PRs. Starting processing..."
129+
130+
for row in $(echo "${PRS_JSON}" | jq -r '.[] | @base64'); do
131+
_jq() {
132+
echo ${row} | base64 --decode | jq -r ${1}
133+
}
134+
135+
number=$(_jq '.number')
136+
action_taken=false
137+
138+
echo ""
139+
echo "--------------------------------------------------------"
140+
log "Processing PR #$number"
141+
142+
# Fetch Details Safely
143+
if ! details=$(gh pr view $number --json title,body,statusCheckRollup,mergeable,headRefName,baseRefName,url 2>/dev/null); then
144+
error_log "PR #$number: Failed to fetch details. Skipping."
145+
FAILED_PRS=$((FAILED_PRS+1))
146+
continue
147+
fi
148+
149+
title=$(echo "$details" | jq -r .title)
150+
body=$(echo "$details" | jq -r .body)
151+
mergeable=$(echo "$details" | jq -r .mergeable)
152+
headRef=$(echo "$details" | jq -r .headRefName)
153+
url=$(echo "$details" | jq -r .url)
154+
155+
# Safe Status Parsing
156+
statuses=$(echo "$details" | jq -r '[.statusCheckRollup[]? | (.conclusion // .status // .state)] | unique | join(",")')
157+
158+
state="UNKNOWN"
159+
if [[ -z "$statuses" ]]; then
160+
state="NONE"
161+
elif [[ "$statuses" == *"FAILURE"* || "$statuses" == *"TIMED_OUT"* || "$statuses" == *"ACTION_REQUIRED"* ]]; then
162+
state="FAILURE"
163+
elif [[ "$statuses" == *"IN_PROGRESS"* || "$statuses" == *"QUEUED"* || "$statuses" == *"PENDING"* ]]; then
164+
state="PENDING"
165+
elif [[ "$statuses" == "SUCCESS" || "$statuses" == "COMPLETED" || "$statuses" == "SUCCESS,COMPLETED" ]]; then
166+
state="SUCCESS"
167+
else
168+
state="UNKNOWN ($statuses)"
169+
fi
170+
171+
echo " Title: $title"
172+
echo " Branch: $headRef"
173+
echo " Mergeable: $mergeable"
174+
echo " CI Status: $state ($statuses)"
175+
echo " URL: $url"
176+
177+
# Step 1: Update Branch
178+
if [[ "$mergeable" == "CONFLICTING" ]]; then
179+
if [ "$RESOLVE_CONFLICTS" = false ]; then
180+
warn "Branch has conflicts. Cannot update or merge. (Use --resolve-conflicts to attempt auto-fix)"
181+
else
182+
log "Attempting to RESOLVE CONFLICTS..."
183+
184+
if [ "$DRY_RUN" = true ]; then
185+
log "[DRY-RUN] Would checkout PR, merge master, accept master's .github/ configs, and push."
186+
else
187+
# Save current state
188+
git stash -u >/dev/null 2>&1 || true
189+
original_branch=$(git branch --show-current)
190+
191+
# Checkout PR (force to overwrite local if exists)
192+
if gh pr checkout $number --force >/dev/null 2>&1; then
193+
pr_branch=$(git branch --show-current)
194+
195+
# Attempt merge master
196+
# We use origin/master to be sure
197+
git fetch origin master >/dev/null 2>&1
198+
199+
if git merge origin/master --no-commit --no-ff >/dev/null 2>&1; then
200+
log " Merge performed cleanly (Status check was stale?). Pushing..."
201+
git push >/dev/null 2>&1
202+
state="PENDING (Update Triggered)"
203+
else
204+
# Conflict detected
205+
# Strategy: Accept Master for .github and composer.lock
206+
# 'Theirs' in a PR merge (we are on PR branch, merging master) -> Master
207+
208+
log " Merge conflict detected. Applying 'Use Master' strategy for config files..."
209+
210+
resolved_count=0
211+
conflicted_files=$(git diff --name-only --diff-filter=U)
212+
213+
for f in $conflicted_files; do
214+
if [[ "$f" == ".github/"* || "$f" == "tests/"* || "$f" == "composer.lock" ]]; then
215+
# Check if file exists in master (theirs)
216+
if git cat-file -e "origin/master:$f" >/dev/null 2>&1; then
217+
log " Resolving $f using MASTER version."
218+
git checkout --theirs "$f" >/dev/null 2>&1
219+
git add "$f"
220+
resolved_count=$((resolved_count+1))
221+
else
222+
# File apparently deleted in master?
223+
log " File $f missing in master. Removing..."
224+
git rm "$f" >/dev/null 2>&1 || rm "$f"
225+
git add "$f"
226+
resolved_count=$((resolved_count+1))
227+
fi
228+
fi
229+
done
230+
231+
# Check if all conflicts resolved
232+
if git diff --name-only --diff-filter=U | grep -q .; then
233+
warn " Could not auto-resolve all conflicts. Aborting."
234+
git merge --abort >/dev/null 2>&1
235+
else
236+
log " All conflicts resolved ($resolved_count files). Committing..."
237+
git commit -m "Merge branch 'master' into $pr_branch (Auto-resolved conflicts)" >/dev/null 2>&1
238+
239+
if git push >/dev/null 2>&1; then
240+
log " Successfully pushed conflict resolutions."
241+
state="PENDING (Update Triggered)"
242+
UPDATED_PRS=$((UPDATED_PRS+1))
243+
else
244+
error_log " Failed to push changes (Permission denied?)."
245+
fi
246+
fi
247+
fi
248+
249+
# Switch back
250+
git checkout "$original_branch" >/dev/null 2>&1
251+
else
252+
error_log " Failed to checkout PR #$number."
253+
fi
254+
255+
# Restore stash if any
256+
git stash pop >/dev/null 2>&1 || true
257+
fi
258+
fi
259+
else
260+
# Only update if not explicitly "SUCCESS" (since update might trigger new tests)
261+
# OR if we just want to ensure it's up to date regardless.
262+
# Let's always try to update to be safe, unless it's already up to date.
263+
264+
if [ "$DRY_RUN" = true ]; then
265+
log "[DRY-RUN] Would update branch."
266+
else
267+
update_out=$(gh api -X PUT "/repos/$REPO/pulls/$number/update-branch" 2>&1 || true)
268+
269+
# Check if output is JSON or raw error
270+
if echo "$update_out" | grep -q "message"; then
271+
msg=$(echo "$update_out" | jq -r .message 2>/dev/null || echo "$update_out")
272+
if [[ "$msg" == "Accepted" ]]; then
273+
log "Update triggered (Accepted)."
274+
UPDATED_PRS=$((UPDATED_PRS+1))
275+
action_taken=true
276+
elif [[ "$msg" == *"already up to date"* ]]; then
277+
echo " Already up-to-date."
278+
else
279+
warn "Update failed: $msg"
280+
fi
281+
else
282+
# Fallback if no message field (unexpected API response)
283+
warn "Update API response unclear: $update_out"
284+
fi
285+
fi
286+
fi
287+
288+
# Step 2: Linked Issues & Merging
289+
issues=$(echo "$body" | grep -oEi "(Fixes|Closes|Resolves) #?[0-9]+" | awk '{print $2}' | tr -d '#' | tr '\n' ' ' | xargs)
290+
291+
if [[ -n "$issues" ]]; then
292+
log "Linked to issue(s): $issues"
293+
294+
if [[ "$mergeable" == "CONFLICTING" ]]; then
295+
# Already warned above
296+
:
297+
elif [[ "$state" == "SUCCESS" ]]; then
298+
if [ "$AUTO_CLOSE" = true ]; then
299+
if [ "$DRY_RUN" = true ]; then
300+
log "[DRY-RUN] Would MERGE PR #$number."
301+
MERGED_PRS=$((MERGED_PRS+1))
302+
action_taken=true
303+
else
304+
log "Tests Passed & Mergeable. Attempting Merge..."
305+
if gh pr merge $number --merge --delete-branch >> /dev/null 2>&1; then
306+
log "Successfully MERGED PR #$number."
307+
MERGED_PRS=$((MERGED_PRS+1))
308+
action_taken=true
309+
else
310+
error_log "PR #$number: Merge command failed."
311+
FAILED_PRS=$((FAILED_PRS+1))
312+
fi
313+
fi
314+
else
315+
log "Tests Passed. Ready to merge (Use --auto-close)."
316+
fi
317+
else
318+
log "Tests not passing (State: $state). Skipping merge."
319+
fi
320+
else
321+
echo " No linked issues found."
322+
fi
323+
324+
if [ "$action_taken" = false ]; then
325+
SKIPPED_PRS=$((SKIPPED_PRS+1))
326+
fi
327+
328+
done

0 commit comments

Comments
 (0)