@plot-pm/board 0.14.0 → 0.14.2
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 +106 -103
- package/package.json +1 -1
- package/plot-deliver.sh +66 -0
- package/plot-dispatch.sh +496 -41
- package/plot-host.sh +285 -29
- package/plot-plan-meta.sh +75 -0
- package/plot-worker-state.sh +201 -2
package/plot-host.sh
CHANGED
|
@@ -716,8 +716,41 @@ EOF
|
|
|
716
716
|
# failed — Jenkins is unreachable (`jen auth status` says so, while EXITING
|
|
717
717
|
# 0 — Done-when 4: the wording decides, never `$?`), or the listing
|
|
718
718
|
# was empty/garbled. `map` is {}; the caller renders rows `unknown`.
|
|
719
|
-
# unknown — the auth wording was unrecognised
|
|
720
|
-
#
|
|
719
|
+
# unknown — the auth wording was unrecognised, or the configured job is a
|
|
720
|
+
# SHAPE NOBODY MEASURED; degrade to failure-shaped (cannot verify),
|
|
721
|
+
# never to ok. `map` is {}.
|
|
722
|
+
#
|
|
723
|
+
# THE JOB'S SHAPE DECIDES THE VERB, and reading the shape off the wrong object
|
|
724
|
+
# is the defect this function carried until 2026-09-15.
|
|
725
|
+
#
|
|
726
|
+
# `job list` enumerates a CONTAINER'S CHILDREN. A `WorkflowMultiBranchProject`
|
|
727
|
+
# has one child per branch, so listing it yields exactly the branch→colour map
|
|
728
|
+
# below. A plain `WorkflowJob` has no children, so the same call yields `null` —
|
|
729
|
+
# not an error, not an empty array — and the `type=="array"` guard reported
|
|
730
|
+
# `failed`, which is the word for an unreachable host. Measured live 2026-09-15
|
|
731
|
+
# on `Quatico.Webseite/quaweb-website`: `job list quaweb/continuous-deploy`
|
|
732
|
+
# answered `null` while `job view` on the same path answered `color blue`,
|
|
733
|
+
# `lastBuild #938 SUCCESS`. A healthy, signed-in, correctly declared pipeline
|
|
734
|
+
# read as *the connector cannot be asked*.
|
|
735
|
+
#
|
|
736
|
+
# THE DECIDING `_class` IS THE CONFIGURED JOB'S OWN, AND IT IS NOT IN THE
|
|
737
|
+
# LISTING THIS FUNCTION ALREADY PERFORMS. A child's `_class` describes the
|
|
738
|
+
# CHILD: measured live, every child of the multibranch `quaweb/continuous-build`
|
|
739
|
+
# carries `...job.WorkflowJob`, and this repository's own fixture agrees. So
|
|
740
|
+
# reading `.[0]._class` from the listing would read a healthy multibranch job as
|
|
741
|
+
# plain, route it to `job view`, and break the half that works today — while
|
|
742
|
+
# every gate still passed. That mistake sank an earlier draft of the plan.
|
|
743
|
+
#
|
|
744
|
+
# SO `job view` IS ASKED FIRST, and it answers BOTH questions in ONE call: the
|
|
745
|
+
# job's own `_class`, and — for a plain job — the `color` and `lastBuild` that
|
|
746
|
+
# are its state. The multibranch path then makes the single `job list` it has
|
|
747
|
+
# always made, so a multibranch refresh costs two calls rather than one per
|
|
748
|
+
# branch, and the branch→checks map it returns is byte-identical.
|
|
749
|
+
#
|
|
750
|
+
# EXACTLY TWO SHAPES ARE READ, and anything else is `unknown` rather than a
|
|
751
|
+
# guess. A `FreeStyleProject` has a `color` and would be readable; it still
|
|
752
|
+
# reports `unknown`, which is the honest word — *a shape nobody measured*. It is
|
|
753
|
+
# deliberately NOT `failed`, which claims the host did not answer.
|
|
721
754
|
jenkins_build_map() {
|
|
722
755
|
local instance="$1"
|
|
723
756
|
local slug job
|
|
@@ -751,6 +784,67 @@ jenkins_build_map() {
|
|
|
751
784
|
printf '{"status":"unknown","map":{}}\n'; return 0
|
|
752
785
|
fi
|
|
753
786
|
|
|
787
|
+
# THE SHAPE, READ FROM THE CONFIGURED JOB ITSELF. `job view` returns that
|
|
788
|
+
# job's own `_class` — never a child's — plus the `color` and `lastBuild` a
|
|
789
|
+
# plain job's state is made of. One call, two answers.
|
|
790
|
+
#
|
|
791
|
+
# A BARE-HOST INSTANCE NAMES NO JOB, so there is nothing to view: `job` is
|
|
792
|
+
# empty, the root scope has no `_class` of its own, and the multibranch path
|
|
793
|
+
# below already handles it by listing at the root. Probing with an empty path
|
|
794
|
+
# would ask about the instance rather than about a job.
|
|
795
|
+
local shape="" view_out=""
|
|
796
|
+
if [ -n "$job" ]; then
|
|
797
|
+
view_out=$(jen -I "$slug" job view "$job" --json 2>&1) || true
|
|
798
|
+
if [ -n "$view_out" ]; then
|
|
799
|
+
shape=$(printf '%s' "$view_out" | jq -r 'if type=="object" then (._class // "") else "" end' 2>/dev/null || echo "")
|
|
800
|
+
fi
|
|
801
|
+
fi
|
|
802
|
+
|
|
803
|
+
case "$shape" in
|
|
804
|
+
# A PLAIN PIPELINE — the case that reported `failed` until 2026-09-15. Its
|
|
805
|
+
# state is already in hand: `job view` answered it, and no `job list`
|
|
806
|
+
# follows, because listing a job with no children is what returned `null`.
|
|
807
|
+
#
|
|
808
|
+
# THE BRANCH KEY IS THE JOB PATH'S LAST SEGMENT. A plain job builds one
|
|
809
|
+
# thing and Jenkins names no branch for it, so there is no branch→colour
|
|
810
|
+
# map to build. Keying on the job's own name is what lets `.map[$branch]`
|
|
811
|
+
# find it — `runs` reads that key, and the op's caller asks by the name the
|
|
812
|
+
# instance declares.
|
|
813
|
+
*'.WorkflowJob')
|
|
814
|
+
printf '%s' "$view_out" | jq -c --arg job "$job" '
|
|
815
|
+
def color_to_checks:
|
|
816
|
+
if . == null or . == "" then "none"
|
|
817
|
+
elif endswith("_anime") then "pending"
|
|
818
|
+
elif . == "blue" then "green"
|
|
819
|
+
elif . == "red" or . == "yellow" then "failing"
|
|
820
|
+
else "none"
|
|
821
|
+
end;
|
|
822
|
+
($job | split("/") | last) as $name
|
|
823
|
+
| { status: "ok",
|
|
824
|
+
map: { ($name): { color: .color,
|
|
825
|
+
checks: (.color | color_to_checks),
|
|
826
|
+
job: $job } } }
|
|
827
|
+
' 2>/dev/null || printf '{"status":"failed","map":{}}\n'
|
|
828
|
+
return 0
|
|
829
|
+
;;
|
|
830
|
+
*'.WorkflowMultiBranchProject')
|
|
831
|
+
: # fall through to the listing below — the path that has always worked
|
|
832
|
+
;;
|
|
833
|
+
'')
|
|
834
|
+
# `job view` answered nothing usable. NOT a shape verdict: an instance
|
|
835
|
+
# naming no job reaches here by design, and so does a `jen` too old to
|
|
836
|
+
# know the verb. Fall through and let the listing decide, which is
|
|
837
|
+
# exactly what this function did before the probe existed.
|
|
838
|
+
:
|
|
839
|
+
;;
|
|
840
|
+
*)
|
|
841
|
+
# A SHAPE NOBODY MEASURED. `unknown` says that; `failed` would claim
|
|
842
|
+
# Jenkins did not answer, when it answered clearly and said something
|
|
843
|
+
# this reader has never been taught to read.
|
|
844
|
+
printf '{"status":"unknown","map":{}}\n'; return 0
|
|
845
|
+
;;
|
|
846
|
+
esac
|
|
847
|
+
|
|
754
848
|
# One call, every branch — the spike's whole point (Done-when 5).
|
|
755
849
|
local out=""
|
|
756
850
|
out=$(jen -I "$slug" job list ${job:+"$job"} --json 2>&1) || true
|
|
@@ -834,6 +928,47 @@ jenkins_no_instance() {
|
|
|
834
928
|
# estimate for. This draws it one step earlier, for a connector that does not
|
|
835
929
|
# exist at all — and `resultOf` (`adapters/run-script.ts:211`) maps exit 4 onto
|
|
836
930
|
# `unaskable`, which is the word the build port already answers with.
|
|
931
|
+
# The Jenkins REST credential, read from where `jen` already stores it.
|
|
932
|
+
#
|
|
933
|
+
# NOT THE KEYCLOAK BEARER. `jen auth token` prints one and Jenkins answers it
|
|
934
|
+
# with an HTML login redirect — measured 2026-09-10, and it is the trap that
|
|
935
|
+
# made an earlier reading conclude the sha was unreachable. Jenkins' own API
|
|
936
|
+
# takes BASIC auth with a user and an API token, which `jen` keeps in the login
|
|
937
|
+
# keychain under service `jen`, accounts `jenkins-user:<host>` and
|
|
938
|
+
# `jenkins-token:<host>`.
|
|
939
|
+
#
|
|
940
|
+
# IT PRINTS `user:token` AND NOTHING ELSE, or nothing at all. The caller passes
|
|
941
|
+
# it straight to `curl -u`; no branch echoes it, and no failure path names it.
|
|
942
|
+
#
|
|
943
|
+
# EMPTY IS THE HONEST ANSWER on a machine with no `security` (Linux, CI), with
|
|
944
|
+
# no keychain entry, or with either half missing — a half credential is not a
|
|
945
|
+
# credential. The caller turns that into exit 4, which is *cannot be asked*.
|
|
946
|
+
jenkins_rest_credential() { # $1 = the bare host
|
|
947
|
+
command -v security >/dev/null 2>&1 || return 1
|
|
948
|
+
local u t
|
|
949
|
+
u=$(security find-generic-password -s jen -a "jenkins-user:$1" -w 2>/dev/null) || return 1
|
|
950
|
+
t=$(security find-generic-password -s jen -a "jenkins-token:$1" -w 2>/dev/null) || return 1
|
|
951
|
+
[ -n "$u" ] && [ -n "$t" ] || return 1
|
|
952
|
+
printf '%s:%s' "$u" "$t"
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
# A job path as Jenkins' REST API spells it: every segment under `job/`.
|
|
956
|
+
#
|
|
957
|
+
# `quaweb/release` is the path a person writes and `job/quaweb/job/release` is
|
|
958
|
+
# the URL, because a Jenkins folder is itself a job. A multibranch branch adds
|
|
959
|
+
# one more segment, PERCENT-ENCODED — `bug/foo` is `bug%2Ffoo`, the same
|
|
960
|
+
# encoding `jenkins_build_map` already decodes on the way back.
|
|
961
|
+
jenkins_job_url_path() { # $1 = job path, $2 = branch or ""
|
|
962
|
+
local out="" seg
|
|
963
|
+
local IFS=/
|
|
964
|
+
for seg in $1; do [ -n "$seg" ] && out="$out/job/$seg"; done
|
|
965
|
+
unset IFS
|
|
966
|
+
if [ -n "${2:-}" ]; then
|
|
967
|
+
out="$out/job/$(printf '%s' "$2" | sed 's|/|%2F|g')"
|
|
968
|
+
fi
|
|
969
|
+
printf '%s' "$out"
|
|
970
|
+
}
|
|
971
|
+
|
|
837
972
|
ci_unaskable() { # $1 = the op's name, $2 = the CI word (may be empty)
|
|
838
973
|
local ci_word="${2:-}"
|
|
839
974
|
if [ -z "$ci_word" ] || [ "$ci_word" = none ]; then
|
|
@@ -1722,12 +1857,47 @@ backend_declared() {
|
|
|
1722
1857
|
return
|
|
1723
1858
|
fi
|
|
1724
1859
|
local v
|
|
1725
|
-
v="$(bash "$here/plot-config.sh" get "Git host" "
|
|
1860
|
+
v="$(bash "$here/plot-config.sh" get "Git host" "" | tr '[:upper:]' '[:lower:]')"
|
|
1726
1861
|
case "$v" in
|
|
1727
|
-
bb) echo "bitbucket" ;;
|
|
1728
|
-
"")
|
|
1729
|
-
*) printf '%s\n' "$v" ;;
|
|
1862
|
+
bb) echo "bitbucket"; return ;;
|
|
1863
|
+
"") : ;;
|
|
1864
|
+
*) printf '%s\n' "$v"; return ;;
|
|
1730
1865
|
esac
|
|
1866
|
+
|
|
1867
|
+
# NOTHING DECLARED — INFER FROM THE REMOTE, AND REFUSE WHERE THERE IS NONE.
|
|
1868
|
+
# This answered `github` unconditionally until 2026-09-11, which is the
|
|
1869
|
+
# reassuring direction: a Bitbucket repo that forgot the key got GitHub's
|
|
1870
|
+
# answer, and a repo with no remote at all got one too. Measured on a fresh
|
|
1871
|
+
# `git init` with no remote: `backend` printed `github` and exited 0.
|
|
1872
|
+
#
|
|
1873
|
+
# THE REMOTE IS EVIDENCE AND THE KEY IS A DECLARATION, so the key still wins
|
|
1874
|
+
# above. What changes is only the case where nobody said: a hostname in the
|
|
1875
|
+
# remote is a reading, and no remote is no reading.
|
|
1876
|
+
local url
|
|
1877
|
+
url="$(git config --get remote.origin.url 2>/dev/null || true)"
|
|
1878
|
+
case "$url" in
|
|
1879
|
+
*github.com*) echo "github"; return ;;
|
|
1880
|
+
*bitbucket.org*) echo "bitbucket"; return ;;
|
|
1881
|
+
esac
|
|
1882
|
+
|
|
1883
|
+
# NOTHING NAMES A HOST, AND THAT IS REPORTED RATHER THAN REFUSED. An earlier
|
|
1884
|
+
# version of this returned exit 4 here, and five contract tests went red:
|
|
1885
|
+
# a sandbox repository with no remote is a legitimate, common shape — six
|
|
1886
|
+
# suites build one — and every op that needs a host in one was relying on
|
|
1887
|
+
# this default. Refusing at the resolver punishes them for a question they
|
|
1888
|
+
# never asked.
|
|
1889
|
+
#
|
|
1890
|
+
# SO THE GUESS SURVIVES AND STOPS BEING SILENT — AND IT SIGNALS THROUGH THE
|
|
1891
|
+
# EXIT CODE, NEVER A VARIABLE. This set a `BACKEND_UNNAMED` global first, and
|
|
1892
|
+
# the caller reads it as `v="$(backend_declared)"` — a COMMAND SUBSTITUTION,
|
|
1893
|
+
# which runs in a subshell, so the assignment died with the child and the
|
|
1894
|
+
# parent always read 0. The warning never printed, and a direct call looked
|
|
1895
|
+
# correct because its stdout was right.
|
|
1896
|
+
#
|
|
1897
|
+
# Exit 9 is arbitrary and deliberately outside the contract's 0/1/3/4: it
|
|
1898
|
+
# never leaves this file, and `backend` maps it back to a successful answer.
|
|
1899
|
+
echo "github"
|
|
1900
|
+
return 9
|
|
1731
1901
|
}
|
|
1732
1902
|
|
|
1733
1903
|
# The resolved backend, refused where this script has no arm for it.
|
|
@@ -1740,12 +1910,27 @@ backend_declared() {
|
|
|
1740
1910
|
# person must fix. `host-shell.ts` reads that code as `unaskable` and reads the
|
|
1741
1911
|
# sentence below for the name.
|
|
1742
1912
|
backend() {
|
|
1743
|
-
local v
|
|
1744
|
-
|
|
1913
|
+
local v rc unnamed=0
|
|
1914
|
+
# THE DECLARED-HOST REFUSAL IS PASSED THROUGH, NOT FLATTENED. `|| return 1`
|
|
1915
|
+
# collapsed exit 4 into 1 here, and 4 is the one code every caller reads as
|
|
1916
|
+
# *this cannot be asked at all* rather than *retry*. Measured 2026-09-11: a
|
|
1917
|
+
# repository with no remote exited 1, which tells a caller to try again.
|
|
1918
|
+
v="$(backend_declared)"; rc=$?
|
|
1919
|
+
# 9 is the resolver's private word for *answered, but nothing named it*.
|
|
1920
|
+
if [ "$rc" -eq 9 ]; then unnamed=1; rc=0; fi
|
|
1921
|
+
[ "$rc" -eq 0 ] || return "$rc"
|
|
1745
1922
|
if ! host_drivable "$v"; then
|
|
1746
1923
|
echo "plot-host: cannot drive '$v' — this script drives ${HOST_DRIVES// /, }; set the 'Git host' key in CLAUDE.md (or \$PLOT_HOST) to one of them" >&2
|
|
1747
1924
|
return 4
|
|
1748
1925
|
fi
|
|
1926
|
+
# THE ANSWER IS PRINTED EITHER WAY, AND A GUESS SAYS SO. `backend` is a
|
|
1927
|
+
# reading, not a gate: a caller that needs a host still gets one, and a
|
|
1928
|
+
# person asking which host this is learns the answer was inferred from
|
|
1929
|
+
# nothing. Exit stays 0 — the value is usable, its provenance is not certain.
|
|
1930
|
+
if [ "$unnamed" = 1 ]; then
|
|
1931
|
+
echo "plot-host: no 'Git host' key and no remote names one — assuming '$v'" >&2
|
|
1932
|
+
echo " Set the 'Git host' key in ## Plot Config (or \$PLOT_HOST) to be sure." >&2
|
|
1933
|
+
fi
|
|
1749
1934
|
printf '%s\n' "$v"
|
|
1750
1935
|
}
|
|
1751
1936
|
|
|
@@ -2991,29 +3176,100 @@ case "$op" in
|
|
|
2991
3176
|
_ci="$(ci_scheme)"
|
|
2992
3177
|
case "$_ci" in
|
|
2993
3178
|
jenkins)
|
|
2994
|
-
#
|
|
2995
|
-
#
|
|
2996
|
-
#
|
|
2997
|
-
#
|
|
2998
|
-
#
|
|
3179
|
+
# THE SHA IS IN JENKINS AND `jen` IS NOT THE TRANSPORT. Measured
|
|
3180
|
+
# 2026-09-10 against a live instance: a build entry from `jen build
|
|
3181
|
+
# list --json` carries `id`, `status`, timings and stages, and a search
|
|
3182
|
+
# of the whole payload for `sha|commit|revision|scm` matches nothing.
|
|
3183
|
+
# `jen` has no changesets subcommand and no raw-API passthrough.
|
|
3184
|
+
#
|
|
3185
|
+
# JENKINS' OWN REST API ANSWERS IT, at
|
|
3186
|
+
# `actions[].lastBuiltRevision.SHA1`, paired with the branch. One
|
|
3187
|
+
# `tree=` query returns a whole history — 4855 bytes for five builds —
|
|
3188
|
+
# so this costs one round trip like the GitHub arm does.
|
|
3189
|
+
#
|
|
3190
|
+
# THIS ARM EXITED 4 UNTIL 2026-09-11, and the refusal was honest for
|
|
3191
|
+
# the transport it had. What changed is the transport, not the rule.
|
|
3192
|
+
_jen_instance="$(jenkins_instance)"
|
|
3193
|
+
[ -n "$_jen_instance" ] || jenkins_no_instance
|
|
3194
|
+
_jen_host="${_jen_instance%%/*}"
|
|
3195
|
+
_jen_job="${_jen_instance#*/}"
|
|
3196
|
+
[ "$_jen_job" = "$_jen_instance" ] && _jen_job=""
|
|
3197
|
+
[ -n "${PLOT_JENKINS_JOB:-}" ] && _jen_job="$PLOT_JENKINS_JOB"
|
|
3198
|
+
if [ -z "$_jen_job" ]; then
|
|
3199
|
+
# A bare-host instance names no job, and a sha lives in a job's
|
|
3200
|
+
# builds. `runs` degrades to an empty map here; this op has no row to
|
|
3201
|
+
# carry that, so the only way to say *cannot be asked* is exit 4.
|
|
3202
|
+
echo "plot-host: run-for-sha — the Jenkins instance names no job path" >&2
|
|
3203
|
+
echo " A sha is a fact about a job's builds, so the instance must be" >&2
|
|
3204
|
+
echo " <slug>/<job/path> rather than a bare host." >&2
|
|
3205
|
+
exit 4
|
|
3206
|
+
fi
|
|
3207
|
+
_jen_cred="$(jenkins_rest_credential "$_jen_host")" || {
|
|
3208
|
+
# NO CREDENTIAL IS *CANNOT BE ASKED*, never *no run*. The keychain is
|
|
3209
|
+
# macOS-only and a Linux agent legitimately has none; saying nothing
|
|
3210
|
+
# at exit 0 would read as a branch that never built.
|
|
3211
|
+
echo "plot-host: run-for-sha — no Jenkins API credential for '$_jen_host'" >&2
|
|
3212
|
+
echo " Jenkins' REST API takes basic auth with an API token, which" >&2
|
|
3213
|
+
echo " \`jen auth login\` stores in the login keychain. The Keycloak" >&2
|
|
3214
|
+
echo " bearer from \`jen auth token\` is NOT it — Jenkins answers that" >&2
|
|
3215
|
+
echo " with a login redirect." >&2
|
|
3216
|
+
exit 4
|
|
3217
|
+
}
|
|
3218
|
+
# A BRANCH IS A JOB SEGMENT ON A MULTIBRANCH JOB AND NOT ON A PLAIN
|
|
3219
|
+
# ONE, and no reading of the configured path says which this is. So the
|
|
3220
|
+
# multibranch URL is tried first and the plain one is the fallback:
|
|
3221
|
+
# `job/quaweb/job/continuous-build/job/content%2Fctas` against
|
|
3222
|
+
# `job/quaweb/job/release`. Measured 2026-09-11 — asking the plain job
|
|
3223
|
+
# for a branch segment answers 404, which is why guessing one shape
|
|
3224
|
+
# cost a run.
|
|
2999
3225
|
#
|
|
3000
|
-
# THE
|
|
3001
|
-
#
|
|
3002
|
-
# `
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3226
|
+
# THE PLAIN JOB REPORTS ITS OWN BUILDS whatever branch was asked about,
|
|
3227
|
+
# and that is honest rather than wrong: a pipeline job builds one
|
|
3228
|
+
# thing, and the `sha` in the answer says which commit it built.
|
|
3229
|
+
_jen_tree="tree=builds%5Bnumber,result,building,timestamp,url,actions%5BlastBuiltRevision%5BSHA1%5D%5D%5D%7B0,$limit%7D"
|
|
3230
|
+
_jen_body=""
|
|
3231
|
+
for _jen_path in \
|
|
3232
|
+
"$(jenkins_job_url_path "$_jen_job" "$branch")" \
|
|
3233
|
+
"$(jenkins_job_url_path "$_jen_job" "")"; do
|
|
3234
|
+
host_slot_take jenkins ''
|
|
3235
|
+
_jen_try=$(curl -sg --max-time 30 -u "$_jen_cred" \
|
|
3236
|
+
"https://$_jen_host$_jen_path/api/json?$_jen_tree" 2>/dev/null) || true
|
|
3237
|
+
host_slot_give
|
|
3238
|
+
budget_record_call jenkins ''
|
|
3239
|
+
if printf '%s' "$_jen_try" | jq -e 'has("builds")' >/dev/null 2>&1; then
|
|
3240
|
+
_jen_body="$_jen_try"; break
|
|
3241
|
+
fi
|
|
3242
|
+
done
|
|
3243
|
+
_jen_cred=""
|
|
3244
|
+
if [ -z "$_jen_body" ] || ! printf '%s' "$_jen_body" | jq -e 'has("builds")' >/dev/null 2>&1; then
|
|
3245
|
+
# AN UNREACHABLE INSTANCE IS NOT AN EMPTY HISTORY. A refused or
|
|
3246
|
+
# redirected request answers HTML, which fails the `builds` test.
|
|
3247
|
+
echo "plot-host: run-for-sha — Jenkins did not answer for '$_jen_host'" >&2
|
|
3248
|
+
exit 4
|
|
3249
|
+
fi
|
|
3250
|
+
# THE SAME FALLBACK RULE AS THE GITHUB ARM, and it is inherited rather
|
|
3251
|
+
# than invented: the asked-for sha if a build carries it, else the
|
|
3252
|
+
# newest build, and `sha` says WHICH. A caller that could not tell the
|
|
3253
|
+
# two apart would be back to the branch-scoped guessing this op ends.
|
|
3006
3254
|
#
|
|
3007
|
-
#
|
|
3008
|
-
#
|
|
3009
|
-
#
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3255
|
+
# `result` is null while a build runs, which is Jenkins' own word for
|
|
3256
|
+
# *in flight* — mapped to the `status`/`conclusion` split the contract
|
|
3257
|
+
# already uses, so the shape does not fork per connector.
|
|
3258
|
+
printf '%s' "$_jen_body" | jq -c --arg sha "$sha" '
|
|
3259
|
+
[ .builds[]
|
|
3260
|
+
| { sha: ([ .actions[]? | select(.lastBuiltRevision) | .lastBuiltRevision.SHA1 ] | first // ""),
|
|
3261
|
+
status: (if .building then "in_progress" else "completed" end),
|
|
3262
|
+
conclusion: (if .building then null else (.result // null) end),
|
|
3263
|
+
url: (.url // ""),
|
|
3264
|
+
startedAt: (if .timestamp then (.timestamp / 1000 | todate) else "" end) } ]
|
|
3265
|
+
| ((map(select(.sha == $sha)) | .[0]) // .[0])
|
|
3266
|
+
| select(. != null)' 2>/dev/null || true
|
|
3267
|
+
# THIS ARM ANSWERS AND THE OP IS OVER. Everything below the `esac` is
|
|
3268
|
+
# the GitHub path — the old jenkins arm reached it only because it
|
|
3269
|
+
# ended in `exit 4`. Measured 2026-09-11: without this the answer was
|
|
3270
|
+
# printed and then a `CI is github-actions but the git host is
|
|
3271
|
+
# 'bitbucket'` refusal followed it on stderr.
|
|
3272
|
+
exit 0
|
|
3017
3273
|
;;
|
|
3018
3274
|
github-actions) : ;;
|
|
3019
3275
|
*) ci_unaskable run-for-sha "$_ci" ;;
|
package/plot-plan-meta.sh
CHANGED
|
@@ -150,6 +150,16 @@
|
|
|
150
150
|
# BECAUSE annotations already work in both slice dialects from
|
|
151
151
|
# one block of code — a field line would need two spellings,
|
|
152
152
|
# and the template writes the list dialect.
|
|
153
|
+
# `<!-- agent: reviewer -->` names which KIND of agent this
|
|
154
|
+
# slice needs, reported as `waves[].branches[].agent` and read
|
|
155
|
+
# by plot-dispatch.sh where `--agent` is absent. OPTIONAL, and
|
|
156
|
+
# the key is ABSENT where none was written, never "" — dispatch
|
|
157
|
+
# reads presence, so a blank would send it looking for a charter
|
|
158
|
+
# called "". The value runs to the closing marker, as `builds:`
|
|
159
|
+
# does. It CANNOT validate its value the way `waits:` does: a
|
|
160
|
+
# charter name is a bare word with no structure to check, so a
|
|
161
|
+
# template documents the marker inside an outer comment block,
|
|
162
|
+
# where this pattern cannot see it.
|
|
153
163
|
# prs PR numbers, sorted and unique, read from EITHER spelling:
|
|
154
164
|
# `→ #NNN` / `→ owner/repo#NNN` links in the `## Branches`
|
|
155
165
|
# section, OR `PR: #NNN` in a `## Waves` `### ` heading. The
|
|
@@ -424,6 +434,7 @@ function reset_state() {
|
|
|
424
434
|
delete deferred_of; delete deferred_why; delete claimed_of; delete ordered_b; n_waves = 0
|
|
425
435
|
delete waits_of; delete waits_set
|
|
426
436
|
delete builds_of; delete builds_set
|
|
437
|
+
delete agent_of; delete agent_set
|
|
427
438
|
delete started; n_started = 0
|
|
428
439
|
fm_changelog = ""
|
|
429
440
|
delete changelog; n_changelog = 0; changelog_seen = 0; cl_open = 0
|
|
@@ -584,6 +595,13 @@ function emit_record( fmt, praw, palt_raw, traw, title, sprint, story, assigne
|
|
|
584
595
|
# names no deliverable emits no key, so a consumer reads a name or
|
|
585
596
|
# nothing. An empty string would read as a deliverable called "".
|
|
586
597
|
if (builds_set[i] == 1) out = out ",\"builds\":\"" jesc(builds_of[i]) "\""
|
|
598
|
+
# ABSENT, NOT EMPTY, the rule the three annotations above already keep.
|
|
599
|
+
# Dispatch reads the PRESENCE of this key to decide whether the plan names
|
|
600
|
+
# a kind at all, so an `"agent":""` would read as a charter called "" and
|
|
601
|
+
# send it looking for `.plot/charters/.json`.
|
|
602
|
+
# NOTE: no apostrophe may appear in this awk region — the whole program is
|
|
603
|
+
# one single-quoted shell string, and one closes it mid-comment.
|
|
604
|
+
if (agent_set[i] == 1) out = out ",\"agent\":\"" jesc(agent_of[i]) "\""
|
|
587
605
|
out = out "}"
|
|
588
606
|
first = 0
|
|
589
607
|
}
|
|
@@ -1012,6 +1030,44 @@ section == "slices" && slice_shape != "heading" {
|
|
|
1012
1030
|
if (waits_note ~ "^(" PREFIXES ")/[^ \t]+$") has_waits = 1
|
|
1013
1031
|
else waits_note = ""
|
|
1014
1032
|
}
|
|
1033
|
+
# WHICH KIND OF AGENT THIS SLICE NEEDS: `<!-- agent: reviewer -->`.
|
|
1034
|
+
#
|
|
1035
|
+
# `--agent <name>` was the only selector and only an operator could type it.
|
|
1036
|
+
# `plot-registryd` hands a queued slice to a free agent with no `--agent`
|
|
1037
|
+
# anywhere in the path, so an unattended fleet ran every slice as the same
|
|
1038
|
+
# undifferentiated worker. The declaration has to live in the PLAN to reach a
|
|
1039
|
+
# dispatch nobody is watching.
|
|
1040
|
+
#
|
|
1041
|
+
# PER-BRANCH, NEVER A `## Status` FIELD. Every Status field is plan-level, so
|
|
1042
|
+
# one there could declare a single kind for a plan with several slices — and a
|
|
1043
|
+
# reviewer slice beside an implementer slice is the population this is for.
|
|
1044
|
+
#
|
|
1045
|
+
# The value runs to the closing marker, the way `builds:` does and unlike
|
|
1046
|
+
# `waits:`. A charter name is a bare word today, so both rules agree; this one
|
|
1047
|
+
# is chosen because it does not silently truncate if a name grows a space.
|
|
1048
|
+
#
|
|
1049
|
+
# IT CANNOT VALIDATE ITS VALUE, and that is the difference from `waits:` above.
|
|
1050
|
+
# A prerequisite has branch-prefix structure, which is what keeps a SYNTAX
|
|
1051
|
+
# EXAMPLE in prose from reading as a declaration; a charter name is a bare word
|
|
1052
|
+
# with no structure to check. What protects the templates instead is NESTING:
|
|
1053
|
+
# their documentation sits inside an outer `<!-- ... -->` block, so the inner
|
|
1054
|
+
# `agent:` carries no `<!--` of its own and this pattern never matches it —
|
|
1055
|
+
# the same accident of shape that already keeps `builds:` out of the parse of
|
|
1056
|
+
# a template. Documenting it as a bare `<!-- agent: reviewer -->` line would
|
|
1057
|
+
# hand a kind to every plan created from that template. A test pins it.
|
|
1058
|
+
#
|
|
1059
|
+
# `has_agent` carries presence separately from the value, as all three
|
|
1060
|
+
# annotations before it do: a slice naming no kind emits no key, so dispatch
|
|
1061
|
+
# reads a name or nothing and never a blank string that looks like one.
|
|
1062
|
+
agent_note = ""
|
|
1063
|
+
has_agent = 0
|
|
1064
|
+
if ($0 ~ /<!--[ \t]*agent:[ \t]*/) {
|
|
1065
|
+
_ag = $0
|
|
1066
|
+
sub(/^.*<!--[ \t]*agent:[ \t]*/, "", _ag)
|
|
1067
|
+
sub(/[ \t]*-->.*$/, "", _ag)
|
|
1068
|
+
agent_note = trim(_ag)
|
|
1069
|
+
if (agent_note != "") has_agent = 1
|
|
1070
|
+
}
|
|
1015
1071
|
# ONE LIST ITEM, AT MOST ONE CLAIM — an `if`, not the `while` this was.
|
|
1016
1072
|
#
|
|
1017
1073
|
# The old loop walked the line taking every backticked name on it, which is
|
|
@@ -1051,6 +1107,10 @@ section == "slices" && slice_shape != "heading" {
|
|
|
1051
1107
|
waits_set[n_branches] = has_waits
|
|
1052
1108
|
builds_of[n_branches] = builds_note
|
|
1053
1109
|
builds_set[n_branches] = has_builds
|
|
1110
|
+
# The kind travels with the branch, presence tracked separately so a slice
|
|
1111
|
+
# declaring none emits no key.
|
|
1112
|
+
agent_of[n_branches] = agent_note
|
|
1113
|
+
agent_set[n_branches] = has_agent
|
|
1054
1114
|
ordered_b[n_branches] = b
|
|
1055
1115
|
}
|
|
1056
1116
|
line = $0
|
|
@@ -1188,6 +1248,19 @@ section == "slices" && slice_shape == "heading" {
|
|
|
1188
1248
|
if (waits_note ~ "^(" PREFIXES ")/[^ \t]+$") has_waits = 1
|
|
1189
1249
|
else waits_note = ""
|
|
1190
1250
|
}
|
|
1251
|
+
# The agent kind, read exactly as the list-item spelling reads it. Both
|
|
1252
|
+
# dialects emit the same waves[], so a field added to one only would break that
|
|
1253
|
+
# contract the first time a plan migrated. See the list-item block for why the
|
|
1254
|
+
# value runs to the closing marker and why this one cannot validate itself.
|
|
1255
|
+
agent_note = ""
|
|
1256
|
+
has_agent = 0
|
|
1257
|
+
if ($0 ~ /<!--[ \t]*agent:[ \t]*/) {
|
|
1258
|
+
_ag = $0
|
|
1259
|
+
sub(/^.*<!--[ \t]*agent:[ \t]*/, "", _ag)
|
|
1260
|
+
sub(/[ \t]*-->.*$/, "", _ag)
|
|
1261
|
+
agent_note = trim(_ag)
|
|
1262
|
+
if (agent_note != "") has_agent = 1
|
|
1263
|
+
}
|
|
1191
1264
|
|
|
1192
1265
|
# The branch is the `Branch:` value, matched against the known prefixes exactly
|
|
1193
1266
|
# as the old shape matched the backticked name. Written unquoted in the heading
|
|
@@ -1211,6 +1284,8 @@ section == "slices" && slice_shape == "heading" {
|
|
|
1211
1284
|
waits_set[n_branches] = has_waits
|
|
1212
1285
|
builds_of[n_branches] = builds_note
|
|
1213
1286
|
builds_set[n_branches] = has_builds
|
|
1287
|
+
agent_of[n_branches] = agent_note
|
|
1288
|
+
agent_set[n_branches] = has_agent
|
|
1214
1289
|
ordered_b[n_branches] = b
|
|
1215
1290
|
}
|
|
1216
1291
|
|