forked from heavy-duty/ceremony
feat: derive release windows from membership records
This commit is contained in:
parent
b7a2b31f84
commit
9a37db4b18
2 changed files with 124 additions and 48 deletions
|
|
@ -830,7 +830,7 @@ 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)
|
reconcile_board_flags() { # $1 issue, $2 concluded queue state — board flags (#293)
|
||||||
# Dedup is the declaration echo's, per family (#293 D4): the marker is
|
# 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
|
# 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
|
# last word on the thread, so a state that changes speaks and a state that
|
||||||
|
|
@ -845,6 +845,7 @@ reconcile_board_flags() { # $1 = issue — the collision and window flags (#293)
|
||||||
# board fact that is true right now, and a board where the fact never
|
# board fact that is true right now, and a board where the fact never
|
||||||
# changed has nothing new to say.
|
# changed has nothing new to say.
|
||||||
local n="$1" state marker rendered
|
local n="$1" state marker rendered
|
||||||
|
board_flags_in_scope "$2" || return 0
|
||||||
state="$(flag_for_issue "$n" "${COLLISION_FLAGS:-}")"
|
state="$(flag_for_issue "$n" "${COLLISION_FLAGS:-}")"
|
||||||
if [ -n "$state" ]; then
|
if [ -n "$state" ]; then
|
||||||
marker="$(state_marker collision "$state")"
|
marker="$(state_marker collision "$state")"
|
||||||
|
|
@ -882,20 +883,22 @@ marker carries the collision itself, so an unchanged one never re-posts.*" >/dev
|
||||||
if state_echo_needed "$n" window-nonmember "$marker"; then
|
if state_echo_needed "$n" window-nonmember "$marker"; then
|
||||||
run forge_issue_comment "$n" "<!-- issueflow:$marker -->
|
run forge_issue_comment "$n" "<!-- issueflow:$marker -->
|
||||||
A release window is standing ($state) and this issue is neither one of its
|
A release window is standing ($state) and this issue is neither one of its
|
||||||
gate members nor an \`epic\` or \`post-merge\` issue.
|
members nor an \`epic\` or \`post-merge\` issue.
|
||||||
|
|
||||||
#292's invariant: during a standing window — an open \`release\`-labeled 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
|
with a non-empty membership record — the \`ready\` set is a subset of that
|
||||||
\`post-merge\` exempt. Every mint during a window is a membership call, binary,
|
record, \`epic\` and \`post-merge\` exempt. Every mint during a window is a
|
||||||
made at mint time: **behind the gate**, this issue's own Dependencies declare
|
membership call, binary, made at mint time: **behind the gate**, this issue's
|
||||||
the release issue as a blocker and the sweep releases it when the release
|
own Dependencies declare the release issue as a blocker and the sweep releases
|
||||||
closes; or **into the graph**, three writes in one tick — this issue declares
|
it when the release closes; or **into the graph**, three writes in one tick —
|
||||||
its immediate predecessors, every member whose immediate predecessor it
|
this issue declares its immediate predecessors, every member whose immediate
|
||||||
becomes re-points to it, and the release issue gains \`Blocked by #N\`, which
|
predecessor it becomes re-points to it, and the release issue gains a row for
|
||||||
records membership and nothing else. Silence is not a state.
|
this issue in its membership record. Silence is not a state.
|
||||||
|
|
||||||
The gate is read from the release issue's own \`Blocked by\` declarations — the
|
Membership is read from the release issue's own \`## Members\` record: the rows
|
||||||
same parse every \`blocked\` issue is gated on, echoed on that issue.
|
under that heading, one member each, the row's first token a bare \`#N\` and
|
||||||
|
everything after it prose. A \`Blocked by\` declaration on a release issue
|
||||||
|
answers its predecessor gate and never its membership (#343).
|
||||||
|
|
||||||
*Comment only: nothing on this path writes a label or changes a state. The
|
*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
|
marker carries the window itself, so an unchanged one never re-posts.*" >/dev/null
|
||||||
|
|
@ -913,9 +916,14 @@ reconcile_issue() {
|
||||||
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=""
|
||||||
local unchecked="" remove_claimed=claimed
|
local unchecked="" remove_claimed=claimed
|
||||||
local attention_active=true attention_suppression=""
|
local attention_active=true attention_suppression=""
|
||||||
|
local concluded_queue_state=""
|
||||||
|
for label in needs-triage epic "${QUEUE_LABELS[@]}"; do
|
||||||
|
has_issue_label "$label" && concluded_queue_state="$label"
|
||||||
|
done
|
||||||
decision="$(queue_decision <<<"$ISSUE_LABELS")"
|
decision="$(queue_decision <<<"$ISSUE_LABELS")"
|
||||||
case "$decision" in
|
case "$decision" in
|
||||||
ADD_NEEDS_TRIAGE)
|
ADD_NEEDS_TRIAGE)
|
||||||
|
concluded_queue_state=needs-triage
|
||||||
run forge_issue_edit "$n" --add-label needs-triage >/dev/null
|
run forge_issue_edit "$n" --add-label needs-triage >/dev/null
|
||||||
log "#$n: needs-triage (no queue state)" ;;
|
log "#$n: needs-triage (no queue state)" ;;
|
||||||
FLAG_CONFLICT)
|
FLAG_CONFLICT)
|
||||||
|
|
@ -969,6 +977,7 @@ The merge releases the claim; no builder owes a draft. Triage owes completion in
|
||||||
--remove-label "$remove_claimed" --add-label post-merge >/dev/null
|
--remove-label "$remove_claimed" --add-label post-merge >/dev/null
|
||||||
fi
|
fi
|
||||||
log "#$n: merged Refs PR -> post-merge; claim released"
|
log "#$n: merged Refs PR -> post-merge; claim released"
|
||||||
|
concluded_queue_state=post-merge
|
||||||
attention_active=false
|
attention_active=false
|
||||||
else
|
else
|
||||||
created="$(jq -r '.created_at' <<<"$ISSUE_JSON")"
|
created="$(jq -r '.created_at' <<<"$ISSUE_JSON")"
|
||||||
|
|
@ -999,6 +1008,7 @@ The merge releases the claim; no builder owes a draft. Triage owes completion in
|
||||||
else
|
else
|
||||||
run forge_issue_edit "$n" --remove-label claimed --add-label ready >/dev/null
|
run forge_issue_edit "$n" --remove-label claimed --add-label ready >/dev/null
|
||||||
fi
|
fi
|
||||||
|
concluded_queue_state=ready
|
||||||
log "#$n: stale claim reclaimed -> ready" ;;
|
log "#$n: stale claim reclaimed -> ready" ;;
|
||||||
esac
|
esac
|
||||||
[ "$decision" != FLAG_UNASSIGNED ] || attention_suppression=claimed-unassigned
|
[ "$decision" != FLAG_UNASSIGNED ] || attention_suppression=claimed-unassigned
|
||||||
|
|
@ -1122,6 +1132,7 @@ itself, so a parse unchanged since the last echo never re-posts.*" >/dev/null
|
||||||
ensure_comment "$n" blockers-cleared \
|
ensure_comment "$n" blockers-cleared \
|
||||||
'Every issue named by `Blocked by` is closed. The sweep is moving this issue to `ready`.'
|
'Every issue named by `Blocked by` is closed. The sweep is moving this issue to `ready`.'
|
||||||
run forge_issue_edit "$n" --remove-label blocked --add-label ready >/dev/null
|
run forge_issue_edit "$n" --remove-label blocked --add-label ready >/dev/null
|
||||||
|
concluded_queue_state=ready
|
||||||
log "#$n: blockers closed -> ready" ;;
|
log "#$n: blockers closed -> ready" ;;
|
||||||
esac
|
esac
|
||||||
elif has_issue_label epic; then
|
elif has_issue_label epic; then
|
||||||
|
|
@ -1138,7 +1149,7 @@ itself, so a parse unchanged since the last echo never re-posts.*" >/dev/null
|
||||||
|
|
||||||
1. Mint the window's members.
|
1. Mint the window's members.
|
||||||
2. Graph hard dependencies and same-file clusters.
|
2. Graph hard dependencies and same-file clusters.
|
||||||
3. Write ordered waves and the progress task list.
|
3. Write ordered waves, the \`## Members\` record, and the progress task list.
|
||||||
4. Ask the operator to bless the order, then open the first wave.
|
4. Ask the operator to bless the order, then open the first wave.
|
||||||
5. Ship the release, close this epic, and trigger the next window.
|
5. Ship the release, close this epic, and trigger the next window.
|
||||||
|
|
||||||
|
|
@ -1168,7 +1179,7 @@ See \`$release_doctrine_path\`. The operator blessing the order is the one step
|
||||||
# compose with every queue state, and FLAG_CONFLICT's early return still
|
# compose with every queue state, and FLAG_CONFLICT's early return still
|
||||||
# short-circuits them, because a board lying about its queue state is
|
# short-circuits them, because a board lying about its queue state is
|
||||||
# repaired before anything is derived from it.
|
# repaired before anything is derived from it.
|
||||||
reconcile_board_flags "$n"
|
reconcile_board_flags "$n" "$concluded_queue_state"
|
||||||
|
|
||||||
# ---- 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
|
||||||
|
|
@ -1351,7 +1362,7 @@ main() {
|
||||||
done < <(printf '%s' "$b64" | base64 -d | refs_references)
|
done < <(printf '%s' "$b64" | base64 -d | refs_references)
|
||||||
done)"
|
done)"
|
||||||
|
|
||||||
local n tail_line issue_numbers board_json release_bodies rn rbody gate body
|
local n tail_line issue_numbers board_json release_numbers rn window_records body
|
||||||
local window_rendered=""
|
local window_rendered=""
|
||||||
SKIPPED_COUNT=0
|
SKIPPED_COUNT=0
|
||||||
SKIPPED_ISSUES=""
|
SKIPPED_ISSUES=""
|
||||||
|
|
@ -1376,31 +1387,39 @@ main() {
|
||||||
| @tsv' \
|
| @tsv' \
|
||||||
<<<"$board_json")"
|
<<<"$board_json")"
|
||||||
issue_numbers="$(cut -f1 <<<"$BOARD_RECORDS")"
|
issue_numbers="$(cut -f1 <<<"$BOARD_RECORDS")"
|
||||||
# A standing window is an open `release`-labeled issue whose gate still
|
# A standing window is an open `release`-labeled issue whose MEMBERSHIP
|
||||||
# holds an OPEN member (#292 D1). The board read IS the open set, so
|
# RECORD still holds an OPEN member (#292 D1 as #343 D3 re-reads it). The
|
||||||
# membership decides openness with no extra call — and an all-closed gate
|
# board read IS the open set, so membership decides openness with no extra
|
||||||
# is exactly the emptied gate the release's own `blocked` -> `ready`
|
# call — and an all-closed record is exactly the emptied window the
|
||||||
# promotion answers, which is why a `ready` release leaves the flag
|
# release's own `blocked` -> `ready` promotion answers, which is why a
|
||||||
# dormant rather than flagging the whole board.
|
# `ready` release leaves the flag dormant rather than flagging the board.
|
||||||
release_bodies="$(jq -r '.[] | select(.pull_request == null)
|
#
|
||||||
|
# The record is read by heading, so each body must reach the parse with its
|
||||||
|
# LINE STRUCTURE INTACT. Taking it from the board payload by issue number
|
||||||
|
# preserves that structure without fetching a second, disagreeing board.
|
||||||
|
release_numbers="$(jq -r '.[] | select(.pull_request == null)
|
||||||
| select((.labels // []) | map(.name) | index("release"))
|
| select((.labels // []) | map(.name) | index("release"))
|
||||||
| [(.number | tostring), ((.body // "") | gsub("[\t\r\n]"; " "))] | @tsv' \
|
| .number' \
|
||||||
<<<"$board_json")"
|
<<<"$board_json")"
|
||||||
WINDOW_CARRIERS=""
|
WINDOW_CARRIERS=""
|
||||||
WINDOW_GATE=""
|
WINDOW_MEMBERS=""
|
||||||
if [ -n "$issue_numbers" ]; then
|
if [ -n "$issue_numbers" ]; then
|
||||||
while IFS=$'\t' read -r rn rbody; do
|
window_records="$(
|
||||||
[ -n "$rn" ] || continue
|
while IFS= read -r rn; do
|
||||||
gate="$(blocked_references <<<"$rbody")"
|
[ -n "$rn" ] || continue
|
||||||
[ -n "$gate" ] || continue
|
jq -r --argjson n "$rn" '.[] | select(.pull_request == null)
|
||||||
grep -qxF -f <(printf '%s\n' "$issue_numbers") <<<"$gate" || continue
|
| select(.number == $n) | .body // ""' <<<"$board_json" \
|
||||||
WINDOW_CARRIERS="${WINDOW_CARRIERS}${rn}"$'\n'
|
| release_window_members "$rn" "$issue_numbers"
|
||||||
WINDOW_GATE="${WINDOW_GATE}${gate}"$'\n'
|
done <<<"$release_numbers"
|
||||||
done <<<"$release_bodies"
|
)"
|
||||||
|
# One record per parsed non-self member keeps the carrier decision and
|
||||||
|
# its WINDOW_MEMBERS contribution coupled to the extracted function.
|
||||||
|
WINDOW_CARRIERS="$(cut -f1 <<<"$window_records" | awk 'NF' | sort -nu)"
|
||||||
|
WINDOW_MEMBERS="$(cut -f2 <<<"$window_records" | awk 'NF' | sort -nu)"
|
||||||
fi
|
fi
|
||||||
[ -z "$WINDOW_CARRIERS" ] || window_rendered="$(window_state "$WINDOW_CARRIERS")"
|
[ -z "$WINDOW_CARRIERS" ] || window_rendered="$(window_state "$WINDOW_CARRIERS")"
|
||||||
COLLISION_FLAGS="$(collision_key_index <<<"$BOARD_RECORDS" | collision_flags)"
|
COLLISION_FLAGS="$(collision_key_index <<<"$BOARD_RECORDS" | collision_flags)"
|
||||||
WINDOW_FLAGS="$(window_flags "$WINDOW_GATE" "$WINDOW_CARRIERS" <<<"$BOARD_RECORDS" \
|
WINDOW_FLAGS="$(window_flags "$WINDOW_MEMBERS" "$WINDOW_CARRIERS" <<<"$BOARD_RECORDS" \
|
||||||
| awk -v state="$window_rendered" 'NF { print $1 "\t" state }')"
|
| 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."
|
||||||
|
|
|
||||||
|
|
@ -502,6 +502,11 @@ check "a release epic with every declared blocker closed announces init" 0 "" \
|
||||||
grep -qF '<!-- issueflow:release-init-due -->' "$TMP/posted-53"
|
grep -qF '<!-- issueflow:release-init-due -->' "$TMP/posted-53"
|
||||||
check "the init announce names all five steps" 0 "5" \
|
check "the init announce names all five steps" 0 "5" \
|
||||||
grep -cE '^[1-5]\. ' "$TMP/posted-53"
|
grep -cE '^[1-5]\. ' "$TMP/posted-53"
|
||||||
|
# Release-init is where the membership record is first written, so step 3
|
||||||
|
# names it beside the waves and the progress task list.
|
||||||
|
# shellcheck disable=SC2016 # backticks are the comment body's own Markdown
|
||||||
|
check "step 3 names the membership record it first writes" 0 "" \
|
||||||
|
grep -qF '3. Write ordered waves, the `## Members` record' "$TMP/posted-53"
|
||||||
# shellcheck disable=SC2016 # backticks are the literal portable doctrine citation
|
# shellcheck disable=SC2016 # backticks are the literal portable doctrine citation
|
||||||
check "the init announce cites the portable vendored doctrine path" 0 "" \
|
check "the init announce cites the portable vendored doctrine path" 0 "" \
|
||||||
grep -qF 'See `.ceremony/RELEASES.md`.' "$TMP/posted-53"
|
grep -qF 'See `.ceremony/RELEASES.md`.' "$TMP/posted-53"
|
||||||
|
|
@ -714,9 +719,16 @@ check "the flag-free control is reclaimed (the clock still runs elsewhere)" 0 ""
|
||||||
|
|
||||||
# -- merged Refs work releases the claim before the reclaim clock ------------
|
# -- merged Refs work releases the claim before the reclaim clock ------------
|
||||||
printf '[]\n' >"$(cfix 35)"
|
printf '[]\n' >"$(cfix 35)"
|
||||||
|
COLLISION_FLAGS=$'35\tissueflow-reconcile=34'
|
||||||
|
WINDOW_FLAGS=$'35\t#50'
|
||||||
transition="$(issue_probe 35 claimed 1 false 350 $'- [x] built\n- [ ] verify dispatch\n * [ ] confirm warning clears')"
|
transition="$(issue_probe 35 claimed 1 false 350 $'- [x] built\n- [ ] verify dispatch\n * [ ] confirm warning clears')"
|
||||||
|
unset COLLISION_FLAGS WINDOW_FLAGS
|
||||||
check "merged Refs + unchecked criteria transitions in the sweep body" 0 "" \
|
check "merged Refs + unchecked criteria transitions in the sweep body" 0 "" \
|
||||||
grep -q 'merged Refs PR -> post-merge; claim released' <<<"$transition"
|
grep -q 'merged Refs PR -> post-merge; claim released' <<<"$transition"
|
||||||
|
check "a pass concluding post-merge draws no precomputed collision flag" 1 "" \
|
||||||
|
grep -q 'collision flag' <<<"$transition"
|
||||||
|
check "...and no precomputed window flag" 1 "" \
|
||||||
|
grep -q 'window flag' <<<"$transition"
|
||||||
# shellcheck disable=SC2016 # positional parameters belong to bash -c
|
# shellcheck disable=SC2016 # positional parameters belong to bash -c
|
||||||
check "...names every remaining criterion verbatim in the comment" 0 "" \
|
check "...names every remaining criterion verbatim in the comment" 0 "" \
|
||||||
bash -c 'grep -qF -- "- [ ] verify dispatch" "$1" &&
|
bash -c 'grep -qF -- "- [ ] verify dispatch" "$1" &&
|
||||||
|
|
@ -1821,12 +1833,12 @@ check "...the null-valued row is TRAVERSED, with an observable outcome" 0 "" \
|
||||||
check "...and the object-valued PR row is not reconciled as an issue" 1 "" \
|
check "...and the object-valued PR row is not reconciled as an issue" 1 "" \
|
||||||
grep -qE '^issueflow: #61' <<<"$fjb_out"
|
grep -qE '^issueflow: #61' <<<"$fjb_out"
|
||||||
|
|
||||||
# release_bodies is the THIRD producer and has its own has() site. A `release`
|
# release_numbers is the THIRD producer and has its own issue-shape filter. A `release`
|
||||||
# issue on a forgejo-shaped board must reach the window gather, or the #292
|
# issue on a forgejo-shaped board must reach the window gather, or the #292
|
||||||
# flags are decided over an empty set (@codex-reviewer-andresmgsl, #210).
|
# flags are decided over an empty set (@codex-reviewer-andresmgsl, #210).
|
||||||
printf '%s\n' \
|
printf '%s\n' \
|
||||||
'[{"number":60,"pull_request":null,"labels":[{"name":"ready"}],"title":"an issue"},
|
'[{"number":60,"pull_request":null,"labels":[{"name":"ready"}],"title":"an issue"},
|
||||||
{"number":62,"pull_request":null,"labels":[{"name":"release"}],"title":"Release 9.9.9","body":"Blocked by #60."},
|
{"number":62,"pull_request":null,"labels":[{"name":"release"}],"title":"Release 9.9.9","body":"Blocked by #59.\n\n## Members\n- #60"},
|
||||||
{"number":63,"pull_request":null,"labels":[{"name":"ready"}],"title":"a claimable non-member"},
|
{"number":63,"pull_request":null,"labels":[{"name":"ready"}],"title":"a claimable non-member"},
|
||||||
{"number":61,"pull_request":{"merged":false},"labels":[],"title":"a pull request"}]' \
|
{"number":61,"pull_request":{"merged":false},"labels":[],"title":"a pull request"}]' \
|
||||||
>"$FORGEJO_BOARD/repos_owner_repo_issues_state_open.json"
|
>"$FORGEJO_BOARD/repos_owner_repo_issues_state_open.json"
|
||||||
|
|
@ -1836,13 +1848,13 @@ jq -n --arg at "$(iso_at "$INOW")" \
|
||||||
>"$FORGEJO_BOARD/repos_owner_repo_issues_63.json"
|
>"$FORGEJO_BOARD/repos_owner_repo_issues_63.json"
|
||||||
printf '[]\n' >"$FORGEJO_BOARD/repos_owner_repo_issues_63_comments.json"
|
printf '[]\n' >"$FORGEJO_BOARD/repos_owner_repo_issues_63_comments.json"
|
||||||
jq -n --arg at "$(iso_at "$INOW")" \
|
jq -n --arg at "$(iso_at "$INOW")" \
|
||||||
'{number:62,user:{login:"triage-one"},created_at:$at,body:"Blocked by #60.",pull_request:null,
|
'{number:62,user:{login:"triage-one"},created_at:$at,body:"Blocked by #59.\n\n## Members\n- #60",pull_request:null,
|
||||||
labels:[{name:"release"}],assignees:[]}' \
|
labels:[{name:"release"}],assignees:[]}' \
|
||||||
>"$FORGEJO_BOARD/repos_owner_repo_issues_62.json"
|
>"$FORGEJO_BOARD/repos_owner_repo_issues_62.json"
|
||||||
printf '[]\n' >"$FORGEJO_BOARD/repos_owner_repo_issues_62_comments.json"
|
printf '[]\n' >"$FORGEJO_BOARD/repos_owner_repo_issues_62_comments.json"
|
||||||
fjb2_out="$(forgejo_board_run)"
|
fjb2_out="$(forgejo_board_run)"
|
||||||
# The observable effect of release_bodies being NON-empty: an open `release`
|
# The observable effect of release_numbers being NON-empty: an open `release`
|
||||||
# issue whose gate still holds an open member makes every claimable non-member
|
# issue whose membership record still holds an open member makes every claimable non-member
|
||||||
# draw a window flag. With that gather empty there are no carriers and no flag,
|
# draw a window flag. With that gather empty there are no carriers and no flag,
|
||||||
# so this row discriminates the site rather than merely reaching it.
|
# so this row discriminates the site rather than merely reaching it.
|
||||||
check "a release issue on a forgejo-shaped board reaches the window gather" 0 "" \
|
check "a release issue on a forgejo-shaped board reaches the window gather" 0 "" \
|
||||||
|
|
@ -2683,7 +2695,7 @@ board_run() {
|
||||||
# them — six `ready` non-members against a standing gate, and one deliverable
|
# them — six `ready` non-members against a standing gate, and one deliverable
|
||||||
# carried three times in two spellings.
|
# carried three times in two spellings.
|
||||||
board_issue 249 blocked,release 'Release 0.6.0 — the board empties into the tag' \
|
board_issue 249 blocked,release 'Release 0.6.0 — the board empties into the tag' \
|
||||||
'Blocked by #253.'
|
"$(printf '%s\n' 'Blocked by #253.' '' '## Members' '- #253')"
|
||||||
board_issue 253 claimed 'issueflow-reconcile — a release epic announces its own release-init' '' 1
|
board_issue 253 claimed 'issueflow-reconcile — a release epic announces its own release-init' '' 1
|
||||||
board_issue 257 ready 'actions/issueflow-reconcile — a failed board read sweeps an empty board'
|
board_issue 257 ready 'actions/issueflow-reconcile — a failed board read sweeps an empty board'
|
||||||
board_issue 264 ready 'TRIAGE.md — the no-assignee clause scopes to the flag'
|
board_issue 264 ready 'TRIAGE.md — the no-assignee clause scopes to the flag'
|
||||||
|
|
@ -2743,7 +2755,18 @@ check "the window comment names #292's invariant" 0 "" \
|
||||||
grep -qF "#292's invariant" "$BOARD/edits"
|
grep -qF "#292's invariant" "$BOARD/edits"
|
||||||
# shellcheck disable=SC2016 # backticks are the comment body's own Markdown
|
# shellcheck disable=SC2016 # backticks are the comment body's own Markdown
|
||||||
check "...and states the subset rule with its exemptions" 0 "" \
|
check "...and states the subset rule with its exemptions" 0 "" \
|
||||||
grep -qF 'the `ready` set is a subset of the gate' "$BOARD/edits"
|
grep -qF 'the `ready` set is a subset of that' "$BOARD/edits"
|
||||||
|
# shellcheck disable=SC2016 # backticks are the comment body's own Markdown
|
||||||
|
check "...and tells triage where membership is actually read from" 0 "" \
|
||||||
|
grep -qF 'Membership is read from the release issue'"'"'s own `## Members` record' \
|
||||||
|
"$BOARD/edits"
|
||||||
|
# shellcheck disable=SC2016 # backticks are the comment body's own Markdown
|
||||||
|
check "...and says what a release issue's Blocked by line does answer" 0 "" \
|
||||||
|
grep -qF 'answers its predecessor gate and never its membership' "$BOARD/edits"
|
||||||
|
check "...and asks the third write for a row, not a declaration" 0 "" \
|
||||||
|
grep -qF 'the release issue gains a row for' "$BOARD/edits"
|
||||||
|
check "no window comment sends triage to a Blocked by declaration" 1 "" \
|
||||||
|
grep -qF 'The gate is read from the release issue' "$BOARD/edits"
|
||||||
check "both comments carry idempotency markers (D4)" 0 "" \
|
check "both comments carry idempotency markers (D4)" 0 "" \
|
||||||
grep -qF '<!-- issueflow:collision-' "$BOARD/edits"
|
grep -qF '<!-- issueflow:collision-' "$BOARD/edits"
|
||||||
check "...the window one too" 0 "" grep -qF '<!-- issueflow:window-nonmember-' "$BOARD/edits"
|
check "...the window one too" 0 "" grep -qF '<!-- issueflow:window-nonmember-' "$BOARD/edits"
|
||||||
|
|
@ -2808,7 +2831,8 @@ check "...while every other collision on the board still speaks" 0 "2" \
|
||||||
# oldest-first, the reconciler chain chained, and every one of them a gate
|
# oldest-first, the reconciler chain chained, and every one of them a gate
|
||||||
# member. Every flag above must go quiet, or the flag is reporting the fix.
|
# member. Every flag above must go quiet, or the flag is reporting the fix.
|
||||||
board_issue 249 blocked,release 'Release 0.6.0 — the board empties into the tag' \
|
board_issue 249 blocked,release 'Release 0.6.0 — the board empties into the tag' \
|
||||||
'Blocked by #253, #257, #264, #266, #276, #281, #282, #284.'
|
"$(printf '%s\n' 'Blocked by #253.' '' '## Members' \
|
||||||
|
'- #253' '- #257' '- #264' '- #266' '- #276' '- #281' '- #282' '- #284')"
|
||||||
board_issue 253 claimed 'issueflow-reconcile — a release epic announces its own release-init' '' 1
|
board_issue 253 claimed 'issueflow-reconcile — a release epic announces its own release-init' '' 1
|
||||||
board_issue 257 blocked 'actions/issueflow-reconcile — a failed board read sweeps an empty board' \
|
board_issue 257 blocked 'actions/issueflow-reconcile — a failed board read sweeps an empty board' \
|
||||||
'Blocked by #253.'
|
'Blocked by #253.'
|
||||||
|
|
@ -2831,21 +2855,23 @@ check "...and no window flag either" 1 "" grep -qF ': window flag' <<<"$ruled_ou
|
||||||
check "...and still reports a whole pass" 0 'issueflow: reconciled.' \
|
check "...and still reports a whole pass" 0 'issueflow: reconciled.' \
|
||||||
printf '%s\n' "$ruled_out"
|
printf '%s\n' "$ruled_out"
|
||||||
|
|
||||||
# -- an emptied gate leaves the window flag dormant (test plan) -------------
|
# -- an emptied window leaves the flag dormant (test plan) ------------------
|
||||||
# A gate DECLARATION never empties: #249 names fifteen members and still names
|
# A membership RECORD never empties: #249 names fifteen members and still
|
||||||
# fifteen after all fifteen close. So the precondition is the gate's OPEN
|
# names fifteen after all fifteen close. So the precondition is the record's OPEN
|
||||||
# members, not its parse — read straight off the board, which already is the
|
# members, not its parse — read straight off the board, which already is the
|
||||||
# open set. Under the declaration reading the release issue, now `ready`, is
|
# open set. Under the declaration reading the release issue, now `ready`, is
|
||||||
# itself an open unblocked non-`epic` non-member, and D3 would flag the sink
|
# itself an open unblocked non-`epic` non-member, and D3 would flag the sink
|
||||||
# at the exact moment the window ends.
|
# at the exact moment the window ends.
|
||||||
board_issue 249 ready,release 'Release 0.6.0 — the board empties into the tag' \
|
board_issue 249 ready,release 'Release 0.6.0 — the board empties into the tag' \
|
||||||
'Blocked by #218, #230, #232, #236, #237, #238, #241, #242, #247, #248, #251, #252, #253, #254, #257.'
|
"$(printf '%s\n' 'Blocked by #217.' '' '## Members' \
|
||||||
|
'- #218' '- #230' '- #232' '- #236' '- #237' '- #238' '- #241' '- #242' \
|
||||||
|
'- #247' '- #248' '- #251' '- #252' '- #253' '- #254' '- #257')"
|
||||||
board_issue 264 ready 'TRIAGE.md — the no-assignee clause scopes to the flag'
|
board_issue 264 ready 'TRIAGE.md — the no-assignee clause scopes to the flag'
|
||||||
# shellcheck disable=SC2016 # the backticks are the real issue title's Markdown
|
# shellcheck disable=SC2016 # the backticks are the real issue title's Markdown
|
||||||
board_issue 266 ready 'TRIAGE.md — the epic task-list heading is literally `## Task list`'
|
board_issue 266 ready 'TRIAGE.md — the epic task-list heading is literally `## Task list`'
|
||||||
board_assemble 249 264 266
|
board_assemble 249 264 266
|
||||||
empty_gate_out="$(board_run)"
|
empty_gate_out="$(board_run)"
|
||||||
check "a fifteen-member declaration with every member closed leaves D3 dormant" 1 "" \
|
check "a fifteen-member record with every member closed leaves D3 dormant" 1 "" \
|
||||||
grep -qF ': window flag' <<<"$empty_gate_out"
|
grep -qF ': window flag' <<<"$empty_gate_out"
|
||||||
check "...and the release issue is never flagged as its own non-member" 1 "" \
|
check "...and the release issue is never flagged as its own non-member" 1 "" \
|
||||||
grep -qF 'issueflow: #249' <<<"$empty_gate_out"
|
grep -qF 'issueflow: #249' <<<"$empty_gate_out"
|
||||||
|
|
@ -2892,7 +2918,7 @@ printf '%s\n' \
|
||||||
'{"data":{"repository":{"pullRequests":{"nodes":[{"number":403,"body":"","closingIssuesReferences":{"nodes":[{"number":402}]}}],"pageInfo":{"hasNextPage":false,"endCursor":null}}}}}' \
|
'{"data":{"repository":{"pullRequests":{"nodes":[{"number":403,"body":"","closingIssuesReferences":{"nodes":[{"number":402}]}}],"pageInfo":{"hasNextPage":false,"endCursor":null}}}}}' \
|
||||||
>"$BOARD/repos_owner_repo_pulls_state_open.json"
|
>"$BOARD/repos_owner_repo_pulls_state_open.json"
|
||||||
board_issue 249 blocked,release 'Release 0.6.0 — the board empties into the tag' \
|
board_issue 249 blocked,release 'Release 0.6.0 — the board empties into the tag' \
|
||||||
'Blocked by #253.'
|
"$(printf '%s\n' 'Blocked by #253.' '' '## Members' '- #253')"
|
||||||
board_issue 253 claimed 'issueflow-reconcile — a member holding the window open' '' 1
|
board_issue 253 claimed 'issueflow-reconcile — a member holding the window open' '' 1
|
||||||
board_issue 402 claimed 'REVIEWER.md — a non-member with a builder and a round' '' 1
|
board_issue 402 claimed 'REVIEWER.md — a non-member with a builder and a round' '' 1
|
||||||
board_assemble 249 253 402
|
board_assemble 249 253 402
|
||||||
|
|
@ -2940,7 +2966,7 @@ printf '%s\n' \
|
||||||
'{"data":{"repository":{"pullRequests":{"nodes":[],"pageInfo":{"hasNextPage":false,"endCursor":null}}}}}' \
|
'{"data":{"repository":{"pullRequests":{"nodes":[],"pageInfo":{"hasNextPage":false,"endCursor":null}}}}}' \
|
||||||
>"$BOARD/repos_owner_repo_pulls_state_open.json"
|
>"$BOARD/repos_owner_repo_pulls_state_open.json"
|
||||||
board_issue 249 blocked,release 'Release 0.6.0 — the board empties into the tag' \
|
board_issue 249 blocked,release 'Release 0.6.0 — the board empties into the tag' \
|
||||||
'Blocked by #293, #307.'
|
"$(printf '%s\n' 'Blocked by #307.' '' '## Members' '- #293' '- #307')"
|
||||||
board_issue 293 claimed 'issueflow-reconcile — the sweep flags what the window and collision rules forbid' '' 1
|
board_issue 293 claimed 'issueflow-reconcile — the sweep flags what the window and collision rules forbid' '' 1
|
||||||
board_issue 307 blocked 'test/issueflow-reconcile.test.sh — the ruling pre-read is unpinned' \
|
board_issue 307 blocked 'test/issueflow-reconcile.test.sh — the ruling pre-read is unpinned' \
|
||||||
'Blocked by #293.'
|
'Blocked by #293.'
|
||||||
|
|
@ -2953,6 +2979,37 @@ check "...and no window flag: the claimed member is a member" 1 "" \
|
||||||
check "...and still reports a whole pass" 0 'issueflow: reconciled.' \
|
check "...and still reports a whole pass" 0 'issueflow: reconciled.' \
|
||||||
printf '%s\n' "$today_out"
|
printf '%s\n' "$today_out"
|
||||||
|
|
||||||
|
# -- a predecessor gate is not a membership record (#343) ------------------
|
||||||
|
board_issue 317 epic,release '0.7.0 — rc becomes native' 'Blocked by #249.'
|
||||||
|
board_issue 249 epic,release 'Release 0.6.0 — the predecessor, still open' ''
|
||||||
|
board_issue 343 ready 'RELEASES.md + TRIAGE.md — membership gets its own record'
|
||||||
|
board_issue 345 ready 'actions/issueflow-reconcile — a failed dependency read'
|
||||||
|
board_assemble 317 249 343 345
|
||||||
|
shut_window_out="$(board_run)"
|
||||||
|
check "the shut window's board replays green" 0 "" test $? -eq 0
|
||||||
|
check "an epic declaring an open predecessor stands no window" 1 "" \
|
||||||
|
grep -qF ': window flag' <<<"$shut_window_out"
|
||||||
|
check "...so the ready issues it would have accused are left alone" 1 "" \
|
||||||
|
grep -qE '#(343|345): window flag' <<<"$shut_window_out"
|
||||||
|
check "...and no window state naming it is ever rendered" 1 "" \
|
||||||
|
grep -qF 'under #317' <<<"$shut_window_out"
|
||||||
|
check "...and the board is still swept whole" 0 'issueflow: reconciled.' \
|
||||||
|
printf '%s\n' "$shut_window_out"
|
||||||
|
|
||||||
|
# Over-correction control: once the record enumerates an open member, the
|
||||||
|
# window stands and still flags the unblocked non-member.
|
||||||
|
board_issue 317 epic,release '0.7.0 — rc becomes native' \
|
||||||
|
"$(printf '%s\n' 'Blocked by #249.' '' '## Members' '- #343 — the first member')"
|
||||||
|
board_assemble 317 249 343 345
|
||||||
|
opened_window_out="$(board_run)"
|
||||||
|
check "the same epic enumerating an open member does stand a window" 0 \
|
||||||
|
'issueflow: #345: window flag — an unblocked non-member under #317' \
|
||||||
|
printf '%s\n' "$opened_window_out"
|
||||||
|
check "...and its enumerated member is not flagged" 1 "" \
|
||||||
|
grep -qF 'issueflow: #343: window flag' <<<"$opened_window_out"
|
||||||
|
check "...one window flag on that board, and only one" 0 "1" \
|
||||||
|
flag_count window "$opened_window_out"
|
||||||
|
|
||||||
# -- the invariant is enforced at the source, not remembered ----------------
|
# -- the invariant is enforced at the source, not remembered ----------------
|
||||||
# Staging only holds while every mutation goes through run(). A future call
|
# Staging only holds while every mutation goes through run(). A future call
|
||||||
# site reaching gh directly would reopen this hole silently, so it is pinned
|
# site reaching gh directly would reopen this hole silently, so it is pinned
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue