@plot-pm/board 0.16.2 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/board-server.mjs +258 -146
- package/package.json +3 -6
- package/plot-agent-manifest.sh +76 -15
- package/plot-agent-monitor.sh +43 -5
- package/plot-approve.sh +224 -73
- package/plot-config.sh +14 -14
- package/plot-deliver.sh +10 -741
- package/plot-dispatch.sh +282 -107
- package/plot-fleet-scan.sh +867 -170
- package/plot-host.sh +310 -76
- package/plot-monitor-subject.sh +61 -10
- package/plot-plan-meta.sh +155 -139
- package/plot-pr-merged.sh +16 -0
- package/plot-reap.sh +61 -28
- package/plot-state-receipt.sh +12 -2
- package/plot-tmp.sh +31 -3
- package/plot-worker-state.sh +458 -24
- package/plot-build-monitor.sh +0 -435
- package/plot-resolve-artifact.sh +0 -423
- package/plot-transcript-quiet.sh +0 -170
- package/plot-worker-monitor.sh +0 -705
package/plot-host.sh
CHANGED
|
@@ -71,7 +71,54 @@
|
|
|
71
71
|
# pr-merge <number> [--squash] [--delete-branch]
|
|
72
72
|
# pr-ready <number> take a PR out of draft
|
|
73
73
|
# merge the PR
|
|
74
|
-
# pr-list [--state open|merged|closed|all] [--limit N] [--rich]
|
|
74
|
+
# pr-list [--state open|merged|closed|all] [--limit N] [--rich|--rich-open]
|
|
75
|
+
# --rich-open asks the rich fields of the OPEN
|
|
76
|
+
# pull requests only, and emits every row in the
|
|
77
|
+
# --rich shape. A MERGED or CLOSED row carries
|
|
78
|
+
# checks:"unknown", mergeable:"unknown",
|
|
79
|
+
# review:"" and failing_checks:[] — the ABSENT
|
|
80
|
+
# values the Bitbucket arm already emits, which
|
|
81
|
+
# mean NOT ASKED and never "no checks".
|
|
82
|
+
# WHY: the four rich fields are verdicts about a
|
|
83
|
+
# PR's HEAD, and a terminal head is finished —
|
|
84
|
+
# no check will run against it and no review will
|
|
85
|
+
# change. Measured 2026-10-01 on this
|
|
86
|
+
# repository, `--rich --state all --limit 1000`
|
|
87
|
+
# took 43.0 s against 7.3 s without the rich
|
|
88
|
+
# fields, and all 1000 rows were terminal, so
|
|
89
|
+
# 36 s bought answers nobody can use.
|
|
90
|
+
# Re-measured 2026-10-02 with 3 PRs open:
|
|
91
|
+
# `--rich --state open` 2.6-3.3 s, `--state all`
|
|
92
|
+
# 8.6-10.5 s, the split read 12.2-13.5 s.
|
|
93
|
+
# ON GITHUB IT IS TWO CALLS and the terminal
|
|
94
|
+
# rows come from the plain one; a PR open in both
|
|
95
|
+
# payloads is emitted once, from the rich call.
|
|
96
|
+
# A FAILURE IN EITHER CALL PRINTS NO ROWS AND
|
|
97
|
+
# EXITS NON-ZERO — a half answer reported as
|
|
98
|
+
# whole is what would let a caller fold a store
|
|
99
|
+
# that lost every open PR.
|
|
100
|
+
# ON BITBUCKET IT ANSWERS EXACTLY AS --rich,
|
|
101
|
+
# because that arm asks the host no verdict: its
|
|
102
|
+
# rows are already `unknown` whatever the state,
|
|
103
|
+
# so there is no second call to save.
|
|
104
|
+
# THE CALLER STILL ASKS ONCE. One `pr-list`
|
|
105
|
+
# invocation is one host question however many
|
|
106
|
+
# calls this makes, so a caller's per-refresh
|
|
107
|
+
# arithmetic is unchanged.
|
|
108
|
+
# `--limit` BOUNDS EACH CALL, NOT THE UNION, so
|
|
109
|
+
# the row count can exceed it by up to the number
|
|
110
|
+
# of open pull requests. Measured 2026-10-02 with
|
|
111
|
+
# 5 open: `--limit 5` answered 6 rows, where
|
|
112
|
+
# `--rich` answered 5 — the open call returned an
|
|
113
|
+
# open PR that fell outside the plain call's
|
|
114
|
+
# newest-5 page. It is NOT bounded per call and
|
|
115
|
+
# then trimmed, because trimming would drop
|
|
116
|
+
# either a verdict the rich call just bought or a
|
|
117
|
+
# terminal row the store needs, and the caller
|
|
118
|
+
# cannot say which. Harmless where `--limit`
|
|
119
|
+
# exceeds the open count, which is every caller
|
|
120
|
+
# today: the board asks 1000 against 5 open, and
|
|
121
|
+
# no other caller passes this flag.
|
|
75
122
|
# [--since <iso>] narrows the listing to pull
|
|
76
123
|
# requests the host has seen change since that
|
|
77
124
|
# stamp. Measured 2026-09-21 on this repository:
|
|
@@ -86,16 +133,19 @@
|
|
|
86
133
|
# two seconds fast excludes the PRs updated in
|
|
87
134
|
# that gap from every later window — forever,
|
|
88
135
|
# because the window never reopens.
|
|
89
|
-
#
|
|
90
|
-
#
|
|
91
|
-
#
|
|
92
|
-
#
|
|
93
|
-
#
|
|
94
|
-
#
|
|
95
|
-
#
|
|
96
|
-
#
|
|
97
|
-
#
|
|
98
|
-
#
|
|
136
|
+
# BOTH HOSTS NARROW, BY DIFFERENT ROUTES.
|
|
137
|
+
# `gh pr list` takes `--search`. `bb pr list`
|
|
138
|
+
# has no query flag (verified against bb 1.9.0:
|
|
139
|
+
# `unknown flag: --query`), so a windowed
|
|
140
|
+
# Bitbucket listing goes through
|
|
141
|
+
# `bb_window_listing`, which asks REST with
|
|
142
|
+
# `q=state="..." AND updated_on>="..."` and
|
|
143
|
+
# walks every page. It refuses a window whose
|
|
144
|
+
# rows fall short of the server's `size` rather
|
|
145
|
+
# than printing it as whole: a full answer
|
|
146
|
+
# reported as a delta is what would let a caller
|
|
147
|
+
# advance a watermark over a window it never
|
|
148
|
+
# applied.
|
|
99
149
|
# [--repo <owner/repo>] pins the list to ONE
|
|
100
150
|
# repository, exactly as pr-state and pr-merged
|
|
101
151
|
# do. A checkout with remotes on two hosts lets
|
|
@@ -169,13 +219,11 @@
|
|
|
169
219
|
# empty: `jen job list` carries no history and
|
|
170
220
|
# no timestamps, and inventing them would be a
|
|
171
221
|
# collector reaching a verdict.
|
|
172
|
-
# run-for-sha <branch> <sha> the run for ONE sha
|
|
173
|
-
#
|
|
174
|
-
#
|
|
175
|
-
#
|
|
176
|
-
#
|
|
177
|
-
# list` names no commit, so there is nothing to
|
|
178
|
-
# match a sha against. Output:
|
|
222
|
+
# run-for-sha <branch> <sha> the run for ONE sha, and nothing for any
|
|
223
|
+
# other sha — as a single JSON object, or
|
|
224
|
+
# nothing when the branch has no run for this
|
|
225
|
+
# sha. Dispatched on the `CI` key like `runs`.
|
|
226
|
+
# Output:
|
|
179
227
|
# {"sha":"…","status":"queued|in_progress|
|
|
180
228
|
# completed|waiting|requested",
|
|
181
229
|
# "conclusion":"success|failure|…|null",
|
|
@@ -185,22 +233,12 @@
|
|
|
185
233
|
# and sha-blind, and `gh run list --branch X`
|
|
186
234
|
# returns runs for every sha that branch ever
|
|
187
235
|
# had — the newest run is NOT necessarily for
|
|
188
|
-
# the newest commit. A
|
|
189
|
-
#
|
|
190
|
-
#
|
|
191
|
-
#
|
|
192
|
-
#
|
|
193
|
-
#
|
|
194
|
-
# re-armed.
|
|
195
|
-
# THE FALLBACK IS WHAT MAKES THAT VISIBLE. Were
|
|
196
|
-
# it to report nothing when the asked-for sha
|
|
197
|
-
# has no run, a run IN FLIGHT for a superseded
|
|
198
|
-
# commit would look exactly like no run at all,
|
|
199
|
-
# and a caller could not tell "CI has not
|
|
200
|
-
# started" from "CI is answering about the
|
|
201
|
-
# past". `sha` names the run's own commit, and
|
|
202
|
-
# comparing it to the one asked about is the
|
|
203
|
-
# CALLER's rule — this decides nothing.
|
|
236
|
+
# the newest commit. A run for any sha but the
|
|
237
|
+
# one asked about is not evidence about it, so
|
|
238
|
+
# reporting it would invite a merge of the
|
|
239
|
+
# wrong thing. Measured 2026-08-30: two merge
|
|
240
|
+
# waiters reported on superseded runs and had
|
|
241
|
+
# to be stopped and re-armed.
|
|
204
242
|
# `status` AND `conclusion` ARE BOTH REPORTED,
|
|
205
243
|
# never collapsed. A run that is `completed` has
|
|
206
244
|
# a conclusion; one that is `waiting` or
|
|
@@ -424,7 +462,6 @@ die3() { echo "plot-host: $*" >&2; exit 3; }
|
|
|
424
462
|
# a person at a terminal want three different answers — and a retry inside the
|
|
425
463
|
# adapter would hide the very state this code exists to surface, turning a
|
|
426
464
|
# reportable fact into an unexplained four-minute call.
|
|
427
|
-
die5() { echo "plot-host: $*" >&2; exit 5; }
|
|
428
465
|
|
|
429
466
|
# Exit 6 — the host refused because too many calls arrived AT ONCE. A secondary
|
|
430
467
|
# limit, and a different ceiling from the one exit 5 reports.
|
|
@@ -447,7 +484,6 @@ die5() { echo "plot-host: $*" >&2; exit 5; }
|
|
|
447
484
|
#
|
|
448
485
|
# NOT A RETRY, for the reason exit 5 states: whether to wait is the caller's
|
|
449
486
|
# decision, and this adapter reports rather than reacts.
|
|
450
|
-
die6() { echo "plot-host: $*" >&2; exit 6; }
|
|
451
487
|
|
|
452
488
|
# WHICH FAILURE, read off the wording — the same shape `bb_issue_exit_code`
|
|
453
489
|
# uses, and for the same reason: the exit code cannot split these cases. `gh`
|
|
@@ -464,18 +500,29 @@ die6() { echo "plot-host: $*" >&2; exit 6; }
|
|
|
464
500
|
# `throttled` for every match of one regex until 2026-09-02, so *"API rate
|
|
465
501
|
# limit exceeded"* and *"You have exceeded a secondary rate limit"* came back
|
|
466
502
|
# the same word and nothing downstream could tell them apart. `secondary` is
|
|
467
|
-
# now its own answer: `
|
|
503
|
+
# now its own answer: `pr_list_failed` exits 6 for it, and the board names which limit was
|
|
468
504
|
# hit rather than printing one reset over both.
|
|
469
505
|
#
|
|
470
506
|
# THE SECONDARY TEST RUNS FIRST, AND THE ORDER IS THE CLASSIFICATION. GitHub's
|
|
471
507
|
# secondary message contains the phrase *"rate limit"* too — *"You have
|
|
472
508
|
# exceeded a secondary rate limit"* — so a quota test applied first claims
|
|
473
509
|
# every secondary refusal and the distinction is lost at the point it is made.
|
|
474
|
-
|
|
510
|
+
#
|
|
511
|
+
# FOUR ANSWERS, NOT THREE — #1087: a GraphQL 504 matched neither limit pattern,
|
|
512
|
+
# so it came back `failed`, and `failed` is the one kind a command fixes —
|
|
513
|
+
# `gh auth login`, wrong advice for a server that took too long to answer.
|
|
514
|
+
# `timeout` is anchored on the HTTP status word (`HTTP 50[234]`), never a bare
|
|
515
|
+
# `\b504\b`, which would match a run id or a line count just as well. It runs
|
|
516
|
+
# AFTER both limit arms: a rate-limit message that also carries a 5xx status
|
|
517
|
+
# must stay `throttled`, because patience until the reset is the right advice
|
|
518
|
+
# for it, and `timeout` would counsel exactly the wrong patience — none.
|
|
519
|
+
host_failure_kind() { # $1=stderr text → secondary|throttled|timeout|failed
|
|
475
520
|
if LC_ALL=C grep -qiE 'secondary rate|exceeded a secondary|abuse detection|abuse-detection|too many requests|\b429\b' <<<"$1"; then
|
|
476
521
|
echo secondary
|
|
477
522
|
elif LC_ALL=C grep -qiE 'rate limit|ratelimit' <<<"$1"; then
|
|
478
523
|
echo throttled
|
|
524
|
+
elif LC_ALL=C grep -qiE "HTTP 50[234]|couldn.t respond to your request in time" <<<"$1"; then
|
|
525
|
+
echo timeout
|
|
479
526
|
else
|
|
480
527
|
echo failed
|
|
481
528
|
fi
|
|
@@ -555,6 +602,16 @@ pr_list_failed() { # $1=stderr text
|
|
|
555
602
|
echo " or run against an account with quota left. No login will help." >&2
|
|
556
603
|
exit 5
|
|
557
604
|
;;
|
|
605
|
+
timeout)
|
|
606
|
+
# NO REPAIR NAMED, FOR THE SAME REASON SECONDARY NAMES NONE — #1087. A
|
|
607
|
+
# 504 means the server took too long, not that the credential is bad, so
|
|
608
|
+
# `host_repair`'s `gh auth login` would send a reader to fix a login
|
|
609
|
+
# that was never broken. The decision is to ask again, not to log in.
|
|
610
|
+
echo "plot-host: pr-list: host timed out — ${err:-the host took too long and said nothing}" >&2
|
|
611
|
+
echo " Nothing is wrong with the login: the server took too long to answer." >&2
|
|
612
|
+
echo " The next refresh asks again." >&2
|
|
613
|
+
exit 3
|
|
614
|
+
;;
|
|
558
615
|
esac
|
|
559
616
|
# THE ONE KIND A COMMAND FIXES. An auth gap and a DNS blip both land here, and
|
|
560
617
|
# the repair for the first is a login this connector can name — see
|
|
@@ -580,7 +637,7 @@ pr_list_failed() { # $1=stderr text
|
|
|
580
637
|
#
|
|
581
638
|
# EVERY CALL SITE MUST WRITE `|| exit $?`, AND IT IS NOT OPTIONAL. This is
|
|
582
639
|
# invoked as `_raw="$(pr_list_call …)"` — a COMMAND SUBSTITUTION, which is a
|
|
583
|
-
# subshell — so the `exit` inside `
|
|
640
|
+
# subshell — so the `exit` inside `pr_list_failed`/`die3` leaves that subshell only. The
|
|
584
641
|
# outer script would carry on with `_raw` empty and `jq` would emit nothing:
|
|
585
642
|
# the silent empty list this whole helper exists to remove, rebuilt one layer
|
|
586
643
|
# further in and harder to see. The same trap `bb_states_for` documents a few
|
|
@@ -2281,6 +2338,47 @@ tracker_scheme() {
|
|
|
2281
2338
|
tracker_raw | awk '{print tolower($1)}'
|
|
2282
2339
|
}
|
|
2283
2340
|
|
|
2341
|
+
# WHO ANSWERS THE OPEN-ISSUE LIST — the domain's answer, not this script's.
|
|
2342
|
+
#
|
|
2343
|
+
# `issue-list` and `issue-view` tested `tracker_scheme = jira` and sent every
|
|
2344
|
+
# other scheme to the git host. That test was a DRIFTED SECOND COPY of the rule:
|
|
2345
|
+
# measured 2026-10-01, `PLOT_HOST=github PLOT_TRACKER=linear issue-list` exited 0
|
|
2346
|
+
# having called `gh issue list`, so a repository tracking in Linear was shown
|
|
2347
|
+
# GitHub's issues under Linear's name — a list not wrong about any single row
|
|
2348
|
+
# and wrong about all of them.
|
|
2349
|
+
#
|
|
2350
|
+
# So the rule is ASKED rather than re-implemented. `issueSource` has eight unit
|
|
2351
|
+
# tests and the board already asks it (`fleet.ts:2369-2380`); a lister list in
|
|
2352
|
+
# shell would be the same copy one directory over, free to drift again the first
|
|
2353
|
+
# time a connector is added.
|
|
2354
|
+
#
|
|
2355
|
+
# THE ONE NODE HOP THIS SCRIPT MAKES, and the cost rule permits it: `plot-host.sh`
|
|
2356
|
+
# runs once per operator command or once per board PR refresh, never once per
|
|
2357
|
+
# agent per pass, and a shipped bundle answers in 39 ms
|
|
2358
|
+
# (`docs/shell-and-domain.md`).
|
|
2359
|
+
#
|
|
2360
|
+
# Prints the entry's line verbatim — `tracker\t<scheme>`, `git-host`, or
|
|
2361
|
+
# `nobody\t<reason>` — and exits 0. Exits 1, naming the entry, when the rule
|
|
2362
|
+
# CANNOT BE ASKED: no node, no bundle, a non-zero exit, or a word outside the
|
|
2363
|
+
# three. THAT CASE NEVER FALLS THROUGH TO THE GIT HOST — the fall-through is
|
|
2364
|
+
# the defect, and `an-outage-is-not-an-answer` is what a list saying *none*
|
|
2365
|
+
# because it could not ask reproduces.
|
|
2366
|
+
issue_source() {
|
|
2367
|
+
local out rc
|
|
2368
|
+
out="$(tracker_raw | node "$here/board/plot-issue-source.mjs" "$be" 2>/dev/null)"; rc=$?
|
|
2369
|
+
if [ "$rc" -ne 0 ]; then
|
|
2370
|
+
echo "plot-host: cannot ask board/plot-issue-source.mjs who lists this repository's issues (exit $rc); the question failed and the git host is not asked in its place" >&2
|
|
2371
|
+
return 1
|
|
2372
|
+
fi
|
|
2373
|
+
case "${out%%$'\t'*}" in
|
|
2374
|
+
tracker|git-host|nobody) printf '%s\n' "$out" ;;
|
|
2375
|
+
*)
|
|
2376
|
+
echo "plot-host: board/plot-issue-source.mjs answered '${out}', which is not one of tracker/git-host/nobody; the question failed and the git host is not asked in its place" >&2
|
|
2377
|
+
return 1
|
|
2378
|
+
;;
|
|
2379
|
+
esac
|
|
2380
|
+
}
|
|
2381
|
+
|
|
2284
2382
|
tracker_base_url() {
|
|
2285
2383
|
# The base URL is the SECOND token; a bare `jira` with no URL yields "".
|
|
2286
2384
|
# PLOT_JIRA_BASE_URL overrides, for a caller that has the URL separately.
|
|
@@ -2610,6 +2708,29 @@ bb_list_page_length() {
|
|
|
2610
2708
|
esac
|
|
2611
2709
|
}
|
|
2612
2710
|
|
|
2711
|
+
# A PAGE THAT IS PROVABLY WHOLE SAYS SO, and that sentence is the licence a
|
|
2712
|
+
# joining caller needs. This reported only truncation until 2026-10-02: a
|
|
2713
|
+
# complete page was SILENT, so a caller could not tell "this page holds every
|
|
2714
|
+
# PR" from "this adapter has no opinion", and the only reading left to it was
|
|
2715
|
+
# the page's own row count against its own limit. That is a coincidence rather
|
|
2716
|
+
# than evidence — measured on this repository, `--state all --limit 1000`
|
|
2717
|
+
# returned exactly 1000 rows of 1064 PRs, so the count-based test failed, the
|
|
2718
|
+
# scan's `.list-complete` was withheld, and every branch the join did not name
|
|
2719
|
+
# fell through to one `pr-state` call each: 26 calls at 3.8 s, 54-61% of the
|
|
2720
|
+
# scan's wall time in slice 1's five runs (#1017).
|
|
2721
|
+
#
|
|
2722
|
+
# THE CLAIM IS THE ADAPTER'S BECAUSE THE PAGING SEMANTICS ARE. Whether a short
|
|
2723
|
+
# page proves anything depends on how the host pages, which is exactly what
|
|
2724
|
+
# this script knows and a caller does not. `pr_sweep_report` already states its
|
|
2725
|
+
# own stronger claim in this shape and `plot-fleet-scan.sh` already reads that
|
|
2726
|
+
# sentence; this is the same contract for the second path, so a listing is no
|
|
2727
|
+
# longer the one answer a caller has to guess at.
|
|
2728
|
+
#
|
|
2729
|
+
# BOTH SENTENCES OR NEITHER. The complete and truncated reports are the two
|
|
2730
|
+
# outcomes of one decision, so they are emitted from one place: a reader can
|
|
2731
|
+
# never see both for one state, and silence now means only that no claim was
|
|
2732
|
+
# owed — no `--limit`, or an empty page.
|
|
2733
|
+
#
|
|
2613
2734
|
# $1 backend $2 requested limit (may be "") $3 state word $4 row count
|
|
2614
2735
|
pr_list_report_truncation() {
|
|
2615
2736
|
local be="$1" limit="$2" state="$3" count="$4" len=""
|
|
@@ -2617,17 +2738,33 @@ pr_list_report_truncation() {
|
|
|
2617
2738
|
[ "$count" -gt 0 ] 2>/dev/null || return 0 # an empty page had nothing to hide
|
|
2618
2739
|
if [ "$be" = "github" ]; then
|
|
2619
2740
|
# github honours the limit: complete unless the page came back AT the limit.
|
|
2620
|
-
|
|
2741
|
+
# Measured 2026-10-02 on this repository (1064 PRs): `--limit 1100`
|
|
2742
|
+
# answered 1065 rows and `--limit 2000` the same 1065, so a page short of
|
|
2743
|
+
# its limit is the whole list and asking generously costs the rows that
|
|
2744
|
+
# exist rather than the rows requested.
|
|
2745
|
+
if [ "$count" -lt "$limit" ] 2>/dev/null; then pr_list_report_complete "$be" "$limit" "$state" "$count"; return 0; fi
|
|
2621
2746
|
else
|
|
2622
2747
|
# A fixed page: complete below a measured page length, unprovable otherwise.
|
|
2623
2748
|
[ "$be" = "bitbucket" ] && len="$(bb_list_page_length)"
|
|
2624
|
-
if [ -n "$len" ] && [ "$count" -lt "$len" ] 2>/dev/null; then return 0; fi
|
|
2749
|
+
if [ -n "$len" ] && [ "$count" -lt "$len" ] 2>/dev/null; then pr_list_report_complete "$be" "$limit" "$state" "$count"; return 0; fi
|
|
2625
2750
|
fi
|
|
2626
2751
|
# Named per state so a caller can resolve exactly the states that were
|
|
2627
2752
|
# capped, not a whole-call flag that over-reports.
|
|
2628
2753
|
echo "plot-host: $be pr-list state=$state possibly truncated ($count rows, requested limit $limit unprovable) — a join against this page may read older branches as 'no PR' (#333)" >&2
|
|
2629
2754
|
}
|
|
2630
2755
|
|
|
2756
|
+
# THE WORDING IS A CONTRACT between this script and `plot-fleet-scan.sh`, pinned
|
|
2757
|
+
# on both sides exactly as `pr-list sweep complete` is. A page that cannot prove
|
|
2758
|
+
# itself whole never prints it, so a match is licence and a miss is silence —
|
|
2759
|
+
# never a guess.
|
|
2760
|
+
#
|
|
2761
|
+
# PER STATE, like the truncation report beside it, because the scan asks for two
|
|
2762
|
+
# states and joins both: a caller needs to know which pages were whole, not that
|
|
2763
|
+
# some were.
|
|
2764
|
+
pr_list_report_complete() { # $1 backend $2 limit $3 state word $4 row count
|
|
2765
|
+
echo "plot-host: $1 pr-list state=$3 page complete ($4 rows below requested limit $2) — this page holds every pull request the host has in this state" >&2
|
|
2766
|
+
}
|
|
2767
|
+
|
|
2631
2768
|
# The backends this script has an arm for, which is what "drivable" means here.
|
|
2632
2769
|
#
|
|
2633
2770
|
# THE LIST LIVES IN THE SCRIPT BECAUSE THE SCRIPT IS WHAT WOULD CHANGE. Adding a
|
|
@@ -3657,6 +3794,10 @@ case "$op" in
|
|
|
3657
3794
|
pr-list)
|
|
3658
3795
|
state="open"
|
|
3659
3796
|
rich=0
|
|
3797
|
+
# Whether the rich fields are asked of the OPEN pull requests only. Set by
|
|
3798
|
+
# `--rich-open`, which also sets `rich`: the flag narrows what is ASKED and
|
|
3799
|
+
# never what is emitted.
|
|
3800
|
+
rich_open=0
|
|
3660
3801
|
# PIN THE LIST TO ONE REPOSITORY, the same `--repo` `pr-state` and
|
|
3661
3802
|
# `pr-merged` already take. A checkout may carry several remotes on several
|
|
3662
3803
|
# hosts, and an unpinned `gh pr list` resolves whichever of them it prefers
|
|
@@ -3681,6 +3822,22 @@ case "$op" in
|
|
|
3681
3822
|
--state) state="${2:?}"; shift 2 ;;
|
|
3682
3823
|
--limit) limit="${2:?}"; shift 2 ;;
|
|
3683
3824
|
--rich) rich=1; shift ;;
|
|
3825
|
+
# THE RICH FIELDS OF THE OPEN PRs AND THE PLAIN FIELDS OF THE REST.
|
|
3826
|
+
# Measured 2026-10-01 on this repository: `--rich --state all --limit
|
|
3827
|
+
# 1000` took 43.0 s and the same call without the rich fields took 7.3 s,
|
|
3828
|
+
# while all 1000 rows were terminal — so 36 s bought four verdicts about
|
|
3829
|
+
# heads nobody can act on. Re-measured 2026-10-02 with 3 PRs open:
|
|
3830
|
+
# `--rich --state open` is 2.6-3.3 s and `--state all` is 8.6-10.5 s.
|
|
3831
|
+
#
|
|
3832
|
+
# IT IMPLIES `--rich`, so every row carries every rich field and no
|
|
3833
|
+
# consumer has to ask which call produced it. A terminal row's verdicts
|
|
3834
|
+
# arrive at the ABSENT values the Bitbucket arm already emits, which the
|
|
3835
|
+
# store's schema accepts — so no `PR_INDEX_VERSION` bump follows.
|
|
3836
|
+
#
|
|
3837
|
+
# THE SPLIT IS ONLY WORTH MAKING OVER A SET THAT HOLDS TERMINAL ROWS.
|
|
3838
|
+
# With `--state open` asked, both calls would list the same PRs and the
|
|
3839
|
+
# second would be waste, so this collapses to plain `--rich` there.
|
|
3840
|
+
--rich-open) rich=1; rich_open=1; shift ;;
|
|
3684
3841
|
--repo) repo_args=(-R "${2:?}"); shift 2 ;;
|
|
3685
3842
|
# `${2:?}` REFUSES AN EMPTY VALUE, and that is the point rather than
|
|
3686
3843
|
# boilerplate. `--since ""` would reach GitHub as `--search "updated:>"`,
|
|
@@ -3759,6 +3916,57 @@ case "$op" in
|
|
|
3759
3916
|
fi
|
|
3760
3917
|
|
|
3761
3918
|
if [ "$be" = "github" ]; then
|
|
3919
|
+
# THE SPLIT READ, AND IT IS TWO CALLS BECAUSE THE COST IS PER ROW.
|
|
3920
|
+
# `gh pr list` pages internally and takes no page-size flag, so 1000 rich
|
|
3921
|
+
# rows cost 1000 rows' worth of `statusCheckRollup` whatever the page.
|
|
3922
|
+
# Narrowing the SET is the only lever: the rich fields are asked of the
|
|
3923
|
+
# open pull requests, and everything else comes back plain.
|
|
3924
|
+
#
|
|
3925
|
+
# ORDER MATTERS — THE RICH ROWS GO FIRST. A PR that is open appears in
|
|
3926
|
+
# both payloads, and a consumer keyed by number takes the last line it
|
|
3927
|
+
# read; the second call is filtered to the terminal states instead, so no
|
|
3928
|
+
# number is emitted twice and no rich row is overwritten by a plain one.
|
|
3929
|
+
# Filtered rather than ordered, because a caller that sorts or de-dupes
|
|
3930
|
+
# differently must not be able to reach a different answer.
|
|
3931
|
+
#
|
|
3932
|
+
# ASKED ONLY WHERE THE SET CAN HOLD A TERMINAL ROW. With `--state open`
|
|
3933
|
+
# the two calls would list the same PRs, so this collapses to `--rich`.
|
|
3934
|
+
if [ "$rich_open" = 1 ] && [ "$state" != "open" ]; then
|
|
3935
|
+
# THE OPEN HALF, RICH. Asked with the caller's own `--limit` and
|
|
3936
|
+
# `--since`: a window the caller set applies to both halves, or the two
|
|
3937
|
+
# would answer about different populations.
|
|
3938
|
+
#
|
|
3939
|
+
# `|| exit $?` ON BOTH, and the rule `pr_list_call`'s header states is
|
|
3940
|
+
# what makes it load-bearing here too: this runs in a command
|
|
3941
|
+
# substitution, so a `die` inside leaves only that subshell and the
|
|
3942
|
+
# outer script would carry on with an empty payload. A failing open call
|
|
3943
|
+
# and a failing all call must each exit non-zero and print no rows.
|
|
3944
|
+
_rich_open_out="$("$0" pr-list --rich --state open \
|
|
3945
|
+
${limit:+--limit "$limit"} ${since:+--since "$since"} \
|
|
3946
|
+
${repo_args[1]+--repo "${repo_args[1]}"} \
|
|
3947
|
+
$(for _b in $branches; do printf -- '--branch %s ' "$_b"; done))" || exit $?
|
|
3948
|
+
# THE REST, PLAIN, AND THE TERMINAL ROWS ARE THE ONES KEPT. `--state
|
|
3949
|
+
# all` is asked rather than merged+closed separately: GitHub answers it
|
|
3950
|
+
# in one call, and two calls would double a cost this exists to halve.
|
|
3951
|
+
_plain_all_out="$("$0" pr-list --state "$state" \
|
|
3952
|
+
${limit:+--limit "$limit"} ${since:+--since "$since"} \
|
|
3953
|
+
${repo_args[1]+--repo "${repo_args[1]}"} \
|
|
3954
|
+
$(for _b in $branches; do printf -- '--branch %s ' "$_b"; done))" || exit $?
|
|
3955
|
+
[ -n "$_rich_open_out" ] && printf '%s\n' "$_rich_open_out"
|
|
3956
|
+
# THE ABSENT VALUES ARE THE BITBUCKET ARM'S, not invented here: a
|
|
3957
|
+
# terminal row carries every field a rich row carries, so one shape
|
|
3958
|
+
# reaches every consumer. `checks:"unknown"` means NOT ASKED and never
|
|
3959
|
+
# *no checks* — the board's fold takes the verdict it already holds for
|
|
3960
|
+
# the same number, and a row with none reads as unavailable.
|
|
3961
|
+
#
|
|
3962
|
+
# `draft`, `url` and `updatedAt` are REAL here — the plain GitHub arm
|
|
3963
|
+
# asks for them — so only the four verdicts are absent.
|
|
3964
|
+
[ -n "$_plain_all_out" ] && printf '%s\n' "$_plain_all_out" \
|
|
3965
|
+
| jq -c 'select(.state=="MERGED" or .state=="CLOSED")
|
|
3966
|
+
| . + {checks:"unknown", mergeable:"unknown", review:"",
|
|
3967
|
+
failing_checks:[]}'
|
|
3968
|
+
exit 0
|
|
3969
|
+
fi
|
|
3762
3970
|
if [ "$rich" = 1 ]; then
|
|
3763
3971
|
# `checks` has FOUR states, and two of them mean "a person is the
|
|
3764
3972
|
# blocker" rather than "a machine is busy":
|
|
@@ -3898,12 +4106,25 @@ case "$op" in
|
|
|
3898
4106
|
}'
|
|
3899
4107
|
fi
|
|
3900
4108
|
else
|
|
4109
|
+
# `isDraft`, `url` AND `updatedAt` TRAVEL ON THE PLAIN ARM TOO, and they
|
|
4110
|
+
# are cheap: measured 2026-09-21, `number,updatedAt` over 937 PRs is
|
|
4111
|
+
# 4715 ms against 5417 ms for the base fields, where
|
|
4112
|
+
# `statusCheckRollup` alone is 18 842 ms. They are scalar columns on the
|
|
4113
|
+
# PR node; the rollup is a per-row resolution, and that is the whole
|
|
4114
|
+
# 36 s `--rich-open` removes.
|
|
4115
|
+
#
|
|
4116
|
+
# ASKED HERE BECAUSE `--rich-open`'S TERMINAL ROWS COME THROUGH THIS
|
|
4117
|
+
# ARM, and a terminal row must carry every field a rich row carries —
|
|
4118
|
+
# `updatedAt` most of all, since it is the field a durable store
|
|
4119
|
+
# advances its watermark by. Without it a split full read would leave
|
|
4120
|
+
# the store unable to narrow and the daily full read would never fall
|
|
4121
|
+
# due against anything but a cold window.
|
|
3901
4122
|
_gh_raw="$(pr_list_call gh ${repo_args[@]+"${repo_args[@]}"} pr list --state "$state" ${limit_args[@]+"${limit_args[@]}"} ${search_args[@]+"${search_args[@]}"} \
|
|
3902
|
-
--json number,title,state,headRefName,author)" || exit $?
|
|
4123
|
+
--json number,title,state,headRefName,isDraft,url,updatedAt,author)" || exit $?
|
|
3903
4124
|
pr_list_report_truncation github "$limit" "$state" \
|
|
3904
4125
|
"$(jq 'length' <<<"$_gh_raw" 2>/dev/null || echo 0)"
|
|
3905
4126
|
printf '%s' "$_gh_raw" \
|
|
3906
|
-
| jq -c '.[] | {number:.number,title:.title,state:.state,head:.headRefName,author:(.author.login // "")}'
|
|
4127
|
+
| jq -c '.[] | {number:.number,title:.title,state:.state,head:.headRefName,draft:.isDraft,url:.url,updatedAt:(.updatedAt // ""),author:(.author.login // "")}'
|
|
3907
4128
|
fi
|
|
3908
4129
|
else
|
|
3909
4130
|
# `author` IS THE `nickname`, the one handle field a Bitbucket user
|
|
@@ -4114,7 +4335,7 @@ case "$op" in
|
|
|
4114
4335
|
;;
|
|
4115
4336
|
|
|
4116
4337
|
run-for-sha)
|
|
4117
|
-
# The
|
|
4338
|
+
# The run for ONE sha, or nothing — the BuildMonitor's only host question.
|
|
4118
4339
|
#
|
|
4119
4340
|
# WHY THIS IS NOT `runs`. `runs` is branch-scoped and reports no sha at all,
|
|
4120
4341
|
# so a caller cannot tell which commit an answer is about. `gh run list
|
|
@@ -4141,9 +4362,9 @@ case "$op" in
|
|
|
4141
4362
|
# deliberately NOT an error: a monitor polling a fresh push sees it on every
|
|
4142
4363
|
# pass until CI wakes up.
|
|
4143
4364
|
#
|
|
4144
|
-
# Bitbucket
|
|
4145
|
-
# `
|
|
4146
|
-
#
|
|
4365
|
+
# A Bitbucket remote exits 4 (`bb` has no run listing), as does a failing
|
|
4366
|
+
# `gh` or Jenkins that cannot be asked: exit 4 reads as unavailable, never
|
|
4367
|
+
# as "this sha has no build".
|
|
4147
4368
|
branch="${1:?run-for-sha needs a branch}"; shift
|
|
4148
4369
|
sha="${1:?run-for-sha needs a sha}"; shift
|
|
4149
4370
|
# Enough runs to find the sha among its neighbours. A branch accumulates
|
|
@@ -4232,10 +4453,9 @@ case "$op" in
|
|
|
4232
4453
|
echo "plot-host: run-for-sha — Jenkins did not answer for '$_jen_host'" >&2
|
|
4233
4454
|
exit 4
|
|
4234
4455
|
fi
|
|
4235
|
-
# THE SAME
|
|
4236
|
-
#
|
|
4237
|
-
#
|
|
4238
|
-
# two apart would be back to the branch-scoped guessing this op ends.
|
|
4456
|
+
# THE SAME MATCH RULE AS THE GITHUB ARM: only the asked-for sha, never
|
|
4457
|
+
# another build's. A build for any other commit is not evidence about
|
|
4458
|
+
# this one, so no match means no output.
|
|
4239
4459
|
#
|
|
4240
4460
|
# `result` is null while a build runs, which is Jenkins' own word for
|
|
4241
4461
|
# *in flight* — mapped to the `status`/`conclusion` split the contract
|
|
@@ -4247,7 +4467,7 @@ case "$op" in
|
|
|
4247
4467
|
conclusion: (if .building then null else (.result // null) end),
|
|
4248
4468
|
url: (.url // ""),
|
|
4249
4469
|
startedAt: (if .timestamp then (.timestamp / 1000 | todate) else "" end) } ]
|
|
4250
|
-
| (
|
|
4470
|
+
| (map(select(.sha == $sha)) | .[0])
|
|
4251
4471
|
| select(. != null)' 2>/dev/null || true
|
|
4252
4472
|
# THIS ARM ANSWERS AND THE OP IS OVER. Everything below the `esac` is
|
|
4253
4473
|
# the GitHub path — the old jenkins arm reached it only because it
|
|
@@ -4277,30 +4497,24 @@ case "$op" in
|
|
|
4277
4497
|
# newest-first, and a sha can carry several (a rerun, or several
|
|
4278
4498
|
# workflows). The newest is the live answer; older ones for the same sha
|
|
4279
4499
|
# are superseded by the same argument that superseded runs for older shas.
|
|
4280
|
-
# THE SHA ASKED ABOUT IF THERE IS ONE, ELSE THE NEWEST RUN ON THE BRANCH —
|
|
4281
|
-
# and `sha` in the output says WHICH, because a caller that could not tell
|
|
4282
|
-
# the two apart would be back to the branch-scoped guessing this op exists
|
|
4283
|
-
# to end.
|
|
4284
4500
|
#
|
|
4285
|
-
#
|
|
4286
|
-
#
|
|
4287
|
-
#
|
|
4288
|
-
#
|
|
4289
|
-
#
|
|
4290
|
-
# the state that had two merge waiters reporting on superseded runs on
|
|
4291
|
-
# 2026-08-30, and it is exactly what a caller needs to see.
|
|
4501
|
+
# ONLY THE ASKED-FOR SHA, NEVER ANOTHER ONE'S RUN. A run for any other
|
|
4502
|
+
# commit is not evidence about this one — reporting it would read as a
|
|
4503
|
+
# live answer for a commit the branch has already moved past, which is
|
|
4504
|
+
# worse than no answer. No match means no output, read the same as a
|
|
4505
|
+
# branch with no runs at all.
|
|
4292
4506
|
#
|
|
4293
|
-
#
|
|
4294
|
-
#
|
|
4295
|
-
#
|
|
4296
|
-
gh run list --branch "$branch" --limit "$limit" \
|
|
4297
|
-
--json headSha,conclusion,status,startedAt,url 2>/dev/null \
|
|
4298
|
-
|
|
4299
|
-
|
|
4300
|
-
|
|
4301
|
-
|
|
4302
|
-
|
|
4303
|
-
|
|
4507
|
+
# A FAILING `gh` IS NOT AN EMPTY HISTORY. An expired token, a rate limit or
|
|
4508
|
+
# a network failure exits 4 here, the way the Jenkins arm does, so the
|
|
4509
|
+
# monitor reads *could not ask* and not *no run yet*.
|
|
4510
|
+
_gh_runs=$(gh run list --branch "$branch" --limit "$limit" \
|
|
4511
|
+
--json headSha,conclusion,status,startedAt,url 2>/dev/null) \
|
|
4512
|
+
|| { echo "plot-host: run-for-sha — gh run list failed for '$branch'" >&2; exit 4; }
|
|
4513
|
+
printf '%s' "$_gh_runs" | jq -c --arg sha "$sha" \
|
|
4514
|
+
'(map(select(.headSha == $sha)) | .[0]) | select(. != null)
|
|
4515
|
+
| {sha:.headSha, status:.status,
|
|
4516
|
+
conclusion:(if (.conclusion // "") == "" then null else .conclusion end),
|
|
4517
|
+
url:.url, startedAt:.startedAt}' || exit 4
|
|
4304
4518
|
;;
|
|
4305
4519
|
|
|
4306
4520
|
issue-list)
|
|
@@ -4324,7 +4538,19 @@ case "$op" in
|
|
|
4324
4538
|
done
|
|
4325
4539
|
limit_args=()
|
|
4326
4540
|
[ -n "$limit" ] && limit_args=(--limit "$limit")
|
|
4327
|
-
|
|
4541
|
+
# WHO LISTS THESE ISSUES IS ASKED ONCE, HERE, BEFORE THE ARMS — after the
|
|
4542
|
+
# argument parsing, so a caller's typo is still reported as a usage error
|
|
4543
|
+
# rather than as a tracker refusal. The arms below then dispatch on the
|
|
4544
|
+
# ANSWER, never on a second reading of the config.
|
|
4545
|
+
source_said="$(issue_source)" || exit 1
|
|
4546
|
+
if [ "${source_said%%$'\t'*}" = "nobody" ]; then
|
|
4547
|
+
# Exit 4, the code this op already documents as *this host cannot be
|
|
4548
|
+
# asked at all* — a configuration a person fixes, not a call to retry.
|
|
4549
|
+
# NOTHING HAS BEEN SPENT: no `gh`, no `bb`, no curl has run yet.
|
|
4550
|
+
echo "plot-host: ${source_said#*$'\t'}" >&2
|
|
4551
|
+
exit 4
|
|
4552
|
+
fi
|
|
4553
|
+
if [ "$source_said" = $'tracker\tjira' ]; then
|
|
4328
4554
|
# Jira, resolved through the REST API — DISPATCHED ON `Tracker`, never on
|
|
4329
4555
|
# `backend()`: a Bitbucket repo tracking in Jira is the normal enterprise
|
|
4330
4556
|
# case, so the git host is irrelevant here (see the Jira helpers up top).
|
|
@@ -4502,7 +4728,15 @@ case "$op" in
|
|
|
4502
4728
|
# 4 to `unsupported` and anything else to `failed` must not need a second
|
|
4503
4729
|
# table to read this op.
|
|
4504
4730
|
num="${1:?issue-view needs an issue number}"; shift
|
|
4505
|
-
|
|
4731
|
+
# Asked once, before the arms, exactly as issue-list does — the same
|
|
4732
|
+
# question with the same three answers and the same exit codes. A consumer
|
|
4733
|
+
# that maps 4 to `unsupported` needs no second table to read this op.
|
|
4734
|
+
source_said="$(issue_source)" || exit 1
|
|
4735
|
+
if [ "${source_said%%$'\t'*}" = "nobody" ]; then
|
|
4736
|
+
echo "plot-host: ${source_said#*$'\t'}" >&2
|
|
4737
|
+
exit 4
|
|
4738
|
+
fi
|
|
4739
|
+
if [ "$source_said" = $'tracker\tjira' ]; then
|
|
4506
4740
|
# Jira, dispatched on `Tracker` not `backend()` — the same rule issue-list
|
|
4507
4741
|
# follows. `num` is a Jira KEY (PROJ-123), read off issue-list moments ago.
|
|
4508
4742
|
#
|