forked from heavy-duty/ceremony
wip: the collision and window board flags, decisions and gather
Both checks ride the existing sweep walk and write nothing but comments (#293 D1). The board read now answers the whole payload, because both decisions are over the WHOLE board — every open issue's labels and title, and every open release issue's body — and a second pagination for the same rows would be a second board free to disagree with this one mid-sweep. Fixtures still owed.
This commit is contained in:
parent
2807d34c74
commit
90a35008a1
1 changed files with 274 additions and 9 deletions
|
|
@ -346,10 +346,18 @@ blocked_parse_marker() { # $1 rendered set -> the echo's idempotency marker
|
||||||
# pair and leave the class: `-`, `_` and `.` are legal in a qualifier token
|
# pair and leave the class: `-`, `_` and `.` are legal in a qualifier token
|
||||||
# and all collapse the same way. The slug stays in front so a human reading
|
# and all collapse the same way. The slug stays in front so a human reading
|
||||||
# the raw comment can still see which set it belongs to; it decides nothing.
|
# the raw comment can still see which set it belongs to; it decides nothing.
|
||||||
|
state_marker blockers-parsed "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
state_marker() { # $1 = marker family, $2 = the state's rendered value
|
||||||
|
# The one spelling of a value-keyed marker. Three flags now key on a state
|
||||||
|
# that changes rather than on "have I ever said this" — the blocked-parse
|
||||||
|
# echo (#252) and the two board flags (#293) — and a second implementation
|
||||||
|
# of the slug-plus-digest rule is the drift a shared helper prevents.
|
||||||
local slug digest
|
local slug digest
|
||||||
slug="$(printf '%s' "$1" | tr -c '[:alnum:]' '-' | sed 's/--*/-/g; s/^-//; s/-$//')"
|
slug="$(printf '%s' "$2" | tr -c '[:alnum:]' '-' | sed 's/--*/-/g; s/^-//; s/-$//')"
|
||||||
digest="$(printf '%s' "$1" | sha256sum | cut -c1-12)"
|
digest="$(printf '%s' "$2" | sha256sum | cut -c1-12)"
|
||||||
printf 'blockers-parsed-%s-%s\n' "${slug:-none}" "$digest"
|
printf '%s-%s-%s\n' "$1" "${slug:-none}" "$digest"
|
||||||
}
|
}
|
||||||
|
|
||||||
blocked_parse_echo_needed() { # $1 issue, $2 this parse's marker → 0 echo, 1 quiet
|
blocked_parse_echo_needed() { # $1 issue, $2 this parse's marker → 0 echo, 1 quiet
|
||||||
|
|
@ -367,13 +375,20 @@ blocked_parse_echo_needed() { # $1 issue, $2 this parse's marker → 0 echo, 1 q
|
||||||
# Comparing markers rather than re-rendering the last set keeps the digest as
|
# Comparing markers rather than re-rendering the last set keeps the digest as
|
||||||
# the only identity: two sets are the same here iff blocked_parse_marker says
|
# the only identity: two sets are the same here iff blocked_parse_marker says
|
||||||
# so, the same rule the marker itself is built on.
|
# so, the same rule the marker itself is built on.
|
||||||
|
state_echo_needed "$1" blockers-parsed "$2"
|
||||||
|
}
|
||||||
|
|
||||||
|
state_echo_needed() { # $1 issue, $2 family, $3 this state's marker → 0 echo, 1 quiet
|
||||||
|
# The value-keyed dedup itself, family-scoped so each flag compares against
|
||||||
|
# its OWN last word and never against another flag's (#293 D4 asks for the
|
||||||
|
# declaration echo's mechanism exactly, and three families now share it).
|
||||||
local bodies last
|
local bodies last
|
||||||
guarded_read bodies gh api --paginate "repos/$REPO/issues/$1/comments" --jq '.[].body' \
|
guarded_read bodies gh api --paginate "repos/$REPO/issues/$1/comments" --jq '.[].body' \
|
||||||
|| skip_issue "$1" "could not read its comments: $(read_failure_reason "$READ_FAILURE_STDERR")"
|
|| skip_issue "$1" "could not read its comments: $(read_failure_reason "$READ_FAILURE_STDERR")"
|
||||||
# The read fails closed above (#247 D1): an unreadable history skips the
|
# The read fails closed above (#247 D1): an unreadable history skips the
|
||||||
# issue rather than answering "nothing echoed yet" and re-posting.
|
# issue rather than answering "nothing echoed yet" and re-posting.
|
||||||
last="$(grep -o '<!-- issueflow:blockers-parsed-[[:alnum:]-]* -->' <<<"$bodies" | tail -n 1)"
|
last="$(grep -o "<!-- issueflow:$2-[[:alnum:]-]* -->" <<<"$bodies" | tail -n 1)"
|
||||||
[ "$last" != "<!-- issueflow:$2 -->" ]
|
[ "$last" != "<!-- issueflow:$3 -->" ]
|
||||||
}
|
}
|
||||||
|
|
||||||
blocked_decision() { # $1 local refs, $2 OPEN/CLOSED states, $3 cross-repo refs
|
blocked_decision() { # $1 local refs, $2 OPEN/CLOSED states, $3 cross-repo refs
|
||||||
|
|
@ -402,6 +417,138 @@ epic_decision() { # $1 refs, $2 states
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ---- the two board flags (#293): collision (#288) and window (#292) --------
|
||||||
|
#
|
||||||
|
# Both are ADVISORY and comment-only (#293 D1). The sweep never guesses
|
||||||
|
# intent, so neither writes a label, changes a state, or invents one: it
|
||||||
|
# states the board fact and triage resolves it. Both are also the first
|
||||||
|
# checks here whose input is the WHOLE board rather than one issue, so the
|
||||||
|
# facts are gathered once in main() and every decision below is pure over
|
||||||
|
# those records — one record per open issue, `number<TAB>labels<TAB>title`.
|
||||||
|
#
|
||||||
|
# Why they exist as guards at all: #288 and #292 are triage prose, and both
|
||||||
|
# failed silently on the same morning (2026-08-04) — #284 minted `ready`
|
||||||
|
# into a claimed file's function, and six `ready` non-members raced an
|
||||||
|
# emptying gate. #262 measured the pattern: the same class of rule, once in
|
||||||
|
# a guard, produced zero misses.
|
||||||
|
|
||||||
|
DELIVERABLE_PATH_PREFIXES=(actions/ lib/ bin/ .github/)
|
||||||
|
|
||||||
|
deliverable_key() { # $1 = one title segment -> its normalized key, or nothing
|
||||||
|
# Normalized, because the 2026-08-04 miss spelled one deliverable two ways
|
||||||
|
# — `actions/issueflow-reconcile` against plain `issueflow-reconcile` — so
|
||||||
|
# exact-prefix matching would have missed the pair it was written for
|
||||||
|
# (#293 D2). One leading path segment comes off, then every extension:
|
||||||
|
# `issueflow-reconcile.test.sh` and `issueflow-reconcile.sh` are the same
|
||||||
|
# spelling habit one more time.
|
||||||
|
local key="$1" prefix
|
||||||
|
key="${key#"${key%%[![:space:]]*}"}"
|
||||||
|
key="${key%"${key##*[![:space:]]}"}"
|
||||||
|
for prefix in "${DELIVERABLE_PATH_PREFIXES[@]}"; do
|
||||||
|
[ "${key#"$prefix"}" = "$key" ] || { key="${key#"$prefix"}"; break; }
|
||||||
|
done
|
||||||
|
while [[ "$key" =~ \.[[:alnum:]]+$ ]]; do key="${key%.*}"; done
|
||||||
|
# Case folds because the key is a spelling, not an identifier, and folding
|
||||||
|
# only ever widens the match — the same direction of error the blocked
|
||||||
|
# parse takes, and the cheap one: a false pair costs a comment a human
|
||||||
|
# dismisses, a missed pair costs two builders one deliverable.
|
||||||
|
printf '%s\n' "$key" | tr '[:upper:]' '[:lower:]'
|
||||||
|
}
|
||||||
|
|
||||||
|
deliverable_keys() { # title on stdin -> its deliverable keys, one per line
|
||||||
|
local title prefix segment key
|
||||||
|
IFS= read -r title
|
||||||
|
# The issue contract forces every title to name its deliverable before the
|
||||||
|
# em dash, so the key exists on every well-formed title by construction
|
||||||
|
# (#288 D5). A title without one names no deliverable, and inventing a key
|
||||||
|
# out of prose is the guessing this sweep never does — the malformed title
|
||||||
|
# is triage's own contract to enforce, not this flag's to infer around.
|
||||||
|
prefix="${title%%—*}"
|
||||||
|
[ "$prefix" != "$title" ] || return 0
|
||||||
|
# A multi-file deliverable joins its files with `+` and collides on any
|
||||||
|
# segment: `TRIAGE.md + RELEASES.md` carries both keys.
|
||||||
|
local segments=()
|
||||||
|
IFS='+' read -r -a segments <<<"$prefix"
|
||||||
|
for segment in "${segments[@]}"; do
|
||||||
|
key="$(deliverable_key "$segment")"
|
||||||
|
[ -z "$key" ] || printf '%s\n' "$key"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
collision_in_scope() { # $1 = comma-joined labels -> 0 in the collision set
|
||||||
|
# `blocked` is out: a chained issue is the GOAL state of #288's rule, and
|
||||||
|
# flagging it would report the fix as the defect. `epic` and `post-merge`
|
||||||
|
# are out by #288 D6 — neither is picked by a builder — and they carry no
|
||||||
|
# queue label to admit them here anyway.
|
||||||
|
case ",$1," in *,blocked,*) return 1 ;; esac
|
||||||
|
case ",$1," in *,ready,*|*,claimed,*) return 0 ;; esac
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
window_in_scope() { # $1 = comma-joined labels -> 0 subject to the window rule
|
||||||
|
# `epic` and `post-merge` are outside the claimable set and exempt by name
|
||||||
|
# (#292 D1); `blocked` is already placed behind something and is what the
|
||||||
|
# non-member leg of the mint-time call writes.
|
||||||
|
case ",$1," in *,blocked,*|*,epic,*|*,post-merge,*) return 1 ;; esac
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
collision_key_index() { # board records on stdin -> "key<TAB>number" in scope
|
||||||
|
local n labels title key
|
||||||
|
while IFS=$'\t' read -r n labels title; do
|
||||||
|
[ -n "$n" ] || continue
|
||||||
|
collision_in_scope "$labels" || continue
|
||||||
|
while IFS= read -r key; do
|
||||||
|
[ -z "$key" ] || printf '%s\t%s\n' "$key" "$n"
|
||||||
|
done < <(deliverable_keys <<<"$title")
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
collision_flags() { # key index on stdin -> "number<TAB>key=carrier[,key=carrier]"
|
||||||
|
# A CHAIN, not a fan (#288 D3): within one key, each issue names the newest
|
||||||
|
# open carrier below it, so the declaration the flag asks for releases
|
||||||
|
# exactly one successor per close. Three issues on one deliverable draw two
|
||||||
|
# comments — #257 naming #253, #284 naming #257 — never three pairs, which
|
||||||
|
# is the fan the rule exists to forbid.
|
||||||
|
#
|
||||||
|
# One line per issue, its keys folded into one state: an issue carrying two
|
||||||
|
# colliding deliverables has ONE offending state and owes one comment (D4),
|
||||||
|
# the same shape the blocked-parse echo takes with its set.
|
||||||
|
sort -t $'\t' -k1,1 -k2,2n \
|
||||||
|
| awk -F '\t' '
|
||||||
|
$1 == key { print $2 "\t" $1 "=" carrier }
|
||||||
|
{ key = $1; carrier = $2 }
|
||||||
|
' \
|
||||||
|
| sort -t $'\t' -k1,1n -k2,2 \
|
||||||
|
| awk -F '\t' '
|
||||||
|
$1 != n { if (n != "") print n "\t" state; n = $1; state = $2; next }
|
||||||
|
{ state = state "," $2 }
|
||||||
|
END { if (n != "") print n "\t" state }
|
||||||
|
'
|
||||||
|
}
|
||||||
|
|
||||||
|
window_flags() { # $1 gate members, $2 window carriers; records on stdin -> numbers
|
||||||
|
local n labels title gate="$1" carriers="$2"
|
||||||
|
[ -n "$carriers" ] || return 0
|
||||||
|
while IFS=$'\t' read -r n labels title; do
|
||||||
|
[ -n "$n" ] || continue
|
||||||
|
window_in_scope "$labels" || continue
|
||||||
|
grep -qxF "$n" <<<"$gate" && continue
|
||||||
|
# The release issue is the graph's SINK, never one of its own members
|
||||||
|
# (#292 D2), so it can never be its own non-member.
|
||||||
|
grep -qxF "$n" <<<"$carriers" && continue
|
||||||
|
printf '%s\n' "$n"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
window_state() { # $1 = window carriers -> the rendered state, "#249" | "#249, #250"
|
||||||
|
awk 'NF { printf "%s#%s", (shown++ ? ", " : ""), $1 } END { printf "\n" }' <<<"$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
flag_for_issue() { # $1 = issue, $2 = flag records "number<TAB>state"
|
||||||
|
awk -F '\t' -v n="$1" '$1 == n { print $2 }' <<<"$2"
|
||||||
|
}
|
||||||
|
|
||||||
offsite_cross_referenced_prs() { # timeline JSON on stdin -> owner/repo#N
|
offsite_cross_referenced_prs() { # timeline JSON on stdin -> owner/repo#N
|
||||||
jq -r '
|
jq -r '
|
||||||
.[]
|
.[]
|
||||||
|
|
@ -544,6 +691,80 @@ last_issue_comment_activity() { # $1 issue, $2 created_at → epoch; non-zero on
|
||||||
issue_activity_at "$1" "$2" comments-only
|
issue_activity_at "$1" "$2" comments-only
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reconcile_board_flags() { # $1 = issue — the collision and window flags (#293)
|
||||||
|
# Dedup is the declaration echo's, per family (#293 D4): the marker is
|
||||||
|
# keyed to the offending state's VALUE and compared against this family's
|
||||||
|
# last word on the thread, so a state that changes speaks and a state that
|
||||||
|
# stands is silent. What that buys over ensure_comment's any-occurrence
|
||||||
|
# grep is the A -> B -> A case — an issue that collides with #257, is
|
||||||
|
# re-declared against #284, and collides with #257 again is saying
|
||||||
|
# something new each time, and an any-occurrence marker would go quiet on
|
||||||
|
# the third. What it does not buy is the state that resolves and returns
|
||||||
|
# unchanged: nothing is posted at the resolution, so the thread's last word
|
||||||
|
# is still the state itself and the return is silent. That is the echo's
|
||||||
|
# own boundary, and it is the right one here — the flag speaks about a
|
||||||
|
# board fact that is true right now, and a board where the fact never
|
||||||
|
# changed has nothing new to say.
|
||||||
|
local n="$1" state marker rendered
|
||||||
|
state="$(flag_for_issue "$n" "${COLLISION_FLAGS:-}")"
|
||||||
|
if [ -n "$state" ]; then
|
||||||
|
marker="$(state_marker collision "$state")"
|
||||||
|
if state_echo_needed "$n" collision "$marker"; then
|
||||||
|
rendered="$(tr ',' '\n' <<<"$state" \
|
||||||
|
| awk -F= '{ print "- `" $1 "` — also carried by #" $2 }')"
|
||||||
|
run gh issue comment "$n" -R "$REPO" --body "<!-- issueflow:$marker -->
|
||||||
|
This issue and the issue named beside each key below are both open and
|
||||||
|
unblocked, and their titles name the same deliverable:
|
||||||
|
|
||||||
|
$rendered
|
||||||
|
|
||||||
|
That owes a **collision edge**, and #288 makes it unconditional: a deliverable
|
||||||
|
already carried by an open \`ready\`, \`claimed\` or \`blocked\` issue owes
|
||||||
|
\`Blocked by #N\` on the newer issue, naming the newest open carrier, so each
|
||||||
|
close releases exactly one successor. Disjoint regions do not waive it —
|
||||||
|
\`ready\` must mean claimable concurrently with every other \`ready\` issue,
|
||||||
|
and an undeclared collision sends two builders at one deliverable.
|
||||||
|
|
||||||
|
The key is the title's em-dash prefix, normalized: one leading \`actions/\`,
|
||||||
|
\`lib/\`, \`bin/\` or \`.github/\` segment comes off, then every extension, and
|
||||||
|
a \`+\`-joined title matches on any segment. That is what the machine read,
|
||||||
|
never a judgment about what the deliverable is — if two spellings normalized
|
||||||
|
to one deliverable that is really two, say so and no edge is owed.
|
||||||
|
|
||||||
|
*Comment only: nothing on this path writes a label or changes a state. The
|
||||||
|
marker carries the collision itself, so an unchanged one never re-posts.*" >/dev/null
|
||||||
|
log "#$n: collision flag — $state"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
state="$(flag_for_issue "$n" "${WINDOW_FLAGS:-}")"
|
||||||
|
if [ -n "$state" ]; then
|
||||||
|
marker="$(state_marker window-nonmember "$state")"
|
||||||
|
if state_echo_needed "$n" window-nonmember "$marker"; then
|
||||||
|
run gh issue comment "$n" -R "$REPO" --body "<!-- issueflow:$marker -->
|
||||||
|
A release window is standing ($state) and this issue is neither one of its
|
||||||
|
gate members nor an \`epic\` or \`post-merge\` issue.
|
||||||
|
|
||||||
|
#292's invariant: during a standing window — an open \`release\`-labeled issue
|
||||||
|
with a non-empty gate — the \`ready\` set is a subset of the gate, \`epic\` and
|
||||||
|
\`post-merge\` exempt. Every mint during a window is a membership call, binary,
|
||||||
|
made at mint time: **behind the gate**, this issue's own Dependencies declare
|
||||||
|
the release issue as a blocker and the sweep releases it when the release
|
||||||
|
closes; or **into the graph**, three writes in one tick — this issue declares
|
||||||
|
its immediate predecessors, every member whose immediate predecessor it
|
||||||
|
becomes re-points to it, and the release issue gains \`Blocked by #N\`, which
|
||||||
|
records membership and nothing else. Silence is not a state.
|
||||||
|
|
||||||
|
The gate is read from the release issue's own \`Blocked by\` declarations — the
|
||||||
|
same parse every \`blocked\` issue is gated on, echoed on that issue.
|
||||||
|
|
||||||
|
*Comment only: nothing on this path writes a label or changes a state. The
|
||||||
|
marker carries the window itself, so an unchanged one never re-posts.*" >/dev/null
|
||||||
|
log "#$n: window flag — a ready non-member under $state"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
reconcile_issue() {
|
reconcile_issue() {
|
||||||
local n="$1" decision refs cross_refs states age evidence_age ruling_age created assignees open_pr=false label owners
|
local n="$1" decision refs cross_refs states age evidence_age ruling_age created assignees open_pr=false label owners
|
||||||
local merged_ref_pr="" transition_marker="" transition_handled=false parsed_set="" parse_marker=""
|
local merged_ref_pr="" transition_marker="" transition_handled=false parsed_set="" parse_marker=""
|
||||||
|
|
@ -794,6 +1015,13 @@ See \`$release_doctrine_path\`. The operator blessing the order is the one step
|
||||||
reconcile_attention "$n" issue "$assignees" "$attention_suppression"
|
reconcile_attention "$n" issue "$assignees" "$attention_suppression"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# ---- the two board flags (#293), on any queue state ----
|
||||||
|
# After the queue branches for the same reason the ruling block is: both
|
||||||
|
# compose with every queue state, and FLAG_CONFLICT's early return still
|
||||||
|
# short-circuits them, because a board lying about its queue state is
|
||||||
|
# repaired before anything is derived from it.
|
||||||
|
reconcile_board_flags "$n"
|
||||||
|
|
||||||
# ---- the ruling invariants (#52), on any queue state ----
|
# ---- the ruling invariants (#52), on any queue state ----
|
||||||
# The flag composes with the queue labels (#50 D8), so this runs after the
|
# The flag composes with the queue labels (#50 D8), so this runs after the
|
||||||
# queue branches rather than inside one of them. The FLAG_CONFLICT return
|
# queue branches rather than inside one of them. The FLAG_CONFLICT return
|
||||||
|
|
@ -935,19 +1163,56 @@ main() {
|
||||||
done < <(refs_references <<<"$body")
|
done < <(refs_references <<<"$body")
|
||||||
done)"
|
done)"
|
||||||
|
|
||||||
local n tail_line issue_numbers
|
local n tail_line issue_numbers board_json release_bodies rn rbody gate
|
||||||
|
local window_rendered=""
|
||||||
SKIPPED_COUNT=0
|
SKIPPED_COUNT=0
|
||||||
SKIPPED_ISSUES=""
|
SKIPPED_ISSUES=""
|
||||||
# A command substitution in a for list suppresses errexit. Capture and
|
# A command substitution in a for list suppresses errexit. Capture and
|
||||||
# check the board read before entering the loop, or a 504 (including one
|
# check the board read before entering the loop, or a 504 (including one
|
||||||
# after partial pagination) reports a full pass over a truncated board
|
# after partial pagination) reports a full pass over a truncated board
|
||||||
# (#257).
|
# (#257).
|
||||||
if ! guarded_read issue_numbers gh api --paginate \
|
#
|
||||||
"repos/$REPO/issues?state=open&per_page=100" \
|
# The read answers the whole payload rather than a projection of it because
|
||||||
--jq '.[] | select(has("pull_request") | not) | .number'; then
|
# the two board flags (#293) are decided over the WHOLE board — every open
|
||||||
|
# issue's labels and title, and every open `release` issue's body. One read
|
||||||
|
# supplies all of it; a second pagination for the same rows would be a
|
||||||
|
# second board, free to disagree with this one mid-sweep.
|
||||||
|
if ! guarded_read board_json gh api --paginate \
|
||||||
|
"repos/$REPO/issues?state=open&per_page=100"; then
|
||||||
log "could not read the issue board: $(read_failure_reason "$READ_FAILURE_STDERR")"
|
log "could not read the issue board: $(read_failure_reason "$READ_FAILURE_STDERR")"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
BOARD_RECORDS="$(jq -r '.[] | select(has("pull_request") | not)
|
||||||
|
| [(.number | tostring), ((.labels // []) | map(.name) | join(",")), (.title // "")]
|
||||||
|
| @tsv' \
|
||||||
|
<<<"$board_json")"
|
||||||
|
issue_numbers="$(cut -f1 <<<"$BOARD_RECORDS")"
|
||||||
|
# A standing window is an open `release`-labeled issue whose gate still
|
||||||
|
# holds an OPEN member (#292 D1). The board read IS the open set, so
|
||||||
|
# membership decides openness with no extra call — and an all-closed gate
|
||||||
|
# is exactly the emptied gate the release's own `blocked` -> `ready`
|
||||||
|
# promotion answers, which is why a `ready` release leaves the flag
|
||||||
|
# dormant rather than flagging the whole board.
|
||||||
|
release_bodies="$(jq -r '.[] | select(has("pull_request") | not)
|
||||||
|
| select((.labels // []) | map(.name) | index("release"))
|
||||||
|
| [(.number | tostring), ((.body // "") | gsub("[\t\r\n]"; " "))] | @tsv' \
|
||||||
|
<<<"$board_json")"
|
||||||
|
WINDOW_CARRIERS=""
|
||||||
|
WINDOW_GATE=""
|
||||||
|
if [ -n "$issue_numbers" ]; then
|
||||||
|
while IFS=$'\t' read -r rn rbody; do
|
||||||
|
[ -n "$rn" ] || continue
|
||||||
|
gate="$(blocked_references <<<"$rbody")"
|
||||||
|
[ -n "$gate" ] || continue
|
||||||
|
grep -qxF -f <(printf '%s\n' "$issue_numbers") <<<"$gate" || continue
|
||||||
|
WINDOW_CARRIERS="${WINDOW_CARRIERS}${rn}"$'\n'
|
||||||
|
WINDOW_GATE="${WINDOW_GATE}${gate}"$'\n'
|
||||||
|
done <<<"$release_bodies"
|
||||||
|
fi
|
||||||
|
[ -z "$WINDOW_CARRIERS" ] || window_rendered="$(window_state "$WINDOW_CARRIERS")"
|
||||||
|
COLLISION_FLAGS="$(collision_key_index <<<"$BOARD_RECORDS" | collision_flags)"
|
||||||
|
WINDOW_FLAGS="$(window_flags "$WINDOW_GATE" "$WINDOW_CARRIERS" <<<"$BOARD_RECORDS" \
|
||||||
|
| awk -v state="$window_rendered" 'NF { print $1 "\t" state }')"
|
||||||
if [ -z "$issue_numbers" ]; then
|
if [ -z "$issue_numbers" ]; then
|
||||||
log "no open issues."
|
log "no open issues."
|
||||||
else
|
else
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue