@plot-pm/board 0.14.2 → 0.14.3
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 +121 -120
- package/package.json +1 -1
- package/plot-deliver.sh +14 -8
- package/plot-dispatch.sh +54 -46
- package/plot-fleet-scan.sh +134 -7
- package/plot-host.sh +591 -32
- package/plot-plan-meta.sh +23 -7
package/plot-host.sh
CHANGED
|
@@ -534,6 +534,405 @@ pr_list_call() { # "$@"=the host command → payload on stdout, or dies
|
|
|
534
534
|
printf '%s' "$out"
|
|
535
535
|
}
|
|
536
536
|
|
|
537
|
+
# The exit code for an answer that is INCOMPLETE rather than absent.
|
|
538
|
+
#
|
|
539
|
+
# SEVEN, BECAUSE THE VOCABULARY IS FULL BELOW IT. `pr_list_failed` spends 6
|
|
540
|
+
# (burst refusal), 5 (throttled) and 3 (everything else); 4 is the backend
|
|
541
|
+
# having no such capability; 1 is a refusal before any call; 0 is a whole
|
|
542
|
+
# answer. Two is left alone deliberately — it is bash's own conventional code
|
|
543
|
+
# for a misused builtin, and a partial answer sharing it could not be told from
|
|
544
|
+
# a shell-level fault the script never intended.
|
|
545
|
+
#
|
|
546
|
+
# NON-ZERO IS THE LOAD-BEARING PART. `plot-fleet-scan.sh:675` reads this code
|
|
547
|
+
# DIRECTLY, not through the transport, and branches on `rc -ne 0`. Exiting 0 on
|
|
548
|
+
# a partial answer would set `HOST_VERDICT=ok` over a page missing a whole
|
|
549
|
+
# state — a complete reading reported over an incomplete one, which is the
|
|
550
|
+
# quiet wrong answer this adapter refuses everywhere else. It would also
|
|
551
|
+
# re-create precisely the state fixed on 2026-08-30, when `pr-list` swallowed
|
|
552
|
+
# its own failure and exited 0 with empty stdout.
|
|
553
|
+
PR_LIST_PARTIAL_RC=7
|
|
554
|
+
|
|
555
|
+
# Run ONE `pr-list` over several host states, printing what answered.
|
|
556
|
+
#
|
|
557
|
+
# THE BITBUCKET ASYMMETRY THIS EXISTS FOR. `bb pr list` has no `all` state, so
|
|
558
|
+
# `bb_states_for all` expands to three and the arm must call `bb` once per
|
|
559
|
+
# state. GitHub takes `--state all` in a single call and can never reach this
|
|
560
|
+
# shape: there, a failure means nothing was printed.
|
|
561
|
+
#
|
|
562
|
+
# ONE MECHANISM FOR THREE CALL SITES, and that is the point rather than a
|
|
563
|
+
# tidy-up — `pr_list_call`'s own header makes the argument: *"a fix applied by
|
|
564
|
+
# hand six times is a fix that drifts, and the arm that drifts is the one
|
|
565
|
+
# nobody's repo exercises."* The three sites (rich+Jenkins, rich, plain) differ
|
|
566
|
+
# ONLY in the jq program they pipe the payload through, so that program and its
|
|
567
|
+
# arguments are what this takes.
|
|
568
|
+
#
|
|
569
|
+
# WHY THE LOOP CANNOT KEEP `|| exit $?`. That propagation is not a style tic:
|
|
570
|
+
# `pr_list_call` is invoked in a command substitution — a subshell — so the
|
|
571
|
+
# `exit` inside `pr_list_failed` leaves only that subshell, and without the
|
|
572
|
+
# propagation the outer script carries on with an empty payload and jq emits
|
|
573
|
+
# nothing. Collecting across states means the first failure can no longer end
|
|
574
|
+
# the run, so the subshell's exit code is captured and classified instead: a
|
|
575
|
+
# non-zero rc is THIS STATE FAILED, which is a different fact from this state
|
|
576
|
+
# returning an empty list.
|
|
577
|
+
#
|
|
578
|
+
# WHAT IT EXITS WITH:
|
|
579
|
+
# 0 every state answered.
|
|
580
|
+
# PR_LIST_PARTIAL_RC some answered and some did not — the rows of those
|
|
581
|
+
# that answered are on stdout, and the failures are
|
|
582
|
+
# named on stderr.
|
|
583
|
+
# the first failure's rc NO state answered. A total outage keeps the code it
|
|
584
|
+
# has always had (3, 5 or 6 by kind), so a genuine
|
|
585
|
+
# outage can never read as a partial page.
|
|
586
|
+
#
|
|
587
|
+
# A SINGLE-STATE CALL HAS NO PARTIAL ANSWER TO REPORT. With one state asked,
|
|
588
|
+
# "some answered and some did not" is unreachable by construction: either the
|
|
589
|
+
# one state answered (0) or none did (its own code). The counting below gives
|
|
590
|
+
# that for free rather than by a special case.
|
|
591
|
+
# TWO VARIADIC LISTS, ONE `"$@"`. The host command and the jq arguments are
|
|
592
|
+
# both open-ended, and bash has one positional array — so the jq side travels
|
|
593
|
+
# in this global, set by the caller immediately before the call. It is read
|
|
594
|
+
# once per state and never written here.
|
|
595
|
+
PR_LIST_JQ_ARGS=()
|
|
596
|
+
|
|
597
|
+
# --- the per-branch sweep (#333) --------------------------------------------
|
|
598
|
+
#
|
|
599
|
+
# THE JOIN ASKS ABOUT BRANCHES AND THE LIST ANSWERS ABOUT A REPOSITORY, and the
|
|
600
|
+
# gap between those two questions is #333. `plot-fleet-scan.sh` holds a dozen
|
|
601
|
+
# branch names and asks `pr-list` for every pull request the repository has
|
|
602
|
+
# ever had, then indexes what came back by `head` and discards the rest.
|
|
603
|
+
# Measured 2026-09-20 on `quatico/quaweb-website`: 12 board rows against 902
|
|
604
|
+
# pull requests, of which the join uses about 1%.
|
|
605
|
+
#
|
|
606
|
+
# THAT WOULD ONLY BE WASTEFUL IF THE LIST WERE WHOLE. It is not. `bb pr list`
|
|
607
|
+
# returns a fixed page of 50 per state, and the repository holds 886 MERGED
|
|
608
|
+
# pull requests — so 836 of them are invisible to the join, and every branch
|
|
609
|
+
# whose pull request is among them reads as having none. That is the fabricated
|
|
610
|
+
# verdict `plot-fleet-scan.sh:876` rules against by name, reached from the one
|
|
611
|
+
# direction the truncation detector below can report but not repair.
|
|
612
|
+
#
|
|
613
|
+
# SO THE SWEEP INVERTS THE QUESTION. Given the branches the caller actually
|
|
614
|
+
# tracks, it asks the REST endpoint about each one by name, through the same
|
|
615
|
+
# `q=` filter `bb` already builds for `--author`:
|
|
616
|
+
#
|
|
617
|
+
# /repositories/{ws}/{repo}/pullrequests
|
|
618
|
+
# ?q=state="MERGED" AND source.branch.name="feature/x"&pagelen=50
|
|
619
|
+
#
|
|
620
|
+
# Measured on that repository: `size: 1 | values: 1 | ids: 902` — PR 902 is one
|
|
621
|
+
# of the 836 a listing cannot reach — and a branch with no pull request answers
|
|
622
|
+
# `size: 0`, which is an EXACT ABSENCE rather than a short page.
|
|
623
|
+
#
|
|
624
|
+
# WHY NOT PAGE THE LIST INSTEAD, since the endpoint carries a `next` cursor.
|
|
625
|
+
# Two measurements refuse it. `fleet.ts:184` declares the per-refresh cost and
|
|
626
|
+
# `prRefreshMsFor` stretches the interval by it so hourly spend stays 60;
|
|
627
|
+
# walking all 18 merged pages makes the cost ~21 and the interval follows
|
|
628
|
+
# mechanically, 240 s → 1260 s, with `MAX_CADENCE_STRETCH = 8` taking the worst
|
|
629
|
+
# case to 2.8 hours. And the growth curve runs the wrong way: paging costs grow
|
|
630
|
+
# with the repository's pull-request history — the very quantity whose growth
|
|
631
|
+
# makes #333 worse — while a sweep costs what the CALLER TRACKS and is constant
|
|
632
|
+
# in pull-request count. `pagelen=100` is refused by Bitbucket (HTTP 400), so
|
|
633
|
+
# 50 is the ceiling and 18 pages was a floor rather than a safe estimate.
|
|
634
|
+
#
|
|
635
|
+
# THE LEADING SLASH IS LOAD-BEARING. `bb api` concatenates "${BB_API}${path}",
|
|
636
|
+
# so a path without it yields `…/2.0repositories/…` and an HTTP 403 that reads
|
|
637
|
+
# exactly like a missing scope. A previous plan was rejected for inferring a
|
|
638
|
+
# scope problem from this symptom. It is written once, here, and pinned.
|
|
639
|
+
#
|
|
640
|
+
# `bb`'s OWN PAGINATOR IS NOT REACHED FOR, and that is deliberate rather than
|
|
641
|
+
# incidental: `bb:203` caps at 10 pages — 500 rows at `pagelen=50`, 386 short
|
|
642
|
+
# of 886 — and exits with no error and no marker. A helper that silently
|
|
643
|
+
# returns a prefix is worse here than one that refuses.
|
|
644
|
+
|
|
645
|
+
# Bitbucket's own state word for one of this adapter's states.
|
|
646
|
+
#
|
|
647
|
+
# The `q=` filter matches Bitbucket's vocabulary, which is upper-case and calls
|
|
648
|
+
# a rejected pull request DECLINED. The adapter's own words are `bb pr list`'s,
|
|
649
|
+
# which are lower-case. One mapping, so a caller never spells a state twice.
|
|
650
|
+
bb_query_state() { # $1=adapter state word → Bitbucket's
|
|
651
|
+
case "$1" in
|
|
652
|
+
open) printf 'OPEN' ;;
|
|
653
|
+
merged) printf 'MERGED' ;;
|
|
654
|
+
declined) printf 'DECLINED' ;;
|
|
655
|
+
superseded) printf 'SUPERSEDED' ;;
|
|
656
|
+
*) die "bb_query_state: unknown state '$1'" ;;
|
|
657
|
+
esac
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
# Percent-encode one value for a URL query string.
|
|
661
|
+
#
|
|
662
|
+
# BRANCH NAMES ARE NOT URL-SAFE and this repository proves it: `feature/x` is
|
|
663
|
+
# the ordinary shape, and `/` inside an unencoded `q=` value ends the filter
|
|
664
|
+
# early, so the query would ask about `feature` and answer about the wrong
|
|
665
|
+
# branch — or about none. Encoding is done here rather than by the caller so
|
|
666
|
+
# every query on this path is encoded the same way.
|
|
667
|
+
#
|
|
668
|
+
# `LC_ALL=C` makes the loop byte-wise, so a multi-byte character is encoded as
|
|
669
|
+
# its bytes rather than mangled into one `?`. Branch names carrying non-ASCII
|
|
670
|
+
# are rare and entirely legal.
|
|
671
|
+
url_encode() { # $1=raw → percent-encoded on stdout
|
|
672
|
+
local _s="$1" _i _c _out=""
|
|
673
|
+
local LC_ALL=C
|
|
674
|
+
for (( _i = 0; _i < ${#_s}; _i++ )); do
|
|
675
|
+
_c="${_s:_i:1}"
|
|
676
|
+
case "$_c" in
|
|
677
|
+
[a-zA-Z0-9.~_-]) _out="$_out$_c" ;;
|
|
678
|
+
*) _out="$_out$(printf '%%%02X' "'$_c")" ;;
|
|
679
|
+
esac
|
|
680
|
+
done
|
|
681
|
+
printf '%s' "$_out"
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
# Ask Bitbucket about ONE branch in ONE state, and print the `values` array.
|
|
685
|
+
#
|
|
686
|
+
# WHAT IT PRINTS is the endpoint's `values` — a JSON array of 0 or 1 pull
|
|
687
|
+
# request objects, in the SAME shape `bb pr list --json` emits, which is what
|
|
688
|
+
# lets the existing jq programs consume it unchanged. The `--rich` field set
|
|
689
|
+
# (`plot-host.sh:3247`) reads `.id`, `.title`, `.state`, `.source.branch.name`,
|
|
690
|
+
# `.draft` and `.links.html.href`; the REST object carries all six.
|
|
691
|
+
#
|
|
692
|
+
# AN ABSENT ANSWER IS NOT A FAILED ONE, and the exit code is what says which.
|
|
693
|
+
# `size: 0` is an honest absence — the branch has no pull request in this state
|
|
694
|
+
# — and exits 0 with `[]` on stdout. A refused call exits non-zero and prints
|
|
695
|
+
# nothing, so a caller reading the CODE can never mistake an outage for an
|
|
696
|
+
# empty repository. `plot-fleet-scan.sh:891` records that exact confusion
|
|
697
|
+
# happening from the other side: a host exiting 0 while printing nothing once
|
|
698
|
+
# read as "this repo has no PRs".
|
|
699
|
+
#
|
|
700
|
+
# `pagelen=50` rather than 1. A branch may legitimately carry several pull
|
|
701
|
+
# requests in one state — a merged attempt and a merged successor — and the
|
|
702
|
+
# consumers rank them (`fleet.ts`'s `prOutranks`, the scan's OPEN-before-MERGED
|
|
703
|
+
# sort). Asking for one would silently hand them whichever the host listed
|
|
704
|
+
# first, which no adapter promises. Fifty is the endpoint's ceiling and costs
|
|
705
|
+
# the same as one.
|
|
706
|
+
# THE HOST'S OWN FAILURE TEXT LEAVES HERE UNCLASSIFIED, and that is the whole
|
|
707
|
+
# reason this does not call `pr_list_call`. That wrapper classifies a failure
|
|
708
|
+
# ONCE — throttled, burst, or everything else — and composes the sentence a
|
|
709
|
+
# reader acts on. Calling it per branch and again around the sweep classifies
|
|
710
|
+
# twice, and the second pass reads the FIRST pass's prose rather than the host's
|
|
711
|
+
# message: measured here, a `429` became *"the host failed the request and said
|
|
712
|
+
# nothing"* because `Rate limit … exceeded` was no longer in the text being
|
|
713
|
+
# matched. So the raw stderr and the raw exit code travel out of this function
|
|
714
|
+
# untouched, and the single `pr_list_call` that `pr_list_states` already wraps
|
|
715
|
+
# the whole sweep in does the one classification — exactly the layering
|
|
716
|
+
# `bb pr list` has always had.
|
|
717
|
+
bb_branch_query() { # $1=branch $2=adapter state; rest=global bb args → values[]
|
|
718
|
+
local _br="$1" _st="$2"; shift 2
|
|
719
|
+
local _q _path _out _rc
|
|
720
|
+
_q="state=$(url_encode "\"$(bb_query_state "$_st")\"") AND source.branch.name=$(url_encode "\"$_br\"")"
|
|
721
|
+
# The space between the two terms is encoded too; `bb api` passes the path to
|
|
722
|
+
# curl verbatim and an unencoded space would truncate the request line.
|
|
723
|
+
_q="${_q// /%20}"
|
|
724
|
+
# THE LEADING SLASH. See the block header — without it this is a 403 that
|
|
725
|
+
# reads as a scope error.
|
|
726
|
+
_path="/repositories/{ws}/{repo}/pullrequests?q=${_q}&pagelen=50"
|
|
727
|
+
# `jq` is applied only to a SUCCESSFUL payload. Piping a failed call into it
|
|
728
|
+
# would turn the host's exit code into jq's, and a parse error and a spent
|
|
729
|
+
# quota are not the same fact.
|
|
730
|
+
_out="$(bb "$@" api "$_path")" || return $?
|
|
731
|
+
printf '%s' "$_out" | jq -c '.values // []'
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
# Ask about EVERY tracked branch in one state, printing one combined array.
|
|
735
|
+
#
|
|
736
|
+
# THE SHAPE `pr_list_states` ALREADY EXPECTS. It calls its host command once
|
|
737
|
+
# per state and pipes the result through a jq program that starts `.[]`, so the
|
|
738
|
+
# sweep's job is to produce the same thing a single `bb pr list --state X`
|
|
739
|
+
# would have: one JSON array of pull request objects. The states loop, the row
|
|
740
|
+
# counting, the truncation report and the partial-answer rule above all stay in
|
|
741
|
+
# exactly one place — `pr_list_call`'s own header makes that argument, and a
|
|
742
|
+
# sweep with its own copy of the loop is the drift it names.
|
|
743
|
+
#
|
|
744
|
+
# A BRANCH THAT FAILS ENDS THE STATE, and that is the conservative direction.
|
|
745
|
+
# The combined array is only an answer if every branch in it was asked; one
|
|
746
|
+
# refused query means absence is no longer derivable for that branch, and a
|
|
747
|
+
# short array reported as whole is what #333 IS. So a failure propagates —
|
|
748
|
+
# `pr_list_call` exits — and `pr_list_states` classifies the state as failed,
|
|
749
|
+
# which reaches the caller as a partial answer (exit 7) when other states
|
|
750
|
+
# answered, or as the failure's own code when none did. One vocabulary.
|
|
751
|
+
# THE CALLING CONVENTION IS `pr_list_states`', NOT THIS FUNCTION'S OWN. That
|
|
752
|
+
# helper appends `--state <s> --json` to whatever command it was given, so a
|
|
753
|
+
# sweep that wants to sit in the same slot must accept those two trailing
|
|
754
|
+
# arguments and read the state out of them. Doing it the other way — teaching
|
|
755
|
+
# `pr_list_states` which of its commands is a sweep — would put a backend's
|
|
756
|
+
# shape inside the one piece of this file that has none.
|
|
757
|
+
#
|
|
758
|
+
# `--json` is accepted and ignored. The REST payload is JSON whether or not it
|
|
759
|
+
# is asked for, and refusing a flag the caller must pass would make the slot
|
|
760
|
+
# incompatible for the sake of a distinction with no consequence.
|
|
761
|
+
bb_branch_sweep() { # global bb args… --state <s> --json → one JSON array
|
|
762
|
+
local _st="" _args=() _acc="[]" _br _rc
|
|
763
|
+
while [ $# -gt 0 ]; do
|
|
764
|
+
case "$1" in
|
|
765
|
+
--state) _st="${2:?}"; shift 2 ;;
|
|
766
|
+
--json) shift ;;
|
|
767
|
+
*) _args+=("$1"); shift ;;
|
|
768
|
+
esac
|
|
769
|
+
done
|
|
770
|
+
[ -n "$_st" ] || die "bb_branch_sweep: no --state"
|
|
771
|
+
# THE ANSWERS ARE SPOOLED TO A FILE AND JOINED ONCE, NEVER PASSED THROUGH
|
|
772
|
+
# ARGV. The first version accumulated with
|
|
773
|
+
# `jq -c --argjson add "$_one" '. + $add'`, which hands a whole branch's
|
|
774
|
+
# payload to `jq` as a command-line argument — and Linux caps one argument at
|
|
775
|
+
# `MAX_ARG_STRLEN` (128 KB) where macOS has no such ceiling.
|
|
776
|
+
#
|
|
777
|
+
# WHAT THAT COST, measured 2026-09-20 against a Debian container: a branch
|
|
778
|
+
# carrying 886 merged pull requests is a 147 KB payload, `jq` died with
|
|
779
|
+
# *"Argument list too long"*, `_acc` came back EMPTY, and the sweep exited 0
|
|
780
|
+
# while printing no rows AND stating its completeness — a confident claim of
|
|
781
|
+
# "no pull requests" over a branch that had 886. That is precisely the
|
|
782
|
+
# fabricated verdict this whole slice exists to remove, rebuilt one layer in.
|
|
783
|
+
# It passed on macOS and failed only on Linux, which is where CI and every
|
|
784
|
+
# board run.
|
|
785
|
+
#
|
|
786
|
+
# A FILE HAS NO SUCH CEILING, and one `jq -s add` over the spool replaces N
|
|
787
|
+
# re-parses of a growing accumulator: the old shape re-read every row it had
|
|
788
|
+
# already seen once per branch, so eleven branches parsed the first branch's
|
|
789
|
+
# payload eleven times.
|
|
790
|
+
local _spool
|
|
791
|
+
_spool="$(mktemp "/tmp/plot-host-sweep.$$.XXXXXX")" || return 3
|
|
792
|
+
for _br in $PR_LIST_BRANCHES; do
|
|
793
|
+
# RETURN, NOT EXIT. This runs inside the command substitution
|
|
794
|
+
# `pr_list_call` wraps the sweep in, so the code must travel back as this
|
|
795
|
+
# function's status for that wrapper to classify it. An `exit` here would
|
|
796
|
+
# leave the substitution with an empty payload and a code the wrapper reads
|
|
797
|
+
# as the sweep's own — the silent empty list `pr_list_call`'s header names.
|
|
798
|
+
#
|
|
799
|
+
# The spool is removed on EVERY exit path, including the failing one: a
|
|
800
|
+
# sweep that gives up mid-way must not leave a payload behind in /tmp.
|
|
801
|
+
bb_branch_query "$_br" "$_st" ${_args[@]+"${_args[@]}"} >> "$_spool" \
|
|
802
|
+
|| { _rc=$?; rm -f "$_spool"; return $_rc; }
|
|
803
|
+
done
|
|
804
|
+
# `-s` reads the whole stream as one array of arrays; `add` flattens it.
|
|
805
|
+
# An EMPTY spool — every branch answered `[]` — makes `add` yield `null`, so
|
|
806
|
+
# the fallback keeps the contract that this prints a JSON ARRAY, which is
|
|
807
|
+
# what the caller's `.[]` needs.
|
|
808
|
+
_acc="$(jq -c -s 'add // []' < "$_spool")" || { rm -f "$_spool"; return 3; }
|
|
809
|
+
rm -f "$_spool"
|
|
810
|
+
printf '%s' "$_acc"
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
# The branches a sweep asks about, newline-or-space separated. Empty means the
|
|
814
|
+
# caller named none, and the arm keeps the bulk listing it has always used.
|
|
815
|
+
#
|
|
816
|
+
# A GLOBAL FOR `PR_LIST_JQ_ARGS`' REASON, stated two hundred lines above: the
|
|
817
|
+
# host command is already variadic and bash has one positional array. It is set
|
|
818
|
+
# by the `pr-list` arm immediately before the call and read nowhere else.
|
|
819
|
+
#
|
|
820
|
+
# SPACE-SEPARATED, AND GIT IS WHAT MAKES THAT SAFE. `bb_branch_sweep` reads this
|
|
821
|
+
# with an unquoted `for`, so a name carrying whitespace would split into two
|
|
822
|
+
# branches that do not exist. `git check-ref-format` REFUSES a ref name
|
|
823
|
+
# containing a space or a tab — verified 2026-09-20, both exit non-zero — so the
|
|
824
|
+
# separator is git's guarantee rather than a hopeful convention.
|
|
825
|
+
PR_LIST_BRANCHES=""
|
|
826
|
+
|
|
827
|
+
# How many branches the last sweep asked about, and how many answered.
|
|
828
|
+
#
|
|
829
|
+
# THE COMPLETENESS SIGNAL, AND WHY THE ARM STATES IT RATHER THAN THE SCAN
|
|
830
|
+
# INFERRING IT. `plot-fleet-scan.sh:901` writes `.list-complete` when
|
|
831
|
+
# `0 < rows < PR_LIST_LIMIT` — completeness read off a single page's size,
|
|
832
|
+
# which is the only evidence a bulk listing offers. A sweep has no page: each
|
|
833
|
+
# query returns 0 or 1, and `size: 0` is already an exact answer for that
|
|
834
|
+
# branch. So completeness stops being a property of a row count and becomes a
|
|
835
|
+
# property of the SWEEP — every tracked branch was asked and each one answered
|
|
836
|
+
# — which is a stronger claim than the page heuristic could ever make, and one
|
|
837
|
+
# this side can state as a fact rather than leave to be guessed from a number.
|
|
838
|
+
#
|
|
839
|
+
# A PARTIAL SWEEP MUST NOT MAKE THE CLAIM. If any branch's query failed, the
|
|
840
|
+
# survivors are still valid answers and are still printed, but absence is no
|
|
841
|
+
# longer derivable for the branches that went unasked. The line is emitted only
|
|
842
|
+
# when every state answered.
|
|
843
|
+
#
|
|
844
|
+
# ONE COUNTER, NOT TWO. How many branches ANSWERED is not tracked beside this,
|
|
845
|
+
# because a failed branch query ends its whole state (see `bb_branch_sweep`) and
|
|
846
|
+
# the states tally `pr_list_states` already keeps is therefore the same fact. A
|
|
847
|
+
# second counter would be a second answer to one question, and the two would
|
|
848
|
+
# drift the first time either side changed.
|
|
849
|
+
PR_SWEEP_ASKED=0
|
|
850
|
+
|
|
851
|
+
# State on stderr that the sweep was WHOLE — the licence `.list-complete` needs.
|
|
852
|
+
#
|
|
853
|
+
# THE LINE IS A CONTRACT, not a log. `plot-fleet-scan.sh` reads it to decide
|
|
854
|
+
# whether a cache miss means "no pull request" or "never asked", so its wording
|
|
855
|
+
# is pinned by a test the same way the truncation report's is. It names both
|
|
856
|
+
# counts, because a reader who sees the claim should be able to check it.
|
|
857
|
+
#
|
|
858
|
+
# WHAT IT LICENSES IS A SHORTCUT, NOT THE ANSWER. Without it, a `--ask` caller
|
|
859
|
+
# whose branch missed the join falls through to a `pr-state` call per branch and
|
|
860
|
+
# still gets a correct answer — the per-branch N+1 that #216 removed, which is a
|
|
861
|
+
# COST regression rather than a wrong one. So withholding the line is always
|
|
862
|
+
# safe and is what a partial sweep does.
|
|
863
|
+
#
|
|
864
|
+
# EVERY STATE MUST HAVE ANSWERED. A sweep asks each branch once per state, and a
|
|
865
|
+
# branch's absence is only established when every state was asked about it: a
|
|
866
|
+
# merged pull request missed because the `merged` state failed reads exactly
|
|
867
|
+
# like a branch that never had one. So the claim is made on the STATES' tally,
|
|
868
|
+
# which `pr_list_states` already keeps, rather than on a per-branch count that
|
|
869
|
+
# would have to be reconciled with it.
|
|
870
|
+
#
|
|
871
|
+
# SILENT WHEN NO SWEEP RAN. The bulk path makes no per-branch claim and keeps
|
|
872
|
+
# the row-count heuristic it has always used, so nothing is printed and no
|
|
873
|
+
# existing caller's behaviour changes.
|
|
874
|
+
pr_sweep_report() { # $1=states answered $2=states asked
|
|
875
|
+
[ -n "$PR_LIST_BRANCHES" ] || return 0
|
|
876
|
+
[ "$1" -eq "$2" ] 2>/dev/null || return 0
|
|
877
|
+
echo "plot-host: pr-list sweep complete ($PR_SWEEP_ASKED branches asked, $1 of $2 states answered) — every tracked branch was asked and each answered" >&2
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
pr_list_states() { # $1=backend $2=limit $3=states $4=jq-program; rest=the host command
|
|
881
|
+
local backend="$1" limit="$2" states="$3" jq_prog="$4"; shift 4
|
|
882
|
+
local _s _raw _rc _err _tmp _ok=0 _failed=0 _first_rc=0 _failed_states=""
|
|
883
|
+
for _s in $states; do
|
|
884
|
+
_tmp="/tmp/plot-host-prlist-state-err.$$.$_s"
|
|
885
|
+
# THE SUBSHELL'S CODE IS THE ONLY CHANNEL OUT, so it is captured rather
|
|
886
|
+
# than propagated. `pr_list_failed` runs INSIDE the substitution and has
|
|
887
|
+
# already composed its report and its repair line; that text is spooled
|
|
888
|
+
# here only so the state's name can be added to it before it is passed on.
|
|
889
|
+
_raw="$(pr_list_call "$@" --state "$_s" --json 2>"$_tmp")"; _rc=$?
|
|
890
|
+
_err="$(cat "$_tmp" 2>/dev/null)"; rm -f "$_tmp"
|
|
891
|
+
if [ "$_rc" -ne 0 ]; then
|
|
892
|
+
# THE HOST'S OWN REPORT GOES FIRST AND IS NEVER PREFIXED. `pr_list_failed`
|
|
893
|
+
# already composed the sentence that says WHY — a spent quota, a burst
|
|
894
|
+
# refusal, a DNS blip — and that sentence is what a reader acts on. A line
|
|
895
|
+
# of this helper's own naming WHICH state, emitted ahead of it, buries the
|
|
896
|
+
# reason under the bookkeeping: the scan reads the first stderr line into
|
|
897
|
+
# its error field, and a reader chasing `HTTP 429` would be shown
|
|
898
|
+
# `state 'open' failed` instead. That is #912's own failure mode — a
|
|
899
|
+
# message describing the wrong thing — reproduced one layer up, and the
|
|
900
|
+
# contract suite caught it.
|
|
901
|
+
[ -n "$_err" ] && printf '%s\n' "$_err" >&2
|
|
902
|
+
_failed=$((_failed + 1))
|
|
903
|
+
[ "$_first_rc" -eq 0 ] && _first_rc=$_rc
|
|
904
|
+
_failed_states="${_failed_states:+$_failed_states, }$_s"
|
|
905
|
+
continue
|
|
906
|
+
fi
|
|
907
|
+
[ -n "$_err" ] && printf '%s\n' "$_err" >&2
|
|
908
|
+
_ok=$((_ok + 1))
|
|
909
|
+
# A SWEEP MAKES NO PAGE CLAIM, so the page detector is not asked. Its rule
|
|
910
|
+
# is about a LISTING that cannot report a total — see its header — and a
|
|
911
|
+
# sweep's row count has no page semantics at all: the count is how many of
|
|
912
|
+
# the asked branches have a pull request in this state, and two of eleven is
|
|
913
|
+
# a complete answer rather than a short one. Running it here would print
|
|
914
|
+
# "possibly truncated" immediately before `pr_sweep_report` states the
|
|
915
|
+
# answer was whole, which is the adapter contradicting itself on one stream.
|
|
916
|
+
#
|
|
917
|
+
# THE DETECTOR ITSELF IS UNTOUCHED and still fires exactly as it did on
|
|
918
|
+
# every listing call — `host.test.mjs:3060` passes unedited. What changed is
|
|
919
|
+
# that a path exists whose premise it was never written about.
|
|
920
|
+
[ -n "$PR_LIST_BRANCHES" ] || pr_list_report_truncation "$backend" "$limit" "$_s" \
|
|
921
|
+
"$(jq 'length' <<<"$_raw" 2>/dev/null || echo 0)"
|
|
922
|
+
printf '%s' "$_raw" | jq -c ${PR_LIST_JQ_ARGS[@]+"${PR_LIST_JQ_ARGS[@]}"} "$jq_prog"
|
|
923
|
+
done
|
|
924
|
+
pr_sweep_report "$_ok" "$((_ok + _failed))"
|
|
925
|
+
[ -z "$_failed_states" ] && return 0
|
|
926
|
+
if [ "$_ok" -eq 0 ]; then
|
|
927
|
+
# NO STATE ANSWERED — a total outage, and it keeps the code it has always
|
|
928
|
+
# had so a real outage can never be read as a partial page.
|
|
929
|
+
echo "plot-host: pr-list: no state answered; this is not a partial answer" >&2
|
|
930
|
+
return "$_first_rc"
|
|
931
|
+
fi
|
|
932
|
+
echo "plot-host: pr-list: answered $_ok of $((_ok + _failed)) states; missing: $_failed_states" >&2
|
|
933
|
+
return "$PR_LIST_PARTIAL_RC"
|
|
934
|
+
}
|
|
935
|
+
|
|
537
936
|
# --- Jenkins CI integration ------------------------------------------------
|
|
538
937
|
# A repo may declare `CI: jenkins` independently of `Git host`. When it does,
|
|
539
938
|
# build status (`checks`) is resolved through `jen` — a multibranch job's
|
|
@@ -1683,6 +2082,33 @@ tracker_projects() {
|
|
|
1683
2082
|
printf '%s' "$raw" | tr ',' '\n' | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//' | grep -v '^$' || true
|
|
1684
2083
|
}
|
|
1685
2084
|
|
|
2085
|
+
# Reads ONE variable from a `.env`-shaped file. Evaluates nothing.
|
|
2086
|
+
#
|
|
2087
|
+
# NOT `set -a; . ./.env; set +a`, which is the usual one-liner. It was measured
|
|
2088
|
+
# aborting in zsh on a file whose third line holds an unquoted JSON object, and
|
|
2089
|
+
# it imports every unrelated variable in the file — which on a credentials path
|
|
2090
|
+
# is a reason of its own.
|
|
2091
|
+
#
|
|
2092
|
+
# NOT `grep '^NAME=' | cut -d= -f2-` either: measured 2026-09-17, that returns
|
|
2093
|
+
# EMPTY for an `export `-prefixed line and KEEPS THE QUOTES on a quoted one, and
|
|
2094
|
+
# a quoted token reaching `curl -u` produces the 401 this read exists to remove.
|
|
2095
|
+
#
|
|
2096
|
+
# THE STRIP ORDER IS THE DEFECT, and it has been got wrong twice. Whitespace is
|
|
2097
|
+
# stripped BEFORE the quotes and again after. On `T="tok" ` the `"$` anchor
|
|
2098
|
+
# misses if the quotes go first, and the value keeps them:
|
|
2099
|
+
#
|
|
2100
|
+
# quotes first: ["tok"] whitespace first: [tok]
|
|
2101
|
+
#
|
|
2102
|
+
# `head -1` takes the first assignment, so a duplicated name resolves the way a
|
|
2103
|
+
# shell reading top-to-bottom would.
|
|
2104
|
+
read_env_var() { # $1=name $2=file → the value, or nothing
|
|
2105
|
+
sed -n "s/^[[:space:]]*\(export[[:space:]]\+\)\{0,1\}$1=//p" "$2" \
|
|
2106
|
+
| head -1 \
|
|
2107
|
+
| sed -e 's/[[:space:]]*$//' \
|
|
2108
|
+
-e 's/^"\(.*\)"$/\1/' -e "s/^'\(.*\)'\$/\1/" \
|
|
2109
|
+
-e 's/[[:space:]]*$//'
|
|
2110
|
+
}
|
|
2111
|
+
|
|
1686
2112
|
# The env var scheme for Jira auth. The plan left the EXACT names open, to be
|
|
1687
2113
|
# confirmed against a real instance; these follow Jira Cloud's documented Basic
|
|
1688
2114
|
# scheme (email + API token, base64'd into an Authorization header):
|
|
@@ -1695,14 +2121,55 @@ tracker_projects() {
|
|
|
1695
2121
|
# This guard is called in the MAIN shell, BEFORE the `$(jira_curl …)` capture —
|
|
1696
2122
|
# `die3` exits the whole script only from there, not from inside a command
|
|
1697
2123
|
# substitution where it would end only the subshell and leak a second error.
|
|
2124
|
+
#
|
|
2125
|
+
# WHERE BOTH ARE UNSET, THE REPOSITORY'S `.env` IS READ. The refusal below named
|
|
2126
|
+
# two variables without ever looking where a repository puts them, so an
|
|
2127
|
+
# operator holding working credentials — measured 2026-09-17, a 200 from
|
|
2128
|
+
# `/rest/api/3/myself` with the same pair — was sent to create a second token.
|
|
2129
|
+
# "Export it in your shell" does not reach the board either: it is a long-lived
|
|
2130
|
+
# process, which is why `plot-fleetctl.sh` bakes an environment into its unit.
|
|
2131
|
+
#
|
|
2132
|
+
# THE ENVIRONMENT WINS AND NOTHING IS READ WHERE IT ANSWERS. The file is opened
|
|
2133
|
+
# only when BOTH are unset, so a deliberate export is never second-guessed and
|
|
2134
|
+
# the common path touches no disk.
|
|
2135
|
+
#
|
|
2136
|
+
# AND THE SOURCE IS NAMED, which is a requirement rather than a nicety: two
|
|
2137
|
+
# tokens may exist, and an operator debugging a 401 must be able to tell which
|
|
2138
|
+
# one was used. `plot-board-probe.sh` reports `auth` as three words rather than
|
|
2139
|
+
# a boolean for the same reason. THE VALUE IS NEVER PRINTED — only the source.
|
|
2140
|
+
jira_load_env_file() {
|
|
2141
|
+
local root env_file email token
|
|
2142
|
+
[ -z "${JIRA_EMAIL:-}" ] && [ -z "${JIRA_API_TOKEN:-}" ] || return 0
|
|
2143
|
+
|
|
2144
|
+
# The repository root, `plot-config.sh:145`'s idiom. NO UPWARD WALK past it:
|
|
2145
|
+
# a search toward $HOME would read a file the operator did not mean for this
|
|
2146
|
+
# repository.
|
|
2147
|
+
root="$(git rev-parse --show-toplevel 2>/dev/null)" || root="."
|
|
2148
|
+
env_file="$root/.env"
|
|
2149
|
+
[ -r "$env_file" ] || return 0
|
|
2150
|
+
|
|
2151
|
+
email="$(read_env_var JIRA_EMAIL "$env_file")"
|
|
2152
|
+
token="$(read_env_var JIRA_API_TOKEN "$env_file")"
|
|
2153
|
+
# BOTH OR NEITHER. Half a Basic credential authenticates nothing, and a
|
|
2154
|
+
# partial pickup would turn today's honest refusal into a 401 further in.
|
|
2155
|
+
[ -n "$email" ] && [ -n "$token" ] || return 0
|
|
2156
|
+
|
|
2157
|
+
JIRA_EMAIL="$email"
|
|
2158
|
+
JIRA_API_TOKEN="$token"
|
|
2159
|
+
export JIRA_EMAIL JIRA_API_TOKEN
|
|
2160
|
+
echo "plot-host: JIRA_EMAIL and JIRA_API_TOKEN read from .env" >&2
|
|
2161
|
+
}
|
|
2162
|
+
|
|
1698
2163
|
jira_require_config() {
|
|
1699
2164
|
if [ -z "$(tracker_base_url)" ]; then
|
|
1700
2165
|
die3 "Tracker is jira but no base URL is configured (write 'Tracker: jira https://your.atlassian.net' or set PLOT_JIRA_BASE_URL)"
|
|
1701
2166
|
fi
|
|
2167
|
+
jira_load_env_file
|
|
1702
2168
|
if [ -z "${JIRA_EMAIL:-}" ] || [ -z "${JIRA_API_TOKEN:-}" ]; then
|
|
1703
2169
|
echo "plot-host: Jira needs JIRA_EMAIL and JIRA_API_TOKEN in the environment — an unauthenticated Jira must not read as an empty inbox" >&2
|
|
1704
2170
|
echo " Create a token at https://id.atlassian.com/manage-profile/security/api-tokens" >&2
|
|
1705
|
-
echo " then export JIRA_EMAIL=<your account email> and JIRA_API_TOKEN=<the token
|
|
2171
|
+
echo " then export JIRA_EMAIL=<your account email> and JIRA_API_TOKEN=<the token>," >&2
|
|
2172
|
+
echo " or put both in this repository's .env (which .gitignore already excludes)." >&2
|
|
1706
2173
|
exit 3
|
|
1707
2174
|
fi
|
|
1708
2175
|
}
|
|
@@ -1734,12 +2201,45 @@ jira_curl() {
|
|
|
1734
2201
|
return $rc
|
|
1735
2202
|
}
|
|
1736
2203
|
|
|
2204
|
+
# The account a Jira record is keyed on — DERIVED, never the email itself.
|
|
2205
|
+
#
|
|
2206
|
+
# THE EMAIL IS HALF A BASIC CREDENTIAL and this ledger is written on every call:
|
|
2207
|
+
# measured 2026-09-17, `$HOME/.plot/state/budget.tsv` held 2448 jira lines on one
|
|
2208
|
+
# machine. Until `.env` was read that happened only where somebody exported the
|
|
2209
|
+
# variable deliberately; it now happens wherever a `.env` exists — a population
|
|
2210
|
+
# that never consented to a machine-local record of it. A change that widens who
|
|
2211
|
+
# gets written down owns the writing down.
|
|
2212
|
+
#
|
|
2213
|
+
# AND IT STAYS PER-ACCOUNT DISTINGUISHABLE, because the field is a MATCH KEY and
|
|
2214
|
+
# not a label: `plot-budget.sh:250` is `if ($2 != want_c || $3 != want_a) next`,
|
|
2215
|
+
# `spend-rate` publishes it, and `decodeEntry`/`sameKey` read it. One machine's
|
|
2216
|
+
# ledger holds three distinct Jira accounts, so a CONSTANT redaction would merge
|
|
2217
|
+
# their rate windows and the rate a connector reads becomes the sum of several
|
|
2218
|
+
# people's. A hash keeps the key one-to-one while carrying no address.
|
|
2219
|
+
#
|
|
2220
|
+
# AT THE SOURCE rather than at `budget.tsv`, because fixing the one known writer
|
|
2221
|
+
# leaves the next to inherit the defect — `slots-file.ts:185` turns an account
|
|
2222
|
+
# into a DIRECTORY NAME and is one `slots.acquire` call away from being live.
|
|
2223
|
+
#
|
|
2224
|
+
# `jira:` prefixed and truncated to 12 hex: long enough that two accounts on one
|
|
2225
|
+
# machine will not collide, short enough to read in a ledger line.
|
|
2226
|
+
jira_budget_account() {
|
|
2227
|
+
local raw="${JIRA_EMAIL:-}"
|
|
2228
|
+
[ -n "$raw" ] || { printf 'unknown\n'; return 0; }
|
|
2229
|
+
local h
|
|
2230
|
+
h="$(printf '%s' "$raw" | shasum -a 256 2>/dev/null | awk '{print $1}')"
|
|
2231
|
+
# No hasher, no guess: a raw email must never be the fallback, so an
|
|
2232
|
+
# unhashable account degrades to the same word an absent one uses.
|
|
2233
|
+
[ -n "$h" ] || { printf 'unknown\n'; return 0; }
|
|
2234
|
+
printf 'jira:%s\n' "${h:0:12}"
|
|
2235
|
+
}
|
|
2236
|
+
|
|
1737
2237
|
# Records one Jira call. Jira meters, publishes no header this adapter reads,
|
|
1738
2238
|
# and this slice does not add header parsing — so the reading is `unknown`,
|
|
1739
2239
|
# which is never read as free.
|
|
1740
2240
|
budget_record_jira() {
|
|
1741
2241
|
[ -z "${PLOT_BUDGET_OFF:-}" ] || return 0
|
|
1742
|
-
budget_append jira "$
|
|
2242
|
+
budget_append jira "$(jira_budget_account)" api 1 - - - unknown
|
|
1743
2243
|
}
|
|
1744
2244
|
|
|
1745
2245
|
# Split a jira_curl response into (body, status) and enforce the three outcomes.
|
|
@@ -1788,12 +2288,20 @@ jira_check() {
|
|
|
1788
2288
|
# AT LEAST the requested limit — the host may have
|
|
1789
2289
|
# had more that the limit hid. Fewer rows than the
|
|
1790
2290
|
# limit PROVES completeness.
|
|
1791
|
-
# bitbucket (IGNORES --limit): `bb pr list` has no --limit and
|
|
1792
|
-
# total
|
|
2291
|
+
# bitbucket (IGNORES --limit): `bb pr list` has no --limit and reports neither
|
|
2292
|
+
# a total nor a cursor, so it can NEVER prove
|
|
1793
2293
|
# completeness for a --limit call. Any non-empty
|
|
1794
2294
|
# page is therefore possibly truncated. An empty
|
|
1795
2295
|
# page had nothing to truncate.
|
|
1796
2296
|
#
|
|
2297
|
+
# THE PREMISE ABOVE IS ABOUT `bb pr list`, AND IT WAS ONCE WRITTEN ABOUT
|
|
2298
|
+
# BITBUCKET. It said the host "cannot report a total or a cursor" — true of the
|
|
2299
|
+
# CLI's listing and false of the REST endpoint behind it, which carries both a
|
|
2300
|
+
# `size` and a `next`. That mattered the moment a path existed that could ask:
|
|
2301
|
+
# the per-branch sweep (#333) proves completeness exactly, per branch, and this
|
|
2302
|
+
# detector is deliberately not asked about it (`pr_list_states`). The rule below
|
|
2303
|
+
# is unchanged and still governs every listing call.
|
|
2304
|
+
#
|
|
1797
2305
|
# No --limit was requested → the caller accepted the host's default page and is
|
|
1798
2306
|
# owed no report, so no existing no-limit caller's behaviour changes.
|
|
1799
2307
|
#
|
|
@@ -2490,13 +2998,30 @@ case "$op" in
|
|
|
2490
2998
|
else
|
|
2491
2999
|
# Establish that bb supports --json BEFORE calling it — Done-when 5.
|
|
2492
3000
|
bb_require_json
|
|
3001
|
+
# THE SAME KEY SET AS THE GITHUB ARM, INCLUDING `mergeCommit`. This arm
|
|
3002
|
+
# dropped that key on all four of its paths until 2026-09-18, and the
|
|
3003
|
+
# consumer reads it as `.mergeCommit // empty` — where `jq` cannot tell an
|
|
3004
|
+
# absent key from an empty one. So `plot-reconcile-scan.sh` reported
|
|
3005
|
+
# `no merge commit → cannot resolve` for every delivered plan on a
|
|
3006
|
+
# Bitbucket repository, which reads as a host that answered rather than an
|
|
3007
|
+
# arm that never asked. `pr-list`'s arm is the precedent: it emits every
|
|
3008
|
+
# key its GitHub arm does, because absent is not false.
|
|
3009
|
+
#
|
|
3010
|
+
# `merge_commit.hash` IS TAKEN FROM THE PAYLOAD ALREADY FETCHED — the same
|
|
3011
|
+
# field `pr-merge-commit` reads from the same shape. A second `bb` call to
|
|
3012
|
+
# re-ask for it would double a cost measured at ~10s per call.
|
|
3013
|
+
#
|
|
3014
|
+
# `// ""` COLLAPSES THREE SHAPES INTO ONE HONEST VALUE: `merge_commit`
|
|
3015
|
+
# absent on an open PR, the object null, or the hash null. `""` is what
|
|
3016
|
+
# the GitHub arm gives for anything unmerged, so a caller cannot tell the
|
|
3017
|
+
# backends apart.
|
|
2493
3018
|
if [[ "$ref" =~ ^[0-9]+$ ]]; then
|
|
2494
3019
|
if out="$(bb ${repo_args[@]+"${repo_args[@]}"} pr view "$ref" --json 2>/tmp/plot-host-err.$$)"; then
|
|
2495
3020
|
rm -f "/tmp/plot-host-err.$$"
|
|
2496
|
-
jq -c '{number:.id,state:(if .state=="DECLINED" then "CLOSED" else .state end),draft:(.draft // false),url:.links.html.href}' <<<"$out"
|
|
3021
|
+
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"
|
|
2497
3022
|
else
|
|
2498
3023
|
err="$(cat "/tmp/plot-host-err.$$" 2>/dev/null)"; rm -f "/tmp/plot-host-err.$$"
|
|
2499
|
-
host_miss_or_fail "$err" '{"number":0,"state":"NONE","draft":false,"url":""}' || exit $?
|
|
3024
|
+
host_miss_or_fail "$err" '{"number":0,"state":"NONE","draft":false,"url":"","mergeCommit":""}' || exit $?
|
|
2500
3025
|
fi
|
|
2501
3026
|
else
|
|
2502
3027
|
# The list CALL succeeding and the branch being absent FROM the list are
|
|
@@ -2534,12 +3059,20 @@ case "$op" in
|
|
|
2534
3059
|
if [ "$bb_rc" = 0 ]; then
|
|
2535
3060
|
rm -f "/tmp/plot-host-err.$$"
|
|
2536
3061
|
out="$(jq -c -s 'add // []' <<<"$out")"
|
|
3062
|
+
# THE BRANCH ARM CARRIES `mergeCommit` TOO, and it is the path that
|
|
3063
|
+
# matters most: `plot-pr-state.sh:33` asks `pr-state "idea/${SLUG}"` —
|
|
3064
|
+
# a branch, not a number — and `:47` reads `.mergeCommit // empty`
|
|
3065
|
+
# from the answer. A fix touching only the numeric pair above would
|
|
3066
|
+
# leave that caller reading an absent key on every Bitbucket repo.
|
|
3067
|
+
#
|
|
3068
|
+
# The hash rides on the page the state walk already fetched, so this
|
|
3069
|
+
# costs no extra call.
|
|
2537
3070
|
jq -c --arg b "$ref" '[.[] | select(.source.branch.name==$b)][0] // null
|
|
2538
|
-
| if .==null then {number:0,state:"NONE",draft:false,url:""}
|
|
2539
|
-
else {number:.id,state:(if .state=="DECLINED" then "CLOSED" else .state end),draft:(.draft // false),url:.links.html.href} end' <<<"$out"
|
|
3071
|
+
| if .==null then {number:0,state:"NONE",draft:false,url:"",mergeCommit:""}
|
|
3072
|
+
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"
|
|
2540
3073
|
else
|
|
2541
3074
|
err="$(cat "/tmp/plot-host-err.$$" 2>/dev/null)"; rm -f "/tmp/plot-host-err.$$"
|
|
2542
|
-
host_miss_or_fail "$err" '{"number":0,"state":"NONE","draft":false,"url":""}' || exit $?
|
|
3075
|
+
host_miss_or_fail "$err" '{"number":0,"state":"NONE","draft":false,"url":"","mergeCommit":""}' || exit $?
|
|
2543
3076
|
fi
|
|
2544
3077
|
fi
|
|
2545
3078
|
fi
|
|
@@ -2768,12 +3301,28 @@ case "$op" in
|
|
|
2768
3301
|
# that wants history says how much; the default stays the host's, so no
|
|
2769
3302
|
# existing caller's result changes.
|
|
2770
3303
|
limit=""
|
|
3304
|
+
branches=""
|
|
2771
3305
|
while [ $# -gt 0 ]; do
|
|
2772
3306
|
case "$1" in
|
|
2773
3307
|
--state) state="${2:?}"; shift 2 ;;
|
|
2774
3308
|
--limit) limit="${2:?}"; shift 2 ;;
|
|
2775
3309
|
--rich) rich=1; shift ;;
|
|
2776
3310
|
--repo) repo_args=(-R "${2:?}"); shift 2 ;;
|
|
3311
|
+
# THE BRANCHES THE CALLER TRACKS, repeatable, and OPT-IN. Given any,
|
|
3312
|
+
# the Bitbucket arm sweeps the REST endpoint once per branch per state
|
|
3313
|
+
# instead of listing the repository; given none, every existing caller
|
|
3314
|
+
# gets exactly the listing it always got. Four callers pass none today
|
|
3315
|
+
# (`plot-fleet-scan.sh`, `plot-open-pr.sh`, `plot-impl-status.sh` and
|
|
3316
|
+
# `fleet.ts`), so the bulk path stays the default rather than the
|
|
3317
|
+
# legacy one.
|
|
3318
|
+
#
|
|
3319
|
+
# WHY THE CALLER NAMES THEM AND THIS OP DOES NOT GUESS. The same rule
|
|
3320
|
+
# `--repo` states a few lines up: the caller knows which branches its
|
|
3321
|
+
# refs came from and this op cannot. Deriving them here — from remote
|
|
3322
|
+
# refs, say — would make the adapter invent a working set, and a sweep
|
|
3323
|
+
# over the wrong set answers confidently about branches nobody asked
|
|
3324
|
+
# about while missing the ones they did.
|
|
3325
|
+
--branch) branches="${branches:+$branches }${2:?}"; shift 2 ;;
|
|
2777
3326
|
*) die "pr-list: unknown arg $1" ;;
|
|
2778
3327
|
esac
|
|
2779
3328
|
done
|
|
@@ -2972,7 +3521,11 @@ case "$op" in
|
|
|
2972
3521
|
# Forwarding it errors with `unknown flag`, and dropping it silently
|
|
2973
3522
|
# would serve a short page as if it were the whole set — the quiet wrong
|
|
2974
3523
|
# answer this adapter refuses elsewhere. So it is dropped AND said.
|
|
2975
|
-
|
|
3524
|
+
# A SWEEP IS NOT A PAGE AND OWES NO SUCH WARNING. `--limit` bounds a
|
|
3525
|
+
# listing; a per-branch query returns that branch's pull requests and
|
|
3526
|
+
# nothing was capped, so the notice would describe a truncation that did
|
|
3527
|
+
# not happen. Said only for the listing it is about.
|
|
3528
|
+
if [ -n "$limit" ] && [ -z "$branches" ]; then
|
|
2976
3529
|
echo "plot-host: bitbucket ignores --limit $limit; bb returns a fixed page (50 at 1.0.0)" >&2
|
|
2977
3530
|
fi
|
|
2978
3531
|
# Establish that bb supports --json BEFORE calling it — Done-when 5.
|
|
@@ -2983,17 +3536,30 @@ case "$op" in
|
|
|
2983
3536
|
# with no output — an unknown state reading as "no PRs matched", which
|
|
2984
3537
|
# is the exact failure this translation exists to remove.
|
|
2985
3538
|
bb_states="$(bb_states_for "$state")" || exit 1
|
|
3539
|
+
# THE ONE PLACE THE SWEEP IS CHOSEN, and it is chosen as a COMMAND rather
|
|
3540
|
+
# than as a flag the three sites below each test. They differ only in the
|
|
3541
|
+
# jq program they pipe the payload through — `pr_list_states`' header says
|
|
3542
|
+
# so — and a sweep that added an `if` to each would make them differ in two
|
|
3543
|
+
# ways, which is how the six hand-applied fixes `pr_list_call` warns about
|
|
3544
|
+
# began. One assignment here; the sites are untouched but for this word.
|
|
3545
|
+
PR_LIST_BRANCHES="$branches"
|
|
3546
|
+
PR_SWEEP_ASKED=0
|
|
3547
|
+
bb_cmd=(bb ${repo_args[@]+"${repo_args[@]}"} pr list)
|
|
3548
|
+
if [ -n "$branches" ]; then
|
|
3549
|
+
# A SWEEP'S COST IS THE CALLER'S WORKING SET, and it is reported so the
|
|
3550
|
+
# caller can check the claim it is about to be handed. Branches × states
|
|
3551
|
+
# — 11 branches over 3 states is 33 exact queries, against 3 listings
|
|
3552
|
+
# that answer for 50 of 902 rows.
|
|
3553
|
+
for _b in $branches; do PR_SWEEP_ASKED=$((PR_SWEEP_ASKED + 1)); done
|
|
3554
|
+
bb_cmd=(bb_branch_sweep ${repo_args[@]+"${repo_args[@]}"})
|
|
3555
|
+
fi
|
|
2986
3556
|
if [ "$rich" = 1 ]; then
|
|
2987
3557
|
if [ "$ci" = "jenkins" ]; then
|
|
2988
3558
|
# Bitbucket PR list, `checks` filled from Jenkins — the SAME overlay
|
|
2989
3559
|
# the GitHub arm uses, which is why it lives above the backend branch.
|
|
2990
3560
|
# `bb`'s standing `unknown` becomes a real value where Jenkins answers.
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
pr_list_report_truncation bitbucket "$limit" "$_s" \
|
|
2994
|
-
"$(jq 'length' <<<"$_bb_raw" 2>/dev/null || echo 0)"
|
|
2995
|
-
printf '%s' "$_bb_raw" \
|
|
2996
|
-
| jq -c --argjson jmap "$jen_map" --arg jstatus "$jen_status" '.[] |
|
|
3561
|
+
PR_LIST_JQ_ARGS=(--argjson jmap "$jen_map" --arg jstatus "$jen_status")
|
|
3562
|
+
pr_list_states bitbucket "$limit" "$bb_states" '.[] |
|
|
2997
3563
|
($jmap[.source.branch.name] // null) as $jentry |
|
|
2998
3564
|
{
|
|
2999
3565
|
number:.id, title:.title,
|
|
@@ -3013,26 +3579,19 @@ case "$op" in
|
|
|
3013
3579
|
then [$jentry.job]
|
|
3014
3580
|
else []
|
|
3015
3581
|
end)
|
|
3016
|
-
}'
|
|
3017
|
-
done
|
|
3582
|
+
}' "${bb_cmd[@]}" || exit $?
|
|
3018
3583
|
else
|
|
3019
3584
|
# Bitbucket without Jenkins: checks remain unknown
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
printf '%s' "$_bb_raw" \
|
|
3025
|
-
| jq -c '.[] | {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:[]}'
|
|
3026
|
-
done
|
|
3585
|
+
PR_LIST_JQ_ARGS=()
|
|
3586
|
+
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:[]}' \
|
|
3588
|
+
"${bb_cmd[@]}" || exit $?
|
|
3027
3589
|
fi
|
|
3028
3590
|
else
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
printf '%s' "$_bb_raw" \
|
|
3034
|
-
| jq -c '.[] | {number:.id,title:.title,state:(if .state=="DECLINED" then "CLOSED" else .state end),head:.source.branch.name}'
|
|
3035
|
-
done
|
|
3591
|
+
PR_LIST_JQ_ARGS=()
|
|
3592
|
+
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}' \
|
|
3594
|
+
"${bb_cmd[@]}" || exit $?
|
|
3036
3595
|
fi
|
|
3037
3596
|
fi
|
|
3038
3597
|
;;
|