Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
300 changes: 300 additions & 0 deletions assets/scripts/pr-stack-resolve.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,300 @@
#!/usr/bin/env bash
# pr-stack-resolve.sh — deterministic PR stack resolution checks
#
# Discovers all open PRs, orders them by stack position, and reports the
# resolution state of each. Does NOT make changes — only reports.
# The agent (dispatched via the formula) does the actual /act work.
#
# Output: JSON report to stdout, human-readable log to stderr.
# Exit: 0 if all PRs resolved, 1 if any need work.
set -euo pipefail

REPO="sverka-dev/sverka"
Comment thread
ThePlenkov marked this conversation as resolved.
cd "$(git rev-parse --show-toplevel)"

TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
log() { echo "[pr-resolve $TIMESTAMP] $*" >&2; }

TMPDIR=$(mktemp -d)
trap 'rm -rf "$TMPDIR"' EXIT

# --- 1. Fetch latest ---
log "Fetching latest from origin..."
git fetch origin --prune 2>&1 | grep -v "^From " || true
Comment thread
qodo-code-review[bot] marked this conversation as resolved.

# --- 2. Discover all open PRs ---
log "Discovering open PRs..."
gh pr list --repo "$REPO" --state open --json number,headRefName,baseRefName,title > "$TMPDIR/prs.json" 2>/dev/null
Comment thread
ThePlenkov marked this conversation as resolved.

PR_COUNT=$(python3 -c "import json; print(len(json.load(open('$TMPDIR/prs.json'))))" 2>/dev/null || echo "0")

if [ "$PR_COUNT" -eq 0 ]; then

Check failure on line 31 in assets/scripts/pr-stack-resolve.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=sverka-dev_sverka&issues=AZ_89OE24HbJenSxs-rL&open=AZ_89OE24HbJenSxs-rL&pullRequest=57
Comment thread
ThePlenkov marked this conversation as resolved.
log "No open PRs. All resolved."
echo '{"prs":[],"all_resolved":true,"prs_resolved":0,"prs_needs_work":0}'
exit 0
fi

log "Found $PR_COUNT open PR(s)."

# --- 3. Order PRs by stack position (base → top) ---
python3 -c "
import json
prs = json.load(open('$TMPDIR/prs.json'))
Comment thread
qodo-code-review[bot] marked this conversation as resolved.
by_base = {}
for p in prs:
by_base.setdefault(p['baseRefName'], []).append(p)

ordered = []
visited = set()

def visit(base):
for p in by_base.get(base, []):
if p['number'] in visited:
continue
visited.add(p['number'])
ordered.append(p)
visit(p['headRefName'])

visit('main')
for p in prs:
if p['number'] not in visited:
ordered.append(p)

with open('$TMPDIR/ordered.json', 'w') as f:
json.dump(ordered, f)

print(' '.join(str(p['number']) for p in ordered))
" > "$TMPDIR/order.txt"

ORDERED_NUMBERS=$(cat "$TMPDIR/order.txt")
log "Stack order (base → top): $ORDERED_NUMBERS"

# --- 4. Check each PR ---
ALL_RESOLVED=true
NEEDS_WORK_COUNT=0
RESOLVED_COUNT=0
ENTRIES=""

for PR_NUM in $ORDERED_NUMBERS; do
PR_DATA=$(python3 -c "
import json
for p in json.load(open('$TMPDIR/ordered.json')):
if p['number'] == $PR_NUM:
print(json.dumps(p))
break
")

HEAD_REF=$(echo "$PR_DATA" | python3 -c "import json,sys; print(json.load(sys.stdin)['headRefName'])")
BASE_REF=$(echo "$PR_DATA" | python3 -c "import json,sys; print(json.load(sys.stdin)['baseRefName'])")
TITLE=$(echo "$PR_DATA" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d['title'].replace('\"','\\'\"'))")
Comment thread
ThePlenkov marked this conversation as resolved.

log "PR #$PR_NUM: $TITLE (head=$HEAD_REF base=$BASE_REF)"

# Check 1: Merge state
MERGE_STATE=$(gh pr view "$PR_NUM" --repo "$REPO" --json mergeStateStatus -q '.mergeStateStatus' 2>/dev/null || echo "unknown")
HAS_CONFLICT=false
if [ "$MERGE_STATE" = "DIRTY" ] || [ "$MERGE_STATE" = "BLOCKED" ]; then

Check failure on line 96 in assets/scripts/pr-stack-resolve.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=sverka-dev_sverka&issues=AZ_89OE24HbJenSxs-rN&open=AZ_89OE24HbJenSxs-rN&pullRequest=57

Check failure on line 96 in assets/scripts/pr-stack-resolve.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=sverka-dev_sverka&issues=AZ_89OE24HbJenSxs-rM&open=AZ_89OE24HbJenSxs-rM&pullRequest=57
HAS_CONFLICT=true
fi
Comment thread
ThePlenkov marked this conversation as resolved.

# Check 2: Unresolved review threads (via GraphQL — REST API can't filter by resolved state)
# GraphQL variables ($owner, $repo, $pr) are not shell expansion
# shellcheck disable=SC2016
OPEN_THREADS=$(gh api graphql -f query='
query($owner: String!, $repo: String!, $pr: Int!) {
repository(owner: $owner, name: $repo) {
pullRequest(number: $pr) {
reviewThreads(first: 100) {
Comment thread
ThePlenkov marked this conversation as resolved.
nodes { isResolved }
}
Comment thread
ThePlenkov marked this conversation as resolved.
Comment thread
qodo-code-review[bot] marked this conversation as resolved.
}
}
}' -F owner=sverka-dev -F repo=sverka -F pr="$PR_NUM" 2>/dev/null | python3 -c "
Comment thread
ThePlenkov marked this conversation as resolved.
import json, sys
try:
data = json.load(sys.stdin)
threads = data['data']['repository']['pullRequest']['reviewThreads']['nodes']
print(sum(1 for t in threads if not t['isResolved']))
except:
print(0)
" 2>/dev/null || echo "0")
Comment thread
qodo-code-review[bot] marked this conversation as resolved.

# Check 3: CI status
CI_STATE="unknown"
CI_CHECKS=$(gh pr checks "$PR_NUM" --repo "$REPO" --json name,bucket --required 2>/dev/null || echo "[]")
CI_STATE=$(echo "$CI_CHECKS" | python3 -c "
import json, sys
try:
checks = json.load(sys.stdin)
failing = [c['name'] for c in checks if c.get('bucket') == 'fail']
pending = [c for c in checks if c.get('bucket') == 'pending']
if failing:
print('FAIL:' + ','.join(failing))
elif pending:
print('PENDING')
else:
print('PASS')
except:
print('UNKNOWN')
")

# Check 4: Is branch behind base?
BEHIND=false
BEHIND_COUNT=0
git fetch origin "$HEAD_REF" 2>/dev/null || true
git fetch origin "$BASE_REF" 2>/dev/null || true
Comment thread
ThePlenkov marked this conversation as resolved.
DIVERGENCE=$(git rev-list --left-right --count "origin/$BASE_REF...origin/$HEAD_REF" 2>/dev/null || echo "0 0")
BEHIND_COUNT=$(echo "$DIVERGENCE" | awk '{print $1}')
Comment thread
ThePlenkov marked this conversation as resolved.
if [ "$BEHIND_COUNT" -gt 0 ] 2>/dev/null; then

Check failure on line 148 in assets/scripts/pr-stack-resolve.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=sverka-dev_sverka&issues=AZ_89OE24HbJenSxs-rO&open=AZ_89OE24HbJenSxs-rO&pullRequest=57
BEHIND=true
fi

# Determine resolution state
RESOLVED=true
REASONS=""

if [ "$HAS_CONFLICT" = "true" ]; then

Check failure on line 156 in assets/scripts/pr-stack-resolve.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=sverka-dev_sverka&issues=AZ_89OE24HbJenSxs-rP&open=AZ_89OE24HbJenSxs-rP&pullRequest=57
RESOLVED=false
REASONS="${REASONS}merge_conflict "
fi

if [ "$OPEN_THREADS" -gt 0 ] 2>/dev/null; then

Check failure on line 161 in assets/scripts/pr-stack-resolve.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=sverka-dev_sverka&issues=AZ_89OE24HbJenSxs-rQ&open=AZ_89OE24HbJenSxs-rQ&pullRequest=57
RESOLVED=false
REASONS="${REASONS}open_threads($OPEN_THREADS) "
fi

if [[ "$CI_STATE" == FAIL* ]]; then
RESOLVED=false
REASONS="${REASONS}ci_failing(${CI_STATE#FAIL:}) "
fi

if [[ "$CI_STATE" == "PENDING" ]]; then
RESOLVED=false
REASONS="${REASONS}ci_pending "
fi

if [ "$BEHIND" = "true" ]; then

Check failure on line 176 in assets/scripts/pr-stack-resolve.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=sverka-dev_sverka&issues=AZ_89OE24HbJenSxs-rR&open=AZ_89OE24HbJenSxs-rR&pullRequest=57
RESOLVED=false
REASONS="${REASONS}behind_base($BEHIND_COUNT) "
fi

if [ "$RESOLVED" = "true" ]; then

Check failure on line 181 in assets/scripts/pr-stack-resolve.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=sverka-dev_sverka&issues=AZ_89OE24HbJenSxs-rS&open=AZ_89OE24HbJenSxs-rS&pullRequest=57
log " ✓ RESOLVED (CI=$CI_STATE, threads=0, no conflicts, rebased)"
RESOLVED_COUNT=$((RESOLVED_COUNT + 1))
else
log " ✗ NEEDS WORK: ${REASONS:-unknown}"
ALL_RESOLVED=false
NEEDS_WORK_COUNT=$((NEEDS_WORK_COUNT + 1))
fi

# Build JSON entry
PY_RESOLVED=$( [ "$RESOLVED" = "true" ] && echo "True" || echo "False" )

Check failure on line 191 in assets/scripts/pr-stack-resolve.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=sverka-dev_sverka&issues=AZ_89OE24HbJenSxs-rT&open=AZ_89OE24HbJenSxs-rT&pullRequest=57
PY_BEHIND=$( [ "$BEHIND" = "true" ] && echo "True" || echo "False" )

Check failure on line 192 in assets/scripts/pr-stack-resolve.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=sverka-dev_sverka&issues=AZ_89OE24HbJenSxs-rU&open=AZ_89OE24HbJenSxs-rU&pullRequest=57
ENTRY=$(python3 -c "
Comment thread
ThePlenkov marked this conversation as resolved.
import json
entry = {
'number': $PR_NUM,
'head': '$HEAD_REF',
'base': '$BASE_REF',
Comment thread
ThePlenkov marked this conversation as resolved.
'resolved': $PY_RESOLVED,
'merge_state': '$MERGE_STATE',
'open_threads': $OPEN_THREADS,
'ci_state': '$CI_STATE',
Comment thread
ThePlenkov marked this conversation as resolved.
'behind': $PY_BEHIND,
'behind_count': $BEHIND_COUNT,
'reasons': '${REASONS:-}'.strip()
}
print(json.dumps(entry))
")
Comment thread
coderabbitai[bot] marked this conversation as resolved.
if [ -n "$ENTRIES" ]; then

Check failure on line 209 in assets/scripts/pr-stack-resolve.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=sverka-dev_sverka&issues=AZ_89OE24HbJenSxs-rV&open=AZ_89OE24HbJenSxs-rV&pullRequest=57
ENTRIES="$ENTRIES,$ENTRY"
else
ENTRIES="$ENTRY"
fi
done

ALL_RESOLVED_JSON=$( [ "$ALL_RESOLVED" = "true" ] && echo "true" || echo "false" )

Check failure on line 216 in assets/scripts/pr-stack-resolve.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=sverka-dev_sverka&issues=AZ_89OE24HbJenSxs-rW&open=AZ_89OE24HbJenSxs-rW&pullRequest=57
ENTRIES_JSON="$ENTRIES"
echo "{\"timestamp\":\"$TIMESTAMP\",\"prs\":[$ENTRIES_JSON],\"all_resolved\":$ALL_RESOLVED_JSON,\"prs_resolved\":$RESOLVED_COUNT,\"prs_needs_work\":$NEEDS_WORK_COUNT}"

# --- 5. Nudge mayor if action is needed ---
if [ "$ALL_RESOLVED" = "true" ]; then

Check failure on line 221 in assets/scripts/pr-stack-resolve.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=sverka-dev_sverka&issues=AZ_89OE24HbJenSxs-rX&open=AZ_89OE24HbJenSxs-rX&pullRequest=57
log "All $RESOLVED_COUNT PR(s) resolved."
exit 0
fi

log "$NEEDS_WORK_COUNT PR(s) need work, $RESOLVED_COUNT resolved. Building nudge for mayor..."

# Build a prioritized action message for the mayor
NUDGE_MSG="PR stack resolve: $NEEDS_WORK_COUNT of $PR_COUNT PR(s) need work.\n\n"

# Priority 1: Merge conflicts (must fix first — blocks everything above)
CONFLICT_PRS=$(echo "$ENTRIES_JSON" | python3 -c "
import json, sys
entries = json.loads('[' + sys.stdin.read() + ']')
conflicts = [e for e in entries if e.get('merge_state') in ('DIRTY', 'BLOCKED')]
for e in conflicts:
print(f\"PR #{e['number']} ({e['head']}): {e['reasons']}\")
" 2>/dev/null || true)

if [ -n "$CONFLICT_PRS" ]; then

Check failure on line 240 in assets/scripts/pr-stack-resolve.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=sverka-dev_sverka&issues=AZ_89OE24HbJenSxs-rY&open=AZ_89OE24HbJenSxs-rY&pullRequest=57
NUDGE_MSG="${NUDGE_MSG}PRIORITY 1 — MERGE CONFLICTS (fix first, cascade rebase up):\n$CONFLICT_PRS\n\n"
NUDGE_MSG="${NUDGE_MSG}For each: git fetch origin, checkout branch, rebase onto origin/<base>, resolve conflicts, git push --force-with-lease. Then rebase all PRs above onto the new base.\n\n"
fi

# Priority 2: Branches behind base (rebase needed)
BEHIND_PRS=$(echo "$ENTRIES_JSON" | python3 -c "
import json, sys
entries = json.loads('[' + sys.stdin.read() + ']')
behind = [e for e in entries if e.get('behind') and e.get('merge_state') not in ('DIRTY', 'BLOCKED')]
for e in behind:
print(f\"PR #{e['number']} ({e['head']}): {e['behind_count']} commits behind {e['base']}\")
" 2>/dev/null || true)

if [ -n "$BEHIND_PRS" ]; then

Check failure on line 254 in assets/scripts/pr-stack-resolve.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=sverka-dev_sverka&issues=AZ_89OE24HbJenSxs-rZ&open=AZ_89OE24HbJenSxs-rZ&pullRequest=57
NUDGE_MSG="${NUDGE_MSG}PRIORITY 2 — BEHIND BASE (rebase):\n$BEHIND_PRS\n\n"
fi

# Priority 3: CI failures
CI_FAIL_PRS=$(echo "$ENTRIES_JSON" | python3 -c "
import json, sys
entries = json.loads('[' + sys.stdin.read() + ']')
fails = [e for e in entries if e.get('ci_state', '').startswith('FAIL')]
for e in fails:
print(f\"PR #{e['number']} ({e['head']}): {e['ci_state']}\")
" 2>/dev/null || true)

if [ -n "$CI_FAIL_PRS" ]; then

Check failure on line 267 in assets/scripts/pr-stack-resolve.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=sverka-dev_sverka&issues=AZ_89OE24HbJenSxs-ra&open=AZ_89OE24HbJenSxs-ra&pullRequest=57
NUDGE_MSG="${NUDGE_MSG}PRIORITY 3 — CI FAILING:\n$CI_FAIL_PRS\n\n"
NUDGE_MSG="${NUDGE_MSG}Read logs with: gh pr checks <PR> --watch. /act to fix, push, wait for re-run.\n\n"
fi

# Priority 4: Open review threads
THREAD_PRS=$(echo "$ENTRIES_JSON" | python3 -c "
import json, sys
entries = json.loads('[' + sys.stdin.read() + ']')
threads = [e for e in entries if e.get('open_threads', 0) > 0]
for e in threads:
print(f\"PR #{e['number']} ({e['head']}): {e['open_threads']} open thread(s)\")
" 2>/dev/null || true)

if [ -n "$THREAD_PRS" ]; then

Check failure on line 281 in assets/scripts/pr-stack-resolve.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=sverka-dev_sverka&issues=AZ_89OE24HbJenSxs-rb&open=AZ_89OE24HbJenSxs-rb&pullRequest=57
NUDGE_MSG="${NUDGE_MSG}PRIORITY 4 — OPEN REVIEW THREADS:\n$THREAD_PRS\n\n"
NUDGE_MSG="${NUDGE_MSG}For each PR: fetch comments with gh api repos/sverka-dev/sverka/pulls/<PR>/comments. For each thread: read comment, /act to fix or reply, push fix, resolve thread. AFTER each /act, RE-FETCH comments to check for NEW replies. Loop until no new comments.\n\n"
fi

NUDGE_MSG="${NUDGE_MSG}After all fixes: re-run this check. Goal: all_resolved=true."

# Send nudge to mayor
if gc session nudge mayor "$(echo -e "$NUDGE_MSG")" 2>/dev/null; then
log "Nudged mayor with $NEEDS_WORK_COUNT PR action items"
else
log "Failed to nudge mayor"
fi

# Also mail human if there are conflicts or CI failures
if [ -n "$CONFLICT_PRS" ] || [ -n "$CI_FAIL_PRS" ]; then

Check failure on line 296 in assets/scripts/pr-stack-resolve.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=sverka-dev_sverka&issues=AZ_89OE24HbJenSxs-rc&open=AZ_89OE24HbJenSxs-rc&pullRequest=57

Check failure on line 296 in assets/scripts/pr-stack-resolve.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=sverka-dev_sverka&issues=AZ_89OE24HbJenSxs-rd&open=AZ_89OE24HbJenSxs-rd&pullRequest=57
gc mail send human "PR stack: critical issues" "$(echo -e "$NUDGE_MSG")" 2>/dev/null || true
fi

exit 1
Loading
Loading