@plot-pm/board 0.14.4 → 0.16.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 +166 -158
- package/package.json +3 -1
- package/plot-agent-manifest.sh +137 -0
- package/plot-approve.sh +25 -1
- package/plot-budget.sh +378 -19
- package/plot-config.sh +86 -2
- package/plot-deliver.sh +26 -3
- package/plot-dispatch.sh +307 -43
- package/plot-fleet-scan.sh +438 -229
- package/plot-host.sh +322 -84
- package/plot-plan-meta.sh +68 -13
- package/plot-pr-merged.sh +29 -2
- package/plot-reap.sh +212 -23
- package/plot-resolve-artifact.sh +4 -2
- package/plot-tmp.sh +108 -0
- package/plot-transcript-quiet.sh +29 -1
- package/plot-worker-monitor.sh +66 -5
package/plot-host.sh
CHANGED
|
@@ -19,7 +19,18 @@
|
|
|
19
19
|
# than a transient to retry. Adding a host is
|
|
20
20
|
# an edit to `HOST_DRIVES` and the arms below,
|
|
21
21
|
# and to nothing in `packages/domain`.
|
|
22
|
-
# default-branch print the repo's default branch name
|
|
22
|
+
# default-branch print the repo's default branch name. THE HOST
|
|
23
|
+
# IS ASKED, on both backends, and the local
|
|
24
|
+
# `origin/HEAD` is the fallback where it cannot
|
|
25
|
+
# answer — that ref is a clone-time cache and a
|
|
26
|
+
# default branch moved since then never updates
|
|
27
|
+
# it. Prints "" where neither can answer.
|
|
28
|
+
# account print the signed-in host user, read from the
|
|
29
|
+
# CLI's config file (no API request).
|
|
30
|
+
# `PLOT_BUDGET_ACCOUNT` overrides it. Exit 3
|
|
31
|
+
# where GitHub's `hosts.yml` names no user;
|
|
32
|
+
# exit 4 on Bitbucket, which stores no username
|
|
33
|
+
# for free. Never prints `unknown`.
|
|
23
34
|
# pr-state <number|branch> [--repo <owner/repo>] one JSON object:
|
|
24
35
|
# {"number":N,"state":"OPEN|MERGED|CLOSED|NONE",
|
|
25
36
|
# "draft":true|false,"url":"..."}
|
|
@@ -380,6 +391,15 @@ set -uo pipefail
|
|
|
380
391
|
|
|
381
392
|
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
382
393
|
|
|
394
|
+
# Every temp path this script creates, and its only EXIT/INT/TERM traps.
|
|
395
|
+
. "$here/plot-tmp.sh"
|
|
396
|
+
|
|
397
|
+
# The host CLI's stderr for one call, read back after the call. One file per
|
|
398
|
+
# process, created here in the parent because the calls redirect into it from
|
|
399
|
+
# inside command substitutions, where an assignment would not reach the caller.
|
|
400
|
+
HOST_ERR=""
|
|
401
|
+
plot_tmpfile HOST_ERR host-err || { echo "plot-host: no temp file could be made under ${TMPDIR:-/tmp}" >&2; exit 3; }
|
|
402
|
+
|
|
383
403
|
die() { echo "plot-host: $*" >&2; exit 1; }
|
|
384
404
|
|
|
385
405
|
# Exit 3 — reserved for "the op itself cannot proceed", distinct from `die`'s
|
|
@@ -564,7 +584,9 @@ pr_list_failed() { # $1=stderr text
|
|
|
564
584
|
# hundred lines below, where a `die` in a subshell turned an unknown state into
|
|
565
585
|
# "no PRs matched".
|
|
566
586
|
pr_list_call() { # "$@"=the host command → payload on stdout, or dies
|
|
567
|
-
local out rc err tmp="
|
|
587
|
+
local out rc err tmp=""
|
|
588
|
+
plot_tmpfile tmp host-prlist-err \
|
|
589
|
+
|| { echo "plot-host: no temp file could be made under ${TMPDIR:-/tmp}" >&2; exit 3; }
|
|
568
590
|
out="$("$@" 2>"$tmp")"; rc=$?
|
|
569
591
|
err="$(cat "$tmp" 2>/dev/null)"; rm -f "$tmp"
|
|
570
592
|
[ "$rc" -eq 0 ] || pr_list_failed "$err"
|
|
@@ -847,7 +869,7 @@ bb_branch_sweep() { # global bb args… --state <s> --json → one JSON array
|
|
|
847
869
|
# already seen once per branch, so eleven branches parsed the first branch's
|
|
848
870
|
# payload eleven times.
|
|
849
871
|
local _spool
|
|
850
|
-
_spool
|
|
872
|
+
plot_tmpfile _spool host-sweep || return 3
|
|
851
873
|
for _br in $PR_LIST_BRANCHES; do
|
|
852
874
|
# RETURN, NOT EXIT. This runs inside the command substitution
|
|
853
875
|
# `pr_list_call` wraps the sweep in, so the code must travel back as this
|
|
@@ -856,7 +878,7 @@ bb_branch_sweep() { # global bb args… --state <s> --json → one JSON array
|
|
|
856
878
|
# as the sweep's own — the silent empty list `pr_list_call`'s header names.
|
|
857
879
|
#
|
|
858
880
|
# The spool is removed on EVERY exit path, including the failing one: a
|
|
859
|
-
# sweep that gives up mid-way must not leave a payload behind in
|
|
881
|
+
# sweep that gives up mid-way must not leave a payload behind in TMPDIR.
|
|
860
882
|
bb_branch_query "$_br" "$_st" ${_args[@]+"${_args[@]}"} >> "$_spool" \
|
|
861
883
|
|| { _rc=$?; rm -f "$_spool"; return $_rc; }
|
|
862
884
|
done
|
|
@@ -869,6 +891,135 @@ bb_branch_sweep() { # global bb args… --state <s> --json → one JSON array
|
|
|
869
891
|
printf '%s' "$_acc"
|
|
870
892
|
}
|
|
871
893
|
|
|
894
|
+
# List every pull request in ONE state updated inside the window, as one array.
|
|
895
|
+
#
|
|
896
|
+
# THE WINDOWED LISTING, and the third command in `pr_list_states`' slot beside
|
|
897
|
+
# `bb pr list` and `bb_branch_sweep`. It takes the same trailing
|
|
898
|
+
# `--state <s> --json` and prints the same JSON array, so the states loop, the
|
|
899
|
+
# partial-answer rule and the one error classification stay in that helper.
|
|
900
|
+
#
|
|
901
|
+
# THE QUERY IS `bb_branch_query`'s WITHOUT THE BRANCH CLAUSE:
|
|
902
|
+
#
|
|
903
|
+
# /repositories/{ws}/{repo}/pullrequests
|
|
904
|
+
# ?q=state="MERGED" AND updated_on>="<since>"&pagelen=50
|
|
905
|
+
#
|
|
906
|
+
# Measured 2026-09-28 on `quatico/quaweb-website`: `size: 9` in one request,
|
|
907
|
+
# against 895 rows for the unwindowed listing of the same state.
|
|
908
|
+
#
|
|
909
|
+
# THE STATE TRAVELS INSIDE `q=`, NEVER BESIDE IT. Bitbucket lets `q=` silently
|
|
910
|
+
# override a `state=` parameter, so `?q=…&state=MERGED` answers for whatever the
|
|
911
|
+
# `q=` says and discards the state. `bb_query_state` maps the adapter's word to
|
|
912
|
+
# Bitbucket's inside the expression.
|
|
913
|
+
#
|
|
914
|
+
# EVERY PAGE IS READ. `bb api` makes ONE request and returns ONE raw page, so a
|
|
915
|
+
# window holding more than 50 matches arrives in several. The payload's `next`
|
|
916
|
+
# names the following page as an absolute URL, and `bb api` prefixes its own
|
|
917
|
+
# base, so the base is removed before the call. The walk ends when `next` is
|
|
918
|
+
# absent.
|
|
919
|
+
#
|
|
920
|
+
# A SHORT WINDOW IS REFUSED, NEVER PRINTED AS WHOLE. `size` is the server's
|
|
921
|
+
# count of matches. When the distinct rows read fall short of it, or a `next`
|
|
922
|
+
# names a URL outside the API base, this prints nothing and exits 3. The board
|
|
923
|
+
# advances its watermark over what a window returns, so a short window printed
|
|
924
|
+
# as whole loses the missing rows until the next full read.
|
|
925
|
+
#
|
|
926
|
+
# THE HOST'S FAILURE TEXT AND EXIT CODE LEAVE UNTOUCHED, as in
|
|
927
|
+
# `bb_branch_query`: `pr_list_call` classifies once, so a `429` still reads as
|
|
928
|
+
# a rate limit.
|
|
929
|
+
BB_API_BASE="https://api.bitbucket.org/2.0"
|
|
930
|
+
bb_window_listing() { # global bb args… --state <s> --json → one JSON array
|
|
931
|
+
local _st="" _args=() _q _path _page _size=0 _next _pages=0 _rows _spool _rc
|
|
932
|
+
while [ $# -gt 0 ]; do
|
|
933
|
+
case "$1" in
|
|
934
|
+
--state) _st="${2:?}"; shift 2 ;;
|
|
935
|
+
--json) shift ;;
|
|
936
|
+
*) _args+=("$1"); shift ;;
|
|
937
|
+
esac
|
|
938
|
+
done
|
|
939
|
+
[ -n "$_st" ] || die "bb_window_listing: no --state"
|
|
940
|
+
[ -n "$PR_LIST_SINCE" ] || die "bb_window_listing: no window"
|
|
941
|
+
_q="state=$(url_encode "\"$(bb_query_state "$_st")\"") AND updated_on>=$(url_encode "\"$PR_LIST_SINCE\"")"
|
|
942
|
+
_q="${_q// /%20}"
|
|
943
|
+
_path="/repositories/{ws}/{repo}/pullrequests?q=${_q}&pagelen=50"
|
|
944
|
+
plot_tmpfile _spool host-window || return 3
|
|
945
|
+
while [ -n "$_path" ]; do
|
|
946
|
+
_page="$(bb ${_args[@]+"${_args[@]}"} api "$_path")" \
|
|
947
|
+
|| { _rc=$?; rm -f "$_spool"; return $_rc; }
|
|
948
|
+
_pages=$((_pages + 1))
|
|
949
|
+
printf '%s\n' "$_page" | jq -c '.values // []' >> "$_spool" \
|
|
950
|
+
|| { rm -f "$_spool"; return 3; }
|
|
951
|
+
_size="$(printf '%s' "$_page" | jq -r '.size // 0')" || { rm -f "$_spool"; return 3; }
|
|
952
|
+
_next="$(printf '%s' "$_page" | jq -r '.next // empty')" || { rm -f "$_spool"; return 3; }
|
|
953
|
+
_path=""
|
|
954
|
+
if [ -n "$_next" ]; then
|
|
955
|
+
case "$_next" in
|
|
956
|
+
"$BB_API_BASE"/*) _path="${_next#"$BB_API_BASE"}" ;;
|
|
957
|
+
*)
|
|
958
|
+
rm -f "$_spool"
|
|
959
|
+
echo "bitbucket window state=$_st: next page '$_next' is outside $BB_API_BASE; refusing a window it cannot read to the end" >&2
|
|
960
|
+
return 3
|
|
961
|
+
;;
|
|
962
|
+
esac
|
|
963
|
+
fi
|
|
964
|
+
done
|
|
965
|
+
# Distinct by id: a pull request updated while the walk runs can move between
|
|
966
|
+
# pages and arrive twice.
|
|
967
|
+
_rows="$(jq -c -s 'add // [] | unique_by(.id)' < "$_spool")" || { rm -f "$_spool"; return 3; }
|
|
968
|
+
rm -f "$_spool"
|
|
969
|
+
local _got
|
|
970
|
+
_got="$(jq 'length' <<<"$_rows")" || return 3
|
|
971
|
+
if [ "$_got" -lt "$_size" ] 2>/dev/null; then
|
|
972
|
+
echo "bitbucket window state=$_st is short ($_got of $_size matches after $_pages pages); refusing it rather than printing it as whole" >&2
|
|
973
|
+
return 3
|
|
974
|
+
fi
|
|
975
|
+
printf '%s' "$_rows"
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
# List the pull requests in ONE state, one page of 50, as one array.
|
|
979
|
+
#
|
|
980
|
+
# THE PLAIN LISTING, and the fourth command in `pr_list_states`' slot. It takes
|
|
981
|
+
# the same trailing `--state <s> --json` as `bb_branch_sweep` and
|
|
982
|
+
# `bb_window_listing` and prints the same JSON array, so the states loop, the
|
|
983
|
+
# truncation report and the partial-answer rule stay in that helper.
|
|
984
|
+
#
|
|
985
|
+
# /repositories/{ws}/{repo}/pullrequests?state=MERGED&pagelen=50
|
|
986
|
+
#
|
|
987
|
+
# ONE REQUEST PER STATE, WHERE `bb pr list` SPENDS UP TO FIVE. `bb pr list`
|
|
988
|
+
# sends no `pagelen`, Bitbucket answers 10 rows a page, and `bb_paginate` walks
|
|
989
|
+
# pages until it holds 50. Asking `pagelen=50` returns the same 50 rows in one
|
|
990
|
+
# request. Measured 2026-09-30 on `quatico/quaweb-website` with bb 1.9.0: the
|
|
991
|
+
# three states cost 8 requests through `bb pr list` (open 1, merged 5,
|
|
992
|
+
# declined 2) and 3 through this path, and both return the same 67 rows
|
|
993
|
+
# (3 open, 50 merged, 14 declined).
|
|
994
|
+
#
|
|
995
|
+
# THE STATES STAY SEPARATE CALLS. One `state=` union over the three returns the
|
|
996
|
+
# 50 most recently updated rows across all of them: measured on the same
|
|
997
|
+
# repository, 50 of 67 rows, 12 of 14 declined PRs lost, and an open PR survives
|
|
998
|
+
# only if it was updated recently. That is the 2026-08-18 failure
|
|
999
|
+
# `bb_states_for` records, reached through the page cap instead of through a
|
|
1000
|
+
# flag.
|
|
1001
|
+
#
|
|
1002
|
+
# ONE PAGE, AS BEFORE. 50 is the endpoint's `pagelen` ceiling and the limit
|
|
1003
|
+
# `bb_paginate` applied, so a state holding more than 50 rows answers with its
|
|
1004
|
+
# 50 most recent here too, and `pr_list_report_truncation` still says so.
|
|
1005
|
+
#
|
|
1006
|
+
# THE HOST'S FAILURE TEXT AND EXIT CODE LEAVE UNTOUCHED, as in
|
|
1007
|
+
# `bb_branch_query`: `pr_list_call` classifies once, so a `429` still reads as
|
|
1008
|
+
# a rate limit.
|
|
1009
|
+
bb_state_listing() { # global bb args… --state <s> --json → one JSON array
|
|
1010
|
+
local _st="" _args=() _out
|
|
1011
|
+
while [ $# -gt 0 ]; do
|
|
1012
|
+
case "$1" in
|
|
1013
|
+
--state) _st="${2:?}"; shift 2 ;;
|
|
1014
|
+
--json) shift ;;
|
|
1015
|
+
*) _args+=("$1"); shift ;;
|
|
1016
|
+
esac
|
|
1017
|
+
done
|
|
1018
|
+
[ -n "$_st" ] || die "bb_state_listing: no --state"
|
|
1019
|
+
_out="$(bb ${_args[@]+"${_args[@]}"} api "/repositories/{ws}/{repo}/pullrequests?state=$(bb_query_state "$_st")&pagelen=50")" || return $?
|
|
1020
|
+
printf '%s' "$_out" | jq -c '.values // []'
|
|
1021
|
+
}
|
|
1022
|
+
|
|
872
1023
|
# The branches a sweep asks about, newline-or-space separated. Empty means the
|
|
873
1024
|
# caller named none, and the arm keeps the bulk listing it has always used.
|
|
874
1025
|
#
|
|
@@ -883,13 +1034,13 @@ bb_branch_sweep() { # global bb args… --state <s> --json → one JSON array
|
|
|
883
1034
|
# separator is git's guarantee rather than a hopeful convention.
|
|
884
1035
|
PR_LIST_BRANCHES=""
|
|
885
1036
|
|
|
886
|
-
# The window a
|
|
1037
|
+
# The window a Bitbucket query carries, the host's own stamp. Empty means ask
|
|
887
1038
|
# about everything, which is what every caller predating `--since` asks.
|
|
888
1039
|
#
|
|
889
1040
|
# A GLOBAL FOR `PR_LIST_BRANCHES`' REASON, and it travels the same route: the
|
|
890
1041
|
# `pr-list` arm sets it immediately before the call, `bb_branch_sweep` passes
|
|
891
|
-
# through without reading it, and `bb_branch_query`
|
|
892
|
-
# `q=` Bitbucket takes. Threading it through the sweep as an argument would mean
|
|
1042
|
+
# through without reading it, and `bb_branch_query` and `bb_window_listing`
|
|
1043
|
+
# compose it into the one `q=` Bitbucket takes. Threading it through the sweep as an argument would mean
|
|
893
1044
|
# teaching that function a parameter it only forwards, and its header already
|
|
894
1045
|
# refuses the mirror of that — "teaching `pr_list_states` which of its commands
|
|
895
1046
|
# is a sweep would put a backend's shape inside the one piece of this file that
|
|
@@ -953,7 +1104,7 @@ pr_list_states() { # $1=backend $2=limit $3=states $4=jq-program; rest=the host
|
|
|
953
1104
|
local backend="$1" limit="$2" states="$3" jq_prog="$4"; shift 4
|
|
954
1105
|
local _s _raw _rc _err _tmp _ok=0 _failed=0 _first_rc=0 _failed_states=""
|
|
955
1106
|
for _s in $states; do
|
|
956
|
-
_tmp
|
|
1107
|
+
plot_tmpfile _tmp "host-prlist-state-err-$_s" || return 3
|
|
957
1108
|
# THE SUBSHELL'S CODE IS THE ONLY CHANNEL OUT, so it is captured rather
|
|
958
1109
|
# than propagated. `pr_list_failed` runs INSIDE the substitution and has
|
|
959
1110
|
# already composed its report and its repair line; that text is spooled
|
|
@@ -989,7 +1140,13 @@ pr_list_states() { # $1=backend $2=limit $3=states $4=jq-program; rest=the host
|
|
|
989
1140
|
# THE DETECTOR ITSELF IS UNTOUCHED and still fires exactly as it did on
|
|
990
1141
|
# every listing call — `host.test.mjs:3060` passes unedited. What changed is
|
|
991
1142
|
# that a path exists whose premise it was never written about.
|
|
992
|
-
|
|
1143
|
+
#
|
|
1144
|
+
# A WINDOWED LISTING MAKES NO PAGE CLAIM EITHER. `bb_window_listing` reads
|
|
1145
|
+
# every page and refuses a window whose rows fall short of the server's
|
|
1146
|
+
# `size`, so an answer that reaches this line is whole. Only Bitbucket sets
|
|
1147
|
+
# `PR_LIST_SINCE`; the GitHub arm carries its window as `--search`.
|
|
1148
|
+
[ -n "$PR_LIST_BRANCHES" ] || [ -n "$PR_LIST_SINCE" ] \
|
|
1149
|
+
|| pr_list_report_truncation "$backend" "$limit" "$_s" \
|
|
993
1150
|
"$(jq 'length' <<<"$_raw" 2>/dev/null || echo 0)"
|
|
994
1151
|
printf '%s' "$_raw" | jq -c ${PR_LIST_JQ_ARGS[@]+"${PR_LIST_JQ_ARGS[@]}"} "$jq_prog"
|
|
995
1152
|
done
|
|
@@ -1793,6 +1950,13 @@ rest_pr_to_state() {
|
|
|
1793
1950
|
# `bb` fix, and the three states partition the set (74 PRs, 74 unique ids,
|
|
1794
1951
|
# 0 duplicates on the repo measured).
|
|
1795
1952
|
#
|
|
1953
|
+
# bb 1.9.0 honours repeated `--state` and sends one union request, and the
|
|
1954
|
+
# states still stay separate. The union returns the 50 most recently updated
|
|
1955
|
+
# rows across all three states: measured 2026-09-29 on
|
|
1956
|
+
# `quatico/quaweb-website`, 50 of 67 rows, 12 of 14 declined PRs lost. The cost
|
|
1957
|
+
# is cut per state instead, by `bb_state_listing`'s `pagelen=50`: 8 requests
|
|
1958
|
+
# become 3 there, with all 67 rows kept.
|
|
1959
|
+
#
|
|
1796
1960
|
# `superseded` is deliberately NOT part of `all`: such a PR is replaced by a
|
|
1797
1961
|
# newer one for the same branch, and a board with one row per branch would
|
|
1798
1962
|
# show that branch twice. `gh`'s `all` has no equivalent, so nothing is lost.
|
|
@@ -2547,11 +2711,11 @@ backend() {
|
|
|
2547
2711
|
# `budget_rate`. A directory keyed on a pid must be removed by the process that
|
|
2548
2712
|
# made it, or a long-lived machine accumulates one per `plot-host.sh` call.
|
|
2549
2713
|
#
|
|
2550
|
-
#
|
|
2551
|
-
#
|
|
2552
|
-
#
|
|
2553
|
-
#
|
|
2554
|
-
|
|
2714
|
+
# Registered with `plot-tmp.sh`, which owns this process's EXIT, INT and TERM
|
|
2715
|
+
# traps: a second `trap … EXIT` here would replace the helper's and leak every
|
|
2716
|
+
# temp path above. A memo that outlives a SIGKILL costs a stale reading at
|
|
2717
|
+
# worst, and `plot-reap.sh --sweep-temp` removes a dead pid's directory.
|
|
2718
|
+
plot_on_exit budget_memo_clear
|
|
2555
2719
|
|
|
2556
2720
|
# WHO IS SPENDING — read from the CLI's own config, never from an API call.
|
|
2557
2721
|
#
|
|
@@ -2774,13 +2938,13 @@ plot_harvest_headers() {
|
|
|
2774
2938
|
# read may not charge.
|
|
2775
2939
|
gh_api_harvest() {
|
|
2776
2940
|
local raw rc hdr_tmp err_tmp
|
|
2777
|
-
hdr_tmp
|
|
2941
|
+
plot_tmpfile hdr_tmp host-hdr || {
|
|
2778
2942
|
# No temp file, no harvest — and the call still happens, recorded by the
|
|
2779
2943
|
# wrapper from its argv alone. Bookkeeping never fails its caller.
|
|
2780
2944
|
gh api "$@"
|
|
2781
2945
|
return $?
|
|
2782
2946
|
}
|
|
2783
|
-
err_tmp
|
|
2947
|
+
plot_tmpfile err_tmp host-herr || {
|
|
2784
2948
|
rm -f "$hdr_tmp"
|
|
2785
2949
|
gh api "$@"
|
|
2786
2950
|
return $?
|
|
@@ -2996,14 +3160,77 @@ case "$op" in
|
|
|
2996
3160
|
;;
|
|
2997
3161
|
|
|
2998
3162
|
default-branch)
|
|
3163
|
+
# THE HOST IS ASKED FIRST, ON BOTH BACKENDS, and `origin/HEAD` is the
|
|
3164
|
+
# fallback rather than the answer. `origin/HEAD` is a LOCAL CACHE WRITTEN AT
|
|
3165
|
+
# CLONE TIME: a default branch changed afterwards never updates it and
|
|
3166
|
+
# nothing in git notices. Reported 2026-09-24 — a clone whose GitHub default
|
|
3167
|
+
# had moved to `develop` kept `origin/HEAD → main`, the board read plans from
|
|
3168
|
+
# `origin/main`, and two of three plans went missing.
|
|
3169
|
+
#
|
|
3170
|
+
# THE BITBUCKET ARM ASKED THE CACHE FIRST UNTIL 2026-09-25, so on Bitbucket
|
|
3171
|
+
# this op answered from the very cache it exists to bypass, the two readings
|
|
3172
|
+
# could never disagree, and the defect was invisible there. Every caller —
|
|
3173
|
+
# `plot-open-pr.sh:73`, `plot-approve.sh:205`, `plot-deliver.sh:218`,
|
|
3174
|
+
# `plot-reap.sh:176`, `plot-release-refs.sh:162`, `/plot-idea:252` — wants
|
|
3175
|
+
# the branch the host calls default, which is what this now answers.
|
|
3176
|
+
#
|
|
3177
|
+
# AND THE FALLBACK WAS DEAD CODE. `git symbolic-ref … | sed … || bb …` takes
|
|
3178
|
+
# the PIPELINE's exit status, which is `sed`'s, and `sed` exits 0 on empty
|
|
3179
|
+
# input — measured 2026-09-25 against a ref that does not exist. So `bb repo
|
|
3180
|
+
# view` had never once been reached, and a Bitbucket clone with no
|
|
3181
|
+
# `origin/HEAD` printed an empty line and exited 0. Each reading is now
|
|
3182
|
+
# tested for a NON-EMPTY value of its own, never for a pipeline's status.
|
|
3183
|
+
_db=""
|
|
2999
3184
|
if [ "$be" = "github" ]; then
|
|
3000
|
-
gh repo view --json defaultBranchRef --jq '.defaultBranchRef.name'
|
|
3185
|
+
_db=$(gh repo view --json defaultBranchRef --jq '.defaultBranchRef.name' 2>/dev/null || true)
|
|
3001
3186
|
else
|
|
3002
|
-
#
|
|
3003
|
-
#
|
|
3004
|
-
|
|
3005
|
-
|
|
3187
|
+
# `--json` IS A CAPABILITY, NOT A GIVEN. Two products share the name `bb`
|
|
3188
|
+
# and craftamap's rejects the flag (see `bb_require_json`) — against it
|
|
3189
|
+
# this call emits `unknown flag: --json`, `jq` reads non-JSON, and the arm
|
|
3190
|
+
# would answer nothing while exiting 0. This call was unguarded while it
|
|
3191
|
+
# was unreachable; asking the host makes it live.
|
|
3192
|
+
bb_require_json
|
|
3193
|
+
_db=$(bb repo view --json 2>/dev/null | jq -r '.mainbranch.name // empty' 2>/dev/null || true)
|
|
3194
|
+
fi
|
|
3195
|
+
# `null` is what `jq` prints for an absent field and it is not a branch name.
|
|
3196
|
+
[ "$_db" = "null" ] && _db=""
|
|
3197
|
+
# THE CACHE IS THE FALLBACK: a host that cannot be asked is not a reason to
|
|
3198
|
+
# answer nothing, since every caller above needs a usable branch name and
|
|
3199
|
+
# worked from this reading alone before today.
|
|
3200
|
+
[ -n "$_db" ] || _db=$(git symbolic-ref --short refs/remotes/origin/HEAD 2>/dev/null | sed 's|^origin/||')
|
|
3201
|
+
printf '%s\n' "$_db"
|
|
3202
|
+
;;
|
|
3203
|
+
|
|
3204
|
+
account)
|
|
3205
|
+
# WHO IS READING, from the same free reading `budget_account` makes. It
|
|
3206
|
+
# costs a file read and no request, which matters because the board asks on
|
|
3207
|
+
# its timer. `PLOT_BUDGET_ACCOUNT` overrides it on every backend.
|
|
3208
|
+
#
|
|
3209
|
+
# THE BITBUCKET ARM IS NOT EXPOSED. `budget_account bitbucket` answers the
|
|
3210
|
+
# remote's owner segment, which is the WORKSPACE: on a team workspace every
|
|
3211
|
+
# contributor gets the same word, and it matches no PR author. Measured
|
|
3212
|
+
# 2026-09-25, neither `bb` stores a username for free. Quatico's keeps only
|
|
3213
|
+
# the Atlassian email, in the Keychain. craftamap's keeps an email in
|
|
3214
|
+
# `configuration.toml`. A Bitbucket PR author carries a nickname, an
|
|
3215
|
+
# account id and a uuid, and never an email. So this backend has no answer,
|
|
3216
|
+
# and exit 4 says so.
|
|
3217
|
+
#
|
|
3218
|
+
# `unknown` IS NEVER PRINTED. It groups budget lines correctly and names
|
|
3219
|
+
# nobody, so an unreadable `hosts.yml` exits 3 with the reason on stderr.
|
|
3220
|
+
_acct=''
|
|
3221
|
+
if [ -n "${PLOT_BUDGET_ACCOUNT:-}" ]; then
|
|
3222
|
+
_acct="$PLOT_BUDGET_ACCOUNT"
|
|
3223
|
+
elif [ "$be" = "github" ]; then
|
|
3224
|
+
_acct="$(budget_account github)"
|
|
3225
|
+
else
|
|
3226
|
+
echo "plot-host.sh: account: $be names no signed-in user without an API request" >&2
|
|
3227
|
+
exit 4
|
|
3006
3228
|
fi
|
|
3229
|
+
if [ -z "$_acct" ] || [ "$_acct" = "unknown" ]; then
|
|
3230
|
+
echo "plot-host.sh: account: no user in ${GH_CONFIG_DIR:-$HOME/.config/gh}/hosts.yml" >&2
|
|
3231
|
+
exit 3
|
|
3232
|
+
fi
|
|
3233
|
+
printf '%s\n' "$_acct"
|
|
3007
3234
|
;;
|
|
3008
3235
|
|
|
3009
3236
|
pr-state)
|
|
@@ -3030,11 +3257,11 @@ case "$op" in
|
|
|
3030
3257
|
# place that knows how to read either.
|
|
3031
3258
|
rest_repo="$(gh_rest_repo)" || exit $?
|
|
3032
3259
|
if [[ "$ref" =~ ^[0-9]+$ ]]; then
|
|
3033
|
-
if out="$(gh_api_harvest "repos/$rest_repo/pulls/$ref" 2
|
|
3034
|
-
rm -f "
|
|
3260
|
+
if out="$(gh_api_harvest "repos/$rest_repo/pulls/$ref" 2>"$HOST_ERR")"; then
|
|
3261
|
+
rm -f "$HOST_ERR"
|
|
3035
3262
|
rest_pr_to_state <<<"$out"
|
|
3036
3263
|
else
|
|
3037
|
-
err="$(cat "
|
|
3264
|
+
err="$(cat "$HOST_ERR" 2>/dev/null)"; rm -f "$HOST_ERR"
|
|
3038
3265
|
host_miss_or_fail "$err" \
|
|
3039
3266
|
'{"number":0,"state":"NONE","draft":false,"url":"","mergeCommit":""}' || exit $?
|
|
3040
3267
|
fi
|
|
@@ -3047,15 +3274,15 @@ case "$op" in
|
|
|
3047
3274
|
# `state=all`, because the default is `open` and a merged PR would
|
|
3048
3275
|
# otherwise read as NONE — wrong in the reassuring direction.
|
|
3049
3276
|
rest_owner="${rest_repo%%/*}"
|
|
3050
|
-
if out="$(gh_api_harvest "repos/$rest_repo/pulls?head=$rest_owner:$ref&state=all&per_page=1" 2
|
|
3051
|
-
rm -f "
|
|
3277
|
+
if out="$(gh_api_harvest "repos/$rest_repo/pulls?head=$rest_owner:$ref&state=all&per_page=1" 2>"$HOST_ERR")"; then
|
|
3278
|
+
rm -f "$HOST_ERR"
|
|
3052
3279
|
if [ "$(jq -r 'length' <<<"$out" 2>/dev/null)" = "0" ]; then
|
|
3053
3280
|
echo '{"number":0,"state":"NONE","draft":false,"url":"","mergeCommit":""}'
|
|
3054
3281
|
else
|
|
3055
3282
|
jq -c '.[0]' <<<"$out" | rest_pr_to_state
|
|
3056
3283
|
fi
|
|
3057
3284
|
else
|
|
3058
|
-
err="$(cat "
|
|
3285
|
+
err="$(cat "$HOST_ERR" 2>/dev/null)"; rm -f "$HOST_ERR"
|
|
3059
3286
|
host_miss_or_fail "$err" \
|
|
3060
3287
|
'{"number":0,"state":"NONE","draft":false,"url":"","mergeCommit":""}' || exit $?
|
|
3061
3288
|
fi
|
|
@@ -3063,11 +3290,11 @@ case "$op" in
|
|
|
3063
3290
|
# mergeCommit is what lets a caller ask "which release contains this?" —
|
|
3064
3291
|
# `git tag --contains <sha>` answers exactly, where dates cannot. It is ""
|
|
3065
3292
|
# for anything unmerged, which is the honest answer rather than a guess.
|
|
3066
|
-
elif out="$(gh ${repo_args[@]+"${repo_args[@]}"} pr view "$ref" --json number,state,isDraft,url,mergeCommit 2
|
|
3067
|
-
rm -f "
|
|
3293
|
+
elif out="$(gh ${repo_args[@]+"${repo_args[@]}"} pr view "$ref" --json number,state,isDraft,url,mergeCommit 2>"$HOST_ERR")"; then
|
|
3294
|
+
rm -f "$HOST_ERR"
|
|
3068
3295
|
jq -c '{number:.number,state:.state,draft:.isDraft,url:.url,mergeCommit:(.mergeCommit.oid // "")}' <<<"$out"
|
|
3069
3296
|
else
|
|
3070
|
-
err="$(cat "
|
|
3297
|
+
err="$(cat "$HOST_ERR" 2>/dev/null)"; rm -f "$HOST_ERR"
|
|
3071
3298
|
# REFUSED FOR RATE IS NOT ANSWERED. The budget gate above could not see
|
|
3072
3299
|
# this coming — `rate_limit` does not report the secondary limit — so the
|
|
3073
3300
|
# cheap path was chosen and then declined. The second path is the one
|
|
@@ -3100,11 +3327,11 @@ case "$op" in
|
|
|
3100
3327
|
# the GitHub arm gives for anything unmerged, so a caller cannot tell the
|
|
3101
3328
|
# backends apart.
|
|
3102
3329
|
if [[ "$ref" =~ ^[0-9]+$ ]]; then
|
|
3103
|
-
if out="$(bb ${repo_args[@]+"${repo_args[@]}"} pr view "$ref" --json 2
|
|
3104
|
-
rm -f "
|
|
3330
|
+
if out="$(bb ${repo_args[@]+"${repo_args[@]}"} pr view "$ref" --json 2>"$HOST_ERR")"; then
|
|
3331
|
+
rm -f "$HOST_ERR"
|
|
3105
3332
|
jq -c '{number:.id,state:(if .state=="DECLINED" then "CLOSED" else .state end),draft:(.draft // false),url:.links.html.href,mergeCommit:(.merge_commit.hash // "")}' <<<"$out"
|
|
3106
3333
|
else
|
|
3107
|
-
err="$(cat "
|
|
3334
|
+
err="$(cat "$HOST_ERR" 2>/dev/null)"; rm -f "$HOST_ERR"
|
|
3108
3335
|
host_miss_or_fail "$err" '{"number":0,"state":"NONE","draft":false,"url":"","mergeCommit":""}' || exit $?
|
|
3109
3336
|
fi
|
|
3110
3337
|
else
|
|
@@ -3129,7 +3356,7 @@ case "$op" in
|
|
|
3129
3356
|
out=""; bb_rc=0
|
|
3130
3357
|
bb_all_states="$(bb_states_for all)" || exit 1
|
|
3131
3358
|
for _s in $bb_all_states; do
|
|
3132
|
-
if _part="$(bb ${repo_args[@]+"${repo_args[@]}"} pr list --state "$_s" --json 2
|
|
3359
|
+
if _part="$(bb ${repo_args[@]+"${repo_args[@]}"} pr list --state "$_s" --json 2>"$HOST_ERR")"; then
|
|
3133
3360
|
out="$out$_part"
|
|
3134
3361
|
# `jq -e` exits non-zero on null/false, so this asks "did this state
|
|
3135
3362
|
# contain the branch?" without a second parse of the whole page.
|
|
@@ -3141,7 +3368,7 @@ case "$op" in
|
|
|
3141
3368
|
fi
|
|
3142
3369
|
done
|
|
3143
3370
|
if [ "$bb_rc" = 0 ]; then
|
|
3144
|
-
rm -f "
|
|
3371
|
+
rm -f "$HOST_ERR"
|
|
3145
3372
|
out="$(jq -c -s 'add // []' <<<"$out")"
|
|
3146
3373
|
# THE BRANCH ARM CARRIES `mergeCommit` TOO, and it is the path that
|
|
3147
3374
|
# matters most: `plot-pr-state.sh:33` asks `pr-state "idea/${SLUG}"` —
|
|
@@ -3155,7 +3382,7 @@ case "$op" in
|
|
|
3155
3382
|
| if .==null then {number:0,state:"NONE",draft:false,url:"",mergeCommit:""}
|
|
3156
3383
|
else {number:.id,state:(if .state=="DECLINED" then "CLOSED" else .state end),draft:(.draft // false),url:.links.html.href,mergeCommit:(.merge_commit.hash // "")} end' <<<"$out"
|
|
3157
3384
|
else
|
|
3158
|
-
err="$(cat "
|
|
3385
|
+
err="$(cat "$HOST_ERR" 2>/dev/null)"; rm -f "$HOST_ERR"
|
|
3159
3386
|
host_miss_or_fail "$err" '{"number":0,"state":"NONE","draft":false,"url":"","mergeCommit":""}' || exit $?
|
|
3160
3387
|
fi
|
|
3161
3388
|
fi
|
|
@@ -3187,15 +3414,15 @@ case "$op" in
|
|
|
3187
3414
|
# --state all, because a merged PR reports CLOSED and the default `open`
|
|
3188
3415
|
# would hide every one of them. --limit 100 rather than 1: the newest PR
|
|
3189
3416
|
# is not the merge, exactly as the state is not the merge.
|
|
3190
|
-
if out="$(gh ${repo_args[@]+"${repo_args[@]}"} pr list --head "$ref" --state all --limit 100 --json mergedAt 2
|
|
3191
|
-
rm -f "
|
|
3417
|
+
if out="$(gh ${repo_args[@]+"${repo_args[@]}"} pr list --head "$ref" --state all --limit 100 --json mergedAt 2>"$HOST_ERR")"; then
|
|
3418
|
+
rm -f "$HOST_ERR"
|
|
3192
3419
|
if jq -e 'any(.[]; .mergedAt != null)' >/dev/null 2>&1 <<<"$out"; then
|
|
3193
3420
|
echo "merged"
|
|
3194
3421
|
else
|
|
3195
3422
|
echo "not-merged"
|
|
3196
3423
|
fi
|
|
3197
3424
|
else
|
|
3198
|
-
err="$(cat "
|
|
3425
|
+
err="$(cat "$HOST_ERR" 2>/dev/null)"; rm -f "$HOST_ERR"
|
|
3199
3426
|
# A LOOKUP MISS IS AN ANSWER — the branch has no PR, so nothing merged.
|
|
3200
3427
|
# Anything else is the host failing to be asked, and that is `unknown`
|
|
3201
3428
|
# rather than exit 3: the caller asked a question with a third value
|
|
@@ -3214,15 +3441,15 @@ case "$op" in
|
|
|
3214
3441
|
# exposes no `mergedAt`, so this is the closest fact the backend holds,
|
|
3215
3442
|
# and it is a positive statement about the merge rather than an inference
|
|
3216
3443
|
# from CLOSED — `DECLINED` is bb's closed-unmerged word and is distinct.
|
|
3217
|
-
if out="$(bb ${repo_args[@]+"${repo_args[@]}"} pr list --state merged --json 2
|
|
3218
|
-
rm -f "
|
|
3444
|
+
if out="$(bb ${repo_args[@]+"${repo_args[@]}"} pr list --state merged --json 2>"$HOST_ERR")"; then
|
|
3445
|
+
rm -f "$HOST_ERR"
|
|
3219
3446
|
if jq -e --arg b "$ref" 'any(.[]; .source.branch.name==$b)' >/dev/null 2>&1 <<<"$out"; then
|
|
3220
3447
|
echo "merged"
|
|
3221
3448
|
else
|
|
3222
3449
|
echo "not-merged"
|
|
3223
3450
|
fi
|
|
3224
3451
|
else
|
|
3225
|
-
err="$(cat "
|
|
3452
|
+
err="$(cat "$HOST_ERR" 2>/dev/null)"; rm -f "$HOST_ERR"
|
|
3226
3453
|
if [ -z "$err" ] || is_lookup_miss "$err"; then
|
|
3227
3454
|
echo "not-merged"
|
|
3228
3455
|
else
|
|
@@ -3262,15 +3489,15 @@ case "$op" in
|
|
|
3262
3489
|
esac
|
|
3263
3490
|
done
|
|
3264
3491
|
if [ "$be" = "github" ]; then
|
|
3265
|
-
if out="$(gh ${repo_args[@]+"${repo_args[@]}"} pr list --head "$ref" --state all --limit 100 --json mergedAt,mergeCommit 2
|
|
3266
|
-
rm -f "
|
|
3492
|
+
if out="$(gh ${repo_args[@]+"${repo_args[@]}"} pr list --head "$ref" --state all --limit 100 --json mergedAt,mergeCommit 2>"$HOST_ERR")"; then
|
|
3493
|
+
rm -f "$HOST_ERR"
|
|
3267
3494
|
# The FIRST merged PR carrying a sha. A branch may hold several merged
|
|
3268
3495
|
# PRs; each names its own merge commit, and any of them is a commit that
|
|
3269
3496
|
# landed this branch's work.
|
|
3270
3497
|
jq -r 'map(select(.mergedAt != null and .mergeCommit != null))
|
|
3271
3498
|
| map(.mergeCommit.oid) | first // empty' <<<"$out"
|
|
3272
3499
|
else
|
|
3273
|
-
err="$(cat "
|
|
3500
|
+
err="$(cat "$HOST_ERR" 2>/dev/null)"; rm -f "$HOST_ERR"
|
|
3274
3501
|
# A lookup miss is an answer: no PR, so no merge commit. Anything else
|
|
3275
3502
|
# is the question failing, and silence must not read as "carried
|
|
3276
3503
|
# nothing" — the caller distinguishes the two by the exit code.
|
|
@@ -3284,12 +3511,12 @@ case "$op" in
|
|
|
3284
3511
|
else
|
|
3285
3512
|
bb_require_json
|
|
3286
3513
|
# Bitbucket names the merge commit `merge_commit.hash` on a merged PR.
|
|
3287
|
-
if out="$(bb ${repo_args[@]+"${repo_args[@]}"} pr list --state merged --json 2
|
|
3288
|
-
rm -f "
|
|
3514
|
+
if out="$(bb ${repo_args[@]+"${repo_args[@]}"} pr list --state merged --json 2>"$HOST_ERR")"; then
|
|
3515
|
+
rm -f "$HOST_ERR"
|
|
3289
3516
|
jq -r --arg b "$ref" 'map(select(.source.branch.name==$b))
|
|
3290
3517
|
| map(.merge_commit.hash // empty) | first // empty' <<<"$out"
|
|
3291
3518
|
else
|
|
3292
|
-
err="$(cat "
|
|
3519
|
+
err="$(cat "$HOST_ERR" 2>/dev/null)"; rm -f "$HOST_ERR"
|
|
3293
3520
|
if [ -z "$err" ] || is_lookup_miss "$err"; then
|
|
3294
3521
|
:
|
|
3295
3522
|
else
|
|
@@ -3550,7 +3777,7 @@ case "$op" in
|
|
|
3550
3777
|
# $jentry == null → the branch has no Jenkins job; `none`.
|
|
3551
3778
|
# otherwise → the joined colour's `checks`, job named on fail.
|
|
3552
3779
|
_gh_raw="$(pr_list_call gh ${repo_args[@]+"${repo_args[@]}"} pr list --state "$state" ${limit_args[@]+"${limit_args[@]}"} ${search_args[@]+"${search_args[@]}"} \
|
|
3553
|
-
--json number,title,state,headRefName,isDraft,mergeable,mergeStateStatus,reviewDecision,url,updatedAt)" || exit $?
|
|
3780
|
+
--json number,title,state,headRefName,isDraft,mergeable,mergeStateStatus,reviewDecision,url,updatedAt,author)" || exit $?
|
|
3554
3781
|
pr_list_report_truncation github "$limit" "$state" \
|
|
3555
3782
|
"$(jq 'length' <<<"$_gh_raw" 2>/dev/null || echo 0)"
|
|
3556
3783
|
printf '%s' "$_gh_raw" \
|
|
@@ -3571,6 +3798,7 @@ case "$op" in
|
|
|
3571
3798
|
review:(.reviewDecision // ""),
|
|
3572
3799
|
url:.url,
|
|
3573
3800
|
updatedAt:(.updatedAt // ""),
|
|
3801
|
+
author:(.author.login // ""),
|
|
3574
3802
|
failing_checks:(
|
|
3575
3803
|
if $jentry != null and $jentry.checks == "failing"
|
|
3576
3804
|
then [$jentry.job]
|
|
@@ -3580,7 +3808,7 @@ case "$op" in
|
|
|
3580
3808
|
else
|
|
3581
3809
|
# GitHub without Jenkins (or Jenkins not configured): use GitHub rollup
|
|
3582
3810
|
_gh_raw="$(pr_list_call gh ${repo_args[@]+"${repo_args[@]}"} pr list --state "$state" ${limit_args[@]+"${limit_args[@]}"} ${search_args[@]+"${search_args[@]}"} \
|
|
3583
|
-
--json number,title,state,headRefName,isDraft,statusCheckRollup,mergeable,mergeStateStatus,reviewDecision,url,updatedAt)" || exit $?
|
|
3811
|
+
--json number,title,state,headRefName,isDraft,statusCheckRollup,mergeable,mergeStateStatus,reviewDecision,url,updatedAt,author)" || exit $?
|
|
3584
3812
|
pr_list_report_truncation github "$limit" "$state" \
|
|
3585
3813
|
"$(jq 'length' <<<"$_gh_raw" 2>/dev/null || echo 0)"
|
|
3586
3814
|
printf '%s' "$_gh_raw" \
|
|
@@ -3603,6 +3831,7 @@ case "$op" in
|
|
|
3603
3831
|
review:(.reviewDecision // ""),
|
|
3604
3832
|
url:.url,
|
|
3605
3833
|
updatedAt:(.updatedAt // ""),
|
|
3834
|
+
author:(.author.login // ""),
|
|
3606
3835
|
failing_checks:[
|
|
3607
3836
|
.statusCheckRollup[]? | select((if (.conclusion // "") != "" then .conclusion else (.status // .state) end) as $c
|
|
3608
3837
|
| $c=="FAILURE" or $c=="ERROR" or $c=="CANCELLED"
|
|
@@ -3612,13 +3841,19 @@ case "$op" in
|
|
|
3612
3841
|
fi
|
|
3613
3842
|
else
|
|
3614
3843
|
_gh_raw="$(pr_list_call gh ${repo_args[@]+"${repo_args[@]}"} pr list --state "$state" ${limit_args[@]+"${limit_args[@]}"} ${search_args[@]+"${search_args[@]}"} \
|
|
3615
|
-
--json number,title,state,headRefName)" || exit $?
|
|
3844
|
+
--json number,title,state,headRefName,author)" || exit $?
|
|
3616
3845
|
pr_list_report_truncation github "$limit" "$state" \
|
|
3617
3846
|
"$(jq 'length' <<<"$_gh_raw" 2>/dev/null || echo 0)"
|
|
3618
3847
|
printf '%s' "$_gh_raw" \
|
|
3619
|
-
| jq -c '.[] | {number:.number,title:.title,state:.state,head:.headRefName}'
|
|
3848
|
+
| jq -c '.[] | {number:.number,title:.title,state:.state,head:.headRefName,author:(.author.login // "")}'
|
|
3620
3849
|
fi
|
|
3621
3850
|
else
|
|
3851
|
+
# `author` IS THE `nickname`, the one handle field a Bitbucket user
|
|
3852
|
+
# object carries. Measured 2026-09-25 against bb 1.9.0: `author` holds
|
|
3853
|
+
# `display_name`, `nickname`, `account_id` and `uuid` and no username,
|
|
3854
|
+
# and on a Quatico workspace `nickname` equals the display name. GitHub's
|
|
3855
|
+
# arm reads `.author.login`. Both answer `""` where the host omits it.
|
|
3856
|
+
#
|
|
3622
3857
|
# Bitbucket carries no check rollup through `bb pr list`, and no
|
|
3623
3858
|
# mergeability verdict either. Rather than guess, --rich reports
|
|
3624
3859
|
# checks:"unknown" and mergeable:"unknown" — a consumer must render those
|
|
@@ -3630,16 +3865,17 @@ case "$op" in
|
|
|
3630
3865
|
# integration GitHub uses, which is why it is ABOVE the backend branch.
|
|
3631
3866
|
# Bitbucket's `unknown` becomes a real value.
|
|
3632
3867
|
#
|
|
3633
|
-
#
|
|
3634
|
-
#
|
|
3868
|
+
# The listing has no --limit: it returns one page of 50 per state
|
|
3869
|
+
# (`bb_state_listing`). `bb pr list` refuses the flag with `unknown flag`,
|
|
3870
|
+
# and the page size is the endpoint's ceiling. Dropping it silently
|
|
3635
3871
|
# would serve a short page as if it were the whole set — the quiet wrong
|
|
3636
3872
|
# answer this adapter refuses elsewhere. So it is dropped AND said.
|
|
3637
3873
|
# A SWEEP IS NOT A PAGE AND OWES NO SUCH WARNING. `--limit` bounds a
|
|
3638
3874
|
# listing; a per-branch query returns that branch's pull requests and
|
|
3639
3875
|
# nothing was capped, so the notice would describe a truncation that did
|
|
3640
3876
|
# not happen. Said only for the listing it is about.
|
|
3641
|
-
if [ -n "$limit" ] && [ -z "$branches" ]; then
|
|
3642
|
-
echo "plot-host: bitbucket ignores --limit $limit;
|
|
3877
|
+
if [ -n "$limit" ] && [ -z "$branches" ] && [ -z "$since" ]; then
|
|
3878
|
+
echo "plot-host: bitbucket ignores --limit $limit; the listing returns one page of 50 per state" >&2
|
|
3643
3879
|
fi
|
|
3644
3880
|
# Establish that bb supports --json BEFORE calling it — Done-when 5.
|
|
3645
3881
|
bb_require_json
|
|
@@ -3656,25 +3892,23 @@ case "$op" in
|
|
|
3656
3892
|
# ways, which is how the six hand-applied fixes `pr_list_call` warns about
|
|
3657
3893
|
# began. One assignment here; the sites are untouched but for this word.
|
|
3658
3894
|
PR_LIST_BRANCHES="$branches"
|
|
3659
|
-
# THE WINDOW REACHES
|
|
3660
|
-
#
|
|
3661
|
-
#
|
|
3662
|
-
#
|
|
3663
|
-
#
|
|
3895
|
+
# THE WINDOW REACHES EVERY BITBUCKET PATH THROUGH `q=`. `bb pr list` takes
|
|
3896
|
+
# no query flag (bb 1.9.0 answers `unknown flag: --query`), so a windowed
|
|
3897
|
+
# LISTING goes through `bb_window_listing`, and a windowed SWEEP through
|
|
3898
|
+
# `bb_branch_query`. Both put `updated_on>=` in the one `q=` Bitbucket
|
|
3899
|
+
# takes, beside the state clause.
|
|
3664
3900
|
#
|
|
3665
|
-
#
|
|
3666
|
-
#
|
|
3667
|
-
#
|
|
3668
|
-
#
|
|
3669
|
-
#
|
|
3670
|
-
# decide whether its answer was complete.
|
|
3901
|
+
# THE REPORT STAYS FOR A PATH THAT CANNOT NARROW. Every path above
|
|
3902
|
+
# narrows today, so `ignores --since` prints nowhere. A path added later
|
|
3903
|
+
# that cannot carry the window clears `PR_LIST_SINCE` and says so: a
|
|
3904
|
+
# caller that asked for a window and got a full listing must not read the
|
|
3905
|
+
# answer as a delta.
|
|
3671
3906
|
PR_LIST_SINCE="$since"
|
|
3672
|
-
if [ -n "$since" ] && [ -z "$branches" ]; then
|
|
3673
|
-
echo "plot-host: bitbucket ignores --since $since on a listing; bb pr list has no query flag (bb 1.9.0) — answering in full" >&2
|
|
3674
|
-
PR_LIST_SINCE=""
|
|
3675
|
-
fi
|
|
3676
3907
|
PR_SWEEP_ASKED=0
|
|
3677
|
-
|
|
3908
|
+
# THE PLAIN LISTING IS `bb_state_listing`, one request per state at
|
|
3909
|
+
# `pagelen=50`, where `bb pr list` walks 10-row pages. Its header has the
|
|
3910
|
+
# measurement.
|
|
3911
|
+
bb_cmd=(bb_state_listing ${repo_args[@]+"${repo_args[@]}"})
|
|
3678
3912
|
if [ -n "$branches" ]; then
|
|
3679
3913
|
# A SWEEP'S COST IS THE CALLER'S WORKING SET, and it is reported so the
|
|
3680
3914
|
# caller can check the claim it is about to be handed. Branches × states
|
|
@@ -3682,6 +3916,8 @@ case "$op" in
|
|
|
3682
3916
|
# that answer for 50 of 902 rows.
|
|
3683
3917
|
for _b in $branches; do PR_SWEEP_ASKED=$((PR_SWEEP_ASKED + 1)); done
|
|
3684
3918
|
bb_cmd=(bb_branch_sweep ${repo_args[@]+"${repo_args[@]}"})
|
|
3919
|
+
elif [ -n "$PR_LIST_SINCE" ]; then
|
|
3920
|
+
bb_cmd=(bb_window_listing ${repo_args[@]+"${repo_args[@]}"})
|
|
3685
3921
|
fi
|
|
3686
3922
|
if [ "$rich" = 1 ]; then
|
|
3687
3923
|
if [ "$ci" = "jenkins" ]; then
|
|
@@ -3705,6 +3941,7 @@ case "$op" in
|
|
|
3705
3941
|
review:"",
|
|
3706
3942
|
url:(.links.html.href // ""),
|
|
3707
3943
|
updatedAt:(.updated_on // ""),
|
|
3944
|
+
author:(.author.nickname // ""),
|
|
3708
3945
|
failing_checks:(
|
|
3709
3946
|
if $jentry != null and $jentry.checks == "failing"
|
|
3710
3947
|
then [$jentry.job]
|
|
@@ -3715,13 +3952,13 @@ case "$op" in
|
|
|
3715
3952
|
# Bitbucket without Jenkins: checks remain unknown
|
|
3716
3953
|
PR_LIST_JQ_ARGS=()
|
|
3717
3954
|
pr_list_states bitbucket "$limit" "$bb_states" \
|
|
3718
|
-
'.[] | {number:.id,title:.title,state:(if .state=="DECLINED" then "CLOSED" else .state end),head:.source.branch.name,draft:(.draft // false),checks:"unknown",mergeable:"unknown",review:"",url:(.links.html.href // ""),updatedAt:(.updated_on // ""),failing_checks:[]}' \
|
|
3955
|
+
'.[] | {number:.id,title:.title,state:(if .state=="DECLINED" then "CLOSED" else .state end),head:.source.branch.name,draft:(.draft // false),checks:"unknown",mergeable:"unknown",review:"",url:(.links.html.href // ""),updatedAt:(.updated_on // ""),author:(.author.nickname // ""),failing_checks:[]}' \
|
|
3719
3956
|
"${bb_cmd[@]}" || exit $?
|
|
3720
3957
|
fi
|
|
3721
3958
|
else
|
|
3722
3959
|
PR_LIST_JQ_ARGS=()
|
|
3723
3960
|
pr_list_states bitbucket "$limit" "$bb_states" \
|
|
3724
|
-
'.[] | {number:.id,title:.title,state:(if .state=="DECLINED" then "CLOSED" else .state end),head:.source.branch.name}' \
|
|
3961
|
+
'.[] | {number:.id,title:.title,state:(if .state=="DECLINED" then "CLOSED" else .state end),head:.source.branch.name,author:(.author.nickname // "")}' \
|
|
3725
3962
|
"${bb_cmd[@]}" || exit $?
|
|
3726
3963
|
fi
|
|
3727
3964
|
fi
|
|
@@ -4111,13 +4348,13 @@ case "$op" in
|
|
|
4111
4348
|
# unstarted by the only vocabulary GitHub has.
|
|
4112
4349
|
gh_state="open"
|
|
4113
4350
|
if out="$(gh issue list --state "$gh_state" ${limit_args[@]+"${limit_args[@]}"} \
|
|
4114
|
-
--json number,title,url,createdAt 2
|
|
4115
|
-
rm -f "
|
|
4351
|
+
--json number,title,url,createdAt 2>"$HOST_ERR")"; then
|
|
4352
|
+
rm -f "$HOST_ERR"
|
|
4116
4353
|
jq -c --arg status "$gh_state" \
|
|
4117
4354
|
'.[] | {number:.number,title:.title,url:(.url // ""),createdAt:(.createdAt // ""),
|
|
4118
4355
|
status:$status,statusCategory:(if $status == "open" then "To Do" else "Done" end)}' <<<"$out"
|
|
4119
4356
|
else
|
|
4120
|
-
err="$(cat "
|
|
4357
|
+
err="$(cat "$HOST_ERR" 2>/dev/null)"; rm -f "$HOST_ERR"
|
|
4121
4358
|
# NO empty-list fallback. `host_miss_or_fail` exists for a lookup whose
|
|
4122
4359
|
# subject is absent — one PR that does not exist. A LIST has no absent
|
|
4123
4360
|
# subject: if the call failed, the answer is unknown, and printing
|
|
@@ -4231,11 +4468,11 @@ case "$op" in
|
|
|
4231
4468
|
url: ($base + "/browse/" + .key)
|
|
4232
4469
|
}'
|
|
4233
4470
|
elif [ "$be" = "github" ]; then
|
|
4234
|
-
if out="$(gh issue view "$num" --json number,title,body,url 2
|
|
4235
|
-
rm -f "
|
|
4471
|
+
if out="$(gh issue view "$num" --json number,title,body,url 2>"$HOST_ERR")"; then
|
|
4472
|
+
rm -f "$HOST_ERR"
|
|
4236
4473
|
jq -c '{number:.number,title:(.title // ""),body:(.body // ""),url:(.url // "")}' <<<"$out"
|
|
4237
4474
|
else
|
|
4238
|
-
err="$(cat "
|
|
4475
|
+
err="$(cat "$HOST_ERR" 2>/dev/null)"; rm -f "$HOST_ERR"
|
|
4239
4476
|
# NO miss/fail split here, and that is deliberate. `host_miss_or_fail`
|
|
4240
4477
|
# exists where an absent subject is a NORMAL answer — a branch with no
|
|
4241
4478
|
# PR. An issue number reaching this op was read off `issue-list`
|
|
@@ -4383,8 +4620,8 @@ case "$op" in
|
|
|
4383
4620
|
# This op REPORTS; it does not decide. A caller that wants to fall back when
|
|
4384
4621
|
# one budget is spent reads this, compares remaining to zero, and acts.
|
|
4385
4622
|
if [ "$be" = "github" ]; then
|
|
4386
|
-
if out="$(gh api rate_limit 2
|
|
4387
|
-
rm -f "
|
|
4623
|
+
if out="$(gh api rate_limit 2>"$HOST_ERR")"; then
|
|
4624
|
+
rm -f "$HOST_ERR"
|
|
4388
4625
|
# The payload has `resources.graphql` and `resources.core`, each with
|
|
4389
4626
|
# `remaining`, `limit`, and `reset`. Extract the two we care about.
|
|
4390
4627
|
jq -c '{
|
|
@@ -4400,7 +4637,7 @@ case "$op" in
|
|
|
4400
4637
|
}
|
|
4401
4638
|
}' <<<"$out"
|
|
4402
4639
|
else
|
|
4403
|
-
err="$(cat "
|
|
4640
|
+
err="$(cat "$HOST_ERR" 2>/dev/null)"; rm -f "$HOST_ERR"
|
|
4404
4641
|
# The host could not be asked. Report unknown rather than failing outright,
|
|
4405
4642
|
# because this is informational — a caller that cannot read the budget
|
|
4406
4643
|
# should proceed with the default path, not error out.
|
|
@@ -4463,7 +4700,8 @@ case "$op" in
|
|
|
4463
4700
|
# inventing a reading nobody took — the mistake `rate_limit` makes by
|
|
4464
4701
|
# answering for both at once.
|
|
4465
4702
|
if [ "$be" = "github" ]; then
|
|
4466
|
-
_hdr_tmp="
|
|
4703
|
+
_hdr_tmp=""
|
|
4704
|
+
plot_tmpfile _hdr_tmp host-limit || die3 "limit: no temp file could be made under ${TMPDIR:-/tmp}"
|
|
4467
4705
|
# `command gh`, NOT THE WRAPPER, and for the ordering reason
|
|
4468
4706
|
# `gh_api_harvest` gives: the wrapper records the moment the call returns,
|
|
4469
4707
|
# before any header has been read, so a wrapped call here would file an
|