Skip to content

Commit 273da4b

Browse files
authored
Merge pull request #18 from HaoZeke/fix/fetch-prs-page-past-caps
fix(examples): page pull request lists past 500 and the file cap
2 parents 155d0ef + 9be50b1 commit 273da4b

8 files changed

Lines changed: 784 additions & 64 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@ All notable changes to this project are documented here.
1515
under `opaque`. Any other unresolved line is still an error and
1616
nothing is written. A list with 22 `--from-commit` lines and seven
1717
site recipes failed whole before.
18+
- `examples/easyconfigs-pr-overlap/fetch-prs.sh` and
19+
`examples/eessi-pr-overlap/fetch-prs.sh` request each page of open pull
20+
requests and of changed files, 100 at a time. The open list does not
21+
stop at 500. A changed-file list that stops at GitHub's cap of 3000 is
22+
kept: the pull request is still written, with `file_list_truncated`
23+
set, and the record notes `truncated at GitHub's limit`.
1824
- `stack order` exits 1 when runtime requirements of one name at one
1925
toolchain have no version that satisfies every one of them. The error
2026
names each requester and the version it resolved. Eigen 3.4.1 required

‎docs/orgmode/howto/easyconfigs-pr-overlap.org‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,11 @@ writes one JSON file per open PR that changes an easyconfig. Run it
138138
from a clone of easybuild-easyconfigs whose ~origin~ is that
139139
repository, with ~gh~ authenticated for public reads. Set
140140
~EB_STACK_CHECKOUT~ to the eb-stack checkout. The script reads base
141-
text from ~develop~ and does not check a PR out.
141+
text from ~develop~ and does not check a PR out. Open pull requests
142+
are read 100 per page, with no stop at 500. Changed files are read
143+
100 per page through GitHub's cap of 3000. A pull request whose file
144+
list stops there is still written, with ~file_list_truncated~ set.
145+
~pr-overlap~ notes ~truncated at GitHub's limit~.
142146

143147
#+begin_src bash
144148
: "${EB_STACK_CHECKOUT:?set this to an eb-stack checkout}"

‎docs/orgmode/howto/pr-overlap.org‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -153,9 +153,12 @@ EasyBuild version these easystacks name.
153153
differ from that version.
154154
~examples/eessi-pr-overlap/fetch-prs.sh~ reads every open PR and writes
155155
one JSON file per PR that changes an easystack for that EESSI version.
156-
It pages through the GitHub pulls API 100 at a time with no upper
157-
limit, so a repository with more open PRs than one listing returns is
158-
still read in full. It fetches each PR head into the clone as
156+
It pages through the GitHub pulls API 100 at a time with no stop at
157+
500, so a repository with more open PRs than one listing returns is
158+
still read in full. Changed files are read 100 per page through
159+
GitHub's cap of 3000. A pull request whose file list stops there is
160+
still written, with ~file_list_truncated~ set, and the record notes
161+
~truncated at GitHub's limit~. It fetches each PR head into the clone as
159162
~refs/remotes/pr/N~ and does not check it out. ~--eessi-version~
160163
defaults to ~2026.06~.
161164

‎examples/easyconfigs-pr-overlap/fetch-prs.sh‎

Lines changed: 126 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,46 +9,155 @@
99
# Run inside a clone of easybuild-easyconfigs whose `origin` is <owner/repo>.
1010
# The base text of a changed file is read from `develop`.
1111
set -euo pipefail
12+
# GitHub returns at most this many items on one page.
13+
page_size=100
14+
# GitHub lists at most this many changed files for one pull request.
15+
file_cap=3000
16+
# Pages of open pull requests read before the list is taken as unending.
17+
open_page_limit=1000
18+
# Pages of changed files read before the list is taken as unending.
19+
file_page_limit=40
1220

1321
repo=$1
1422
out=${2:?usage: fetch-prs.sh <owner/repo> <out-dir>}
1523
# Rewrite outputs from scratch on every run, so closed pull requests do not
1624
# persist from stale raw/ and prs/ directories.
17-
rm -rf "$out/raw" "$out/prs"
18-
mkdir -p "$out/prs"
25+
rm -rf "$out/raw" "$out/prs" "$out/trunc"
26+
mkdir -p "$out/prs" "$out/trunc"
1927
rm -f "$out/refspecs"
2028
prefix="easybuild/easyconfigs/"
2129

2230
git fetch --quiet --no-tags origin develop
23-
# Every open pull request, 100 per page, with no upper limit.
24-
gh api --method GET --paginate "repos/${repo}/pulls?state=open&per_page=100" \
25-
--jq '.[] | {number, headRefOid: .head.sha, title}' >"$out/open.jsonl"
31+
# Every open pull request, 100 per page. Each page is requested on its own,
32+
# and a short page is the end. There is no stop at 500.
33+
: >"$out/open.jsonl"
34+
page=1
35+
while true; do
36+
if ! chunk=$(gh api --method GET \
37+
"repos/${repo}/pulls?state=open&per_page=${page_size}&page=${page}" \
38+
--jq '.[] | {number, headRefOid: .head.sha, title}' 2>"$out/.gh-err"); then
39+
cat "$out/.gh-err" >&2
40+
echo "fetch-prs.sh: listing open pull requests failed on page ${page}" >&2
41+
exit 1
42+
fi
43+
if [[ -z "${chunk//[$' \t\n']/}" ]]; then
44+
break
45+
fi
46+
printf '%s\n' "$chunk" >>"$out/open.jsonl"
47+
count=0
48+
while IFS= read -r line; do
49+
[[ -n "$line" ]] || continue
50+
count=$((count + 1))
51+
done <<<"$chunk"
52+
if (( count < page_size )); then
53+
break
54+
fi
55+
page=$((page + 1))
56+
if (( page > open_page_limit )); then
57+
echo "fetch-prs.sh: open pull request list did not end" >&2
58+
exit 1
59+
fi
60+
done
61+
rm -f "$out/.gh-err"
2662
python3 - "$out/open.jsonl" "$out/open.json" <<'PY'
2763
import json, sys
2864
rows = [json.loads(line) for line in open(sys.argv[1]) if line.strip()]
2965
json.dump(rows, open(sys.argv[2], "w"))
3066
print("open pull requests:", len(rows))
3167
PY
3268

69+
# Matching easyconfig paths for pull request $1, one per line.
70+
# Pages are 100 files. GitHub stops at 3000: a later page answers 422, or
71+
# another page is non-empty after 3000 names. The pull request is kept and
72+
# $out/trunc/$1 is created. Paths past that cap are not kept.
73+
pr_changed_files() {
74+
local number=$1
75+
local page=1
76+
local total=0
77+
local err="$out/.gh-err"
78+
while true; do
79+
if ! chunk=$(gh api --method GET \
80+
"repos/${repo}/pulls/${number}/files?per_page=${page_size}&page=${page}" \
81+
--jq '.[] | .filename' 2>"$err"); then
82+
if (( page > 1 )) && grep -Eqi "${file_cap}|pagination is limited|maximum number of files|too many files" "$err"; then
83+
: >"$out/trunc/${number}"
84+
return 0
85+
fi
86+
cat "$err" >&2
87+
echo "fetch-prs.sh: file list for pull request ${number} failed on page ${page}" >&2
88+
return 1
89+
fi
90+
if [[ -z "${chunk//[$' \t\n']/}" ]]; then
91+
return 0
92+
fi
93+
local count=0
94+
local name
95+
while IFS= read -r name; do
96+
[[ -n "$name" ]] || continue
97+
count=$((count + 1))
98+
case "$name" in
99+
"${prefix}"*.eb) printf '%s\n' "$name" ;;
100+
esac
101+
done <<<"$chunk"
102+
total=$((total + count))
103+
if (( count < page_size )); then
104+
return 0
105+
fi
106+
if (( total >= file_cap )); then
107+
page=$((page + 1))
108+
if ! chunk=$(gh api --method GET \
109+
"repos/${repo}/pulls/${number}/files?per_page=${page_size}&page=${page}" \
110+
--jq '.[] | .filename' 2>"$err"); then
111+
if grep -Eqi "${file_cap}|pagination is limited|maximum number of files|too many files" "$err"; then
112+
: >"$out/trunc/${number}"
113+
return 0
114+
fi
115+
cat "$err" >&2
116+
echo "fetch-prs.sh: file list for pull request ${number} failed on page ${page}" >&2
117+
return 1
118+
fi
119+
if [[ -n "${chunk//[$' \t\n']/}" ]]; then
120+
: >"$out/trunc/${number}"
121+
fi
122+
return 0
123+
fi
124+
page=$((page + 1))
125+
if (( page > file_page_limit )); then
126+
echo "fetch-prs.sh: file list for pull request ${number} did not end" >&2
127+
return 1
128+
fi
129+
done
130+
}
131+
33132
python3 - "$out/open.json" <<'PY' | while read -r number sha; do
34133
import json, sys
35134
for pr in json.load(open(sys.argv[1])):
36135
print(pr["number"], pr["headRefOid"])
37136
PY
38137
# Changed files, from the API so no diff of fork history is needed.
39-
mapfile -t files < <(gh api --paginate "repos/${repo}/pulls/${number}/files" \
40-
--jq ".[] | select(.filename | startswith(\"${prefix}\")) | select(.filename | endswith(\".eb\")) | .filename")
41-
[[ ${#files[@]} -eq 0 ]] && continue
42138
dir="$out/raw/${number}"
43139
mkdir -p "$dir"
140+
if ! pr_changed_files "$number" >"$dir/names"; then
141+
exit 1
142+
fi
143+
if [[ -f "$out/trunc/${number}" ]]; then
144+
mv "$out/trunc/${number}" "$dir/truncated"
145+
fi
146+
mapfile -t files <"$dir/names"
147+
if [[ ${#files[@]} -eq 0 && ! -f "$dir/truncated" ]]; then
148+
rm -rf "$dir"
149+
continue
150+
fi
44151
i=0
45152
for f in "${files[@]}"; do
46153
printf '%s\n' "$f" >"$dir/${i}.path"
47154
i=$((i + 1))
48155
done
49156
printf '%s\n' "$sha" >"$dir/head_sha"
50157
# GitHub keeps every pull request head, fork or not, at refs/pull/N/head.
51-
printf '+refs/pull/%s/head:refs/remotes/pr/%s\n' "$number" "$number" >>"$out/refspecs"
158+
if [[ ${#files[@]} -gt 0 ]]; then
159+
printf '+refs/pull/%s/head:refs/remotes/pr/%s\n' "$number" "$number" >>"$out/refspecs"
160+
fi
52161
done
53162

54163
# One shallow fetch for every head. A shallow fetch per pull request
@@ -101,6 +210,13 @@ for number in sorted(os.listdir(raw) if os.path.isdir(raw) else [], key=int):
101210
i += 1
102211
doc = {"number": int(number), "head_sha": read(f"{d}/head_sha").strip(),
103212
"title": titles.get(int(number)), "easyconfigs": easyconfigs}
213+
if os.path.exists(f"{d}/truncated"):
214+
doc["file_list_truncated"] = True
104215
json.dump(doc, open(f"{out}/prs/{number}.json", "w"), indent=1, sort_keys=True)
105216
PY
106-
ls "$out/prs" | wc -l | xargs echo "pull requests with easyconfig changes:"
217+
rm -rf "$out/trunc"
218+
rm -f "$out/.gh-err"
219+
shopt -s nullglob
220+
prs=("$out"/prs/*.json)
221+
shopt -u nullglob
222+
echo "pull requests with easyconfig changes: ${#prs[@]}"

‎examples/eessi-pr-overlap/README.md‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -153,9 +153,12 @@ eb-stack pr-overlap --pr in/prs/*.json \
153153
--out-dir out/
154154
```
155155

156-
`fetch-prs.sh` reads every open pull request, 100 per page with no upper
157-
limit, and writes `in/prs/<N>.json` for each one that changes a 2026.06
158-
easystack. On EESSI/software-layer on 2026-10-31 it printed:
156+
`fetch-prs.sh` reads every open pull request, 100 per page, and does not
157+
stop at 500. Changed files are read the same way, through GitHub's cap of
158+
3000. A pull request whose file list stops there is still written, with
159+
`file_list_truncated` set, and the record notes `truncated at GitHub's limit`.
160+
It writes `in/prs/<N>.json` for each one that changes a 2026.06 easystack.
161+
On EESSI/software-layer on 2026-10-31 it printed:
159162

160163
```
161164
open pull requests: 52

0 commit comments

Comments
 (0)