@plot-pm/board 0.13.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/board-server.mjs +116 -115
- package/package.json +2 -1
- package/plot-approve.sh +18 -0
- package/plot-config.sh +17 -0
- package/plot-deliver.sh +39 -0
- package/plot-dispatch.sh +16 -0
- package/plot-host.sh +566 -77
- package/plot-reap.sh +106 -4
- package/plot-state-receipt.sh +308 -0
package/plot-host.sh
CHANGED
|
@@ -46,6 +46,15 @@
|
|
|
46
46
|
# deciding whether to REMOVE something and
|
|
47
47
|
# silence is never permission. A call that
|
|
48
48
|
# failed outright still exits 3.
|
|
49
|
+
# pr-merge-commit <branch> the merge commit sha of a merged PR for this
|
|
50
|
+
# branch, or EMPTY (exit 0) where none merged.
|
|
51
|
+
# The same query `pr-merged` runs, reading a
|
|
52
|
+
# second field of the one response. A separate
|
|
53
|
+
# subcommand because `pr-merged` prints one word
|
|
54
|
+
# and its callers read one word.
|
|
55
|
+
# EXIT 3 is the question failing, and it is not
|
|
56
|
+
# the same as an empty answer: a diff that could
|
|
57
|
+
# not be read is not a diff that was empty.
|
|
49
58
|
# pr-create --title T [--body B] [--base BR] [--head BR] [--draft]
|
|
50
59
|
# create a PR, print its URL
|
|
51
60
|
# pr-merge <number> [--squash] [--delete-branch]
|
|
@@ -96,13 +105,25 @@
|
|
|
96
105
|
# and a real failure presents identically.
|
|
97
106
|
# Nothing here compares runs or concludes.
|
|
98
107
|
# METERED: ask only for a branch already known
|
|
99
|
-
# to be failing.
|
|
100
|
-
#
|
|
101
|
-
#
|
|
108
|
+
# to be failing.
|
|
109
|
+
# DISPATCHED ON THE `CI` KEY, never on the git
|
|
110
|
+
# host: a build is the CI system's fact, and the
|
|
111
|
+
# two keys are independent. A repository that
|
|
112
|
+
# declared no CI, or one Plot has no connector
|
|
113
|
+
# for, EXITS 4 — not being able to ask is not an
|
|
114
|
+
# empty history.
|
|
115
|
+
# On jenkins it reports ONE current state as a
|
|
116
|
+
# history of one, with `startedAt` and `url`
|
|
117
|
+
# empty: `jen job list` carries no history and
|
|
118
|
+
# no timestamps, and inventing them would be a
|
|
119
|
+
# collector reaching a verdict.
|
|
102
120
|
# run-for-sha <branch> <sha> the run for ONE sha — else the branch's newest
|
|
103
121
|
# run, with `sha` saying which it is — as a
|
|
104
122
|
# single JSON object, or nothing when the branch
|
|
105
|
-
# has no runs at all
|
|
123
|
+
# has no runs at all. Dispatched on the `CI` key
|
|
124
|
+
# like `runs`, and EXITS 4 on jenkins: `jen job
|
|
125
|
+
# list` names no commit, so there is nothing to
|
|
126
|
+
# match a sha against. Output:
|
|
106
127
|
# {"sha":"…","status":"queued|in_progress|
|
|
107
128
|
# completed|waiting|requested",
|
|
108
129
|
# "conclusion":"success|failure|…|null",
|
|
@@ -138,7 +159,26 @@
|
|
|
138
159
|
# unavailable, never "no run".
|
|
139
160
|
# issue-list [--limit N] open tracker issues as JSON lines:
|
|
140
161
|
# {"number":N,"title":"…","url":"…",
|
|
141
|
-
# "createdAt":"…"
|
|
162
|
+
# "createdAt":"…","status":"…",
|
|
163
|
+
# "statusCategory":"…"}
|
|
164
|
+
# `status` is the tracker's OWN word for the
|
|
165
|
+
# stage — per-workflow, possibly localised
|
|
166
|
+
# (*Internal Approving*). `statusCategory` is
|
|
167
|
+
# the stable three-value vocabulary (To Do /
|
|
168
|
+
# In Progress / Done) a board may group on.
|
|
169
|
+
# BOTH, because neither substitutes for the
|
|
170
|
+
# other: the name is what a person reads, the
|
|
171
|
+
# category is what a board decides on. EVERY
|
|
172
|
+
# backend answers both — GitHub derives them
|
|
173
|
+
# from the `--state` this op asks for, jira
|
|
174
|
+
# reads .fields.status, and bitbucket maps the
|
|
175
|
+
# state badge it already parses. A Jira-only
|
|
176
|
+
# field is what this adapter exists to prevent.
|
|
177
|
+
# `statusCategory` is "" where the tracker's
|
|
178
|
+
# vocabulary has no word for it — bitbucket's
|
|
179
|
+
# ON HOLD/INVALID/DUPLICATE/WONTFIX. Empty is
|
|
180
|
+
# an honest answer; an invented category files
|
|
181
|
+
# abandoned work beside finished work.
|
|
142
182
|
# READ-ONLY. Plot writes no label, no assignee
|
|
143
183
|
# and no close-on-merge, because a copy of
|
|
144
184
|
# tracker state ages into a lie. The one write
|
|
@@ -519,17 +559,141 @@ pr_list_call() { # "$@"=the host command → payload on stdout, or dies
|
|
|
519
559
|
# per row rather than blanking the entire list, and exit 3 is reserved for
|
|
520
560
|
# when the operation itself cannot proceed.
|
|
521
561
|
|
|
522
|
-
#
|
|
523
|
-
#
|
|
524
|
-
#
|
|
525
|
-
#
|
|
526
|
-
#
|
|
527
|
-
|
|
562
|
+
# The raw `CI` declaration: `$PLOT_CI`, then the `CI` key, else empty.
|
|
563
|
+
#
|
|
564
|
+
# `PLOT_CI` IS A PRODUCTION CONTRACT, not a test override. The function this
|
|
565
|
+
# replaced called it *"overrides for tests"*, and that had been false since
|
|
566
|
+
# the build connectors landed: `build-actions.ts:39` passes
|
|
567
|
+
# `buildReads({ context, system: SYSTEM }, { PLOT_CI: SYSTEM })` as the dispatch
|
|
568
|
+
# contract the arm reads. It keeps its precedence and is split the same way, so
|
|
569
|
+
# a connector's bare `SYSTEM` word and a person's prose reach one comparison.
|
|
570
|
+
ci_raw() {
|
|
528
571
|
if [ -n "${PLOT_CI:-}" ]; then
|
|
529
|
-
printf '%s\n' "$PLOT_CI"
|
|
572
|
+
printf '%s\n' "$PLOT_CI"
|
|
530
573
|
return
|
|
531
574
|
fi
|
|
532
|
-
bash "$here/plot-config.sh" get "CI" ""
|
|
575
|
+
bash "$here/plot-config.sh" get "CI" ""
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
# The CI system this repository declares — the FIRST token, lowercased.
|
|
579
|
+
#
|
|
580
|
+
# THE ONE READER OF THE `CI` KEY'S SCHEME, and every arm that dispatches on the
|
|
581
|
+
# CI system compares this. The function it replaced returned the WHOLE value
|
|
582
|
+
# while its four callers each matched a bare word (`jenkins`,
|
|
583
|
+
# `github-actions`), so a repository declaring
|
|
584
|
+
# `` CI: Jenkins at `jenkins-ci-ewz…` `` missed every arm and read
|
|
585
|
+
# `basis: unknown` where the Jenkins connector had `predicted: 60` ready.
|
|
586
|
+
# Measured 2026-09-09 against a reachable instance: only the bare word matched,
|
|
587
|
+
# and no repository on the estate writes it.
|
|
588
|
+
#
|
|
589
|
+
# THE PAYLOAD WAS THE SECOND HALF OF THAT COST. `ci-limit` reported the prose
|
|
590
|
+
# value as its `connector`, and `build-shell.ts` filters readings by equality
|
|
591
|
+
# against the connector's bare system word — so the reading was discarded before
|
|
592
|
+
# any caller saw it, and a metered Jenkins reported as metering nothing.
|
|
593
|
+
#
|
|
594
|
+
# Shaped after `tracker_scheme()` (`:1340`) — the same key packing a scheme
|
|
595
|
+
# ahead of its detail, split the same way. Empty when nothing is declared, and
|
|
596
|
+
# ABSENT IS NOT FALSE: `ci_unaskable()` reads an empty scheme as *this
|
|
597
|
+
# repository named no CI system*, which is a different answer from *this system
|
|
598
|
+
# has no connector*.
|
|
599
|
+
ci_scheme() {
|
|
600
|
+
ci_raw | awk '{print tolower($1)}'
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
# Whether a candidate token is a host `jen -I` could be given.
|
|
604
|
+
#
|
|
605
|
+
# A URL passes whole. Otherwise it must be dot-separated labels ending in an
|
|
606
|
+
# alphabetic TLD, carrying no path separator and no `@`.
|
|
607
|
+
#
|
|
608
|
+
# THE FINAL FILTER IS THE FILE EXTENSION, and that is a measurement rather than
|
|
609
|
+
# a preference. `ci.yml` and `package.json` both match the dotted-label pattern,
|
|
610
|
+
# while `.ch` and `.io` are real two-letter TLDs — so TLD LENGTH cannot separate
|
|
611
|
+
# a host from a filename in either direction, and a `CI:` value naming a
|
|
612
|
+
# workflow file (`github-actions (see .github/workflows/ci.yml)`) is the
|
|
613
|
+
# measured case that would otherwise be read as a server.
|
|
614
|
+
ci_looks_like_host() {
|
|
615
|
+
case "$1" in
|
|
616
|
+
https://*|http://*) return 0 ;;
|
|
617
|
+
*/*|*@*) return 1 ;;
|
|
618
|
+
esac
|
|
619
|
+
printf '%s\n' "$1" \
|
|
620
|
+
| grep -qE '^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*\.[a-z]{2,}$' || return 1
|
|
621
|
+
case "$1" in
|
|
622
|
+
*.yml|*.yaml|*.json|*.md|*.sh|*.js|*.ts|*.mjs|*.cjs|*.toml|*.xml|*.txt|*.lock|*.gradle) return 1 ;;
|
|
623
|
+
esac
|
|
624
|
+
return 0
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
# Where this repository's CI lives: the `Jenkins instance` key, then
|
|
628
|
+
# `$JENKINS_INSTANCE`, then a host read out of the `CI` value. Empty when none
|
|
629
|
+
# of the three names one.
|
|
630
|
+
#
|
|
631
|
+
# THE ORDER IS NOT THE TRACKER'S, and the reason is information content.
|
|
632
|
+
# `tracker_base_url()` lets `$PLOT_JIRA_BASE_URL` win over its config value
|
|
633
|
+
# because both carry the same shape — a URL. Here the key carries MORE than the
|
|
634
|
+
# prose can express: `jenkins_instance()` (`:665`) returns `<slug>/<job/path>`,
|
|
635
|
+
# and the container path is a fact no file in a repository states
|
|
636
|
+
# (`stack.ts:98` calls the host *"the SLUG half"*). A prose-derived slug alone
|
|
637
|
+
# degrades `jenkins_build_map()` to root-scope listing, where its own comment
|
|
638
|
+
# says *"every branch reads `none`"* — and `quaweb` is nested
|
|
639
|
+
# (`job/quaweb/job/release`), so that renders an empty board for the exact
|
|
640
|
+
# repository this serves. So the explicit key is the primary and the prose is
|
|
641
|
+
# the fallback, never the other way round.
|
|
642
|
+
#
|
|
643
|
+
# IT EXTRACTS A HOST, NOT A REMAINDER. "Everything after the first token" was
|
|
644
|
+
# measured against all three real values and fails on every one:
|
|
645
|
+
# `at jenkins-ci-ewz… (Bitbucket PRs…`, `pipelines in .build/pipelines/ …`,
|
|
646
|
+
# `(e.g. continuous-build…)`. None is a host, and handing `jen -I` a sentence
|
|
647
|
+
# is worse than handing it nothing. `Tracker:` gets away with the naive split
|
|
648
|
+
# because its value is a scheme and a URL; `CI:` is prose in every repository
|
|
649
|
+
# that declares it.
|
|
650
|
+
#
|
|
651
|
+
# A BACKTICKED SPAN IS PREFERRED because the backticks are a deliberate act —
|
|
652
|
+
# the writer pointed at the value. It must still look like a host: one real
|
|
653
|
+
# repository marks up a PATH (`` `.build/pipelines/` ``), which is not a server.
|
|
654
|
+
#
|
|
655
|
+
# BACKTICKS ARE STRIPPED AND NO SCHEME IS NORMALISED ON. `ewz` writes a bare
|
|
656
|
+
# host in backticks and the probe used a full `https://` URL; `jen -I` accepts
|
|
657
|
+
# either, so inventing a canonical form would break whichever caller passes the
|
|
658
|
+
# other one.
|
|
659
|
+
#
|
|
660
|
+
# EMPTY IS AN ANSWER, not a failure: `Jenkins (e.g. continuous-build, …)` names
|
|
661
|
+
# no instance. Read the exit code, never the emptiness.
|
|
662
|
+
ci_instance() {
|
|
663
|
+
local instance candidate
|
|
664
|
+
instance=$(bash "$here/plot-config.sh" get "Jenkins instance" "" 2>/dev/null || echo "")
|
|
665
|
+
[ -n "$instance" ] || instance="${JENKINS_INSTANCE:-}"
|
|
666
|
+
if [ -n "$instance" ]; then
|
|
667
|
+
printf '%s' "$instance"
|
|
668
|
+
return
|
|
669
|
+
fi
|
|
670
|
+
|
|
671
|
+
local value
|
|
672
|
+
value="$(ci_raw)"
|
|
673
|
+
|
|
674
|
+
# The backticked spans, in order: `tr` puts each on its own line, so the
|
|
675
|
+
# even-numbered lines are what sat between a pair of backticks.
|
|
676
|
+
while IFS= read -r candidate; do
|
|
677
|
+
[ -n "$candidate" ] || continue
|
|
678
|
+
if ci_looks_like_host "$candidate"; then
|
|
679
|
+
printf '%s' "$candidate"
|
|
680
|
+
return
|
|
681
|
+
fi
|
|
682
|
+
done <<EOF
|
|
683
|
+
$(printf '%s\n' "$value" | tr '\`' '\n' | awk 'NR % 2 == 0')
|
|
684
|
+
EOF
|
|
685
|
+
|
|
686
|
+
# Otherwise the first bare token that looks like a host. Punctuation that
|
|
687
|
+
# ends a clause is split off so `(jenkins.acme.dev)` reads as the host.
|
|
688
|
+
while IFS= read -r candidate; do
|
|
689
|
+
[ -n "$candidate" ] || continue
|
|
690
|
+
if ci_looks_like_host "$candidate"; then
|
|
691
|
+
printf '%s' "$candidate"
|
|
692
|
+
return
|
|
693
|
+
fi
|
|
694
|
+
done <<EOF
|
|
695
|
+
$(printf '%s\n' "$value" | tr -d '\`' | tr ' \t,()[]<>"'"'" '\n')
|
|
696
|
+
EOF
|
|
533
697
|
}
|
|
534
698
|
|
|
535
699
|
# Fetch Jenkins build statuses for every branch in a multibranch job, in ONE
|
|
@@ -634,6 +798,56 @@ jenkins_build_map() {
|
|
|
634
798
|
'
|
|
635
799
|
}
|
|
636
800
|
|
|
801
|
+
# Where this repository's Jenkins lives — the `Jenkins instance` key, else
|
|
802
|
+
# `JENKINS_INSTANCE`. Prints nothing when neither is set.
|
|
803
|
+
#
|
|
804
|
+
# Read through ONE function because three ops now ask it. `pr-list --rich` read
|
|
805
|
+
# it inline first; `runs` and `run-for-sha` ask the same question and must get
|
|
806
|
+
# the same answer and the same refusal, or a repository would be configured for
|
|
807
|
+
# one op and not the others.
|
|
808
|
+
jenkins_instance() {
|
|
809
|
+
local instance
|
|
810
|
+
instance=$(bash "$here/plot-config.sh" get "Jenkins instance" "" 2>/dev/null || echo "")
|
|
811
|
+
[ -n "$instance" ] || instance="${JENKINS_INSTANCE:-}"
|
|
812
|
+
printf '%s' "$instance"
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
# The refusal a Jenkins op prints when it has no instance to ask, naming the
|
|
816
|
+
# three repairs. Exit 3 — the op itself cannot proceed, which is a config error
|
|
817
|
+
# only a person can fix, and deliberately NOT exit 4: a `CI: jenkins` repository
|
|
818
|
+
# CAN be asked about builds, it just has not said where.
|
|
819
|
+
jenkins_no_instance() {
|
|
820
|
+
echo "plot-host: CI is jenkins but no Jenkins instance is configured" >&2
|
|
821
|
+
echo " Add a 'Jenkins instance' key to the ## Plot Config section of CLAUDE.md," >&2
|
|
822
|
+
echo " naming the instance \`jen\` knows, or set JENKINS_INSTANCE." >&2
|
|
823
|
+
echo " Or drop the 'CI: jenkins' key to read build status from the git host." >&2
|
|
824
|
+
exit 3
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
# NOT BEING ABLE TO ASK IS NOT AN EMPTY ANSWER, and exit 4 is where the two are
|
|
828
|
+
# kept apart. An empty run list means *this branch has no runs*; a repository
|
|
829
|
+
# that declared no CI system has nothing that could hold one, and a caller
|
|
830
|
+
# reading `[]` for that would report *nothing has ever failed here* about a
|
|
831
|
+
# question nobody asked.
|
|
832
|
+
#
|
|
833
|
+
# `ci-limit`'s `*)` arm already draws this line for a connector nobody wrote an
|
|
834
|
+
# estimate for. This draws it one step earlier, for a connector that does not
|
|
835
|
+
# exist at all — and `resultOf` (`adapters/run-script.ts:211`) maps exit 4 onto
|
|
836
|
+
# `unaskable`, which is the word the build port already answers with.
|
|
837
|
+
ci_unaskable() { # $1 = the op's name, $2 = the CI word (may be empty)
|
|
838
|
+
local ci_word="${2:-}"
|
|
839
|
+
if [ -z "$ci_word" ] || [ "$ci_word" = none ]; then
|
|
840
|
+
echo "plot-host: $1 — this repository declares no CI system, so there is nothing to ask" >&2
|
|
841
|
+
echo " Add a 'CI' key to the ## Plot Config section of CLAUDE.md naming the" >&2
|
|
842
|
+
echo " system that builds this repository — \`github-actions\` or \`jenkins\`." >&2
|
|
843
|
+
else
|
|
844
|
+
echo "plot-host: $1 — no connector for CI system '$ci_word'" >&2
|
|
845
|
+
echo " Plot answers this op for \`github-actions\` and \`jenkins\`. An unknown" >&2
|
|
846
|
+
echo " system cannot be asked, which is not the same as having no runs." >&2
|
|
847
|
+
fi
|
|
848
|
+
exit 4
|
|
849
|
+
}
|
|
850
|
+
|
|
637
851
|
# A LOOKUP MISS AND A TRANSPORT FAILURE ARE TWO ANSWERS, AND THE CLI GIVES ONE
|
|
638
852
|
# EXIT CODE FOR BOTH.
|
|
639
853
|
#
|
|
@@ -1280,6 +1494,60 @@ tracker_base_url() {
|
|
|
1280
1494
|
tracker_raw | awk '{print $2}' | sed 's:/*$::'
|
|
1281
1495
|
}
|
|
1282
1496
|
|
|
1497
|
+
# The Jira projects this repository's work lives in, one per line, or nothing.
|
|
1498
|
+
#
|
|
1499
|
+
# WHY A KEY AND NOT A QUERY. The inbox's default JQL scopes by PERSON
|
|
1500
|
+
# (`assignee = currentUser()`) and by STATE (`resolution = EMPTY`), and by
|
|
1501
|
+
# nothing else. On a shared Jira instance that is instance-wide: one reporter's
|
|
1502
|
+
# board showed twelve issues, of which one belonged to a different customer
|
|
1503
|
+
# entirely. Jira has no notion of the repository a board serves — no
|
|
1504
|
+
# `currentProject()` function exists — and that mapping lives only here, in this
|
|
1505
|
+
# repository's own config. Asking Jira which projects the user can see answers
|
|
1506
|
+
# the wrong question: that list is instance-wide too, and is what produced the
|
|
1507
|
+
# bug.
|
|
1508
|
+
#
|
|
1509
|
+
# IT HOLDS A LIST, and the single prefix adoption seeds only STARTS it.
|
|
1510
|
+
# `plot-detect-repo.sh` takes `head -1`, so a freshly adopted repo declares one
|
|
1511
|
+
# project. Filtering on that alone would be a second defect wearing the fix's
|
|
1512
|
+
# clothes — on the reported repository it shows 3 of 12 issues and hides the
|
|
1513
|
+
# other two projects' work under a heading claiming nobody had planned it. A
|
|
1514
|
+
# repository mapping to several Jira projects is the normal case.
|
|
1515
|
+
#
|
|
1516
|
+
# NAMED `Ticket prefixes`, not `Jira projects` (which puts a vendor in a config
|
|
1517
|
+
# key and severs the word the probe already uses) and not `Tracker projects`
|
|
1518
|
+
# (which generalises over a set of one). It sits next to `Branch prefixes` in
|
|
1519
|
+
# every adopting repo's config — an unrelated structural key holding `idea/`,
|
|
1520
|
+
# `feature/`, `bug/` — and only this docstring keeps the two apart.
|
|
1521
|
+
#
|
|
1522
|
+
# EMPTY IS THE ANSWER FOR AN UNDECLARED KEY, and callers must keep it meaning
|
|
1523
|
+
# *do not scope*: the absent case has to leave today's query untouched, or an
|
|
1524
|
+
# upgrade empties every existing board's inbox — a worse failure than the one
|
|
1525
|
+
# being fixed, because it looks like *no tickets* rather than the wrong ones.
|
|
1526
|
+
#
|
|
1527
|
+
# PARSING IS STATED HERE, not inherited: this is the estate's first list-valued
|
|
1528
|
+
# key with a consumer (`Implementation home` is documented as taking a list and
|
|
1529
|
+
# nothing splits one). Commas separate, surrounding whitespace is not part of a
|
|
1530
|
+
# value, and an empty element is dropped rather than emitted — a join over a
|
|
1531
|
+
# stray comma would otherwise produce `IN (PROJ-A,)`, which Jira rejects. A
|
|
1532
|
+
# value that is punctuation alone therefore reads as *undeclared*: it restores
|
|
1533
|
+
# the unscoped query rather than sending a query no instance would accept.
|
|
1534
|
+
#
|
|
1535
|
+
# `PLOT_TICKET_PREFIXES` overrides, the shape `PLOT_TRACKER` already sets. It
|
|
1536
|
+
# cannot express ABSENCE — an empty override falls through to the config — so
|
|
1537
|
+
# the absent case is tested with a repository that declares nothing.
|
|
1538
|
+
tracker_projects() {
|
|
1539
|
+
local raw
|
|
1540
|
+
if [ -n "${PLOT_TICKET_PREFIXES:-}" ]; then
|
|
1541
|
+
raw="$PLOT_TICKET_PREFIXES"
|
|
1542
|
+
else
|
|
1543
|
+
raw="$(bash "$here/plot-config.sh" get "Ticket prefixes" "")"
|
|
1544
|
+
fi
|
|
1545
|
+
# One per line, trimmed, empties dropped. `plot-config.sh` already normalises
|
|
1546
|
+
# `A,B` to `A, B`, but an override arrives unnormalised and a hand-edited
|
|
1547
|
+
# value can carry a stray comma — so the split is done here regardless.
|
|
1548
|
+
printf '%s' "$raw" | tr ',' '\n' | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//' | grep -v '^$' || true
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1283
1551
|
# The env var scheme for Jira auth. The plan left the EXACT names open, to be
|
|
1284
1552
|
# confirmed against a real instance; these follow Jira Cloud's documented Basic
|
|
1285
1553
|
# scheme (email + API token, base64'd into an Authorization header):
|
|
@@ -2163,6 +2431,73 @@ case "$op" in
|
|
|
2163
2431
|
fi
|
|
2164
2432
|
;;
|
|
2165
2433
|
|
|
2434
|
+
pr-merge-commit)
|
|
2435
|
+
# THE MERGE COMMIT OF A BRANCH'S MERGED PR, and nothing else.
|
|
2436
|
+
#
|
|
2437
|
+
# A SECOND SUBCOMMAND RATHER THAN A FIELD ON `pr-merged`, because that one
|
|
2438
|
+
# prints ONE WORD and eleven callers read it as one — `plot-reap.sh`,
|
|
2439
|
+
# `plot-release-refs.sh` and `plot-dispatch.sh` among them, each deciding
|
|
2440
|
+
# whether to remove something. Widening its output to carry a sha would
|
|
2441
|
+
# rewrite a contract those callers depend on, to serve a question none of
|
|
2442
|
+
# them asks.
|
|
2443
|
+
#
|
|
2444
|
+
# THE QUERY IS `pr-merged`'s, so a caller asking both spends TWO calls where
|
|
2445
|
+
# one would do — and that is a cost this path does not pay: delivery asks
|
|
2446
|
+
# this only for a branch it has already been told merged, and the plan it
|
|
2447
|
+
# serves budgets one host call per branch either way.
|
|
2448
|
+
#
|
|
2449
|
+
# `--state all` and `--limit 100` for `pr-merged`'s own two reasons: a
|
|
2450
|
+
# merged PR reports CLOSED, and the newest PR is not the merge.
|
|
2451
|
+
#
|
|
2452
|
+
# PRINTS NOTHING AND EXITS 0 where no PR merged — an ANSWER, the same shape
|
|
2453
|
+
# `pr-state` gives for a missing PR. Exit 3 is the question failing.
|
|
2454
|
+
ref="${1:?pr-merge-commit needs a branch}"; shift || true
|
|
2455
|
+
repo_args=()
|
|
2456
|
+
while [ $# -gt 0 ]; do
|
|
2457
|
+
case "$1" in
|
|
2458
|
+
--repo) repo_args=(-R "${2:?}"); shift 2 ;;
|
|
2459
|
+
*) die "pr-merge-commit: unknown arg $1" ;;
|
|
2460
|
+
esac
|
|
2461
|
+
done
|
|
2462
|
+
if [ "$be" = "github" ]; then
|
|
2463
|
+
if out="$(gh ${repo_args[@]+"${repo_args[@]}"} pr list --head "$ref" --state all --limit 100 --json mergedAt,mergeCommit 2>/tmp/plot-host-err.$$)"; then
|
|
2464
|
+
rm -f "/tmp/plot-host-err.$$"
|
|
2465
|
+
# The FIRST merged PR carrying a sha. A branch may hold several merged
|
|
2466
|
+
# PRs; each names its own merge commit, and any of them is a commit that
|
|
2467
|
+
# landed this branch's work.
|
|
2468
|
+
jq -r 'map(select(.mergedAt != null and .mergeCommit != null))
|
|
2469
|
+
| map(.mergeCommit.oid) | first // empty' <<<"$out"
|
|
2470
|
+
else
|
|
2471
|
+
err="$(cat "/tmp/plot-host-err.$$" 2>/dev/null)"; rm -f "/tmp/plot-host-err.$$"
|
|
2472
|
+
# A lookup miss is an answer: no PR, so no merge commit. Anything else
|
|
2473
|
+
# is the question failing, and silence must not read as "carried
|
|
2474
|
+
# nothing" — the caller distinguishes the two by the exit code.
|
|
2475
|
+
if [ -z "$err" ] || is_lookup_miss "$err"; then
|
|
2476
|
+
:
|
|
2477
|
+
else
|
|
2478
|
+
echo "plot-host: $err" >&2
|
|
2479
|
+
exit 3
|
|
2480
|
+
fi
|
|
2481
|
+
fi
|
|
2482
|
+
else
|
|
2483
|
+
bb_require_json
|
|
2484
|
+
# Bitbucket names the merge commit `merge_commit.hash` on a merged PR.
|
|
2485
|
+
if out="$(bb ${repo_args[@]+"${repo_args[@]}"} pr list --state merged --json 2>/tmp/plot-host-err.$$)"; then
|
|
2486
|
+
rm -f "/tmp/plot-host-err.$$"
|
|
2487
|
+
jq -r --arg b "$ref" 'map(select(.source.branch.name==$b))
|
|
2488
|
+
| map(.merge_commit.hash // empty) | first // empty' <<<"$out"
|
|
2489
|
+
else
|
|
2490
|
+
err="$(cat "/tmp/plot-host-err.$$" 2>/dev/null)"; rm -f "/tmp/plot-host-err.$$"
|
|
2491
|
+
if [ -z "$err" ] || is_lookup_miss "$err"; then
|
|
2492
|
+
:
|
|
2493
|
+
else
|
|
2494
|
+
echo "plot-host: $err" >&2
|
|
2495
|
+
exit 3
|
|
2496
|
+
fi
|
|
2497
|
+
fi
|
|
2498
|
+
fi
|
|
2499
|
+
;;
|
|
2500
|
+
|
|
2166
2501
|
pr-create)
|
|
2167
2502
|
title=""; body=""; base=""; head=""; draft=0
|
|
2168
2503
|
while [ $# -gt 0 ]; do
|
|
@@ -2276,19 +2611,12 @@ case "$op" in
|
|
|
2276
2611
|
# the board rejects a non-zero pr-list and keeps its last good map; one
|
|
2277
2612
|
# dead Jenkins must not darken every row. This reconciles Done-when 4's
|
|
2278
2613
|
# "exits 3" with the brief's "prefer unknown on the affected rows".
|
|
2279
|
-
ci="$(
|
|
2614
|
+
ci="$(ci_scheme)"
|
|
2280
2615
|
jen_map=""
|
|
2281
2616
|
jen_status=""
|
|
2282
2617
|
if [ "$ci" = "jenkins" ] && [ "$rich" = 1 ]; then
|
|
2283
|
-
jen_instance
|
|
2284
|
-
[ -n "$jen_instance" ] ||
|
|
2285
|
-
if [ -z "$jen_instance" ]; then
|
|
2286
|
-
echo "plot-host: CI is jenkins but no Jenkins instance is configured" >&2
|
|
2287
|
-
echo " Add a 'Jenkins instance' key to the ## Plot Config section of CLAUDE.md," >&2
|
|
2288
|
-
echo " naming the instance \`jen\` knows, or set JENKINS_INSTANCE." >&2
|
|
2289
|
-
echo " Or drop the 'CI: jenkins' key to read build status from the git host." >&2
|
|
2290
|
-
exit 3
|
|
2291
|
-
fi
|
|
2618
|
+
jen_instance="$(jenkins_instance)"
|
|
2619
|
+
[ -n "$jen_instance" ] || jenkins_no_instance
|
|
2292
2620
|
jen_payload=$(jenkins_build_map "$jen_instance")
|
|
2293
2621
|
jen_status=$(printf '%s' "$jen_payload" | jq -r '.status // "failed"' 2>/dev/null || echo "failed")
|
|
2294
2622
|
jen_map=$(printf '%s' "$jen_payload" | jq -c '.map // {}' 2>/dev/null || echo "{}")
|
|
@@ -2543,9 +2871,13 @@ case "$op" in
|
|
|
2543
2871
|
# and failing branches are rare by construction; a caller that asked for
|
|
2544
2872
|
# every branch would spend a budget the board has already exhausted once.
|
|
2545
2873
|
#
|
|
2546
|
-
#
|
|
2547
|
-
#
|
|
2548
|
-
#
|
|
2874
|
+
# IT DISPATCHES ON THE CI SYSTEM, NEVER ON THE GIT HOST. This arm gated on
|
|
2875
|
+
# `be` until 2026-09-08, which made this repository's accident — GitHub is
|
|
2876
|
+
# both — the shape of the op: a Bitbucket team building on Jenkins got
|
|
2877
|
+
# silence, and a GitHub team building on Jenkins got GitHub Actions runs
|
|
2878
|
+
# for a repository whose CI is not GitHub Actions. `CI` and `Git host` are
|
|
2879
|
+
# independent `## Plot Config` keys, and this is the op that reads the
|
|
2880
|
+
# first of them.
|
|
2549
2881
|
branch="${1:?runs needs a branch}"; shift
|
|
2550
2882
|
limit=10
|
|
2551
2883
|
while [ $# -gt 0 ]; do
|
|
@@ -2554,13 +2886,61 @@ case "$op" in
|
|
|
2554
2886
|
*) die "runs: unknown arg $1" ;;
|
|
2555
2887
|
esac
|
|
2556
2888
|
done
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2889
|
+
_ci="$(ci_scheme)"
|
|
2890
|
+
case "$_ci" in
|
|
2891
|
+
github-actions)
|
|
2892
|
+
# THE GIT HOST STILL HAS TO BE GITHUB, and that is a second condition
|
|
2893
|
+
# rather than the same one. `gh run list` reads the runs of the
|
|
2894
|
+
# repository the CWD's remote names, so a repository declaring
|
|
2895
|
+
# `CI: github-actions` on a Bitbucket remote has declared something
|
|
2896
|
+
# `gh` cannot reach — an unaskable configuration, not an empty history.
|
|
2897
|
+
if [ "$be" != "github" ]; then
|
|
2898
|
+
echo "plot-host: runs — CI is github-actions but the git host is '$be'" >&2
|
|
2899
|
+
echo " \`gh run list\` reads the runs of the repository its remote names," >&2
|
|
2900
|
+
echo " so there is no GitHub repository here to ask about." >&2
|
|
2901
|
+
exit 4
|
|
2902
|
+
fi
|
|
2903
|
+
gh run list --branch "$branch" --limit "$limit" \
|
|
2904
|
+
--json workflowName,conclusion,status,startedAt,url 2>/dev/null \
|
|
2905
|
+
| jq -c '.[] | {workflow:.workflowName,
|
|
2906
|
+
conclusion:(if (.conclusion // "") == "" then .status else .conclusion end),
|
|
2907
|
+
startedAt:.startedAt, url:.url}' 2>/dev/null || true
|
|
2908
|
+
;;
|
|
2909
|
+
jenkins)
|
|
2910
|
+
# ONE STATE PER BRANCH, REPORTED AS A HISTORY OF ONE — and the PR says
|
|
2911
|
+
# so, because it is the honest limit of the transport this arm has.
|
|
2912
|
+
# `jenkins_build_map` answers `{color, checks, job}` for every branch in
|
|
2913
|
+
# one call: a CURRENT state, with no timestamps, no URL and no earlier
|
|
2914
|
+
# runs. A history is what Jenkins holds and what its REST API answers;
|
|
2915
|
+
# reading it is `the-ci-connector-is-jenkins`'s own slice, and inventing
|
|
2916
|
+
# entries here to fill the shape would be the collector reaching a
|
|
2917
|
+
# verdict (Principle 3).
|
|
2918
|
+
#
|
|
2919
|
+
# SO THE FIELDS THIS CANNOT MEASURE ARE EMPTY, never guessed. `workflow`
|
|
2920
|
+
# names the Jenkins job, which is the one thing the map does carry and
|
|
2921
|
+
# the detail a reader opens; `conclusion` is the `checks` word the
|
|
2922
|
+
# colour table already settled; `startedAt` and `url` are empty,
|
|
2923
|
+
# because a timestamp this arm invented would read as measurement.
|
|
2924
|
+
_jen_instance="$(jenkins_instance)"
|
|
2925
|
+
[ -n "$_jen_instance" ] || jenkins_no_instance
|
|
2926
|
+
_jen_payload=$(jenkins_build_map "$_jen_instance")
|
|
2927
|
+
_jen_status=$(printf '%s' "$_jen_payload" | jq -r '.status // "failed"' 2>/dev/null || echo "failed")
|
|
2928
|
+
if [ "$_jen_status" != "ok" ]; then
|
|
2929
|
+
# UNREACHABLE IS NOT EMPTY EITHER. `pr-list --rich` can mark its rows
|
|
2930
|
+
# `unknown` and keep them; this op has no row to carry the word, so
|
|
2931
|
+
# the only way to say *cannot verify* is the exit code. Printing
|
|
2932
|
+
# nothing at exit 0 would read as *this branch has never run*.
|
|
2933
|
+
echo "plot-host: runs — jenkins unreachable ($_jen_status)" >&2
|
|
2934
|
+
exit 4
|
|
2935
|
+
fi
|
|
2936
|
+
printf '%s' "$_jen_payload" | jq -c --arg branch "$branch" \
|
|
2937
|
+
'.map[$branch] // empty
|
|
2938
|
+
| {workflow:(.job // ""), conclusion:(.checks // ""), startedAt:"", url:""}' 2>/dev/null || true
|
|
2939
|
+
;;
|
|
2940
|
+
*)
|
|
2941
|
+
ci_unaskable runs "$_ci"
|
|
2942
|
+
;;
|
|
2943
|
+
esac
|
|
2564
2944
|
;;
|
|
2565
2945
|
|
|
2566
2946
|
run-for-sha)
|
|
@@ -2606,39 +2986,80 @@ case "$op" in
|
|
|
2606
2986
|
*) die "run-for-sha: unknown arg $1" ;;
|
|
2607
2987
|
esac
|
|
2608
2988
|
done
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2989
|
+
# DISPATCHED ON THE CI SYSTEM, for the reason `runs` above states: the git
|
|
2990
|
+
# host is a separate key, and a build is the CI system's fact.
|
|
2991
|
+
_ci="$(ci_scheme)"
|
|
2992
|
+
case "$_ci" in
|
|
2993
|
+
jenkins)
|
|
2994
|
+
# JENKINS CANNOT ANSWER THIS THROUGH `jen`, AND THAT IS A MEASUREMENT
|
|
2995
|
+
# RATHER THAN A GAP LEFT OPEN. `jenkins_build_map` — the only Jenkins
|
|
2996
|
+
# reader this script has — answers `{color, checks, job}` per BRANCH and
|
|
2997
|
+
# carries no commit at all, so there is nothing here to match a sha
|
|
2998
|
+
# against.
|
|
2999
|
+
#
|
|
3000
|
+
# THE ANSWER EXISTS AND THIS TRANSPORT DOES NOT REACH IT. Measured
|
|
3001
|
+
# 2026-09-08 against a live instance, a build names its commit at
|
|
3002
|
+
# `actions[].BuildData.lastBuiltRevision.SHA1`, paired with its branch —
|
|
3003
|
+
# over the REST API, which `the-ci-connector-is-jenkins` reads. Until
|
|
3004
|
+
# that lands, `unaskable` is the true word: this connector cannot be
|
|
3005
|
+
# asked, which is not the same as the branch having no run for the sha.
|
|
3006
|
+
#
|
|
3007
|
+
# FALLING BACK TO THE BRANCH'S CURRENT STATE WOULD BE THE ONE ANSWER
|
|
3008
|
+
# THAT COSTS A MERGE. This op exists because a run for a superseded
|
|
3009
|
+
# commit reads identically to a run for the current one, and reporting
|
|
3010
|
+
# a branch-scoped state with no sha in it is exactly the guessing it was
|
|
3011
|
+
# written to end — two merge waiters were stopped for it on 2026-08-30.
|
|
3012
|
+
echo "plot-host: run-for-sha — jenkins has no sha-scoped answer through \`jen\`" >&2
|
|
3013
|
+
echo " \`jen job list\` reports one state per branch and names no commit." >&2
|
|
3014
|
+
echo " A build's sha is in Jenkins and reached over its REST API, which" >&2
|
|
3015
|
+
echo " the Jenkins build connector reads. Until then this cannot be asked." >&2
|
|
3016
|
+
exit 4
|
|
3017
|
+
;;
|
|
3018
|
+
github-actions) : ;;
|
|
3019
|
+
*) ci_unaskable run-for-sha "$_ci" ;;
|
|
3020
|
+
esac
|
|
3021
|
+
if [ "$be" != "github" ]; then
|
|
3022
|
+
# SAME SECOND CONDITION AS `runs`: `gh run list` reads the repository its
|
|
3023
|
+
# remote names, so `CI: github-actions` on a non-GitHub remote names runs
|
|
3024
|
+
# that cannot be reached from here.
|
|
3025
|
+
echo "plot-host: run-for-sha — CI is github-actions but the git host is '$be'" >&2
|
|
3026
|
+
echo " \`gh run list\` reads the runs of the repository its remote names," >&2
|
|
3027
|
+
echo " so there is no GitHub repository here to ask about." >&2
|
|
3028
|
+
exit 4
|
|
2641
3029
|
fi
|
|
3030
|
+
# From here the CI system is `github-actions` and the git host is GitHub;
|
|
3031
|
+
# both were established above, so nothing guards the call.
|
|
3032
|
+
# `headSha` is the field that makes this answerable at all; `runs` omits
|
|
3033
|
+
# it, which is why that op cannot be reused here.
|
|
3034
|
+
#
|
|
3035
|
+
# NEWEST FIRST, then the FIRST match is taken: `gh run list` returns runs
|
|
3036
|
+
# newest-first, and a sha can carry several (a rerun, or several
|
|
3037
|
+
# workflows). The newest is the live answer; older ones for the same sha
|
|
3038
|
+
# are superseded by the same argument that superseded runs for older shas.
|
|
3039
|
+
# THE SHA ASKED ABOUT IF THERE IS ONE, ELSE THE NEWEST RUN ON THE BRANCH —
|
|
3040
|
+
# and `sha` in the output says WHICH, because a caller that could not tell
|
|
3041
|
+
# the two apart would be back to the branch-scoped guessing this op exists
|
|
3042
|
+
# to end.
|
|
3043
|
+
#
|
|
3044
|
+
# WHY IT FALLS BACK AT ALL, rather than reporting nothing. Filtering to
|
|
3045
|
+
# the asked-for sha and stopping makes the most important case invisible:
|
|
3046
|
+
# a run IN FLIGHT for a commit the branch has already moved past reports
|
|
3047
|
+
# identically to no run at all, so a caller cannot distinguish *CI has not
|
|
3048
|
+
# started yet* from *CI is busy answering about the past*. The second is
|
|
3049
|
+
# the state that had two merge waiters reporting on superseded runs on
|
|
3050
|
+
# 2026-08-30, and it is exactly what a caller needs to see.
|
|
3051
|
+
#
|
|
3052
|
+
# IT STILL DECIDES NOTHING (Principle 3). It reports the run it found and
|
|
3053
|
+
# the sha that run is for; whether that sha being different from the one
|
|
3054
|
+
# asked about means "superseded" is the caller's rule. This collects.
|
|
3055
|
+
gh run list --branch "$branch" --limit "$limit" \
|
|
3056
|
+
--json headSha,conclusion,status,startedAt,url 2>/dev/null \
|
|
3057
|
+
| jq -c --arg sha "$sha" \
|
|
3058
|
+
'(map(select(.headSha == $sha)) | .[0]) // .[0]
|
|
3059
|
+
| select(. != null)
|
|
3060
|
+
| {sha:.headSha, status:.status,
|
|
3061
|
+
conclusion:(if (.conclusion // "") == "" then null else .conclusion end),
|
|
3062
|
+
url:.url, startedAt:.startedAt}' 2>/dev/null || true
|
|
2642
3063
|
;;
|
|
2643
3064
|
|
|
2644
3065
|
issue-list)
|
|
@@ -2674,7 +3095,29 @@ case "$op" in
|
|
|
2674
3095
|
# narrower inbox. ORDER BY created DESC so the newest ticket is first, the
|
|
2675
3096
|
# same order `createdAt` gives the GitHub arm.
|
|
2676
3097
|
jira_require_config
|
|
2677
|
-
|
|
3098
|
+
# `Ticket prefixes` SCOPES THE DEFAULT to this repository's projects. The
|
|
3099
|
+
# default alone scopes by person and by state, so on a shared instance it
|
|
3100
|
+
# is instance-wide: it returned another customer's ticket to a reporter's
|
|
3101
|
+
# board (see `tracker_projects` for the measurement and the naming).
|
|
3102
|
+
#
|
|
3103
|
+
# THE CLAUSE IS SPLICED, NOT APPENDED, and the split below is why: JQL
|
|
3104
|
+
# requires ORDER BY to close the query, so a clause added to the end of
|
|
3105
|
+
# the default string makes a query Jira rejects.
|
|
3106
|
+
#
|
|
3107
|
+
# AN UNDECLARED KEY LEAVES THE QUERY BYTE-IDENTICAL. `$scope` is empty,
|
|
3108
|
+
# the two halves rejoin exactly as they were written, and an upgrade
|
|
3109
|
+
# changes nothing for a repository that never set the key.
|
|
3110
|
+
scope=""
|
|
3111
|
+
if projects="$(tracker_projects)" && [ -n "$projects" ]; then
|
|
3112
|
+
# `IN (A, B)`, never a trailing comma: paste joins N-1 separators over N
|
|
3113
|
+
# elements, so a one-element list — what adoption's seed writes — yields
|
|
3114
|
+
# `IN (A)` rather than `IN (A,)`.
|
|
3115
|
+
scope=" AND project IN ($(printf '%s' "$projects" | paste -sd, - | sed 's/,/, /g'))"
|
|
3116
|
+
fi
|
|
3117
|
+
# PLOT_JIRA_JQL STILL WINS OVER BOTH. Teams worked around this bug with
|
|
3118
|
+
# their own JQL; an override that stopped overriding would break exactly
|
|
3119
|
+
# the people who noticed the problem first.
|
|
3120
|
+
jql="${PLOT_JIRA_JQL:-assignee = currentUser() AND resolution = EMPTY${scope} ORDER BY created DESC}"
|
|
2678
3121
|
# maxResults bounds ONE page. The inbox is small by construction (a
|
|
2679
3122
|
# person's open tickets), so no nextPageToken loop is needed; the caller's
|
|
2680
3123
|
# --limit caps it, else Jira's default page. v2 `search/jql` takes the same
|
|
@@ -2686,7 +3129,7 @@ case "$op" in
|
|
|
2686
3129
|
raw="$(jira_curl "/rest/api/2/search/jql" \
|
|
2687
3130
|
-G \
|
|
2688
3131
|
--data-urlencode "jql=$jql" \
|
|
2689
|
-
--data-urlencode "fields=summary,created" \
|
|
3132
|
+
--data-urlencode "fields=summary,created,status" \
|
|
2690
3133
|
--data-urlencode "maxResults=$max")"; curl_rc=$?
|
|
2691
3134
|
body="$(jira_check "$raw" "$curl_rc")" || exit $?
|
|
2692
3135
|
# `number` is the Jira KEY (PROJ-123), a string — #447 taught the parser to
|
|
@@ -2694,21 +3137,39 @@ case "$op" in
|
|
|
2694
3137
|
# search payload carries no browse URL, and the base is ours to know
|
|
2695
3138
|
# (Principle 3: this script is the one place that knows a host URL's shape).
|
|
2696
3139
|
base="$(tracker_base_url)"
|
|
3140
|
+
# `status` is the workflow's own word (*Internal Approving*) and may be
|
|
3141
|
+
# localised; `statusCategory` is Jira's stable three-value vocabulary
|
|
3142
|
+
# (To Do / In Progress / Done). BOTH are carried because neither
|
|
3143
|
+
# substitutes for the other: the name is what a person reads, the
|
|
3144
|
+
# category is what a board groups on. A board grouping on the name
|
|
3145
|
+
# fragments across projects that spell the same stage differently.
|
|
2697
3146
|
printf '%s' "$body" | jq -c --arg base "$base" '.issues[]? | {
|
|
2698
3147
|
number: .key,
|
|
2699
3148
|
title: (.fields.summary // ""),
|
|
2700
3149
|
url: ($base + "/browse/" + .key),
|
|
2701
|
-
createdAt: (.fields.created // "")
|
|
3150
|
+
createdAt: (.fields.created // ""),
|
|
3151
|
+
status: (.fields.status.name // ""),
|
|
3152
|
+
statusCategory: (.fields.status.statusCategory.name // "")
|
|
2702
3153
|
}'
|
|
2703
3154
|
elif [ "$be" = "github" ]; then
|
|
2704
3155
|
# `gh issue list` — not `gh api /issues`. On GitHub every PR IS an issue,
|
|
2705
3156
|
# so the REST endpoint returns both, and every open PR would arrive here
|
|
2706
3157
|
# as a signal nobody had planned. The `gh` subcommand filters PRs out;
|
|
2707
3158
|
# this note exists because that trap is invisible while it works.
|
|
2708
|
-
|
|
3159
|
+
# `status` is DERIVED from the state this call asks for, never written
|
|
3160
|
+
# twice. GitHub has no workflow states: an issue is open or closed, and
|
|
3161
|
+
# this op asks for open ones only. So the state passed to `gh` is the
|
|
3162
|
+
# answer, and `$gh_state` is read by both the flag and the projection —
|
|
3163
|
+
# a second literal would be a copy that a widened filter silently
|
|
3164
|
+
# outdates. `To Do` is the category because an open GitHub issue is
|
|
3165
|
+
# unstarted by the only vocabulary GitHub has.
|
|
3166
|
+
gh_state="open"
|
|
3167
|
+
if out="$(gh issue list --state "$gh_state" ${limit_args[@]+"${limit_args[@]}"} \
|
|
2709
3168
|
--json number,title,url,createdAt 2>/tmp/plot-host-err.$$)"; then
|
|
2710
3169
|
rm -f "/tmp/plot-host-err.$$"
|
|
2711
|
-
jq -c
|
|
3170
|
+
jq -c --arg status "$gh_state" \
|
|
3171
|
+
'.[] | {number:.number,title:.title,url:(.url // ""),createdAt:(.createdAt // ""),
|
|
3172
|
+
status:$status,statusCategory:(if $status == "open" then "To Do" else "Done" end)}' <<<"$out"
|
|
2712
3173
|
else
|
|
2713
3174
|
err="$(cat "/tmp/plot-host-err.$$" 2>/dev/null)"; rm -f "/tmp/plot-host-err.$$"
|
|
2714
3175
|
# NO empty-list fallback. `host_miss_or_fail` exists for a lookup whose
|
|
@@ -2748,15 +3209,37 @@ case "$op" in
|
|
|
2748
3209
|
[[ "$line" =~ ^#0*([0-9]+)[[:space:]]+(.*)$ ]] || continue
|
|
2749
3210
|
num="${BASH_REMATCH[1]}"
|
|
2750
3211
|
rest="${BASH_REMATCH[2]}"
|
|
2751
|
-
#
|
|
2752
|
-
# padding; the title starts after it.
|
|
2753
|
-
|
|
3212
|
+
# Lift the leading state badge word (NEW/OPEN/RESOLVED/…), then strip
|
|
3213
|
+
# it and its padding; the title starts after it. The badge was parsed
|
|
3214
|
+
# here before and THROWN AWAY to recover the title — it is captured
|
|
3215
|
+
# now rather than fetched, so this costs no extra call. ONE
|
|
3216
|
+
# vocabulary, matched once: a second copy in a mapping table is a list
|
|
3217
|
+
# that drifts from the one that strips.
|
|
3218
|
+
state=""
|
|
3219
|
+
if [[ "$rest" =~ ^(NEW|OPEN|ON\ HOLD|INVALID|RESOLVED|DUPLICATE|WONTFIX|CLOSED)[[:space:]]+(.*)$ ]]; then
|
|
3220
|
+
state="${BASH_REMATCH[1]}"
|
|
3221
|
+
rest="${BASH_REMATCH[2]}"
|
|
3222
|
+
fi
|
|
3223
|
+
# Map the badge onto the three-value category the board groups on.
|
|
3224
|
+
# NEW/OPEN are unstarted; RESOLVED/CLOSED are finished. The rest —
|
|
3225
|
+
# ON HOLD, INVALID, DUPLICATE, WONTFIX — get "" DELIBERATELY: they are
|
|
3226
|
+
# terminal-without-being-done or started-without-progressing, and
|
|
3227
|
+
# neither is a Jira category. Empty says *this vocabulary has no word
|
|
3228
|
+
# for it*, which a board can render; inventing `Done` for WONTFIX
|
|
3229
|
+
# would file abandoned work beside finished work.
|
|
3230
|
+
case "$state" in
|
|
3231
|
+
NEW|OPEN) category="To Do" ;;
|
|
3232
|
+
RESOLVED|CLOSED) category="Done" ;;
|
|
3233
|
+
*) category="" ;;
|
|
3234
|
+
esac
|
|
2754
3235
|
# Drop the trailing reporter: bb prints three spaces then `by <name>`.
|
|
2755
3236
|
title="$(sed -E 's/[[:space:]]{2,}by [^[:space:]].*$//' <<<"$rest")"
|
|
2756
3237
|
# `url` is "" — bb issue list prints none; a consumer renders the number
|
|
2757
3238
|
# as plain text, the rule the header states.
|
|
2758
3239
|
jq -cn --argjson number "$num" --arg title "$title" \
|
|
2759
|
-
|
|
3240
|
+
--arg status "$state" --arg category "$category" \
|
|
3241
|
+
'{number:$number,title:$title,url:"",createdAt:"",
|
|
3242
|
+
status:$status,statusCategory:$category}'
|
|
2760
3243
|
count=$((count + 1))
|
|
2761
3244
|
# Honour the caller's --limit HERE: bb has no --limit, so a bound the
|
|
2762
3245
|
# caller asked for is enforced by the adapter after parsing (Done-when 7).
|
|
@@ -3102,14 +3585,14 @@ case "$op" in
|
|
|
3102
3585
|
ci-limit)
|
|
3103
3586
|
# The CI connector's limit, which is a THIRD axis and does not follow the
|
|
3104
3587
|
# git host. This repo runs GitHub Actions on a GitHub remote; `ekzweb` runs
|
|
3105
|
-
# Jenkins against Bitbucket. `
|
|
3588
|
+
# Jenkins against Bitbucket. `ci_scheme()` already resolves it separately.
|
|
3106
3589
|
#
|
|
3107
3590
|
# JENKINS IS THE `predicted` CASE THE DESIGN NAMES. A Jenkins instance
|
|
3108
3591
|
# reports no rate limit — there is no header and no endpoint to ask — so the
|
|
3109
3592
|
# ceiling is this adapter's estimate of what a shared controller tolerates,
|
|
3110
3593
|
# tagged for what it is. It is NOT unlimited: a Jenkins that is hammered
|
|
3111
3594
|
# refuses, and the refusal is what corrects the estimate.
|
|
3112
|
-
_ci="$(
|
|
3595
|
+
_ci="$(ci_scheme)"
|
|
3113
3596
|
case "$_ci" in
|
|
3114
3597
|
jenkins)
|
|
3115
3598
|
echo '{"connector":"jenkins","bucket":"","limit":60,"remaining":null,"reset":null,"basis":"predicted"}'
|
|
@@ -3119,9 +3602,15 @@ case "$op" in
|
|
|
3119
3602
|
# answer, not a limit of zero.
|
|
3120
3603
|
;;
|
|
3121
3604
|
*)
|
|
3122
|
-
# `
|
|
3605
|
+
# `ci_scheme()` validates nothing, and neither does this — the list is
|
|
3123
3606
|
# open, and GitLab and Trello are named as next. A connector nobody has
|
|
3124
3607
|
# written an estimate for answers `unknown`, which is the honest word.
|
|
3608
|
+
#
|
|
3609
|
+
# IT REPORTS THE SCHEME, never the whole `CI:` value. `build-shell.ts`
|
|
3610
|
+
# filters readings by `reading.connector === shell.system`, and the
|
|
3611
|
+
# system is a bare word — so a prose connector here is discarded before
|
|
3612
|
+
# any caller sees it, and the port defines that empty answer as *a
|
|
3613
|
+
# connector that meters nothing*.
|
|
3125
3614
|
echo "{\"connector\":\"$_ci\",\"bucket\":\"\",\"limit\":null,\"remaining\":null,\"reset\":null,\"basis\":\"unknown\"}"
|
|
3126
3615
|
;;
|
|
3127
3616
|
esac
|