Skip to content

Commit b8aa527

Browse files
committed
Read the state marker and PR lists without result limits
gh pr view --json comments returns the first 100 comments, so on a busy PR the state marker could fall outside the window and the resume would give up. Read the comments through gh api graphql --paginate, which walks every page, and keep the last marker among our own comments. The two gh pr list calls had the same shape of problem at their default limit of 30: a silently truncated fan-out, or a missed conflicted sibling letting the shared base branch be deleted. List pulls through gh api --paginate so every page is read. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01JHvKryT4QUpHYdNq9YEQxX
1 parent dbc48ba commit b8aa527

3 files changed

Lines changed: 31 additions & 21 deletions

File tree

tests/mock_gh.sh

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,10 @@
33
# Mock gh CLI for unit tests.
44
# Only direct children are queried now (no recursive updates of indirect children).
55

6-
if [[ "$1" == "pr" && "$2" == "list" ]]; then
7-
# Parse the --base argument to determine which PRs to return
8-
base=""
9-
for ((i=1; i<=$#; i++)); do
10-
if [[ "${!i}" == "--base" ]]; then
11-
next=$((i+1))
12-
base="${!next}"
13-
fi
14-
done
15-
6+
if [[ "$1" == "api" && "$2" == repos/*"/pulls?base="* ]]; then
7+
# Open PRs based on a branch (already --jq filtered to "<number> <head>").
8+
base="${2#*pulls\?base=}"
9+
base="${base%%&*}"
1610
if [[ "$base" == "feature1" ]]; then
1711
# feature2 is a direct child of feature1 (PR #2)
1812
echo '2 feature2'

tests/test_conflict_resolution_resume.sh

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ ok() { echo "✅ $1"; PASS=$((PASS+1)); }
2121
# Build a configurable gh mock in a temp dir. It records every invocation to
2222
# $CALLS and is driven by env vars set per scenario:
2323
# MOCK_LABELS newline-separated labels returned by `pr view --json labels`
24-
# MOCK_COMMENTS_FILE file whose contents are returned by `pr view --json comments`
24+
# MOCK_COMMENTS_FILE file served as the body of our own PR comments
2525
# The PR's base branch is not mocked: the script must take it from PR_BASE
2626
# (event payload), so a baseRefName query is an unhandled call and fails.
2727
make_mock_gh() {
@@ -33,18 +33,18 @@ echo "gh $*" >> "$CALLS"
3333
if [[ "$1 $2" == "pr view" ]]; then
3434
case "$*" in
3535
*--json\ labels*) printf '%s\n' "${MOCK_LABELS:-}";;
36-
*--json\ comments*)
37-
# The comments file stands for our own comments only, so the query
38-
# must restrict itself to those.
39-
[[ "$*" == *viewerDidAuthor* ]] || { echo "comments query must filter by viewerDidAuthor" >&2; exit 1; }
40-
cat "${MOCK_COMMENTS_FILE:-/dev/null}";;
4136
*) echo "unhandled pr view: $*" >&2; exit 1;;
4237
esac
38+
elif [[ "$1 $2" == "api graphql" ]]; then
39+
# The comments file stands for our own comments only, so the query must
40+
# restrict itself to those.
41+
[[ "$*" == *viewerDidAuthor* ]] || { echo "comments query must filter by viewerDidAuthor" >&2; exit 1; }
42+
cat "${MOCK_COMMENTS_FILE:-/dev/null}"
4343
elif [[ "$1 $2" == "pr comment" ]]; then
4444
cat >/dev/null # consume the -F - body
4545
elif [[ "$1 $2" == "pr edit" ]]; then
4646
:
47-
elif [[ "$1 $2" == "pr list" ]]; then
47+
elif [[ "$1" == "api" ]]; then
4848
: # no sibling conflicts
4949
elif [[ "$1 $2" == "label create" ]]; then
5050
:
@@ -93,6 +93,7 @@ setup_repo() {
9393

9494
run_resume() {
9595
env ACTION_MODE=conflict-resolved PR_BRANCH=child PR_NUMBER=5 PR_BASE="$PR_BASE" \
96+
GITHUB_REPOSITORY=tester/repo \
9697
GH="$MOCK_DIR/mock_gh.sh" GIT="$MOCK_DIR/mock_git.sh" \
9798
MOCK_LABELS="$MOCK_LABELS" \
9899
MOCK_COMMENTS_FILE="$MOCK_COMMENTS_FILE" CALLS="$CALLS" \

update-pr-stack.sh

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
# PR_BRANCH - The head branch of the PR being resumed
1313
# PR_NUMBER - Its PR number, from the event payload
1414
# PR_BASE - Its base branch, from the event payload
15+
# GITHUB_REPOSITORY - "owner/repo", provided by Actions
1516
#
1617
# Design note:
1718
# This script aims to output a transcript of "plain" git/gh commands that a
@@ -44,8 +45,19 @@ format_state_marker() {
4445
read_state_marker() {
4546
local PR_NUMBER="$1"
4647
local BODIES
47-
if ! BODIES=$(gh pr view "$PR_NUMBER" --json comments \
48-
--jq '.comments[] | select(.viewerDidAuthor) | .body'); then
48+
if ! BODIES=$(gh api graphql --paginate \
49+
-F owner="${GITHUB_REPOSITORY%/*}" -F repo="${GITHUB_REPOSITORY#*/}" \
50+
-F number="$PR_NUMBER" -f query='
51+
query($owner: String!, $repo: String!, $number: Int!, $endCursor: String) {
52+
repository(owner: $owner, name: $repo) {
53+
pullRequest(number: $number) {
54+
comments(first: 100, after: $endCursor) {
55+
pageInfo { hasNextPage endCursor }
56+
nodes { viewerDidAuthor body }
57+
}
58+
}
59+
}
60+
}' --jq '.data.repository.pullRequest.comments.nodes[] | select(.viewerDidAuthor) | .body'); then
4961
echo "Error: could not read comments of PR #$PR_NUMBER" >&2
5062
exit 1
5163
fi
@@ -139,7 +151,8 @@ is_rebase_merge() {
139151

140152
# Echoes "<number> <head branch>" for each open PR based on the merged branch.
141153
list_child_prs() {
142-
log_cmd gh pr list --base "$MERGED_BRANCH" --json number,headRefName --jq '.[] | "\(.number) \(.headRefName)"'
154+
log_cmd gh api "repos/{owner}/{repo}/pulls?base=$MERGED_BRANCH&state=open&per_page=100" \
155+
--paginate --jq '.[] | "\(.number) \(.head.ref)"'
143156
}
144157

145158
# Args: head branch, base branch, PR number. git commands use the branch; gh
@@ -255,7 +268,8 @@ has_sibling_conflicts() {
255268

256269
# Find all open PRs with the conflict label that are based on BASE_BRANCH
257270
local CONFLICTED_SIBLINGS
258-
CONFLICTED_SIBLINGS=$(gh pr list --base "$BASE_BRANCH" --label "$CONFLICT_LABEL" --json headRefName --jq '.[].headRefName' 2>/dev/null || echo "")
271+
CONFLICTED_SIBLINGS=$(gh api "repos/{owner}/{repo}/pulls?base=$BASE_BRANCH&state=open&per_page=100" \
272+
--paginate --jq ".[] | select(any(.labels[]; .name == \"$CONFLICT_LABEL\")) | .head.ref" 2>/dev/null || echo "")
259273

260274
for SIBLING in $CONFLICTED_SIBLINGS; do
261275
if [[ "$SIBLING" != "$EXCLUDE_BRANCH" ]]; then
@@ -281,6 +295,7 @@ continue_after_resolution() {
281295
check_env_var "PR_BRANCH"
282296
check_env_var "PR_NUMBER"
283297
check_env_var "PR_BASE"
298+
check_env_var "GITHUB_REPOSITORY"
284299

285300
echo "Checking if PR #$PR_NUMBER ($PR_BRANCH) needs continuation after conflict resolution..."
286301

0 commit comments

Comments
 (0)