@plot-pm/board 0.14.3 → 0.15.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 +152 -145
- package/package.json +2 -1
- package/plot-agent-manifest.sh +58 -0
- package/plot-approve.sh +23 -1
- package/plot-budget.sh +166 -1
- package/plot-config.sh +34 -1
- package/plot-deliver.sh +24 -3
- package/plot-dispatch.sh +283 -13
- package/plot-fleet-scan.sh +387 -79
- package/plot-host.sh +230 -16
- package/plot-plan-meta.sh +10 -3
- package/plot-pr-merged.sh +29 -2
- package/plot-reap.sh +152 -32
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":"..."}
|
|
@@ -61,6 +72,30 @@
|
|
|
61
72
|
# pr-ready <number> take a PR out of draft
|
|
62
73
|
# merge the PR
|
|
63
74
|
# pr-list [--state open|merged|closed|all] [--limit N] [--rich]
|
|
75
|
+
# [--since <iso>] narrows the listing to pull
|
|
76
|
+
# requests the host has seen change since that
|
|
77
|
+
# stamp. Measured 2026-09-21 on this repository:
|
|
78
|
+
# one `--state all` over 933 pull requests takes
|
|
79
|
+
# 29 811 ms with the fields the board needs, and
|
|
80
|
+
# the same call over one day takes 943 ms for 3
|
|
81
|
+
# rows — factor 32, with every expensive field
|
|
82
|
+
# still included.
|
|
83
|
+
# THE STAMP IS THE HOST'S OWN, passed through
|
|
84
|
+
# byte-for-byte: a caller that re-renders it
|
|
85
|
+
# sends its own clock's spelling, and a client
|
|
86
|
+
# two seconds fast excludes the PRs updated in
|
|
87
|
+
# that gap from every later window — forever,
|
|
88
|
+
# because the window never reopens.
|
|
89
|
+
# GITHUB NARROWS AND BITBUCKET'S BULK LISTING
|
|
90
|
+
# CANNOT. `gh pr list` takes `--search`; `bb pr
|
|
91
|
+
# list` has no query flag at all (verified
|
|
92
|
+
# against bb 1.9.0: `unknown flag: --query`), so
|
|
93
|
+
# only the per-branch sweep's REST `q=` can
|
|
94
|
+
# carry it. The bulk Bitbucket listing SAYS it
|
|
95
|
+
# could not narrow and answers in full, because
|
|
96
|
+
# a full answer reported as a delta is what
|
|
97
|
+
# would let a caller advance a watermark over a
|
|
98
|
+
# window it never applied.
|
|
64
99
|
# [--repo <owner/repo>] pins the list to ONE
|
|
65
100
|
# repository, exactly as pr-state and pr-merged
|
|
66
101
|
# do. A checkout with remotes on two hosts lets
|
|
@@ -70,7 +105,24 @@
|
|
|
70
105
|
# JSON lines: {"number":N,"title":"...",
|
|
71
106
|
# "state":"...","head":"..."}
|
|
72
107
|
# --rich adds: draft, checks, mergeable, review,
|
|
73
|
-
# url, failing_checks —
|
|
108
|
+
# url, updatedAt, failing_checks —
|
|
109
|
+
# `updatedAt` is WHEN THE HOST LAST SAW THE PR
|
|
110
|
+
# CHANGE, in the host's own words and on the
|
|
111
|
+
# host's own clock (`updatedAt` on GitHub,
|
|
112
|
+
# `updated_on` on Bitbucket), "" where the CLI
|
|
113
|
+
# omits it. It is the field a durable store
|
|
114
|
+
# advances its watermark by, and it is the
|
|
115
|
+
# host's rather than this machine's because a
|
|
116
|
+
# client clock two seconds fast would exclude
|
|
117
|
+
# every PR updated in that gap from every later
|
|
118
|
+
# window — permanently, and silently.
|
|
119
|
+
# Measured 2026-09-21: over 937 PRs,
|
|
120
|
+
# `number,updatedAt` is 4715 ms against 5417 ms
|
|
121
|
+
# for the base fields and 18842 ms for
|
|
122
|
+
# `statusCheckRollup`. Asked on --rich ONLY —
|
|
123
|
+
# the plain arm is the cheap one and no shell
|
|
124
|
+
# caller reads a watermark.
|
|
125
|
+
# `failing_checks` names
|
|
74
126
|
# WHICH checks failed, the detail `checks`
|
|
75
127
|
# collapses to one word, from the same response
|
|
76
128
|
# at no extra call; [] on bitbucket and wherever
|
|
@@ -718,6 +770,24 @@ bb_branch_query() { # $1=branch $2=adapter state; rest=global bb args → values
|
|
|
718
770
|
local _br="$1" _st="$2"; shift 2
|
|
719
771
|
local _q _path _out _rc
|
|
720
772
|
_q="state=$(url_encode "\"$(bb_query_state "$_st")\"") AND source.branch.name=$(url_encode "\"$_br\"")"
|
|
773
|
+
# THE WINDOW COMPOSES WITH THE STATE FILTER AND NEVER REPLACES IT. Bitbucket
|
|
774
|
+
# takes ONE `q=` parameter, so a second one would silently win or lose
|
|
775
|
+
# depending on the host's parsing — and either way the state clause this query
|
|
776
|
+
# is built around would be the term at risk. `AND` is the same conjunction the
|
|
777
|
+
# two terms above already use.
|
|
778
|
+
#
|
|
779
|
+
# ENCODED LIKE EVERY OTHER TERM ON THIS PATH, and an ISO stamp needs it: `:`
|
|
780
|
+
# and `-` are not in `url_encode`'s safe set, and the block header records
|
|
781
|
+
# what an unencoded value costs — `/` ends the filter early, so the query asks
|
|
782
|
+
# about one thing and answers about another.
|
|
783
|
+
#
|
|
784
|
+
# `>=` RATHER THAN `>`, and the asymmetry with the GitHub arm is deliberate.
|
|
785
|
+
# Bitbucket's `updated_on` carries microseconds and GitHub's stamp does not,
|
|
786
|
+
# so an exclusive comparison against a truncated stamp would drop a PR updated
|
|
787
|
+
# inside the same second. Re-reading one row the caller already holds costs a
|
|
788
|
+
# row; missing one costs a change nobody ever sees again.
|
|
789
|
+
[ -n "$PR_LIST_SINCE" ] \
|
|
790
|
+
&& _q="$_q AND updated_on>=$(url_encode "\"$PR_LIST_SINCE\"")"
|
|
721
791
|
# The space between the two terms is encoded too; `bb api` passes the path to
|
|
722
792
|
# curl verbatim and an unencoded space would truncate the request line.
|
|
723
793
|
_q="${_q// /%20}"
|
|
@@ -824,6 +894,19 @@ bb_branch_sweep() { # global bb args… --state <s> --json → one JSON array
|
|
|
824
894
|
# separator is git's guarantee rather than a hopeful convention.
|
|
825
895
|
PR_LIST_BRANCHES=""
|
|
826
896
|
|
|
897
|
+
# The window a sweep's query carries, the host's own stamp. Empty means ask
|
|
898
|
+
# about everything, which is what every caller predating `--since` asks.
|
|
899
|
+
#
|
|
900
|
+
# A GLOBAL FOR `PR_LIST_BRANCHES`' REASON, and it travels the same route: the
|
|
901
|
+
# `pr-list` arm sets it immediately before the call, `bb_branch_sweep` passes
|
|
902
|
+
# through without reading it, and `bb_branch_query` composes it into the one
|
|
903
|
+
# `q=` Bitbucket takes. Threading it through the sweep as an argument would mean
|
|
904
|
+
# teaching that function a parameter it only forwards, and its header already
|
|
905
|
+
# refuses the mirror of that — "teaching `pr_list_states` which of its commands
|
|
906
|
+
# is a sweep would put a backend's shape inside the one piece of this file that
|
|
907
|
+
# has none."
|
|
908
|
+
PR_LIST_SINCE=""
|
|
909
|
+
|
|
827
910
|
# How many branches the last sweep asked about, and how many answered.
|
|
828
911
|
#
|
|
829
912
|
# THE COMPLETENESS SIGNAL, AND WHY THE ARM STATES IT RATHER THAN THE SCAN
|
|
@@ -2469,6 +2552,18 @@ backend() {
|
|
|
2469
2552
|
# written beside it.
|
|
2470
2553
|
. "$here/plot-budget.sh"
|
|
2471
2554
|
|
|
2555
|
+
# THE MEMO IS SWEPT ON THE WAY OUT. `budget_rate` caches its reading under
|
|
2556
|
+
# `$PLOT_BUDGET_HOME/memo/$$` because a command substitution is a subshell and a
|
|
2557
|
+
# variable set inside one does not survive it — see the block above
|
|
2558
|
+
# `budget_rate`. A directory keyed on a pid must be removed by the process that
|
|
2559
|
+
# made it, or a long-lived machine accumulates one per `plot-host.sh` call.
|
|
2560
|
+
#
|
|
2561
|
+
# `EXIT` ALONE, deliberately. It runs on a normal return and on an uncaught
|
|
2562
|
+
# signal's default termination path is irrelevant here: the sweep is an
|
|
2563
|
+
# optimisation's housekeeping, and a cache that outlives one run costs a stale
|
|
2564
|
+
# reading at worst, which is the same staleness the memo grants by design.
|
|
2565
|
+
trap 'budget_memo_clear' EXIT
|
|
2566
|
+
|
|
2472
2567
|
# WHO IS SPENDING — read from the CLI's own config, never from an API call.
|
|
2473
2568
|
#
|
|
2474
2569
|
# `gh api user` would answer authoritatively and cost one request against the
|
|
@@ -2912,14 +3007,77 @@ case "$op" in
|
|
|
2912
3007
|
;;
|
|
2913
3008
|
|
|
2914
3009
|
default-branch)
|
|
3010
|
+
# THE HOST IS ASKED FIRST, ON BOTH BACKENDS, and `origin/HEAD` is the
|
|
3011
|
+
# fallback rather than the answer. `origin/HEAD` is a LOCAL CACHE WRITTEN AT
|
|
3012
|
+
# CLONE TIME: a default branch changed afterwards never updates it and
|
|
3013
|
+
# nothing in git notices. Reported 2026-09-24 — a clone whose GitHub default
|
|
3014
|
+
# had moved to `develop` kept `origin/HEAD → main`, the board read plans from
|
|
3015
|
+
# `origin/main`, and two of three plans went missing.
|
|
3016
|
+
#
|
|
3017
|
+
# THE BITBUCKET ARM ASKED THE CACHE FIRST UNTIL 2026-09-25, so on Bitbucket
|
|
3018
|
+
# this op answered from the very cache it exists to bypass, the two readings
|
|
3019
|
+
# could never disagree, and the defect was invisible there. Every caller —
|
|
3020
|
+
# `plot-open-pr.sh:73`, `plot-approve.sh:205`, `plot-deliver.sh:218`,
|
|
3021
|
+
# `plot-reap.sh:176`, `plot-release-refs.sh:162`, `/plot-idea:252` — wants
|
|
3022
|
+
# the branch the host calls default, which is what this now answers.
|
|
3023
|
+
#
|
|
3024
|
+
# AND THE FALLBACK WAS DEAD CODE. `git symbolic-ref … | sed … || bb …` takes
|
|
3025
|
+
# the PIPELINE's exit status, which is `sed`'s, and `sed` exits 0 on empty
|
|
3026
|
+
# input — measured 2026-09-25 against a ref that does not exist. So `bb repo
|
|
3027
|
+
# view` had never once been reached, and a Bitbucket clone with no
|
|
3028
|
+
# `origin/HEAD` printed an empty line and exited 0. Each reading is now
|
|
3029
|
+
# tested for a NON-EMPTY value of its own, never for a pipeline's status.
|
|
3030
|
+
_db=""
|
|
2915
3031
|
if [ "$be" = "github" ]; then
|
|
2916
|
-
gh repo view --json defaultBranchRef --jq '.defaultBranchRef.name'
|
|
3032
|
+
_db=$(gh repo view --json defaultBranchRef --jq '.defaultBranchRef.name' 2>/dev/null || true)
|
|
2917
3033
|
else
|
|
2918
|
-
#
|
|
2919
|
-
#
|
|
2920
|
-
|
|
2921
|
-
|
|
3034
|
+
# `--json` IS A CAPABILITY, NOT A GIVEN. Two products share the name `bb`
|
|
3035
|
+
# and craftamap's rejects the flag (see `bb_require_json`) — against it
|
|
3036
|
+
# this call emits `unknown flag: --json`, `jq` reads non-JSON, and the arm
|
|
3037
|
+
# would answer nothing while exiting 0. This call was unguarded while it
|
|
3038
|
+
# was unreachable; asking the host makes it live.
|
|
3039
|
+
bb_require_json
|
|
3040
|
+
_db=$(bb repo view --json 2>/dev/null | jq -r '.mainbranch.name // empty' 2>/dev/null || true)
|
|
2922
3041
|
fi
|
|
3042
|
+
# `null` is what `jq` prints for an absent field and it is not a branch name.
|
|
3043
|
+
[ "$_db" = "null" ] && _db=""
|
|
3044
|
+
# THE CACHE IS THE FALLBACK: a host that cannot be asked is not a reason to
|
|
3045
|
+
# answer nothing, since every caller above needs a usable branch name and
|
|
3046
|
+
# worked from this reading alone before today.
|
|
3047
|
+
[ -n "$_db" ] || _db=$(git symbolic-ref --short refs/remotes/origin/HEAD 2>/dev/null | sed 's|^origin/||')
|
|
3048
|
+
printf '%s\n' "$_db"
|
|
3049
|
+
;;
|
|
3050
|
+
|
|
3051
|
+
account)
|
|
3052
|
+
# WHO IS READING, from the same free reading `budget_account` makes. It
|
|
3053
|
+
# costs a file read and no request, which matters because the board asks on
|
|
3054
|
+
# its timer. `PLOT_BUDGET_ACCOUNT` overrides it on every backend.
|
|
3055
|
+
#
|
|
3056
|
+
# THE BITBUCKET ARM IS NOT EXPOSED. `budget_account bitbucket` answers the
|
|
3057
|
+
# remote's owner segment, which is the WORKSPACE: on a team workspace every
|
|
3058
|
+
# contributor gets the same word, and it matches no PR author. Measured
|
|
3059
|
+
# 2026-09-25, neither `bb` stores a username for free. Quatico's keeps only
|
|
3060
|
+
# the Atlassian email, in the Keychain. craftamap's keeps an email in
|
|
3061
|
+
# `configuration.toml`. A Bitbucket PR author carries a nickname, an
|
|
3062
|
+
# account id and a uuid, and never an email. So this backend has no answer,
|
|
3063
|
+
# and exit 4 says so.
|
|
3064
|
+
#
|
|
3065
|
+
# `unknown` IS NEVER PRINTED. It groups budget lines correctly and names
|
|
3066
|
+
# nobody, so an unreadable `hosts.yml` exits 3 with the reason on stderr.
|
|
3067
|
+
_acct=''
|
|
3068
|
+
if [ -n "${PLOT_BUDGET_ACCOUNT:-}" ]; then
|
|
3069
|
+
_acct="$PLOT_BUDGET_ACCOUNT"
|
|
3070
|
+
elif [ "$be" = "github" ]; then
|
|
3071
|
+
_acct="$(budget_account github)"
|
|
3072
|
+
else
|
|
3073
|
+
echo "plot-host.sh: account: $be names no signed-in user without an API request" >&2
|
|
3074
|
+
exit 4
|
|
3075
|
+
fi
|
|
3076
|
+
if [ -z "$_acct" ] || [ "$_acct" = "unknown" ]; then
|
|
3077
|
+
echo "plot-host.sh: account: no user in ${GH_CONFIG_DIR:-$HOME/.config/gh}/hosts.yml" >&2
|
|
3078
|
+
exit 3
|
|
3079
|
+
fi
|
|
3080
|
+
printf '%s\n' "$_acct"
|
|
2923
3081
|
;;
|
|
2924
3082
|
|
|
2925
3083
|
pr-state)
|
|
@@ -3302,12 +3460,22 @@ case "$op" in
|
|
|
3302
3460
|
# existing caller's result changes.
|
|
3303
3461
|
limit=""
|
|
3304
3462
|
branches=""
|
|
3463
|
+
# THE WINDOW, AND IT IS THE HOST'S OWN STAMP. Empty means ask about
|
|
3464
|
+
# everything, which is what every caller predating this asks and what a
|
|
3465
|
+
# caller with no stored watermark must ask. A caller that has one sends it
|
|
3466
|
+
# verbatim — see the header: re-rendering it is how a window closes forever.
|
|
3467
|
+
since=""
|
|
3305
3468
|
while [ $# -gt 0 ]; do
|
|
3306
3469
|
case "$1" in
|
|
3307
3470
|
--state) state="${2:?}"; shift 2 ;;
|
|
3308
3471
|
--limit) limit="${2:?}"; shift 2 ;;
|
|
3309
3472
|
--rich) rich=1; shift ;;
|
|
3310
3473
|
--repo) repo_args=(-R "${2:?}"); shift 2 ;;
|
|
3474
|
+
# `${2:?}` REFUSES AN EMPTY VALUE, and that is the point rather than
|
|
3475
|
+
# boilerplate. `--since ""` would reach GitHub as `--search "updated:>"`,
|
|
3476
|
+
# a syntax error the host may answer with everything or with nothing —
|
|
3477
|
+
# and a caller whose watermark was null would send exactly that.
|
|
3478
|
+
--since) since="${2:?}"; shift 2 ;;
|
|
3311
3479
|
# THE BRANCHES THE CALLER TRACKS, repeatable, and OPT-IN. Given any,
|
|
3312
3480
|
# the Bitbucket arm sweeps the REST endpoint once per branch per state
|
|
3313
3481
|
# instead of listing the repository; given none, every existing caller
|
|
@@ -3328,6 +3496,23 @@ case "$op" in
|
|
|
3328
3496
|
done
|
|
3329
3497
|
limit_args=()
|
|
3330
3498
|
[ -n "$limit" ] && limit_args=(--limit "$limit")
|
|
3499
|
+
# THE WINDOW AS GITHUB TAKES IT, built once and appended at all three call
|
|
3500
|
+
# sites — the same argument `pr_list_call`'s header makes about the six
|
|
3501
|
+
# hand-applied fixes: three sites each composing their own search string is
|
|
3502
|
+
# three places for the next window's shape to drift.
|
|
3503
|
+
#
|
|
3504
|
+
# `updated:>` is EXCLUSIVE, and that is the safe direction here. The
|
|
3505
|
+
# watermark is the stamp of a row the caller has ALREADY stored, so
|
|
3506
|
+
# excluding it re-asks nothing; including it would re-fetch that row every
|
|
3507
|
+
# pass for no new information. A PR updated in the same second as the
|
|
3508
|
+
# watermark is the one this can miss, and the periodic full read the caller
|
|
3509
|
+
# is required to keep making is what corrects it.
|
|
3510
|
+
#
|
|
3511
|
+
# THE STAMP IS NOT QUOTED INSIDE THE QUERY. `gh` sends `--search`'s value as
|
|
3512
|
+
# one API parameter and an ISO-8601 stamp carries no space, so a quote would
|
|
3513
|
+
# travel to GitHub as part of the term and match nothing.
|
|
3514
|
+
search_args=()
|
|
3515
|
+
[ -n "$since" ] && search_args=(--search "updated:>$since")
|
|
3331
3516
|
|
|
3332
3517
|
# --- Jenkins CI integration (orthogonal to Git host) ---
|
|
3333
3518
|
# When `CI: jenkins` is configured, build status comes from Jenkins rather
|
|
@@ -3438,8 +3623,8 @@ case "$op" in
|
|
|
3438
3623
|
# $jstatus != "ok" → Jenkins could not answer; every row `unknown`.
|
|
3439
3624
|
# $jentry == null → the branch has no Jenkins job; `none`.
|
|
3440
3625
|
# otherwise → the joined colour's `checks`, job named on fail.
|
|
3441
|
-
_gh_raw="$(pr_list_call gh ${repo_args[@]+"${repo_args[@]}"} pr list --state "$state" ${limit_args[@]+"${limit_args[@]}"} \
|
|
3442
|
-
--json number,title,state,headRefName,isDraft,mergeable,mergeStateStatus,reviewDecision,url)" || exit $?
|
|
3626
|
+
_gh_raw="$(pr_list_call gh ${repo_args[@]+"${repo_args[@]}"} pr list --state "$state" ${limit_args[@]+"${limit_args[@]}"} ${search_args[@]+"${search_args[@]}"} \
|
|
3627
|
+
--json number,title,state,headRefName,isDraft,mergeable,mergeStateStatus,reviewDecision,url,updatedAt,author)" || exit $?
|
|
3443
3628
|
pr_list_report_truncation github "$limit" "$state" \
|
|
3444
3629
|
"$(jq 'length' <<<"$_gh_raw" 2>/dev/null || echo 0)"
|
|
3445
3630
|
printf '%s' "$_gh_raw" \
|
|
@@ -3459,6 +3644,8 @@ case "$op" in
|
|
|
3459
3644
|
else "unknown" end),
|
|
3460
3645
|
review:(.reviewDecision // ""),
|
|
3461
3646
|
url:.url,
|
|
3647
|
+
updatedAt:(.updatedAt // ""),
|
|
3648
|
+
author:(.author.login // ""),
|
|
3462
3649
|
failing_checks:(
|
|
3463
3650
|
if $jentry != null and $jentry.checks == "failing"
|
|
3464
3651
|
then [$jentry.job]
|
|
@@ -3467,8 +3654,8 @@ case "$op" in
|
|
|
3467
3654
|
}'
|
|
3468
3655
|
else
|
|
3469
3656
|
# GitHub without Jenkins (or Jenkins not configured): use GitHub rollup
|
|
3470
|
-
_gh_raw="$(pr_list_call gh ${repo_args[@]+"${repo_args[@]}"} pr list --state "$state" ${limit_args[@]+"${limit_args[@]}"} \
|
|
3471
|
-
--json number,title,state,headRefName,isDraft,statusCheckRollup,mergeable,mergeStateStatus,reviewDecision,url)" || exit $?
|
|
3657
|
+
_gh_raw="$(pr_list_call gh ${repo_args[@]+"${repo_args[@]}"} pr list --state "$state" ${limit_args[@]+"${limit_args[@]}"} ${search_args[@]+"${search_args[@]}"} \
|
|
3658
|
+
--json number,title,state,headRefName,isDraft,statusCheckRollup,mergeable,mergeStateStatus,reviewDecision,url,updatedAt,author)" || exit $?
|
|
3472
3659
|
pr_list_report_truncation github "$limit" "$state" \
|
|
3473
3660
|
"$(jq 'length' <<<"$_gh_raw" 2>/dev/null || echo 0)"
|
|
3474
3661
|
printf '%s' "$_gh_raw" \
|
|
@@ -3490,6 +3677,8 @@ case "$op" in
|
|
|
3490
3677
|
else "unknown" end),
|
|
3491
3678
|
review:(.reviewDecision // ""),
|
|
3492
3679
|
url:.url,
|
|
3680
|
+
updatedAt:(.updatedAt // ""),
|
|
3681
|
+
author:(.author.login // ""),
|
|
3493
3682
|
failing_checks:[
|
|
3494
3683
|
.statusCheckRollup[]? | select((if (.conclusion // "") != "" then .conclusion else (.status // .state) end) as $c
|
|
3495
3684
|
| $c=="FAILURE" or $c=="ERROR" or $c=="CANCELLED"
|
|
@@ -3498,14 +3687,20 @@ case "$op" in
|
|
|
3498
3687
|
}'
|
|
3499
3688
|
fi
|
|
3500
3689
|
else
|
|
3501
|
-
_gh_raw="$(pr_list_call gh ${repo_args[@]+"${repo_args[@]}"} pr list --state "$state" ${limit_args[@]+"${limit_args[@]}"} \
|
|
3502
|
-
--json number,title,state,headRefName)" || exit $?
|
|
3690
|
+
_gh_raw="$(pr_list_call gh ${repo_args[@]+"${repo_args[@]}"} pr list --state "$state" ${limit_args[@]+"${limit_args[@]}"} ${search_args[@]+"${search_args[@]}"} \
|
|
3691
|
+
--json number,title,state,headRefName,author)" || exit $?
|
|
3503
3692
|
pr_list_report_truncation github "$limit" "$state" \
|
|
3504
3693
|
"$(jq 'length' <<<"$_gh_raw" 2>/dev/null || echo 0)"
|
|
3505
3694
|
printf '%s' "$_gh_raw" \
|
|
3506
|
-
| jq -c '.[] | {number:.number,title:.title,state:.state,head:.headRefName}'
|
|
3695
|
+
| jq -c '.[] | {number:.number,title:.title,state:.state,head:.headRefName,author:(.author.login // "")}'
|
|
3507
3696
|
fi
|
|
3508
3697
|
else
|
|
3698
|
+
# `author` IS THE `nickname`, the one handle field a Bitbucket user
|
|
3699
|
+
# object carries. Measured 2026-09-25 against bb 1.9.0: `author` holds
|
|
3700
|
+
# `display_name`, `nickname`, `account_id` and `uuid` and no username,
|
|
3701
|
+
# and on a Quatico workspace `nickname` equals the display name. GitHub's
|
|
3702
|
+
# arm reads `.author.login`. Both answer `""` where the host omits it.
|
|
3703
|
+
#
|
|
3509
3704
|
# Bitbucket carries no check rollup through `bb pr list`, and no
|
|
3510
3705
|
# mergeability verdict either. Rather than guess, --rich reports
|
|
3511
3706
|
# checks:"unknown" and mergeable:"unknown" — a consumer must render those
|
|
@@ -3543,6 +3738,23 @@ case "$op" in
|
|
|
3543
3738
|
# ways, which is how the six hand-applied fixes `pr_list_call` warns about
|
|
3544
3739
|
# began. One assignment here; the sites are untouched but for this word.
|
|
3545
3740
|
PR_LIST_BRANCHES="$branches"
|
|
3741
|
+
# THE WINDOW REACHES THE SWEEP AND NOT THE LISTING, and the asymmetry is
|
|
3742
|
+
# the CLI's rather than a choice. `bb pr list` takes `--state`, `--author`,
|
|
3743
|
+
# `--json` and `--jq` and no query flag at all — verified against bb 1.9.0,
|
|
3744
|
+
# which answers `unknown flag: --query` — so the only Bitbucket path that
|
|
3745
|
+
# can carry `updated_on` is `bb_branch_query`'s own REST `q=`.
|
|
3746
|
+
#
|
|
3747
|
+
# SAID RATHER THAN SWALLOWED, for the reason `--limit` two blocks up is
|
|
3748
|
+
# said: a caller that asked for a window and got a full listing must not
|
|
3749
|
+
# read the answer as a delta. It would advance its watermark over a window
|
|
3750
|
+
# it never applied — harmless this pass, since a full listing holds every
|
|
3751
|
+
# row a narrow one would, and wrong the moment the caller uses the flag to
|
|
3752
|
+
# decide whether its answer was complete.
|
|
3753
|
+
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
|
|
3546
3758
|
PR_SWEEP_ASKED=0
|
|
3547
3759
|
bb_cmd=(bb ${repo_args[@]+"${repo_args[@]}"} pr list)
|
|
3548
3760
|
if [ -n "$branches" ]; then
|
|
@@ -3574,6 +3786,8 @@ case "$op" in
|
|
|
3574
3786
|
mergeable:"unknown",
|
|
3575
3787
|
review:"",
|
|
3576
3788
|
url:(.links.html.href // ""),
|
|
3789
|
+
updatedAt:(.updated_on // ""),
|
|
3790
|
+
author:(.author.nickname // ""),
|
|
3577
3791
|
failing_checks:(
|
|
3578
3792
|
if $jentry != null and $jentry.checks == "failing"
|
|
3579
3793
|
then [$jentry.job]
|
|
@@ -3584,13 +3798,13 @@ case "$op" in
|
|
|
3584
3798
|
# Bitbucket without Jenkins: checks remain unknown
|
|
3585
3799
|
PR_LIST_JQ_ARGS=()
|
|
3586
3800
|
pr_list_states bitbucket "$limit" "$bb_states" \
|
|
3587
|
-
'.[] | {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 // ""),failing_checks:[]}' \
|
|
3801
|
+
'.[] | {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:[]}' \
|
|
3588
3802
|
"${bb_cmd[@]}" || exit $?
|
|
3589
3803
|
fi
|
|
3590
3804
|
else
|
|
3591
3805
|
PR_LIST_JQ_ARGS=()
|
|
3592
3806
|
pr_list_states bitbucket "$limit" "$bb_states" \
|
|
3593
|
-
'.[] | {number:.id,title:.title,state:(if .state=="DECLINED" then "CLOSED" else .state end),head:.source.branch.name}' \
|
|
3807
|
+
'.[] | {number:.id,title:.title,state:(if .state=="DECLINED" then "CLOSED" else .state end),head:.source.branch.name,author:(.author.nickname // "")}' \
|
|
3594
3808
|
"${bb_cmd[@]}" || exit $?
|
|
3595
3809
|
fi
|
|
3596
3810
|
fi
|
package/plot-plan-meta.sh
CHANGED
|
@@ -107,8 +107,9 @@
|
|
|
107
107
|
# placeholder
|
|
108
108
|
# story story slug the plan belongs to (`## Status` `Story:` or
|
|
109
109
|
# front matter `story:`); "" if absent or a placeholder
|
|
110
|
-
# assignee github handle from
|
|
111
|
-
#
|
|
110
|
+
# assignee github handle from front matter `assignee:`, else the
|
|
111
|
+
# `## Approval` `Assignee:` line, else the `## Status`
|
|
112
|
+
# `Assignee:` line; "" if absent or a placeholder
|
|
112
113
|
# branches branch names, sorted and unique, read from EITHER spelling:
|
|
113
114
|
# the old `## Branches` section (a LIST ITEM whose first token
|
|
114
115
|
# is the backtick-quoted name, matching the known prefixes) OR
|
|
@@ -415,7 +416,7 @@ function reset_state() {
|
|
|
415
416
|
fm_delivered = ""; fm_design = ""
|
|
416
417
|
fm_rounds = ""
|
|
417
418
|
canon_state = ""; canon_phase = ""; canon_type = ""
|
|
418
|
-
canon_sprint = ""; canon_story = ""; canon_assignee = ""
|
|
419
|
+
canon_sprint = ""; canon_story = ""; canon_assignee = ""; status_assignee = ""
|
|
419
420
|
canon_review = ""; canon_impl = ""; canon_approved = ""; canon_released = ""
|
|
420
421
|
canon_delivered = ""; canon_design = ""
|
|
421
422
|
canon_rounds = ""
|
|
@@ -478,6 +479,9 @@ function emit_record( fmt, praw, palt_raw, traw, title, sprint, story, assigne
|
|
|
478
479
|
sprint = strip_placeholder((fm_sprint != "") ? fm_sprint : canon_sprint)
|
|
479
480
|
story = strip_placeholder((fm_story != "") ? fm_story : canon_story)
|
|
480
481
|
assignee = strip_placeholder((fm_assignee != "") ? fm_assignee : canon_assignee)
|
|
482
|
+
# `## Status` is the section both templates offer; `## Approval` is older and
|
|
483
|
+
# outranks it, so a plan writing both keeps the answer it parsed to before.
|
|
484
|
+
if (fm_assignee == "" && assignee == "") assignee = status_assignee
|
|
481
485
|
review = strip_placeholder((fm_review != "") ? fm_review : canon_review)
|
|
482
486
|
impl = strip_placeholder((fm_impl != "") ? fm_impl : canon_impl)
|
|
483
487
|
design = strip_placeholder((fm_design != "") ? fm_design : canon_design)
|
|
@@ -823,6 +827,9 @@ section == "status" {
|
|
|
823
827
|
else if (lower ~ /^[ \t]*[-*]?[ \t]*\**issue[:*]/ && canon_issue == "") canon_issue = val_after_colon($0)
|
|
824
828
|
else if (lower ~ /^[ \t]*[-*]?[ \t]*\**review[:*]/ && canon_review == "") canon_review = val_after_colon($0)
|
|
825
829
|
else if (lower ~ /^[ \t]*[-*]?[ \t]*\**impl[:*]/ && canon_impl == "") canon_impl = val_after_colon($0)
|
|
830
|
+
# Its own slot, not `canon_assignee`: the `## Approval` line outranks this one
|
|
831
|
+
# in `emit_record`, whichever section comes first.
|
|
832
|
+
else if (lower ~ /^[ \t]*[-*]?[ \t]*\**assignee[:*]/ && status_assignee == "") status_assignee = strip_placeholder(val_after_colon($0))
|
|
826
833
|
# EACH TAKES THE FIRST LINE THAT CARRIES A VALUE, NOT THE FIRST LINE.
|
|
827
834
|
#
|
|
828
835
|
# A plan may hold both a record and an unfilled placeholder for the same
|
package/plot-pr-merged.sh
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# Plot helper: the ONE answer to "did the host merge ANY PR for this branch?"
|
|
3
3
|
#
|
|
4
|
-
# SOURCED, NOT RUN. `. "$script_dir/plot-pr-merged.sh"` defines `pr_merged
|
|
5
|
-
# `pr_open`; the file does nothing else on load. That is what makes sourcing it
|
|
4
|
+
# SOURCED, NOT RUN. `. "$script_dir/plot-pr-merged.sh"` defines `pr_merged`,
|
|
5
|
+
# `pr_open` and `pr_merged_heads`; the file does nothing else on load. That is what makes sourcing it
|
|
6
6
|
# safe, and it is the same shape — and the same reason — as
|
|
7
7
|
# `plot-worker-state.sh`: the logic could not simply stay in `plot-reap.sh`,
|
|
8
8
|
# because that file parses `$@` and `exit 2`s on an unknown argument at load
|
|
@@ -178,3 +178,30 @@ pr_open() {
|
|
|
178
178
|
answer=$(_plot_landed none "$(_plot_open_lookup "$1")") || return 1
|
|
179
179
|
case "$answer" in *" open-pr") return 0 ;; *) return 1 ;; esac
|
|
180
180
|
}
|
|
181
|
+
|
|
182
|
+
# Which commits did the host merge for this branch? One `headRefOid` per merged
|
|
183
|
+
# PR, one per line.
|
|
184
|
+
#
|
|
185
|
+
# A THIRD QUESTION, asked by `plot-reap.sh` only. After a squash merge the
|
|
186
|
+
# host deletes the branch (`delete_branch_on_merge`), `git fetch --prune`
|
|
187
|
+
# drops the remote-tracking ref, and the branch's own commits are then on no
|
|
188
|
+
# remote ref at all. So "which commits did no remote ever hold?" needs the
|
|
189
|
+
# head the host merged: a commit reachable from it was pushed; a commit beyond
|
|
190
|
+
# it exists only on the desk.
|
|
191
|
+
#
|
|
192
|
+
# Returns 1 when the host cannot be asked. The caller reads that as
|
|
193
|
+
# `unknown`, which keeps the desk: here a failure to observe is the case that
|
|
194
|
+
# loses work. It prints nothing and returns 0 when the host answered and no PR
|
|
195
|
+
# merged.
|
|
196
|
+
pr_merged_heads() {
|
|
197
|
+
local br="$1" out
|
|
198
|
+
command -v gh >/dev/null 2>&1 || return 1
|
|
199
|
+
out=$(gh pr list --head "$br" --state all --limit 100 --json mergedAt,headRefOid 2>/dev/null) \
|
|
200
|
+
|| return 1
|
|
201
|
+
printf '%s' "$out" | node -e '
|
|
202
|
+
let s = "";
|
|
203
|
+
process.stdin.on("data", (d) => { s += d; }).on("end", () => {
|
|
204
|
+
const rows = JSON.parse(s);
|
|
205
|
+
for (const r of rows) if (r.mergedAt && r.headRefOid) console.log(r.headRefOid);
|
|
206
|
+
});' 2>/dev/null || return 1
|
|
207
|
+
}
|