@plot-pm/board 0.15.0 → 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 +109 -107
- package/package.json +2 -1
- package/plot-agent-manifest.sh +80 -1
- package/plot-approve.sh +3 -1
- package/plot-budget.sh +378 -19
- package/plot-config.sh +52 -1
- package/plot-deliver.sh +3 -1
- package/plot-dispatch.sh +24 -30
- package/plot-fleet-scan.sh +51 -150
- package/plot-host.sh +227 -72
- package/plot-plan-meta.sh +58 -10
- package/plot-reap.sh +104 -8
- 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
|
@@ -391,6 +391,15 @@ set -uo pipefail
|
|
|
391
391
|
|
|
392
392
|
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
393
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
|
+
|
|
394
403
|
die() { echo "plot-host: $*" >&2; exit 1; }
|
|
395
404
|
|
|
396
405
|
# Exit 3 — reserved for "the op itself cannot proceed", distinct from `die`'s
|
|
@@ -575,7 +584,9 @@ pr_list_failed() { # $1=stderr text
|
|
|
575
584
|
# hundred lines below, where a `die` in a subshell turned an unknown state into
|
|
576
585
|
# "no PRs matched".
|
|
577
586
|
pr_list_call() { # "$@"=the host command → payload on stdout, or dies
|
|
578
|
-
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; }
|
|
579
590
|
out="$("$@" 2>"$tmp")"; rc=$?
|
|
580
591
|
err="$(cat "$tmp" 2>/dev/null)"; rm -f "$tmp"
|
|
581
592
|
[ "$rc" -eq 0 ] || pr_list_failed "$err"
|
|
@@ -858,7 +869,7 @@ bb_branch_sweep() { # global bb args… --state <s> --json → one JSON array
|
|
|
858
869
|
# already seen once per branch, so eleven branches parsed the first branch's
|
|
859
870
|
# payload eleven times.
|
|
860
871
|
local _spool
|
|
861
|
-
_spool
|
|
872
|
+
plot_tmpfile _spool host-sweep || return 3
|
|
862
873
|
for _br in $PR_LIST_BRANCHES; do
|
|
863
874
|
# RETURN, NOT EXIT. This runs inside the command substitution
|
|
864
875
|
# `pr_list_call` wraps the sweep in, so the code must travel back as this
|
|
@@ -867,7 +878,7 @@ bb_branch_sweep() { # global bb args… --state <s> --json → one JSON array
|
|
|
867
878
|
# as the sweep's own — the silent empty list `pr_list_call`'s header names.
|
|
868
879
|
#
|
|
869
880
|
# The spool is removed on EVERY exit path, including the failing one: a
|
|
870
|
-
# 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.
|
|
871
882
|
bb_branch_query "$_br" "$_st" ${_args[@]+"${_args[@]}"} >> "$_spool" \
|
|
872
883
|
|| { _rc=$?; rm -f "$_spool"; return $_rc; }
|
|
873
884
|
done
|
|
@@ -880,6 +891,135 @@ bb_branch_sweep() { # global bb args… --state <s> --json → one JSON array
|
|
|
880
891
|
printf '%s' "$_acc"
|
|
881
892
|
}
|
|
882
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
|
+
|
|
883
1023
|
# The branches a sweep asks about, newline-or-space separated. Empty means the
|
|
884
1024
|
# caller named none, and the arm keeps the bulk listing it has always used.
|
|
885
1025
|
#
|
|
@@ -894,13 +1034,13 @@ bb_branch_sweep() { # global bb args… --state <s> --json → one JSON array
|
|
|
894
1034
|
# separator is git's guarantee rather than a hopeful convention.
|
|
895
1035
|
PR_LIST_BRANCHES=""
|
|
896
1036
|
|
|
897
|
-
# The window a
|
|
1037
|
+
# The window a Bitbucket query carries, the host's own stamp. Empty means ask
|
|
898
1038
|
# about everything, which is what every caller predating `--since` asks.
|
|
899
1039
|
#
|
|
900
1040
|
# A GLOBAL FOR `PR_LIST_BRANCHES`' REASON, and it travels the same route: the
|
|
901
1041
|
# `pr-list` arm sets it immediately before the call, `bb_branch_sweep` passes
|
|
902
|
-
# through without reading it, and `bb_branch_query`
|
|
903
|
-
# `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
|
|
904
1044
|
# teaching that function a parameter it only forwards, and its header already
|
|
905
1045
|
# refuses the mirror of that — "teaching `pr_list_states` which of its commands
|
|
906
1046
|
# is a sweep would put a backend's shape inside the one piece of this file that
|
|
@@ -964,7 +1104,7 @@ pr_list_states() { # $1=backend $2=limit $3=states $4=jq-program; rest=the host
|
|
|
964
1104
|
local backend="$1" limit="$2" states="$3" jq_prog="$4"; shift 4
|
|
965
1105
|
local _s _raw _rc _err _tmp _ok=0 _failed=0 _first_rc=0 _failed_states=""
|
|
966
1106
|
for _s in $states; do
|
|
967
|
-
_tmp
|
|
1107
|
+
plot_tmpfile _tmp "host-prlist-state-err-$_s" || return 3
|
|
968
1108
|
# THE SUBSHELL'S CODE IS THE ONLY CHANNEL OUT, so it is captured rather
|
|
969
1109
|
# than propagated. `pr_list_failed` runs INSIDE the substitution and has
|
|
970
1110
|
# already composed its report and its repair line; that text is spooled
|
|
@@ -1000,7 +1140,13 @@ pr_list_states() { # $1=backend $2=limit $3=states $4=jq-program; rest=the host
|
|
|
1000
1140
|
# THE DETECTOR ITSELF IS UNTOUCHED and still fires exactly as it did on
|
|
1001
1141
|
# every listing call — `host.test.mjs:3060` passes unedited. What changed is
|
|
1002
1142
|
# that a path exists whose premise it was never written about.
|
|
1003
|
-
|
|
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" \
|
|
1004
1150
|
"$(jq 'length' <<<"$_raw" 2>/dev/null || echo 0)"
|
|
1005
1151
|
printf '%s' "$_raw" | jq -c ${PR_LIST_JQ_ARGS[@]+"${PR_LIST_JQ_ARGS[@]}"} "$jq_prog"
|
|
1006
1152
|
done
|
|
@@ -1804,6 +1950,13 @@ rest_pr_to_state() {
|
|
|
1804
1950
|
# `bb` fix, and the three states partition the set (74 PRs, 74 unique ids,
|
|
1805
1951
|
# 0 duplicates on the repo measured).
|
|
1806
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
|
+
#
|
|
1807
1960
|
# `superseded` is deliberately NOT part of `all`: such a PR is replaced by a
|
|
1808
1961
|
# newer one for the same branch, and a board with one row per branch would
|
|
1809
1962
|
# show that branch twice. `gh`'s `all` has no equivalent, so nothing is lost.
|
|
@@ -2558,11 +2711,11 @@ backend() {
|
|
|
2558
2711
|
# `budget_rate`. A directory keyed on a pid must be removed by the process that
|
|
2559
2712
|
# made it, or a long-lived machine accumulates one per `plot-host.sh` call.
|
|
2560
2713
|
#
|
|
2561
|
-
#
|
|
2562
|
-
#
|
|
2563
|
-
#
|
|
2564
|
-
#
|
|
2565
|
-
|
|
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
|
|
2566
2719
|
|
|
2567
2720
|
# WHO IS SPENDING — read from the CLI's own config, never from an API call.
|
|
2568
2721
|
#
|
|
@@ -2785,13 +2938,13 @@ plot_harvest_headers() {
|
|
|
2785
2938
|
# read may not charge.
|
|
2786
2939
|
gh_api_harvest() {
|
|
2787
2940
|
local raw rc hdr_tmp err_tmp
|
|
2788
|
-
hdr_tmp
|
|
2941
|
+
plot_tmpfile hdr_tmp host-hdr || {
|
|
2789
2942
|
# No temp file, no harvest — and the call still happens, recorded by the
|
|
2790
2943
|
# wrapper from its argv alone. Bookkeeping never fails its caller.
|
|
2791
2944
|
gh api "$@"
|
|
2792
2945
|
return $?
|
|
2793
2946
|
}
|
|
2794
|
-
err_tmp
|
|
2947
|
+
plot_tmpfile err_tmp host-herr || {
|
|
2795
2948
|
rm -f "$hdr_tmp"
|
|
2796
2949
|
gh api "$@"
|
|
2797
2950
|
return $?
|
|
@@ -3104,11 +3257,11 @@ case "$op" in
|
|
|
3104
3257
|
# place that knows how to read either.
|
|
3105
3258
|
rest_repo="$(gh_rest_repo)" || exit $?
|
|
3106
3259
|
if [[ "$ref" =~ ^[0-9]+$ ]]; then
|
|
3107
|
-
if out="$(gh_api_harvest "repos/$rest_repo/pulls/$ref" 2
|
|
3108
|
-
rm -f "
|
|
3260
|
+
if out="$(gh_api_harvest "repos/$rest_repo/pulls/$ref" 2>"$HOST_ERR")"; then
|
|
3261
|
+
rm -f "$HOST_ERR"
|
|
3109
3262
|
rest_pr_to_state <<<"$out"
|
|
3110
3263
|
else
|
|
3111
|
-
err="$(cat "
|
|
3264
|
+
err="$(cat "$HOST_ERR" 2>/dev/null)"; rm -f "$HOST_ERR"
|
|
3112
3265
|
host_miss_or_fail "$err" \
|
|
3113
3266
|
'{"number":0,"state":"NONE","draft":false,"url":"","mergeCommit":""}' || exit $?
|
|
3114
3267
|
fi
|
|
@@ -3121,15 +3274,15 @@ case "$op" in
|
|
|
3121
3274
|
# `state=all`, because the default is `open` and a merged PR would
|
|
3122
3275
|
# otherwise read as NONE — wrong in the reassuring direction.
|
|
3123
3276
|
rest_owner="${rest_repo%%/*}"
|
|
3124
|
-
if out="$(gh_api_harvest "repos/$rest_repo/pulls?head=$rest_owner:$ref&state=all&per_page=1" 2
|
|
3125
|
-
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"
|
|
3126
3279
|
if [ "$(jq -r 'length' <<<"$out" 2>/dev/null)" = "0" ]; then
|
|
3127
3280
|
echo '{"number":0,"state":"NONE","draft":false,"url":"","mergeCommit":""}'
|
|
3128
3281
|
else
|
|
3129
3282
|
jq -c '.[0]' <<<"$out" | rest_pr_to_state
|
|
3130
3283
|
fi
|
|
3131
3284
|
else
|
|
3132
|
-
err="$(cat "
|
|
3285
|
+
err="$(cat "$HOST_ERR" 2>/dev/null)"; rm -f "$HOST_ERR"
|
|
3133
3286
|
host_miss_or_fail "$err" \
|
|
3134
3287
|
'{"number":0,"state":"NONE","draft":false,"url":"","mergeCommit":""}' || exit $?
|
|
3135
3288
|
fi
|
|
@@ -3137,11 +3290,11 @@ case "$op" in
|
|
|
3137
3290
|
# mergeCommit is what lets a caller ask "which release contains this?" —
|
|
3138
3291
|
# `git tag --contains <sha>` answers exactly, where dates cannot. It is ""
|
|
3139
3292
|
# for anything unmerged, which is the honest answer rather than a guess.
|
|
3140
|
-
elif out="$(gh ${repo_args[@]+"${repo_args[@]}"} pr view "$ref" --json number,state,isDraft,url,mergeCommit 2
|
|
3141
|
-
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"
|
|
3142
3295
|
jq -c '{number:.number,state:.state,draft:.isDraft,url:.url,mergeCommit:(.mergeCommit.oid // "")}' <<<"$out"
|
|
3143
3296
|
else
|
|
3144
|
-
err="$(cat "
|
|
3297
|
+
err="$(cat "$HOST_ERR" 2>/dev/null)"; rm -f "$HOST_ERR"
|
|
3145
3298
|
# REFUSED FOR RATE IS NOT ANSWERED. The budget gate above could not see
|
|
3146
3299
|
# this coming — `rate_limit` does not report the secondary limit — so the
|
|
3147
3300
|
# cheap path was chosen and then declined. The second path is the one
|
|
@@ -3174,11 +3327,11 @@ case "$op" in
|
|
|
3174
3327
|
# the GitHub arm gives for anything unmerged, so a caller cannot tell the
|
|
3175
3328
|
# backends apart.
|
|
3176
3329
|
if [[ "$ref" =~ ^[0-9]+$ ]]; then
|
|
3177
|
-
if out="$(bb ${repo_args[@]+"${repo_args[@]}"} pr view "$ref" --json 2
|
|
3178
|
-
rm -f "
|
|
3330
|
+
if out="$(bb ${repo_args[@]+"${repo_args[@]}"} pr view "$ref" --json 2>"$HOST_ERR")"; then
|
|
3331
|
+
rm -f "$HOST_ERR"
|
|
3179
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"
|
|
3180
3333
|
else
|
|
3181
|
-
err="$(cat "
|
|
3334
|
+
err="$(cat "$HOST_ERR" 2>/dev/null)"; rm -f "$HOST_ERR"
|
|
3182
3335
|
host_miss_or_fail "$err" '{"number":0,"state":"NONE","draft":false,"url":"","mergeCommit":""}' || exit $?
|
|
3183
3336
|
fi
|
|
3184
3337
|
else
|
|
@@ -3203,7 +3356,7 @@ case "$op" in
|
|
|
3203
3356
|
out=""; bb_rc=0
|
|
3204
3357
|
bb_all_states="$(bb_states_for all)" || exit 1
|
|
3205
3358
|
for _s in $bb_all_states; do
|
|
3206
|
-
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
|
|
3207
3360
|
out="$out$_part"
|
|
3208
3361
|
# `jq -e` exits non-zero on null/false, so this asks "did this state
|
|
3209
3362
|
# contain the branch?" without a second parse of the whole page.
|
|
@@ -3215,7 +3368,7 @@ case "$op" in
|
|
|
3215
3368
|
fi
|
|
3216
3369
|
done
|
|
3217
3370
|
if [ "$bb_rc" = 0 ]; then
|
|
3218
|
-
rm -f "
|
|
3371
|
+
rm -f "$HOST_ERR"
|
|
3219
3372
|
out="$(jq -c -s 'add // []' <<<"$out")"
|
|
3220
3373
|
# THE BRANCH ARM CARRIES `mergeCommit` TOO, and it is the path that
|
|
3221
3374
|
# matters most: `plot-pr-state.sh:33` asks `pr-state "idea/${SLUG}"` —
|
|
@@ -3229,7 +3382,7 @@ case "$op" in
|
|
|
3229
3382
|
| if .==null then {number:0,state:"NONE",draft:false,url:"",mergeCommit:""}
|
|
3230
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"
|
|
3231
3384
|
else
|
|
3232
|
-
err="$(cat "
|
|
3385
|
+
err="$(cat "$HOST_ERR" 2>/dev/null)"; rm -f "$HOST_ERR"
|
|
3233
3386
|
host_miss_or_fail "$err" '{"number":0,"state":"NONE","draft":false,"url":"","mergeCommit":""}' || exit $?
|
|
3234
3387
|
fi
|
|
3235
3388
|
fi
|
|
@@ -3261,15 +3414,15 @@ case "$op" in
|
|
|
3261
3414
|
# --state all, because a merged PR reports CLOSED and the default `open`
|
|
3262
3415
|
# would hide every one of them. --limit 100 rather than 1: the newest PR
|
|
3263
3416
|
# is not the merge, exactly as the state is not the merge.
|
|
3264
|
-
if out="$(gh ${repo_args[@]+"${repo_args[@]}"} pr list --head "$ref" --state all --limit 100 --json mergedAt 2
|
|
3265
|
-
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"
|
|
3266
3419
|
if jq -e 'any(.[]; .mergedAt != null)' >/dev/null 2>&1 <<<"$out"; then
|
|
3267
3420
|
echo "merged"
|
|
3268
3421
|
else
|
|
3269
3422
|
echo "not-merged"
|
|
3270
3423
|
fi
|
|
3271
3424
|
else
|
|
3272
|
-
err="$(cat "
|
|
3425
|
+
err="$(cat "$HOST_ERR" 2>/dev/null)"; rm -f "$HOST_ERR"
|
|
3273
3426
|
# A LOOKUP MISS IS AN ANSWER — the branch has no PR, so nothing merged.
|
|
3274
3427
|
# Anything else is the host failing to be asked, and that is `unknown`
|
|
3275
3428
|
# rather than exit 3: the caller asked a question with a third value
|
|
@@ -3288,15 +3441,15 @@ case "$op" in
|
|
|
3288
3441
|
# exposes no `mergedAt`, so this is the closest fact the backend holds,
|
|
3289
3442
|
# and it is a positive statement about the merge rather than an inference
|
|
3290
3443
|
# from CLOSED — `DECLINED` is bb's closed-unmerged word and is distinct.
|
|
3291
|
-
if out="$(bb ${repo_args[@]+"${repo_args[@]}"} pr list --state merged --json 2
|
|
3292
|
-
rm -f "
|
|
3444
|
+
if out="$(bb ${repo_args[@]+"${repo_args[@]}"} pr list --state merged --json 2>"$HOST_ERR")"; then
|
|
3445
|
+
rm -f "$HOST_ERR"
|
|
3293
3446
|
if jq -e --arg b "$ref" 'any(.[]; .source.branch.name==$b)' >/dev/null 2>&1 <<<"$out"; then
|
|
3294
3447
|
echo "merged"
|
|
3295
3448
|
else
|
|
3296
3449
|
echo "not-merged"
|
|
3297
3450
|
fi
|
|
3298
3451
|
else
|
|
3299
|
-
err="$(cat "
|
|
3452
|
+
err="$(cat "$HOST_ERR" 2>/dev/null)"; rm -f "$HOST_ERR"
|
|
3300
3453
|
if [ -z "$err" ] || is_lookup_miss "$err"; then
|
|
3301
3454
|
echo "not-merged"
|
|
3302
3455
|
else
|
|
@@ -3336,15 +3489,15 @@ case "$op" in
|
|
|
3336
3489
|
esac
|
|
3337
3490
|
done
|
|
3338
3491
|
if [ "$be" = "github" ]; then
|
|
3339
|
-
if out="$(gh ${repo_args[@]+"${repo_args[@]}"} pr list --head "$ref" --state all --limit 100 --json mergedAt,mergeCommit 2
|
|
3340
|
-
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"
|
|
3341
3494
|
# The FIRST merged PR carrying a sha. A branch may hold several merged
|
|
3342
3495
|
# PRs; each names its own merge commit, and any of them is a commit that
|
|
3343
3496
|
# landed this branch's work.
|
|
3344
3497
|
jq -r 'map(select(.mergedAt != null and .mergeCommit != null))
|
|
3345
3498
|
| map(.mergeCommit.oid) | first // empty' <<<"$out"
|
|
3346
3499
|
else
|
|
3347
|
-
err="$(cat "
|
|
3500
|
+
err="$(cat "$HOST_ERR" 2>/dev/null)"; rm -f "$HOST_ERR"
|
|
3348
3501
|
# A lookup miss is an answer: no PR, so no merge commit. Anything else
|
|
3349
3502
|
# is the question failing, and silence must not read as "carried
|
|
3350
3503
|
# nothing" — the caller distinguishes the two by the exit code.
|
|
@@ -3358,12 +3511,12 @@ case "$op" in
|
|
|
3358
3511
|
else
|
|
3359
3512
|
bb_require_json
|
|
3360
3513
|
# Bitbucket names the merge commit `merge_commit.hash` on a merged PR.
|
|
3361
|
-
if out="$(bb ${repo_args[@]+"${repo_args[@]}"} pr list --state merged --json 2
|
|
3362
|
-
rm -f "
|
|
3514
|
+
if out="$(bb ${repo_args[@]+"${repo_args[@]}"} pr list --state merged --json 2>"$HOST_ERR")"; then
|
|
3515
|
+
rm -f "$HOST_ERR"
|
|
3363
3516
|
jq -r --arg b "$ref" 'map(select(.source.branch.name==$b))
|
|
3364
3517
|
| map(.merge_commit.hash // empty) | first // empty' <<<"$out"
|
|
3365
3518
|
else
|
|
3366
|
-
err="$(cat "
|
|
3519
|
+
err="$(cat "$HOST_ERR" 2>/dev/null)"; rm -f "$HOST_ERR"
|
|
3367
3520
|
if [ -z "$err" ] || is_lookup_miss "$err"; then
|
|
3368
3521
|
:
|
|
3369
3522
|
else
|
|
@@ -3712,16 +3865,17 @@ case "$op" in
|
|
|
3712
3865
|
# integration GitHub uses, which is why it is ABOVE the backend branch.
|
|
3713
3866
|
# Bitbucket's `unknown` becomes a real value.
|
|
3714
3867
|
#
|
|
3715
|
-
#
|
|
3716
|
-
#
|
|
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
|
|
3717
3871
|
# would serve a short page as if it were the whole set — the quiet wrong
|
|
3718
3872
|
# answer this adapter refuses elsewhere. So it is dropped AND said.
|
|
3719
3873
|
# A SWEEP IS NOT A PAGE AND OWES NO SUCH WARNING. `--limit` bounds a
|
|
3720
3874
|
# listing; a per-branch query returns that branch's pull requests and
|
|
3721
3875
|
# nothing was capped, so the notice would describe a truncation that did
|
|
3722
3876
|
# not happen. Said only for the listing it is about.
|
|
3723
|
-
if [ -n "$limit" ] && [ -z "$branches" ]; then
|
|
3724
|
-
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
|
|
3725
3879
|
fi
|
|
3726
3880
|
# Establish that bb supports --json BEFORE calling it — Done-when 5.
|
|
3727
3881
|
bb_require_json
|
|
@@ -3738,25 +3892,23 @@ case "$op" in
|
|
|
3738
3892
|
# ways, which is how the six hand-applied fixes `pr_list_call` warns about
|
|
3739
3893
|
# began. One assignment here; the sites are untouched but for this word.
|
|
3740
3894
|
PR_LIST_BRANCHES="$branches"
|
|
3741
|
-
# THE WINDOW REACHES
|
|
3742
|
-
#
|
|
3743
|
-
#
|
|
3744
|
-
#
|
|
3745
|
-
#
|
|
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.
|
|
3746
3900
|
#
|
|
3747
|
-
#
|
|
3748
|
-
#
|
|
3749
|
-
#
|
|
3750
|
-
#
|
|
3751
|
-
#
|
|
3752
|
-
# 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.
|
|
3753
3906
|
PR_LIST_SINCE="$since"
|
|
3754
|
-
if [ -n "$since" ] && [ -z "$branches" ]; then
|
|
3755
|
-
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
|
|
3756
|
-
PR_LIST_SINCE=""
|
|
3757
|
-
fi
|
|
3758
3907
|
PR_SWEEP_ASKED=0
|
|
3759
|
-
|
|
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[@]}"})
|
|
3760
3912
|
if [ -n "$branches" ]; then
|
|
3761
3913
|
# A SWEEP'S COST IS THE CALLER'S WORKING SET, and it is reported so the
|
|
3762
3914
|
# caller can check the claim it is about to be handed. Branches × states
|
|
@@ -3764,6 +3916,8 @@ case "$op" in
|
|
|
3764
3916
|
# that answer for 50 of 902 rows.
|
|
3765
3917
|
for _b in $branches; do PR_SWEEP_ASKED=$((PR_SWEEP_ASKED + 1)); done
|
|
3766
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[@]}"})
|
|
3767
3921
|
fi
|
|
3768
3922
|
if [ "$rich" = 1 ]; then
|
|
3769
3923
|
if [ "$ci" = "jenkins" ]; then
|
|
@@ -4194,13 +4348,13 @@ case "$op" in
|
|
|
4194
4348
|
# unstarted by the only vocabulary GitHub has.
|
|
4195
4349
|
gh_state="open"
|
|
4196
4350
|
if out="$(gh issue list --state "$gh_state" ${limit_args[@]+"${limit_args[@]}"} \
|
|
4197
|
-
--json number,title,url,createdAt 2
|
|
4198
|
-
rm -f "
|
|
4351
|
+
--json number,title,url,createdAt 2>"$HOST_ERR")"; then
|
|
4352
|
+
rm -f "$HOST_ERR"
|
|
4199
4353
|
jq -c --arg status "$gh_state" \
|
|
4200
4354
|
'.[] | {number:.number,title:.title,url:(.url // ""),createdAt:(.createdAt // ""),
|
|
4201
4355
|
status:$status,statusCategory:(if $status == "open" then "To Do" else "Done" end)}' <<<"$out"
|
|
4202
4356
|
else
|
|
4203
|
-
err="$(cat "
|
|
4357
|
+
err="$(cat "$HOST_ERR" 2>/dev/null)"; rm -f "$HOST_ERR"
|
|
4204
4358
|
# NO empty-list fallback. `host_miss_or_fail` exists for a lookup whose
|
|
4205
4359
|
# subject is absent — one PR that does not exist. A LIST has no absent
|
|
4206
4360
|
# subject: if the call failed, the answer is unknown, and printing
|
|
@@ -4314,11 +4468,11 @@ case "$op" in
|
|
|
4314
4468
|
url: ($base + "/browse/" + .key)
|
|
4315
4469
|
}'
|
|
4316
4470
|
elif [ "$be" = "github" ]; then
|
|
4317
|
-
if out="$(gh issue view "$num" --json number,title,body,url 2
|
|
4318
|
-
rm -f "
|
|
4471
|
+
if out="$(gh issue view "$num" --json number,title,body,url 2>"$HOST_ERR")"; then
|
|
4472
|
+
rm -f "$HOST_ERR"
|
|
4319
4473
|
jq -c '{number:.number,title:(.title // ""),body:(.body // ""),url:(.url // "")}' <<<"$out"
|
|
4320
4474
|
else
|
|
4321
|
-
err="$(cat "
|
|
4475
|
+
err="$(cat "$HOST_ERR" 2>/dev/null)"; rm -f "$HOST_ERR"
|
|
4322
4476
|
# NO miss/fail split here, and that is deliberate. `host_miss_or_fail`
|
|
4323
4477
|
# exists where an absent subject is a NORMAL answer — a branch with no
|
|
4324
4478
|
# PR. An issue number reaching this op was read off `issue-list`
|
|
@@ -4466,8 +4620,8 @@ case "$op" in
|
|
|
4466
4620
|
# This op REPORTS; it does not decide. A caller that wants to fall back when
|
|
4467
4621
|
# one budget is spent reads this, compares remaining to zero, and acts.
|
|
4468
4622
|
if [ "$be" = "github" ]; then
|
|
4469
|
-
if out="$(gh api rate_limit 2
|
|
4470
|
-
rm -f "
|
|
4623
|
+
if out="$(gh api rate_limit 2>"$HOST_ERR")"; then
|
|
4624
|
+
rm -f "$HOST_ERR"
|
|
4471
4625
|
# The payload has `resources.graphql` and `resources.core`, each with
|
|
4472
4626
|
# `remaining`, `limit`, and `reset`. Extract the two we care about.
|
|
4473
4627
|
jq -c '{
|
|
@@ -4483,7 +4637,7 @@ case "$op" in
|
|
|
4483
4637
|
}
|
|
4484
4638
|
}' <<<"$out"
|
|
4485
4639
|
else
|
|
4486
|
-
err="$(cat "
|
|
4640
|
+
err="$(cat "$HOST_ERR" 2>/dev/null)"; rm -f "$HOST_ERR"
|
|
4487
4641
|
# The host could not be asked. Report unknown rather than failing outright,
|
|
4488
4642
|
# because this is informational — a caller that cannot read the budget
|
|
4489
4643
|
# should proceed with the default path, not error out.
|
|
@@ -4546,7 +4700,8 @@ case "$op" in
|
|
|
4546
4700
|
# inventing a reading nobody took — the mistake `rate_limit` makes by
|
|
4547
4701
|
# answering for both at once.
|
|
4548
4702
|
if [ "$be" = "github" ]; then
|
|
4549
|
-
_hdr_tmp="
|
|
4703
|
+
_hdr_tmp=""
|
|
4704
|
+
plot_tmpfile _hdr_tmp host-limit || die3 "limit: no temp file could be made under ${TMPDIR:-/tmp}"
|
|
4550
4705
|
# `command gh`, NOT THE WRAPPER, and for the ordering reason
|
|
4551
4706
|
# `gh_api_harvest` gives: the wrapper records the moment the call returns,
|
|
4552
4707
|
# before any header has been read, so a wrapped call here would file an
|