@plot-pm/board 0.14.2 → 0.14.4
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 +144 -142
- package/package.json +1 -1
- package/plot-budget.sh +166 -1
- package/plot-deliver.sh +14 -8
- package/plot-dispatch.sh +54 -46
- package/plot-fleet-scan.sh +134 -7
- package/plot-host.sh +728 -38
- package/plot-plan-meta.sh +23 -7
- package/plot-reap.sh +37 -10
package/plot-host.sh
CHANGED
|
@@ -61,6 +61,30 @@
|
|
|
61
61
|
# pr-ready <number> take a PR out of draft
|
|
62
62
|
# merge the PR
|
|
63
63
|
# pr-list [--state open|merged|closed|all] [--limit N] [--rich]
|
|
64
|
+
# [--since <iso>] narrows the listing to pull
|
|
65
|
+
# requests the host has seen change since that
|
|
66
|
+
# stamp. Measured 2026-09-21 on this repository:
|
|
67
|
+
# one `--state all` over 933 pull requests takes
|
|
68
|
+
# 29 811 ms with the fields the board needs, and
|
|
69
|
+
# the same call over one day takes 943 ms for 3
|
|
70
|
+
# rows — factor 32, with every expensive field
|
|
71
|
+
# still included.
|
|
72
|
+
# THE STAMP IS THE HOST'S OWN, passed through
|
|
73
|
+
# byte-for-byte: a caller that re-renders it
|
|
74
|
+
# sends its own clock's spelling, and a client
|
|
75
|
+
# two seconds fast excludes the PRs updated in
|
|
76
|
+
# that gap from every later window — forever,
|
|
77
|
+
# because the window never reopens.
|
|
78
|
+
# GITHUB NARROWS AND BITBUCKET'S BULK LISTING
|
|
79
|
+
# CANNOT. `gh pr list` takes `--search`; `bb pr
|
|
80
|
+
# list` has no query flag at all (verified
|
|
81
|
+
# against bb 1.9.0: `unknown flag: --query`), so
|
|
82
|
+
# only the per-branch sweep's REST `q=` can
|
|
83
|
+
# carry it. The bulk Bitbucket listing SAYS it
|
|
84
|
+
# could not narrow and answers in full, because
|
|
85
|
+
# a full answer reported as a delta is what
|
|
86
|
+
# would let a caller advance a watermark over a
|
|
87
|
+
# window it never applied.
|
|
64
88
|
# [--repo <owner/repo>] pins the list to ONE
|
|
65
89
|
# repository, exactly as pr-state and pr-merged
|
|
66
90
|
# do. A checkout with remotes on two hosts lets
|
|
@@ -70,7 +94,24 @@
|
|
|
70
94
|
# JSON lines: {"number":N,"title":"...",
|
|
71
95
|
# "state":"...","head":"..."}
|
|
72
96
|
# --rich adds: draft, checks, mergeable, review,
|
|
73
|
-
# url, failing_checks —
|
|
97
|
+
# url, updatedAt, failing_checks —
|
|
98
|
+
# `updatedAt` is WHEN THE HOST LAST SAW THE PR
|
|
99
|
+
# CHANGE, in the host's own words and on the
|
|
100
|
+
# host's own clock (`updatedAt` on GitHub,
|
|
101
|
+
# `updated_on` on Bitbucket), "" where the CLI
|
|
102
|
+
# omits it. It is the field a durable store
|
|
103
|
+
# advances its watermark by, and it is the
|
|
104
|
+
# host's rather than this machine's because a
|
|
105
|
+
# client clock two seconds fast would exclude
|
|
106
|
+
# every PR updated in that gap from every later
|
|
107
|
+
# window — permanently, and silently.
|
|
108
|
+
# Measured 2026-09-21: over 937 PRs,
|
|
109
|
+
# `number,updatedAt` is 4715 ms against 5417 ms
|
|
110
|
+
# for the base fields and 18842 ms for
|
|
111
|
+
# `statusCheckRollup`. Asked on --rich ONLY —
|
|
112
|
+
# the plain arm is the cheap one and no shell
|
|
113
|
+
# caller reads a watermark.
|
|
114
|
+
# `failing_checks` names
|
|
74
115
|
# WHICH checks failed, the detail `checks`
|
|
75
116
|
# collapses to one word, from the same response
|
|
76
117
|
# at no extra call; [] on bitbucket and wherever
|
|
@@ -534,6 +575,436 @@ pr_list_call() { # "$@"=the host command → payload on stdout, or dies
|
|
|
534
575
|
printf '%s' "$out"
|
|
535
576
|
}
|
|
536
577
|
|
|
578
|
+
# The exit code for an answer that is INCOMPLETE rather than absent.
|
|
579
|
+
#
|
|
580
|
+
# SEVEN, BECAUSE THE VOCABULARY IS FULL BELOW IT. `pr_list_failed` spends 6
|
|
581
|
+
# (burst refusal), 5 (throttled) and 3 (everything else); 4 is the backend
|
|
582
|
+
# having no such capability; 1 is a refusal before any call; 0 is a whole
|
|
583
|
+
# answer. Two is left alone deliberately — it is bash's own conventional code
|
|
584
|
+
# for a misused builtin, and a partial answer sharing it could not be told from
|
|
585
|
+
# a shell-level fault the script never intended.
|
|
586
|
+
#
|
|
587
|
+
# NON-ZERO IS THE LOAD-BEARING PART. `plot-fleet-scan.sh:675` reads this code
|
|
588
|
+
# DIRECTLY, not through the transport, and branches on `rc -ne 0`. Exiting 0 on
|
|
589
|
+
# a partial answer would set `HOST_VERDICT=ok` over a page missing a whole
|
|
590
|
+
# state — a complete reading reported over an incomplete one, which is the
|
|
591
|
+
# quiet wrong answer this adapter refuses everywhere else. It would also
|
|
592
|
+
# re-create precisely the state fixed on 2026-08-30, when `pr-list` swallowed
|
|
593
|
+
# its own failure and exited 0 with empty stdout.
|
|
594
|
+
PR_LIST_PARTIAL_RC=7
|
|
595
|
+
|
|
596
|
+
# Run ONE `pr-list` over several host states, printing what answered.
|
|
597
|
+
#
|
|
598
|
+
# THE BITBUCKET ASYMMETRY THIS EXISTS FOR. `bb pr list` has no `all` state, so
|
|
599
|
+
# `bb_states_for all` expands to three and the arm must call `bb` once per
|
|
600
|
+
# state. GitHub takes `--state all` in a single call and can never reach this
|
|
601
|
+
# shape: there, a failure means nothing was printed.
|
|
602
|
+
#
|
|
603
|
+
# ONE MECHANISM FOR THREE CALL SITES, and that is the point rather than a
|
|
604
|
+
# tidy-up — `pr_list_call`'s own header makes the argument: *"a fix applied by
|
|
605
|
+
# hand six times is a fix that drifts, and the arm that drifts is the one
|
|
606
|
+
# nobody's repo exercises."* The three sites (rich+Jenkins, rich, plain) differ
|
|
607
|
+
# ONLY in the jq program they pipe the payload through, so that program and its
|
|
608
|
+
# arguments are what this takes.
|
|
609
|
+
#
|
|
610
|
+
# WHY THE LOOP CANNOT KEEP `|| exit $?`. That propagation is not a style tic:
|
|
611
|
+
# `pr_list_call` is invoked in a command substitution — a subshell — so the
|
|
612
|
+
# `exit` inside `pr_list_failed` leaves only that subshell, and without the
|
|
613
|
+
# propagation the outer script carries on with an empty payload and jq emits
|
|
614
|
+
# nothing. Collecting across states means the first failure can no longer end
|
|
615
|
+
# the run, so the subshell's exit code is captured and classified instead: a
|
|
616
|
+
# non-zero rc is THIS STATE FAILED, which is a different fact from this state
|
|
617
|
+
# returning an empty list.
|
|
618
|
+
#
|
|
619
|
+
# WHAT IT EXITS WITH:
|
|
620
|
+
# 0 every state answered.
|
|
621
|
+
# PR_LIST_PARTIAL_RC some answered and some did not — the rows of those
|
|
622
|
+
# that answered are on stdout, and the failures are
|
|
623
|
+
# named on stderr.
|
|
624
|
+
# the first failure's rc NO state answered. A total outage keeps the code it
|
|
625
|
+
# has always had (3, 5 or 6 by kind), so a genuine
|
|
626
|
+
# outage can never read as a partial page.
|
|
627
|
+
#
|
|
628
|
+
# A SINGLE-STATE CALL HAS NO PARTIAL ANSWER TO REPORT. With one state asked,
|
|
629
|
+
# "some answered and some did not" is unreachable by construction: either the
|
|
630
|
+
# one state answered (0) or none did (its own code). The counting below gives
|
|
631
|
+
# that for free rather than by a special case.
|
|
632
|
+
# TWO VARIADIC LISTS, ONE `"$@"`. The host command and the jq arguments are
|
|
633
|
+
# both open-ended, and bash has one positional array — so the jq side travels
|
|
634
|
+
# in this global, set by the caller immediately before the call. It is read
|
|
635
|
+
# once per state and never written here.
|
|
636
|
+
PR_LIST_JQ_ARGS=()
|
|
637
|
+
|
|
638
|
+
# --- the per-branch sweep (#333) --------------------------------------------
|
|
639
|
+
#
|
|
640
|
+
# THE JOIN ASKS ABOUT BRANCHES AND THE LIST ANSWERS ABOUT A REPOSITORY, and the
|
|
641
|
+
# gap between those two questions is #333. `plot-fleet-scan.sh` holds a dozen
|
|
642
|
+
# branch names and asks `pr-list` for every pull request the repository has
|
|
643
|
+
# ever had, then indexes what came back by `head` and discards the rest.
|
|
644
|
+
# Measured 2026-09-20 on `quatico/quaweb-website`: 12 board rows against 902
|
|
645
|
+
# pull requests, of which the join uses about 1%.
|
|
646
|
+
#
|
|
647
|
+
# THAT WOULD ONLY BE WASTEFUL IF THE LIST WERE WHOLE. It is not. `bb pr list`
|
|
648
|
+
# returns a fixed page of 50 per state, and the repository holds 886 MERGED
|
|
649
|
+
# pull requests — so 836 of them are invisible to the join, and every branch
|
|
650
|
+
# whose pull request is among them reads as having none. That is the fabricated
|
|
651
|
+
# verdict `plot-fleet-scan.sh:876` rules against by name, reached from the one
|
|
652
|
+
# direction the truncation detector below can report but not repair.
|
|
653
|
+
#
|
|
654
|
+
# SO THE SWEEP INVERTS THE QUESTION. Given the branches the caller actually
|
|
655
|
+
# tracks, it asks the REST endpoint about each one by name, through the same
|
|
656
|
+
# `q=` filter `bb` already builds for `--author`:
|
|
657
|
+
#
|
|
658
|
+
# /repositories/{ws}/{repo}/pullrequests
|
|
659
|
+
# ?q=state="MERGED" AND source.branch.name="feature/x"&pagelen=50
|
|
660
|
+
#
|
|
661
|
+
# Measured on that repository: `size: 1 | values: 1 | ids: 902` — PR 902 is one
|
|
662
|
+
# of the 836 a listing cannot reach — and a branch with no pull request answers
|
|
663
|
+
# `size: 0`, which is an EXACT ABSENCE rather than a short page.
|
|
664
|
+
#
|
|
665
|
+
# WHY NOT PAGE THE LIST INSTEAD, since the endpoint carries a `next` cursor.
|
|
666
|
+
# Two measurements refuse it. `fleet.ts:184` declares the per-refresh cost and
|
|
667
|
+
# `prRefreshMsFor` stretches the interval by it so hourly spend stays 60;
|
|
668
|
+
# walking all 18 merged pages makes the cost ~21 and the interval follows
|
|
669
|
+
# mechanically, 240 s → 1260 s, with `MAX_CADENCE_STRETCH = 8` taking the worst
|
|
670
|
+
# case to 2.8 hours. And the growth curve runs the wrong way: paging costs grow
|
|
671
|
+
# with the repository's pull-request history — the very quantity whose growth
|
|
672
|
+
# makes #333 worse — while a sweep costs what the CALLER TRACKS and is constant
|
|
673
|
+
# in pull-request count. `pagelen=100` is refused by Bitbucket (HTTP 400), so
|
|
674
|
+
# 50 is the ceiling and 18 pages was a floor rather than a safe estimate.
|
|
675
|
+
#
|
|
676
|
+
# THE LEADING SLASH IS LOAD-BEARING. `bb api` concatenates "${BB_API}${path}",
|
|
677
|
+
# so a path without it yields `…/2.0repositories/…` and an HTTP 403 that reads
|
|
678
|
+
# exactly like a missing scope. A previous plan was rejected for inferring a
|
|
679
|
+
# scope problem from this symptom. It is written once, here, and pinned.
|
|
680
|
+
#
|
|
681
|
+
# `bb`'s OWN PAGINATOR IS NOT REACHED FOR, and that is deliberate rather than
|
|
682
|
+
# incidental: `bb:203` caps at 10 pages — 500 rows at `pagelen=50`, 386 short
|
|
683
|
+
# of 886 — and exits with no error and no marker. A helper that silently
|
|
684
|
+
# returns a prefix is worse here than one that refuses.
|
|
685
|
+
|
|
686
|
+
# Bitbucket's own state word for one of this adapter's states.
|
|
687
|
+
#
|
|
688
|
+
# The `q=` filter matches Bitbucket's vocabulary, which is upper-case and calls
|
|
689
|
+
# a rejected pull request DECLINED. The adapter's own words are `bb pr list`'s,
|
|
690
|
+
# which are lower-case. One mapping, so a caller never spells a state twice.
|
|
691
|
+
bb_query_state() { # $1=adapter state word → Bitbucket's
|
|
692
|
+
case "$1" in
|
|
693
|
+
open) printf 'OPEN' ;;
|
|
694
|
+
merged) printf 'MERGED' ;;
|
|
695
|
+
declined) printf 'DECLINED' ;;
|
|
696
|
+
superseded) printf 'SUPERSEDED' ;;
|
|
697
|
+
*) die "bb_query_state: unknown state '$1'" ;;
|
|
698
|
+
esac
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
# Percent-encode one value for a URL query string.
|
|
702
|
+
#
|
|
703
|
+
# BRANCH NAMES ARE NOT URL-SAFE and this repository proves it: `feature/x` is
|
|
704
|
+
# the ordinary shape, and `/` inside an unencoded `q=` value ends the filter
|
|
705
|
+
# early, so the query would ask about `feature` and answer about the wrong
|
|
706
|
+
# branch — or about none. Encoding is done here rather than by the caller so
|
|
707
|
+
# every query on this path is encoded the same way.
|
|
708
|
+
#
|
|
709
|
+
# `LC_ALL=C` makes the loop byte-wise, so a multi-byte character is encoded as
|
|
710
|
+
# its bytes rather than mangled into one `?`. Branch names carrying non-ASCII
|
|
711
|
+
# are rare and entirely legal.
|
|
712
|
+
url_encode() { # $1=raw → percent-encoded on stdout
|
|
713
|
+
local _s="$1" _i _c _out=""
|
|
714
|
+
local LC_ALL=C
|
|
715
|
+
for (( _i = 0; _i < ${#_s}; _i++ )); do
|
|
716
|
+
_c="${_s:_i:1}"
|
|
717
|
+
case "$_c" in
|
|
718
|
+
[a-zA-Z0-9.~_-]) _out="$_out$_c" ;;
|
|
719
|
+
*) _out="$_out$(printf '%%%02X' "'$_c")" ;;
|
|
720
|
+
esac
|
|
721
|
+
done
|
|
722
|
+
printf '%s' "$_out"
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
# Ask Bitbucket about ONE branch in ONE state, and print the `values` array.
|
|
726
|
+
#
|
|
727
|
+
# WHAT IT PRINTS is the endpoint's `values` — a JSON array of 0 or 1 pull
|
|
728
|
+
# request objects, in the SAME shape `bb pr list --json` emits, which is what
|
|
729
|
+
# lets the existing jq programs consume it unchanged. The `--rich` field set
|
|
730
|
+
# (`plot-host.sh:3247`) reads `.id`, `.title`, `.state`, `.source.branch.name`,
|
|
731
|
+
# `.draft` and `.links.html.href`; the REST object carries all six.
|
|
732
|
+
#
|
|
733
|
+
# AN ABSENT ANSWER IS NOT A FAILED ONE, and the exit code is what says which.
|
|
734
|
+
# `size: 0` is an honest absence — the branch has no pull request in this state
|
|
735
|
+
# — and exits 0 with `[]` on stdout. A refused call exits non-zero and prints
|
|
736
|
+
# nothing, so a caller reading the CODE can never mistake an outage for an
|
|
737
|
+
# empty repository. `plot-fleet-scan.sh:891` records that exact confusion
|
|
738
|
+
# happening from the other side: a host exiting 0 while printing nothing once
|
|
739
|
+
# read as "this repo has no PRs".
|
|
740
|
+
#
|
|
741
|
+
# `pagelen=50` rather than 1. A branch may legitimately carry several pull
|
|
742
|
+
# requests in one state — a merged attempt and a merged successor — and the
|
|
743
|
+
# consumers rank them (`fleet.ts`'s `prOutranks`, the scan's OPEN-before-MERGED
|
|
744
|
+
# sort). Asking for one would silently hand them whichever the host listed
|
|
745
|
+
# first, which no adapter promises. Fifty is the endpoint's ceiling and costs
|
|
746
|
+
# the same as one.
|
|
747
|
+
# THE HOST'S OWN FAILURE TEXT LEAVES HERE UNCLASSIFIED, and that is the whole
|
|
748
|
+
# reason this does not call `pr_list_call`. That wrapper classifies a failure
|
|
749
|
+
# ONCE — throttled, burst, or everything else — and composes the sentence a
|
|
750
|
+
# reader acts on. Calling it per branch and again around the sweep classifies
|
|
751
|
+
# twice, and the second pass reads the FIRST pass's prose rather than the host's
|
|
752
|
+
# message: measured here, a `429` became *"the host failed the request and said
|
|
753
|
+
# nothing"* because `Rate limit … exceeded` was no longer in the text being
|
|
754
|
+
# matched. So the raw stderr and the raw exit code travel out of this function
|
|
755
|
+
# untouched, and the single `pr_list_call` that `pr_list_states` already wraps
|
|
756
|
+
# the whole sweep in does the one classification — exactly the layering
|
|
757
|
+
# `bb pr list` has always had.
|
|
758
|
+
bb_branch_query() { # $1=branch $2=adapter state; rest=global bb args → values[]
|
|
759
|
+
local _br="$1" _st="$2"; shift 2
|
|
760
|
+
local _q _path _out _rc
|
|
761
|
+
_q="state=$(url_encode "\"$(bb_query_state "$_st")\"") AND source.branch.name=$(url_encode "\"$_br\"")"
|
|
762
|
+
# THE WINDOW COMPOSES WITH THE STATE FILTER AND NEVER REPLACES IT. Bitbucket
|
|
763
|
+
# takes ONE `q=` parameter, so a second one would silently win or lose
|
|
764
|
+
# depending on the host's parsing — and either way the state clause this query
|
|
765
|
+
# is built around would be the term at risk. `AND` is the same conjunction the
|
|
766
|
+
# two terms above already use.
|
|
767
|
+
#
|
|
768
|
+
# ENCODED LIKE EVERY OTHER TERM ON THIS PATH, and an ISO stamp needs it: `:`
|
|
769
|
+
# and `-` are not in `url_encode`'s safe set, and the block header records
|
|
770
|
+
# what an unencoded value costs — `/` ends the filter early, so the query asks
|
|
771
|
+
# about one thing and answers about another.
|
|
772
|
+
#
|
|
773
|
+
# `>=` RATHER THAN `>`, and the asymmetry with the GitHub arm is deliberate.
|
|
774
|
+
# Bitbucket's `updated_on` carries microseconds and GitHub's stamp does not,
|
|
775
|
+
# so an exclusive comparison against a truncated stamp would drop a PR updated
|
|
776
|
+
# inside the same second. Re-reading one row the caller already holds costs a
|
|
777
|
+
# row; missing one costs a change nobody ever sees again.
|
|
778
|
+
[ -n "$PR_LIST_SINCE" ] \
|
|
779
|
+
&& _q="$_q AND updated_on>=$(url_encode "\"$PR_LIST_SINCE\"")"
|
|
780
|
+
# The space between the two terms is encoded too; `bb api` passes the path to
|
|
781
|
+
# curl verbatim and an unencoded space would truncate the request line.
|
|
782
|
+
_q="${_q// /%20}"
|
|
783
|
+
# THE LEADING SLASH. See the block header — without it this is a 403 that
|
|
784
|
+
# reads as a scope error.
|
|
785
|
+
_path="/repositories/{ws}/{repo}/pullrequests?q=${_q}&pagelen=50"
|
|
786
|
+
# `jq` is applied only to a SUCCESSFUL payload. Piping a failed call into it
|
|
787
|
+
# would turn the host's exit code into jq's, and a parse error and a spent
|
|
788
|
+
# quota are not the same fact.
|
|
789
|
+
_out="$(bb "$@" api "$_path")" || return $?
|
|
790
|
+
printf '%s' "$_out" | jq -c '.values // []'
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
# Ask about EVERY tracked branch in one state, printing one combined array.
|
|
794
|
+
#
|
|
795
|
+
# THE SHAPE `pr_list_states` ALREADY EXPECTS. It calls its host command once
|
|
796
|
+
# per state and pipes the result through a jq program that starts `.[]`, so the
|
|
797
|
+
# sweep's job is to produce the same thing a single `bb pr list --state X`
|
|
798
|
+
# would have: one JSON array of pull request objects. The states loop, the row
|
|
799
|
+
# counting, the truncation report and the partial-answer rule above all stay in
|
|
800
|
+
# exactly one place — `pr_list_call`'s own header makes that argument, and a
|
|
801
|
+
# sweep with its own copy of the loop is the drift it names.
|
|
802
|
+
#
|
|
803
|
+
# A BRANCH THAT FAILS ENDS THE STATE, and that is the conservative direction.
|
|
804
|
+
# The combined array is only an answer if every branch in it was asked; one
|
|
805
|
+
# refused query means absence is no longer derivable for that branch, and a
|
|
806
|
+
# short array reported as whole is what #333 IS. So a failure propagates —
|
|
807
|
+
# `pr_list_call` exits — and `pr_list_states` classifies the state as failed,
|
|
808
|
+
# which reaches the caller as a partial answer (exit 7) when other states
|
|
809
|
+
# answered, or as the failure's own code when none did. One vocabulary.
|
|
810
|
+
# THE CALLING CONVENTION IS `pr_list_states`', NOT THIS FUNCTION'S OWN. That
|
|
811
|
+
# helper appends `--state <s> --json` to whatever command it was given, so a
|
|
812
|
+
# sweep that wants to sit in the same slot must accept those two trailing
|
|
813
|
+
# arguments and read the state out of them. Doing it the other way — teaching
|
|
814
|
+
# `pr_list_states` which of its commands is a sweep — would put a backend's
|
|
815
|
+
# shape inside the one piece of this file that has none.
|
|
816
|
+
#
|
|
817
|
+
# `--json` is accepted and ignored. The REST payload is JSON whether or not it
|
|
818
|
+
# is asked for, and refusing a flag the caller must pass would make the slot
|
|
819
|
+
# incompatible for the sake of a distinction with no consequence.
|
|
820
|
+
bb_branch_sweep() { # global bb args… --state <s> --json → one JSON array
|
|
821
|
+
local _st="" _args=() _acc="[]" _br _rc
|
|
822
|
+
while [ $# -gt 0 ]; do
|
|
823
|
+
case "$1" in
|
|
824
|
+
--state) _st="${2:?}"; shift 2 ;;
|
|
825
|
+
--json) shift ;;
|
|
826
|
+
*) _args+=("$1"); shift ;;
|
|
827
|
+
esac
|
|
828
|
+
done
|
|
829
|
+
[ -n "$_st" ] || die "bb_branch_sweep: no --state"
|
|
830
|
+
# THE ANSWERS ARE SPOOLED TO A FILE AND JOINED ONCE, NEVER PASSED THROUGH
|
|
831
|
+
# ARGV. The first version accumulated with
|
|
832
|
+
# `jq -c --argjson add "$_one" '. + $add'`, which hands a whole branch's
|
|
833
|
+
# payload to `jq` as a command-line argument — and Linux caps one argument at
|
|
834
|
+
# `MAX_ARG_STRLEN` (128 KB) where macOS has no such ceiling.
|
|
835
|
+
#
|
|
836
|
+
# WHAT THAT COST, measured 2026-09-20 against a Debian container: a branch
|
|
837
|
+
# carrying 886 merged pull requests is a 147 KB payload, `jq` died with
|
|
838
|
+
# *"Argument list too long"*, `_acc` came back EMPTY, and the sweep exited 0
|
|
839
|
+
# while printing no rows AND stating its completeness — a confident claim of
|
|
840
|
+
# "no pull requests" over a branch that had 886. That is precisely the
|
|
841
|
+
# fabricated verdict this whole slice exists to remove, rebuilt one layer in.
|
|
842
|
+
# It passed on macOS and failed only on Linux, which is where CI and every
|
|
843
|
+
# board run.
|
|
844
|
+
#
|
|
845
|
+
# A FILE HAS NO SUCH CEILING, and one `jq -s add` over the spool replaces N
|
|
846
|
+
# re-parses of a growing accumulator: the old shape re-read every row it had
|
|
847
|
+
# already seen once per branch, so eleven branches parsed the first branch's
|
|
848
|
+
# payload eleven times.
|
|
849
|
+
local _spool
|
|
850
|
+
_spool="$(mktemp "/tmp/plot-host-sweep.$$.XXXXXX")" || return 3
|
|
851
|
+
for _br in $PR_LIST_BRANCHES; do
|
|
852
|
+
# RETURN, NOT EXIT. This runs inside the command substitution
|
|
853
|
+
# `pr_list_call` wraps the sweep in, so the code must travel back as this
|
|
854
|
+
# function's status for that wrapper to classify it. An `exit` here would
|
|
855
|
+
# leave the substitution with an empty payload and a code the wrapper reads
|
|
856
|
+
# as the sweep's own — the silent empty list `pr_list_call`'s header names.
|
|
857
|
+
#
|
|
858
|
+
# The spool is removed on EVERY exit path, including the failing one: a
|
|
859
|
+
# sweep that gives up mid-way must not leave a payload behind in /tmp.
|
|
860
|
+
bb_branch_query "$_br" "$_st" ${_args[@]+"${_args[@]}"} >> "$_spool" \
|
|
861
|
+
|| { _rc=$?; rm -f "$_spool"; return $_rc; }
|
|
862
|
+
done
|
|
863
|
+
# `-s` reads the whole stream as one array of arrays; `add` flattens it.
|
|
864
|
+
# An EMPTY spool — every branch answered `[]` — makes `add` yield `null`, so
|
|
865
|
+
# the fallback keeps the contract that this prints a JSON ARRAY, which is
|
|
866
|
+
# what the caller's `.[]` needs.
|
|
867
|
+
_acc="$(jq -c -s 'add // []' < "$_spool")" || { rm -f "$_spool"; return 3; }
|
|
868
|
+
rm -f "$_spool"
|
|
869
|
+
printf '%s' "$_acc"
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
# The branches a sweep asks about, newline-or-space separated. Empty means the
|
|
873
|
+
# caller named none, and the arm keeps the bulk listing it has always used.
|
|
874
|
+
#
|
|
875
|
+
# A GLOBAL FOR `PR_LIST_JQ_ARGS`' REASON, stated two hundred lines above: the
|
|
876
|
+
# host command is already variadic and bash has one positional array. It is set
|
|
877
|
+
# by the `pr-list` arm immediately before the call and read nowhere else.
|
|
878
|
+
#
|
|
879
|
+
# SPACE-SEPARATED, AND GIT IS WHAT MAKES THAT SAFE. `bb_branch_sweep` reads this
|
|
880
|
+
# with an unquoted `for`, so a name carrying whitespace would split into two
|
|
881
|
+
# branches that do not exist. `git check-ref-format` REFUSES a ref name
|
|
882
|
+
# containing a space or a tab — verified 2026-09-20, both exit non-zero — so the
|
|
883
|
+
# separator is git's guarantee rather than a hopeful convention.
|
|
884
|
+
PR_LIST_BRANCHES=""
|
|
885
|
+
|
|
886
|
+
# The window a sweep's query carries, the host's own stamp. Empty means ask
|
|
887
|
+
# about everything, which is what every caller predating `--since` asks.
|
|
888
|
+
#
|
|
889
|
+
# A GLOBAL FOR `PR_LIST_BRANCHES`' REASON, and it travels the same route: the
|
|
890
|
+
# `pr-list` arm sets it immediately before the call, `bb_branch_sweep` passes
|
|
891
|
+
# through without reading it, and `bb_branch_query` composes it into the one
|
|
892
|
+
# `q=` Bitbucket takes. Threading it through the sweep as an argument would mean
|
|
893
|
+
# teaching that function a parameter it only forwards, and its header already
|
|
894
|
+
# refuses the mirror of that — "teaching `pr_list_states` which of its commands
|
|
895
|
+
# is a sweep would put a backend's shape inside the one piece of this file that
|
|
896
|
+
# has none."
|
|
897
|
+
PR_LIST_SINCE=""
|
|
898
|
+
|
|
899
|
+
# How many branches the last sweep asked about, and how many answered.
|
|
900
|
+
#
|
|
901
|
+
# THE COMPLETENESS SIGNAL, AND WHY THE ARM STATES IT RATHER THAN THE SCAN
|
|
902
|
+
# INFERRING IT. `plot-fleet-scan.sh:901` writes `.list-complete` when
|
|
903
|
+
# `0 < rows < PR_LIST_LIMIT` — completeness read off a single page's size,
|
|
904
|
+
# which is the only evidence a bulk listing offers. A sweep has no page: each
|
|
905
|
+
# query returns 0 or 1, and `size: 0` is already an exact answer for that
|
|
906
|
+
# branch. So completeness stops being a property of a row count and becomes a
|
|
907
|
+
# property of the SWEEP — every tracked branch was asked and each one answered
|
|
908
|
+
# — which is a stronger claim than the page heuristic could ever make, and one
|
|
909
|
+
# this side can state as a fact rather than leave to be guessed from a number.
|
|
910
|
+
#
|
|
911
|
+
# A PARTIAL SWEEP MUST NOT MAKE THE CLAIM. If any branch's query failed, the
|
|
912
|
+
# survivors are still valid answers and are still printed, but absence is no
|
|
913
|
+
# longer derivable for the branches that went unasked. The line is emitted only
|
|
914
|
+
# when every state answered.
|
|
915
|
+
#
|
|
916
|
+
# ONE COUNTER, NOT TWO. How many branches ANSWERED is not tracked beside this,
|
|
917
|
+
# because a failed branch query ends its whole state (see `bb_branch_sweep`) and
|
|
918
|
+
# the states tally `pr_list_states` already keeps is therefore the same fact. A
|
|
919
|
+
# second counter would be a second answer to one question, and the two would
|
|
920
|
+
# drift the first time either side changed.
|
|
921
|
+
PR_SWEEP_ASKED=0
|
|
922
|
+
|
|
923
|
+
# State on stderr that the sweep was WHOLE — the licence `.list-complete` needs.
|
|
924
|
+
#
|
|
925
|
+
# THE LINE IS A CONTRACT, not a log. `plot-fleet-scan.sh` reads it to decide
|
|
926
|
+
# whether a cache miss means "no pull request" or "never asked", so its wording
|
|
927
|
+
# is pinned by a test the same way the truncation report's is. It names both
|
|
928
|
+
# counts, because a reader who sees the claim should be able to check it.
|
|
929
|
+
#
|
|
930
|
+
# WHAT IT LICENSES IS A SHORTCUT, NOT THE ANSWER. Without it, a `--ask` caller
|
|
931
|
+
# whose branch missed the join falls through to a `pr-state` call per branch and
|
|
932
|
+
# still gets a correct answer — the per-branch N+1 that #216 removed, which is a
|
|
933
|
+
# COST regression rather than a wrong one. So withholding the line is always
|
|
934
|
+
# safe and is what a partial sweep does.
|
|
935
|
+
#
|
|
936
|
+
# EVERY STATE MUST HAVE ANSWERED. A sweep asks each branch once per state, and a
|
|
937
|
+
# branch's absence is only established when every state was asked about it: a
|
|
938
|
+
# merged pull request missed because the `merged` state failed reads exactly
|
|
939
|
+
# like a branch that never had one. So the claim is made on the STATES' tally,
|
|
940
|
+
# which `pr_list_states` already keeps, rather than on a per-branch count that
|
|
941
|
+
# would have to be reconciled with it.
|
|
942
|
+
#
|
|
943
|
+
# SILENT WHEN NO SWEEP RAN. The bulk path makes no per-branch claim and keeps
|
|
944
|
+
# the row-count heuristic it has always used, so nothing is printed and no
|
|
945
|
+
# existing caller's behaviour changes.
|
|
946
|
+
pr_sweep_report() { # $1=states answered $2=states asked
|
|
947
|
+
[ -n "$PR_LIST_BRANCHES" ] || return 0
|
|
948
|
+
[ "$1" -eq "$2" ] 2>/dev/null || return 0
|
|
949
|
+
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
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
pr_list_states() { # $1=backend $2=limit $3=states $4=jq-program; rest=the host command
|
|
953
|
+
local backend="$1" limit="$2" states="$3" jq_prog="$4"; shift 4
|
|
954
|
+
local _s _raw _rc _err _tmp _ok=0 _failed=0 _first_rc=0 _failed_states=""
|
|
955
|
+
for _s in $states; do
|
|
956
|
+
_tmp="/tmp/plot-host-prlist-state-err.$$.$_s"
|
|
957
|
+
# THE SUBSHELL'S CODE IS THE ONLY CHANNEL OUT, so it is captured rather
|
|
958
|
+
# than propagated. `pr_list_failed` runs INSIDE the substitution and has
|
|
959
|
+
# already composed its report and its repair line; that text is spooled
|
|
960
|
+
# here only so the state's name can be added to it before it is passed on.
|
|
961
|
+
_raw="$(pr_list_call "$@" --state "$_s" --json 2>"$_tmp")"; _rc=$?
|
|
962
|
+
_err="$(cat "$_tmp" 2>/dev/null)"; rm -f "$_tmp"
|
|
963
|
+
if [ "$_rc" -ne 0 ]; then
|
|
964
|
+
# THE HOST'S OWN REPORT GOES FIRST AND IS NEVER PREFIXED. `pr_list_failed`
|
|
965
|
+
# already composed the sentence that says WHY — a spent quota, a burst
|
|
966
|
+
# refusal, a DNS blip — and that sentence is what a reader acts on. A line
|
|
967
|
+
# of this helper's own naming WHICH state, emitted ahead of it, buries the
|
|
968
|
+
# reason under the bookkeeping: the scan reads the first stderr line into
|
|
969
|
+
# its error field, and a reader chasing `HTTP 429` would be shown
|
|
970
|
+
# `state 'open' failed` instead. That is #912's own failure mode — a
|
|
971
|
+
# message describing the wrong thing — reproduced one layer up, and the
|
|
972
|
+
# contract suite caught it.
|
|
973
|
+
[ -n "$_err" ] && printf '%s\n' "$_err" >&2
|
|
974
|
+
_failed=$((_failed + 1))
|
|
975
|
+
[ "$_first_rc" -eq 0 ] && _first_rc=$_rc
|
|
976
|
+
_failed_states="${_failed_states:+$_failed_states, }$_s"
|
|
977
|
+
continue
|
|
978
|
+
fi
|
|
979
|
+
[ -n "$_err" ] && printf '%s\n' "$_err" >&2
|
|
980
|
+
_ok=$((_ok + 1))
|
|
981
|
+
# A SWEEP MAKES NO PAGE CLAIM, so the page detector is not asked. Its rule
|
|
982
|
+
# is about a LISTING that cannot report a total — see its header — and a
|
|
983
|
+
# sweep's row count has no page semantics at all: the count is how many of
|
|
984
|
+
# the asked branches have a pull request in this state, and two of eleven is
|
|
985
|
+
# a complete answer rather than a short one. Running it here would print
|
|
986
|
+
# "possibly truncated" immediately before `pr_sweep_report` states the
|
|
987
|
+
# answer was whole, which is the adapter contradicting itself on one stream.
|
|
988
|
+
#
|
|
989
|
+
# THE DETECTOR ITSELF IS UNTOUCHED and still fires exactly as it did on
|
|
990
|
+
# every listing call — `host.test.mjs:3060` passes unedited. What changed is
|
|
991
|
+
# that a path exists whose premise it was never written about.
|
|
992
|
+
[ -n "$PR_LIST_BRANCHES" ] || pr_list_report_truncation "$backend" "$limit" "$_s" \
|
|
993
|
+
"$(jq 'length' <<<"$_raw" 2>/dev/null || echo 0)"
|
|
994
|
+
printf '%s' "$_raw" | jq -c ${PR_LIST_JQ_ARGS[@]+"${PR_LIST_JQ_ARGS[@]}"} "$jq_prog"
|
|
995
|
+
done
|
|
996
|
+
pr_sweep_report "$_ok" "$((_ok + _failed))"
|
|
997
|
+
[ -z "$_failed_states" ] && return 0
|
|
998
|
+
if [ "$_ok" -eq 0 ]; then
|
|
999
|
+
# NO STATE ANSWERED — a total outage, and it keeps the code it has always
|
|
1000
|
+
# had so a real outage can never be read as a partial page.
|
|
1001
|
+
echo "plot-host: pr-list: no state answered; this is not a partial answer" >&2
|
|
1002
|
+
return "$_first_rc"
|
|
1003
|
+
fi
|
|
1004
|
+
echo "plot-host: pr-list: answered $_ok of $((_ok + _failed)) states; missing: $_failed_states" >&2
|
|
1005
|
+
return "$PR_LIST_PARTIAL_RC"
|
|
1006
|
+
}
|
|
1007
|
+
|
|
537
1008
|
# --- Jenkins CI integration ------------------------------------------------
|
|
538
1009
|
# A repo may declare `CI: jenkins` independently of `Git host`. When it does,
|
|
539
1010
|
# build status (`checks`) is resolved through `jen` — a multibranch job's
|
|
@@ -1683,6 +2154,33 @@ tracker_projects() {
|
|
|
1683
2154
|
printf '%s' "$raw" | tr ',' '\n' | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//' | grep -v '^$' || true
|
|
1684
2155
|
}
|
|
1685
2156
|
|
|
2157
|
+
# Reads ONE variable from a `.env`-shaped file. Evaluates nothing.
|
|
2158
|
+
#
|
|
2159
|
+
# NOT `set -a; . ./.env; set +a`, which is the usual one-liner. It was measured
|
|
2160
|
+
# aborting in zsh on a file whose third line holds an unquoted JSON object, and
|
|
2161
|
+
# it imports every unrelated variable in the file — which on a credentials path
|
|
2162
|
+
# is a reason of its own.
|
|
2163
|
+
#
|
|
2164
|
+
# NOT `grep '^NAME=' | cut -d= -f2-` either: measured 2026-09-17, that returns
|
|
2165
|
+
# EMPTY for an `export `-prefixed line and KEEPS THE QUOTES on a quoted one, and
|
|
2166
|
+
# a quoted token reaching `curl -u` produces the 401 this read exists to remove.
|
|
2167
|
+
#
|
|
2168
|
+
# THE STRIP ORDER IS THE DEFECT, and it has been got wrong twice. Whitespace is
|
|
2169
|
+
# stripped BEFORE the quotes and again after. On `T="tok" ` the `"$` anchor
|
|
2170
|
+
# misses if the quotes go first, and the value keeps them:
|
|
2171
|
+
#
|
|
2172
|
+
# quotes first: ["tok"] whitespace first: [tok]
|
|
2173
|
+
#
|
|
2174
|
+
# `head -1` takes the first assignment, so a duplicated name resolves the way a
|
|
2175
|
+
# shell reading top-to-bottom would.
|
|
2176
|
+
read_env_var() { # $1=name $2=file → the value, or nothing
|
|
2177
|
+
sed -n "s/^[[:space:]]*\(export[[:space:]]\+\)\{0,1\}$1=//p" "$2" \
|
|
2178
|
+
| head -1 \
|
|
2179
|
+
| sed -e 's/[[:space:]]*$//' \
|
|
2180
|
+
-e 's/^"\(.*\)"$/\1/' -e "s/^'\(.*\)'\$/\1/" \
|
|
2181
|
+
-e 's/[[:space:]]*$//'
|
|
2182
|
+
}
|
|
2183
|
+
|
|
1686
2184
|
# The env var scheme for Jira auth. The plan left the EXACT names open, to be
|
|
1687
2185
|
# confirmed against a real instance; these follow Jira Cloud's documented Basic
|
|
1688
2186
|
# scheme (email + API token, base64'd into an Authorization header):
|
|
@@ -1695,14 +2193,55 @@ tracker_projects() {
|
|
|
1695
2193
|
# This guard is called in the MAIN shell, BEFORE the `$(jira_curl …)` capture —
|
|
1696
2194
|
# `die3` exits the whole script only from there, not from inside a command
|
|
1697
2195
|
# substitution where it would end only the subshell and leak a second error.
|
|
2196
|
+
#
|
|
2197
|
+
# WHERE BOTH ARE UNSET, THE REPOSITORY'S `.env` IS READ. The refusal below named
|
|
2198
|
+
# two variables without ever looking where a repository puts them, so an
|
|
2199
|
+
# operator holding working credentials — measured 2026-09-17, a 200 from
|
|
2200
|
+
# `/rest/api/3/myself` with the same pair — was sent to create a second token.
|
|
2201
|
+
# "Export it in your shell" does not reach the board either: it is a long-lived
|
|
2202
|
+
# process, which is why `plot-fleetctl.sh` bakes an environment into its unit.
|
|
2203
|
+
#
|
|
2204
|
+
# THE ENVIRONMENT WINS AND NOTHING IS READ WHERE IT ANSWERS. The file is opened
|
|
2205
|
+
# only when BOTH are unset, so a deliberate export is never second-guessed and
|
|
2206
|
+
# the common path touches no disk.
|
|
2207
|
+
#
|
|
2208
|
+
# AND THE SOURCE IS NAMED, which is a requirement rather than a nicety: two
|
|
2209
|
+
# tokens may exist, and an operator debugging a 401 must be able to tell which
|
|
2210
|
+
# one was used. `plot-board-probe.sh` reports `auth` as three words rather than
|
|
2211
|
+
# a boolean for the same reason. THE VALUE IS NEVER PRINTED — only the source.
|
|
2212
|
+
jira_load_env_file() {
|
|
2213
|
+
local root env_file email token
|
|
2214
|
+
[ -z "${JIRA_EMAIL:-}" ] && [ -z "${JIRA_API_TOKEN:-}" ] || return 0
|
|
2215
|
+
|
|
2216
|
+
# The repository root, `plot-config.sh:145`'s idiom. NO UPWARD WALK past it:
|
|
2217
|
+
# a search toward $HOME would read a file the operator did not mean for this
|
|
2218
|
+
# repository.
|
|
2219
|
+
root="$(git rev-parse --show-toplevel 2>/dev/null)" || root="."
|
|
2220
|
+
env_file="$root/.env"
|
|
2221
|
+
[ -r "$env_file" ] || return 0
|
|
2222
|
+
|
|
2223
|
+
email="$(read_env_var JIRA_EMAIL "$env_file")"
|
|
2224
|
+
token="$(read_env_var JIRA_API_TOKEN "$env_file")"
|
|
2225
|
+
# BOTH OR NEITHER. Half a Basic credential authenticates nothing, and a
|
|
2226
|
+
# partial pickup would turn today's honest refusal into a 401 further in.
|
|
2227
|
+
[ -n "$email" ] && [ -n "$token" ] || return 0
|
|
2228
|
+
|
|
2229
|
+
JIRA_EMAIL="$email"
|
|
2230
|
+
JIRA_API_TOKEN="$token"
|
|
2231
|
+
export JIRA_EMAIL JIRA_API_TOKEN
|
|
2232
|
+
echo "plot-host: JIRA_EMAIL and JIRA_API_TOKEN read from .env" >&2
|
|
2233
|
+
}
|
|
2234
|
+
|
|
1698
2235
|
jira_require_config() {
|
|
1699
2236
|
if [ -z "$(tracker_base_url)" ]; then
|
|
1700
2237
|
die3 "Tracker is jira but no base URL is configured (write 'Tracker: jira https://your.atlassian.net' or set PLOT_JIRA_BASE_URL)"
|
|
1701
2238
|
fi
|
|
2239
|
+
jira_load_env_file
|
|
1702
2240
|
if [ -z "${JIRA_EMAIL:-}" ] || [ -z "${JIRA_API_TOKEN:-}" ]; then
|
|
1703
2241
|
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
2242
|
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
|
|
2243
|
+
echo " then export JIRA_EMAIL=<your account email> and JIRA_API_TOKEN=<the token>," >&2
|
|
2244
|
+
echo " or put both in this repository's .env (which .gitignore already excludes)." >&2
|
|
1706
2245
|
exit 3
|
|
1707
2246
|
fi
|
|
1708
2247
|
}
|
|
@@ -1734,12 +2273,45 @@ jira_curl() {
|
|
|
1734
2273
|
return $rc
|
|
1735
2274
|
}
|
|
1736
2275
|
|
|
2276
|
+
# The account a Jira record is keyed on — DERIVED, never the email itself.
|
|
2277
|
+
#
|
|
2278
|
+
# THE EMAIL IS HALF A BASIC CREDENTIAL and this ledger is written on every call:
|
|
2279
|
+
# measured 2026-09-17, `$HOME/.plot/state/budget.tsv` held 2448 jira lines on one
|
|
2280
|
+
# machine. Until `.env` was read that happened only where somebody exported the
|
|
2281
|
+
# variable deliberately; it now happens wherever a `.env` exists — a population
|
|
2282
|
+
# that never consented to a machine-local record of it. A change that widens who
|
|
2283
|
+
# gets written down owns the writing down.
|
|
2284
|
+
#
|
|
2285
|
+
# AND IT STAYS PER-ACCOUNT DISTINGUISHABLE, because the field is a MATCH KEY and
|
|
2286
|
+
# not a label: `plot-budget.sh:250` is `if ($2 != want_c || $3 != want_a) next`,
|
|
2287
|
+
# `spend-rate` publishes it, and `decodeEntry`/`sameKey` read it. One machine's
|
|
2288
|
+
# ledger holds three distinct Jira accounts, so a CONSTANT redaction would merge
|
|
2289
|
+
# their rate windows and the rate a connector reads becomes the sum of several
|
|
2290
|
+
# people's. A hash keeps the key one-to-one while carrying no address.
|
|
2291
|
+
#
|
|
2292
|
+
# AT THE SOURCE rather than at `budget.tsv`, because fixing the one known writer
|
|
2293
|
+
# leaves the next to inherit the defect — `slots-file.ts:185` turns an account
|
|
2294
|
+
# into a DIRECTORY NAME and is one `slots.acquire` call away from being live.
|
|
2295
|
+
#
|
|
2296
|
+
# `jira:` prefixed and truncated to 12 hex: long enough that two accounts on one
|
|
2297
|
+
# machine will not collide, short enough to read in a ledger line.
|
|
2298
|
+
jira_budget_account() {
|
|
2299
|
+
local raw="${JIRA_EMAIL:-}"
|
|
2300
|
+
[ -n "$raw" ] || { printf 'unknown\n'; return 0; }
|
|
2301
|
+
local h
|
|
2302
|
+
h="$(printf '%s' "$raw" | shasum -a 256 2>/dev/null | awk '{print $1}')"
|
|
2303
|
+
# No hasher, no guess: a raw email must never be the fallback, so an
|
|
2304
|
+
# unhashable account degrades to the same word an absent one uses.
|
|
2305
|
+
[ -n "$h" ] || { printf 'unknown\n'; return 0; }
|
|
2306
|
+
printf 'jira:%s\n' "${h:0:12}"
|
|
2307
|
+
}
|
|
2308
|
+
|
|
1737
2309
|
# Records one Jira call. Jira meters, publishes no header this adapter reads,
|
|
1738
2310
|
# and this slice does not add header parsing — so the reading is `unknown`,
|
|
1739
2311
|
# which is never read as free.
|
|
1740
2312
|
budget_record_jira() {
|
|
1741
2313
|
[ -z "${PLOT_BUDGET_OFF:-}" ] || return 0
|
|
1742
|
-
budget_append jira "$
|
|
2314
|
+
budget_append jira "$(jira_budget_account)" api 1 - - - unknown
|
|
1743
2315
|
}
|
|
1744
2316
|
|
|
1745
2317
|
# Split a jira_curl response into (body, status) and enforce the three outcomes.
|
|
@@ -1788,12 +2360,20 @@ jira_check() {
|
|
|
1788
2360
|
# AT LEAST the requested limit — the host may have
|
|
1789
2361
|
# had more that the limit hid. Fewer rows than the
|
|
1790
2362
|
# limit PROVES completeness.
|
|
1791
|
-
# bitbucket (IGNORES --limit): `bb pr list` has no --limit and
|
|
1792
|
-
# total
|
|
2363
|
+
# bitbucket (IGNORES --limit): `bb pr list` has no --limit and reports neither
|
|
2364
|
+
# a total nor a cursor, so it can NEVER prove
|
|
1793
2365
|
# completeness for a --limit call. Any non-empty
|
|
1794
2366
|
# page is therefore possibly truncated. An empty
|
|
1795
2367
|
# page had nothing to truncate.
|
|
1796
2368
|
#
|
|
2369
|
+
# THE PREMISE ABOVE IS ABOUT `bb pr list`, AND IT WAS ONCE WRITTEN ABOUT
|
|
2370
|
+
# BITBUCKET. It said the host "cannot report a total or a cursor" — true of the
|
|
2371
|
+
# CLI's listing and false of the REST endpoint behind it, which carries both a
|
|
2372
|
+
# `size` and a `next`. That mattered the moment a path existed that could ask:
|
|
2373
|
+
# the per-branch sweep (#333) proves completeness exactly, per branch, and this
|
|
2374
|
+
# detector is deliberately not asked about it (`pr_list_states`). The rule below
|
|
2375
|
+
# is unchanged and still governs every listing call.
|
|
2376
|
+
#
|
|
1797
2377
|
# No --limit was requested → the caller accepted the host's default page and is
|
|
1798
2378
|
# owed no report, so no existing no-limit caller's behaviour changes.
|
|
1799
2379
|
#
|
|
@@ -1961,6 +2541,18 @@ backend() {
|
|
|
1961
2541
|
# written beside it.
|
|
1962
2542
|
. "$here/plot-budget.sh"
|
|
1963
2543
|
|
|
2544
|
+
# THE MEMO IS SWEPT ON THE WAY OUT. `budget_rate` caches its reading under
|
|
2545
|
+
# `$PLOT_BUDGET_HOME/memo/$$` because a command substitution is a subshell and a
|
|
2546
|
+
# variable set inside one does not survive it — see the block above
|
|
2547
|
+
# `budget_rate`. A directory keyed on a pid must be removed by the process that
|
|
2548
|
+
# made it, or a long-lived machine accumulates one per `plot-host.sh` call.
|
|
2549
|
+
#
|
|
2550
|
+
# `EXIT` ALONE, deliberately. It runs on a normal return and on an uncaught
|
|
2551
|
+
# signal's default termination path is irrelevant here: the sweep is an
|
|
2552
|
+
# optimisation's housekeeping, and a cache that outlives one run costs a stale
|
|
2553
|
+
# reading at worst, which is the same staleness the memo grants by design.
|
|
2554
|
+
trap 'budget_memo_clear' EXIT
|
|
2555
|
+
|
|
1964
2556
|
# WHO IS SPENDING — read from the CLI's own config, never from an API call.
|
|
1965
2557
|
#
|
|
1966
2558
|
# `gh api user` would answer authoritatively and cost one request against the
|
|
@@ -2490,13 +3082,30 @@ case "$op" in
|
|
|
2490
3082
|
else
|
|
2491
3083
|
# Establish that bb supports --json BEFORE calling it — Done-when 5.
|
|
2492
3084
|
bb_require_json
|
|
3085
|
+
# THE SAME KEY SET AS THE GITHUB ARM, INCLUDING `mergeCommit`. This arm
|
|
3086
|
+
# dropped that key on all four of its paths until 2026-09-18, and the
|
|
3087
|
+
# consumer reads it as `.mergeCommit // empty` — where `jq` cannot tell an
|
|
3088
|
+
# absent key from an empty one. So `plot-reconcile-scan.sh` reported
|
|
3089
|
+
# `no merge commit → cannot resolve` for every delivered plan on a
|
|
3090
|
+
# Bitbucket repository, which reads as a host that answered rather than an
|
|
3091
|
+
# arm that never asked. `pr-list`'s arm is the precedent: it emits every
|
|
3092
|
+
# key its GitHub arm does, because absent is not false.
|
|
3093
|
+
#
|
|
3094
|
+
# `merge_commit.hash` IS TAKEN FROM THE PAYLOAD ALREADY FETCHED — the same
|
|
3095
|
+
# field `pr-merge-commit` reads from the same shape. A second `bb` call to
|
|
3096
|
+
# re-ask for it would double a cost measured at ~10s per call.
|
|
3097
|
+
#
|
|
3098
|
+
# `// ""` COLLAPSES THREE SHAPES INTO ONE HONEST VALUE: `merge_commit`
|
|
3099
|
+
# absent on an open PR, the object null, or the hash null. `""` is what
|
|
3100
|
+
# the GitHub arm gives for anything unmerged, so a caller cannot tell the
|
|
3101
|
+
# backends apart.
|
|
2493
3102
|
if [[ "$ref" =~ ^[0-9]+$ ]]; then
|
|
2494
3103
|
if out="$(bb ${repo_args[@]+"${repo_args[@]}"} pr view "$ref" --json 2>/tmp/plot-host-err.$$)"; then
|
|
2495
3104
|
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"
|
|
3105
|
+
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
3106
|
else
|
|
2498
3107
|
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 $?
|
|
3108
|
+
host_miss_or_fail "$err" '{"number":0,"state":"NONE","draft":false,"url":"","mergeCommit":""}' || exit $?
|
|
2500
3109
|
fi
|
|
2501
3110
|
else
|
|
2502
3111
|
# The list CALL succeeding and the branch being absent FROM the list are
|
|
@@ -2534,12 +3143,20 @@ case "$op" in
|
|
|
2534
3143
|
if [ "$bb_rc" = 0 ]; then
|
|
2535
3144
|
rm -f "/tmp/plot-host-err.$$"
|
|
2536
3145
|
out="$(jq -c -s 'add // []' <<<"$out")"
|
|
3146
|
+
# THE BRANCH ARM CARRIES `mergeCommit` TOO, and it is the path that
|
|
3147
|
+
# matters most: `plot-pr-state.sh:33` asks `pr-state "idea/${SLUG}"` —
|
|
3148
|
+
# a branch, not a number — and `:47` reads `.mergeCommit // empty`
|
|
3149
|
+
# from the answer. A fix touching only the numeric pair above would
|
|
3150
|
+
# leave that caller reading an absent key on every Bitbucket repo.
|
|
3151
|
+
#
|
|
3152
|
+
# The hash rides on the page the state walk already fetched, so this
|
|
3153
|
+
# costs no extra call.
|
|
2537
3154
|
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"
|
|
3155
|
+
| if .==null then {number:0,state:"NONE",draft:false,url:"",mergeCommit:""}
|
|
3156
|
+
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
3157
|
else
|
|
2541
3158
|
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 $?
|
|
3159
|
+
host_miss_or_fail "$err" '{"number":0,"state":"NONE","draft":false,"url":"","mergeCommit":""}' || exit $?
|
|
2543
3160
|
fi
|
|
2544
3161
|
fi
|
|
2545
3162
|
fi
|
|
@@ -2768,17 +3385,60 @@ case "$op" in
|
|
|
2768
3385
|
# that wants history says how much; the default stays the host's, so no
|
|
2769
3386
|
# existing caller's result changes.
|
|
2770
3387
|
limit=""
|
|
3388
|
+
branches=""
|
|
3389
|
+
# THE WINDOW, AND IT IS THE HOST'S OWN STAMP. Empty means ask about
|
|
3390
|
+
# everything, which is what every caller predating this asks and what a
|
|
3391
|
+
# caller with no stored watermark must ask. A caller that has one sends it
|
|
3392
|
+
# verbatim — see the header: re-rendering it is how a window closes forever.
|
|
3393
|
+
since=""
|
|
2771
3394
|
while [ $# -gt 0 ]; do
|
|
2772
3395
|
case "$1" in
|
|
2773
3396
|
--state) state="${2:?}"; shift 2 ;;
|
|
2774
3397
|
--limit) limit="${2:?}"; shift 2 ;;
|
|
2775
3398
|
--rich) rich=1; shift ;;
|
|
2776
3399
|
--repo) repo_args=(-R "${2:?}"); shift 2 ;;
|
|
3400
|
+
# `${2:?}` REFUSES AN EMPTY VALUE, and that is the point rather than
|
|
3401
|
+
# boilerplate. `--since ""` would reach GitHub as `--search "updated:>"`,
|
|
3402
|
+
# a syntax error the host may answer with everything or with nothing —
|
|
3403
|
+
# and a caller whose watermark was null would send exactly that.
|
|
3404
|
+
--since) since="${2:?}"; shift 2 ;;
|
|
3405
|
+
# THE BRANCHES THE CALLER TRACKS, repeatable, and OPT-IN. Given any,
|
|
3406
|
+
# the Bitbucket arm sweeps the REST endpoint once per branch per state
|
|
3407
|
+
# instead of listing the repository; given none, every existing caller
|
|
3408
|
+
# gets exactly the listing it always got. Four callers pass none today
|
|
3409
|
+
# (`plot-fleet-scan.sh`, `plot-open-pr.sh`, `plot-impl-status.sh` and
|
|
3410
|
+
# `fleet.ts`), so the bulk path stays the default rather than the
|
|
3411
|
+
# legacy one.
|
|
3412
|
+
#
|
|
3413
|
+
# WHY THE CALLER NAMES THEM AND THIS OP DOES NOT GUESS. The same rule
|
|
3414
|
+
# `--repo` states a few lines up: the caller knows which branches its
|
|
3415
|
+
# refs came from and this op cannot. Deriving them here — from remote
|
|
3416
|
+
# refs, say — would make the adapter invent a working set, and a sweep
|
|
3417
|
+
# over the wrong set answers confidently about branches nobody asked
|
|
3418
|
+
# about while missing the ones they did.
|
|
3419
|
+
--branch) branches="${branches:+$branches }${2:?}"; shift 2 ;;
|
|
2777
3420
|
*) die "pr-list: unknown arg $1" ;;
|
|
2778
3421
|
esac
|
|
2779
3422
|
done
|
|
2780
3423
|
limit_args=()
|
|
2781
3424
|
[ -n "$limit" ] && limit_args=(--limit "$limit")
|
|
3425
|
+
# THE WINDOW AS GITHUB TAKES IT, built once and appended at all three call
|
|
3426
|
+
# sites — the same argument `pr_list_call`'s header makes about the six
|
|
3427
|
+
# hand-applied fixes: three sites each composing their own search string is
|
|
3428
|
+
# three places for the next window's shape to drift.
|
|
3429
|
+
#
|
|
3430
|
+
# `updated:>` is EXCLUSIVE, and that is the safe direction here. The
|
|
3431
|
+
# watermark is the stamp of a row the caller has ALREADY stored, so
|
|
3432
|
+
# excluding it re-asks nothing; including it would re-fetch that row every
|
|
3433
|
+
# pass for no new information. A PR updated in the same second as the
|
|
3434
|
+
# watermark is the one this can miss, and the periodic full read the caller
|
|
3435
|
+
# is required to keep making is what corrects it.
|
|
3436
|
+
#
|
|
3437
|
+
# THE STAMP IS NOT QUOTED INSIDE THE QUERY. `gh` sends `--search`'s value as
|
|
3438
|
+
# one API parameter and an ISO-8601 stamp carries no space, so a quote would
|
|
3439
|
+
# travel to GitHub as part of the term and match nothing.
|
|
3440
|
+
search_args=()
|
|
3441
|
+
[ -n "$since" ] && search_args=(--search "updated:>$since")
|
|
2782
3442
|
|
|
2783
3443
|
# --- Jenkins CI integration (orthogonal to Git host) ---
|
|
2784
3444
|
# When `CI: jenkins` is configured, build status comes from Jenkins rather
|
|
@@ -2889,8 +3549,8 @@ case "$op" in
|
|
|
2889
3549
|
# $jstatus != "ok" → Jenkins could not answer; every row `unknown`.
|
|
2890
3550
|
# $jentry == null → the branch has no Jenkins job; `none`.
|
|
2891
3551
|
# otherwise → the joined colour's `checks`, job named on fail.
|
|
2892
|
-
_gh_raw="$(pr_list_call gh ${repo_args[@]+"${repo_args[@]}"} pr list --state "$state" ${limit_args[@]+"${limit_args[@]}"} \
|
|
2893
|
-
--json number,title,state,headRefName,isDraft,mergeable,mergeStateStatus,reviewDecision,url)" || exit $?
|
|
3552
|
+
_gh_raw="$(pr_list_call gh ${repo_args[@]+"${repo_args[@]}"} pr list --state "$state" ${limit_args[@]+"${limit_args[@]}"} ${search_args[@]+"${search_args[@]}"} \
|
|
3553
|
+
--json number,title,state,headRefName,isDraft,mergeable,mergeStateStatus,reviewDecision,url,updatedAt)" || exit $?
|
|
2894
3554
|
pr_list_report_truncation github "$limit" "$state" \
|
|
2895
3555
|
"$(jq 'length' <<<"$_gh_raw" 2>/dev/null || echo 0)"
|
|
2896
3556
|
printf '%s' "$_gh_raw" \
|
|
@@ -2910,6 +3570,7 @@ case "$op" in
|
|
|
2910
3570
|
else "unknown" end),
|
|
2911
3571
|
review:(.reviewDecision // ""),
|
|
2912
3572
|
url:.url,
|
|
3573
|
+
updatedAt:(.updatedAt // ""),
|
|
2913
3574
|
failing_checks:(
|
|
2914
3575
|
if $jentry != null and $jentry.checks == "failing"
|
|
2915
3576
|
then [$jentry.job]
|
|
@@ -2918,8 +3579,8 @@ case "$op" in
|
|
|
2918
3579
|
}'
|
|
2919
3580
|
else
|
|
2920
3581
|
# GitHub without Jenkins (or Jenkins not configured): use GitHub rollup
|
|
2921
|
-
_gh_raw="$(pr_list_call gh ${repo_args[@]+"${repo_args[@]}"} pr list --state "$state" ${limit_args[@]+"${limit_args[@]}"} \
|
|
2922
|
-
--json number,title,state,headRefName,isDraft,statusCheckRollup,mergeable,mergeStateStatus,reviewDecision,url)" || exit $?
|
|
3582
|
+
_gh_raw="$(pr_list_call gh ${repo_args[@]+"${repo_args[@]}"} pr list --state "$state" ${limit_args[@]+"${limit_args[@]}"} ${search_args[@]+"${search_args[@]}"} \
|
|
3583
|
+
--json number,title,state,headRefName,isDraft,statusCheckRollup,mergeable,mergeStateStatus,reviewDecision,url,updatedAt)" || exit $?
|
|
2923
3584
|
pr_list_report_truncation github "$limit" "$state" \
|
|
2924
3585
|
"$(jq 'length' <<<"$_gh_raw" 2>/dev/null || echo 0)"
|
|
2925
3586
|
printf '%s' "$_gh_raw" \
|
|
@@ -2941,6 +3602,7 @@ case "$op" in
|
|
|
2941
3602
|
else "unknown" end),
|
|
2942
3603
|
review:(.reviewDecision // ""),
|
|
2943
3604
|
url:.url,
|
|
3605
|
+
updatedAt:(.updatedAt // ""),
|
|
2944
3606
|
failing_checks:[
|
|
2945
3607
|
.statusCheckRollup[]? | select((if (.conclusion // "") != "" then .conclusion else (.status // .state) end) as $c
|
|
2946
3608
|
| $c=="FAILURE" or $c=="ERROR" or $c=="CANCELLED"
|
|
@@ -2949,7 +3611,7 @@ case "$op" in
|
|
|
2949
3611
|
}'
|
|
2950
3612
|
fi
|
|
2951
3613
|
else
|
|
2952
|
-
_gh_raw="$(pr_list_call gh ${repo_args[@]+"${repo_args[@]}"} pr list --state "$state" ${limit_args[@]+"${limit_args[@]}"} \
|
|
3614
|
+
_gh_raw="$(pr_list_call gh ${repo_args[@]+"${repo_args[@]}"} pr list --state "$state" ${limit_args[@]+"${limit_args[@]}"} ${search_args[@]+"${search_args[@]}"} \
|
|
2953
3615
|
--json number,title,state,headRefName)" || exit $?
|
|
2954
3616
|
pr_list_report_truncation github "$limit" "$state" \
|
|
2955
3617
|
"$(jq 'length' <<<"$_gh_raw" 2>/dev/null || echo 0)"
|
|
@@ -2972,7 +3634,11 @@ case "$op" in
|
|
|
2972
3634
|
# Forwarding it errors with `unknown flag`, and dropping it silently
|
|
2973
3635
|
# would serve a short page as if it were the whole set — the quiet wrong
|
|
2974
3636
|
# answer this adapter refuses elsewhere. So it is dropped AND said.
|
|
2975
|
-
|
|
3637
|
+
# A SWEEP IS NOT A PAGE AND OWES NO SUCH WARNING. `--limit` bounds a
|
|
3638
|
+
# listing; a per-branch query returns that branch's pull requests and
|
|
3639
|
+
# nothing was capped, so the notice would describe a truncation that did
|
|
3640
|
+
# not happen. Said only for the listing it is about.
|
|
3641
|
+
if [ -n "$limit" ] && [ -z "$branches" ]; then
|
|
2976
3642
|
echo "plot-host: bitbucket ignores --limit $limit; bb returns a fixed page (50 at 1.0.0)" >&2
|
|
2977
3643
|
fi
|
|
2978
3644
|
# Establish that bb supports --json BEFORE calling it — Done-when 5.
|
|
@@ -2983,17 +3649,47 @@ case "$op" in
|
|
|
2983
3649
|
# with no output — an unknown state reading as "no PRs matched", which
|
|
2984
3650
|
# is the exact failure this translation exists to remove.
|
|
2985
3651
|
bb_states="$(bb_states_for "$state")" || exit 1
|
|
3652
|
+
# THE ONE PLACE THE SWEEP IS CHOSEN, and it is chosen as a COMMAND rather
|
|
3653
|
+
# than as a flag the three sites below each test. They differ only in the
|
|
3654
|
+
# jq program they pipe the payload through — `pr_list_states`' header says
|
|
3655
|
+
# so — and a sweep that added an `if` to each would make them differ in two
|
|
3656
|
+
# ways, which is how the six hand-applied fixes `pr_list_call` warns about
|
|
3657
|
+
# began. One assignment here; the sites are untouched but for this word.
|
|
3658
|
+
PR_LIST_BRANCHES="$branches"
|
|
3659
|
+
# THE WINDOW REACHES THE SWEEP AND NOT THE LISTING, and the asymmetry is
|
|
3660
|
+
# the CLI's rather than a choice. `bb pr list` takes `--state`, `--author`,
|
|
3661
|
+
# `--json` and `--jq` and no query flag at all — verified against bb 1.9.0,
|
|
3662
|
+
# which answers `unknown flag: --query` — so the only Bitbucket path that
|
|
3663
|
+
# can carry `updated_on` is `bb_branch_query`'s own REST `q=`.
|
|
3664
|
+
#
|
|
3665
|
+
# SAID RATHER THAN SWALLOWED, for the reason `--limit` two blocks up is
|
|
3666
|
+
# said: a caller that asked for a window and got a full listing must not
|
|
3667
|
+
# read the answer as a delta. It would advance its watermark over a window
|
|
3668
|
+
# it never applied — harmless this pass, since a full listing holds every
|
|
3669
|
+
# row a narrow one would, and wrong the moment the caller uses the flag to
|
|
3670
|
+
# decide whether its answer was complete.
|
|
3671
|
+
PR_LIST_SINCE="$since"
|
|
3672
|
+
if [ -n "$since" ] && [ -z "$branches" ]; then
|
|
3673
|
+
echo "plot-host: bitbucket ignores --since $since on a listing; bb pr list has no query flag (bb 1.9.0) — answering in full" >&2
|
|
3674
|
+
PR_LIST_SINCE=""
|
|
3675
|
+
fi
|
|
3676
|
+
PR_SWEEP_ASKED=0
|
|
3677
|
+
bb_cmd=(bb ${repo_args[@]+"${repo_args[@]}"} pr list)
|
|
3678
|
+
if [ -n "$branches" ]; then
|
|
3679
|
+
# A SWEEP'S COST IS THE CALLER'S WORKING SET, and it is reported so the
|
|
3680
|
+
# caller can check the claim it is about to be handed. Branches × states
|
|
3681
|
+
# — 11 branches over 3 states is 33 exact queries, against 3 listings
|
|
3682
|
+
# that answer for 50 of 902 rows.
|
|
3683
|
+
for _b in $branches; do PR_SWEEP_ASKED=$((PR_SWEEP_ASKED + 1)); done
|
|
3684
|
+
bb_cmd=(bb_branch_sweep ${repo_args[@]+"${repo_args[@]}"})
|
|
3685
|
+
fi
|
|
2986
3686
|
if [ "$rich" = 1 ]; then
|
|
2987
3687
|
if [ "$ci" = "jenkins" ]; then
|
|
2988
3688
|
# Bitbucket PR list, `checks` filled from Jenkins — the SAME overlay
|
|
2989
3689
|
# the GitHub arm uses, which is why it lives above the backend branch.
|
|
2990
3690
|
# `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" '.[] |
|
|
3691
|
+
PR_LIST_JQ_ARGS=(--argjson jmap "$jen_map" --arg jstatus "$jen_status")
|
|
3692
|
+
pr_list_states bitbucket "$limit" "$bb_states" '.[] |
|
|
2997
3693
|
($jmap[.source.branch.name] // null) as $jentry |
|
|
2998
3694
|
{
|
|
2999
3695
|
number:.id, title:.title,
|
|
@@ -3008,31 +3704,25 @@ case "$op" in
|
|
|
3008
3704
|
mergeable:"unknown",
|
|
3009
3705
|
review:"",
|
|
3010
3706
|
url:(.links.html.href // ""),
|
|
3707
|
+
updatedAt:(.updated_on // ""),
|
|
3011
3708
|
failing_checks:(
|
|
3012
3709
|
if $jentry != null and $jentry.checks == "failing"
|
|
3013
3710
|
then [$jentry.job]
|
|
3014
3711
|
else []
|
|
3015
3712
|
end)
|
|
3016
|
-
}'
|
|
3017
|
-
done
|
|
3713
|
+
}' "${bb_cmd[@]}" || exit $?
|
|
3018
3714
|
else
|
|
3019
3715
|
# 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
|
|
3716
|
+
PR_LIST_JQ_ARGS=()
|
|
3717
|
+
pr_list_states bitbucket "$limit" "$bb_states" \
|
|
3718
|
+
'.[] | {number:.id,title:.title,state:(if .state=="DECLINED" then "CLOSED" else .state end),head:.source.branch.name,draft:(.draft // false),checks:"unknown",mergeable:"unknown",review:"",url:(.links.html.href // ""),updatedAt:(.updated_on // ""),failing_checks:[]}' \
|
|
3719
|
+
"${bb_cmd[@]}" || exit $?
|
|
3027
3720
|
fi
|
|
3028
3721
|
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
|
|
3722
|
+
PR_LIST_JQ_ARGS=()
|
|
3723
|
+
pr_list_states bitbucket "$limit" "$bb_states" \
|
|
3724
|
+
'.[] | {number:.id,title:.title,state:(if .state=="DECLINED" then "CLOSED" else .state end),head:.source.branch.name}' \
|
|
3725
|
+
"${bb_cmd[@]}" || exit $?
|
|
3036
3726
|
fi
|
|
3037
3727
|
fi
|
|
3038
3728
|
;;
|