From 9cfdb97306483ffb5e1104bca01ed510c1b648c4 Mon Sep 17 00:00:00 2001 From: Werner Stein Date: Fri, 9 Oct 2026 11:42:46 +0200 Subject: [PATCH] chore(board): drop the Ready to push status from board-snapshot The product no longer has the Ready to push board status (#514, #516). The tool no longer requires the option, so configure creates a board with Todo, In progress, Blocked, In review and Done. A board that still carries the option keeps it and passes the readback; sync still leaves a card in that legacy status alone. The Release and milestones view had been filtered on the removed status and was reported invalid. It now lists all open issues of the repository; grouping by Milestone stays a UI step as documented. Tests and the board-tooling manual page follow. The Ready to push? Decision card is untouched. Refs: #515 Co-Authored-By: Claude Sonnet 5.5 --- docs/content/docs/manual/board-tooling.md | 13 ++++--- scripts/board-snapshot.sh | 14 ++++--- scripts/board_snapshot_test.go | 46 +++++++++++++++++++++-- 3 files changed, 57 insertions(+), 16 deletions(-) diff --git a/docs/content/docs/manual/board-tooling.md b/docs/content/docs/manual/board-tooling.md index 3167eb52..438fff83 100644 --- a/docs/content/docs/manual/board-tooling.md +++ b/docs/content/docs/manual/board-tooling.md @@ -72,8 +72,7 @@ bash scripts/board-snapshot.sh configure PVT_kwHNjWrOAZaiCg bash scripts/board-snapshot.sh metadata schema ``` -The schema is Status (`Todo`, `In progress`, `Blocked`, `In review`, `Ready to -push`, `Done`), Priority (`P1`, `P2`, `P3`) and Session (configured project lanes). +The schema is Status (`Todo`, `In progress`, `Blocked`, `In review`, `Done`), Priority (`P1`, `P2`, `P3`) and Session (configured project lanes). Views show Title, Status, Priority, built-in Assignees, Session and Milestone. Assignees identify the accountable human; configuration does not change issue assignees. Assign a human only with the appropriate authorization. @@ -91,9 +90,10 @@ population alone does not establish that an issue has reached review. | Active work | In progress | | Review queue | In review | | Blocked work | Blocked | -| Release and milestones | Ready to push | +| Release and milestones | (all open issues) | -Each filter restricts results to open Issues in the configured repository. +Each filter restricts results to open Issues in the configured repository; +**Release and milestones** adds no Status clause. The [documented Projects API](https://docs.github.com/en/graphql/reference/projects) supports view layout, visible fields and filters. In the UI, open **Dispatch queue**, choose **Sort**, set **Priority** ascending (`P1`, `P2`, `P3`) and save @@ -117,7 +117,8 @@ an enabled flag alone is insufficient evidence. Verify a real closure only when the human has authorized closing that issue. `move` still refuses `Done`, which follows from the human closing or merging -the PR. The board's own **Ready to push** option is unused. Configuration never promotes -cards. Dispatch enforces worker limits from the project profile. Board column +the PR. Configuration never promotes +cards. A board that still has a legacy `Ready to push` option keeps it (configuration +does not remove it); `move` never sets it. Dispatch enforces worker limits from the project profile. Board column limits are visual aids and do not enforce concurrency; verify profile limits and saved board settings separately. diff --git a/scripts/board-snapshot.sh b/scripts/board-snapshot.sh index ca8481be..c992e1ff 100755 --- a/scripts/board-snapshot.sh +++ b/scripts/board-snapshot.sh @@ -533,10 +533,11 @@ if [ "$mode" = configure ] || [ "$mode" = configure-fields ]; then schema_fetch || die "could not read complete schema metadata; no configuration changed" fi desired=$(jq -cn --arg prefix "$lane_prefix" --arg roles "$roles" --argjson fields "$schema_fields" '{ - Status:["Todo","In progress","Blocked","In review","Ready to push","Done"], + Status:["Todo","In progress","Blocked","In review","Done"], Priority:["P1","P2","P3"], Session:($roles | split(",") | map($prefix + "/" + .)) - } | if $prefix == "wh" or any($fields[]; .name == "Session" and any(.options[]?; .name == "Werner")) then .Session += ["Werner"] else . end') + } | if any($fields[]; .name == "Status" and any(.options[]?; .name == "Ready to push")) then .Status = ["Todo","In progress","Blocked","In review","Ready to push","Done"] else . end + | if $prefix == "wh" or any($fields[]; .name == "Session" and any(.options[]?; .name == "Werner")) then .Session += ["Werner"] else . end') normalized_fields=$(printf '%s' "$schema_fields" | jq -c --arg prefix "$lane_prefix" 'map(if .name == "Session" then .options |= map(if (.name | startswith("wh/")) then .name = ($prefix + "/" + (.name | ltrimstr("wh/"))) else . end) else . end)') @@ -581,9 +582,10 @@ if [ "$mode" = configure ] || [ "$mode" = configure-fields ]; then "Active work") status="In progress" ;; "Review queue") status="In review" ;; "Blocked work") status=Blocked ;; - "Release and milestones") status="Ready to push" ;; + "Release and milestones") status="" ;; esac - filter="repo:$repository is:issue -is:closed status:\"$status\"" + filter="repo:$repository is:issue -is:closed" + [ -z "$status" ] || filter="$filter status:\"$status\"" current=$(printf '%s' "$schema_views" | jq -c --arg name "$name" '[.[] | select(.name == $name)][0] // {}') vid=$(printf '%s' "$current" | jq -r '.id // empty') if [ -z "$vid" ]; then @@ -599,9 +601,9 @@ if [ "$mode" = configure ] || [ "$mode" = configure-fields ]; then schema_fetch || die "configuration may be partial: final metadata readback failed" printf '%s' "$schema_fields" | jq -e --argjson desired "$desired" '. as $fields | all(["Status","Priority","Session"][]; . as $name | [$fields[] | select(.name == $name)] | length == 1 and (.[0].options | map(.name)) == $desired[$name])' >/dev/null || die "configuration readback does not match required options" printf '%s' "$schema_views" | jq -e --arg repo "$repository" --argjson visible "$visible" '. as $views | - {"Dispatch queue":"Todo","Active work":"In progress","Review queue":"In review","Blocked work":"Blocked","Release and milestones":"Ready to push"} | to_entries | + {"Dispatch queue":"Todo","Active work":"In progress","Review queue":"In review","Blocked work":"Blocked","Release and milestones":""} | to_entries | all(.[]; . as $expected | [$views[] | select(.name == $expected.key)] | length == 1 and - .[0].filter == ("repo:" + $repo + " is:issue -is:closed status:\"" + $expected.value + "\"") and .[0].layout == "TABLE_LAYOUT" and + .[0].filter == ("repo:" + $repo + " is:issue -is:closed" + (if $expected.value == "" then "" else " status:\"" + $expected.value + "\"" end)) and .[0].layout == "TABLE_LAYOUT" and ([.[0].configuration.visibleFields.nodes[].id] == $visible))' >/dev/null || die "configuration readback does not match required views" schema_print exit 0 diff --git a/scripts/board_snapshot_test.go b/scripts/board_snapshot_test.go index df22633e..d187687b 100644 --- a/scripts/board_snapshot_test.go +++ b/scripts/board_snapshot_test.go @@ -366,7 +366,7 @@ func TestBoardSnapshotConfigureRepeatPreservesOptions(t *testing.T) { } for _, field := range metadata.Fields { if field.Name == "Status" { - if len(field.Options) != 6 || field.Options[0].ID != "O_todo" || field.Options[5].ID != "O_done" { + if len(field.Options) != 5 || field.Options[0].ID != "O_todo" || field.Options[4].ID != "O_done" { t.Fatalf("status identities changed: %+v", field.Options) } } @@ -462,7 +462,7 @@ func sessionSchemaFixture(t *testing.T, options string) board { current := field.(map[string]any) if current["name"] == "Status" { statuses := []map[string]any{} - for index, name := range []string{"Todo", "In progress", "Blocked", "In review", "Ready to push", "Done"} { + for index, name := range []string{"Todo", "In progress", "Blocked", "In review", "Done"} { statuses = append(statuses, map[string]any{"id": "status-" + strconv.Itoa(index), "name": name, "color": "GRAY", "description": ""}) } current["options"] = statuses @@ -580,7 +580,7 @@ case "$*" in *updateProjectV2ItemFieldValue*) for a in "$@"; do case "$a" in i=PVTI_*) item=${a#i=PVTI_} ;; o=*) opt=${a#o=} ;; f=*) fld=${a#f=} ;; esac; done if [ -z "$FAKE_NOAPPLY" ] && [ "$fld" = F_status ]; then - case "$opt" in O_todo) st=Todo ;; O_ip) st="In progress" ;; O_bl) st=Blocked ;; O_ir) st="In review" ;; O_rp) st="Ready to push" ;; esac + case "$opt" in O_todo) st=Todo ;; O_ip) st="In progress" ;; O_bl) st=Blocked ;; O_ir) st="In review" ;; esac printf '%s' "$st" > "` + b.bin + `/cur.$item" fi echo '{"data":{}}'; exit 0 ;; @@ -590,7 +590,7 @@ case "$*" in status=null if [ -f "` + b.bin + `/cur.$number" ]; then status="{\"name\":\"$(cat "` + b.bin + `/cur.$number")\"}"; fi echo '{"data":{"repository":{"issue":{"projectItems":{"nodes":[{"id":"PVTI_other","project":{"id":"PVT_other"}},{"id":"PVTI_'$number'","project":{"id":"PVT_kwHNjWrOAZVCuA"},"status":'"$status"'}]}}}}}'; exit 0 ;; -*"fields(first"*) echo '{"data":{"node":{"id":"PVT_kwHNjWrOAZVCuA","fields":{"pageInfo":{"hasNextPage":false,"endCursor":null},"nodes":[{},{"id":"F_status","name":"Status","options":[{"id":"O_todo","name":"Todo"},{"id":"O_ip","name":"In progress"},{"id":"O_bl","name":"Blocked"},{"id":"O_ir","name":"In review"},{"id":"O_rp","name":"Ready to push"}]},{"id":"F_sess","name":"Session","options":[{"id":"O_s1","name":"wh/review"},{"id":"O_s2","name":"Werner"}]},{"id":"F_prio","name":"Priority","options":[{"id":"O_p1","name":"P1"},{"id":"O_p3","name":"P3"}]}]}}}}'; exit 0 ;; +*"fields(first"*) echo '{"data":{"node":{"id":"PVT_kwHNjWrOAZVCuA","fields":{"pageInfo":{"hasNextPage":false,"endCursor":null},"nodes":[{},{"id":"F_status","name":"Status","options":[{"id":"O_todo","name":"Todo"},{"id":"O_ip","name":"In progress"},{"id":"O_bl","name":"Blocked"},{"id":"O_ir","name":"In review"}]},{"id":"F_sess","name":"Session","options":[{"id":"O_s1","name":"wh/review"},{"id":"O_s2","name":"Werner"}]},{"id":"F_prio","name":"Priority","options":[{"id":"O_p1","name":"P1"},{"id":"O_p3","name":"P3"}]}]}}}}'; exit 0 ;; *"items(first"*) cur=first for a in "$@"; do case "$a" in after=*) cur=${a#after=} ;; esac; done @@ -1509,3 +1509,41 @@ func TestBoardSnapshotSyncRefusesAStaleSnapshot(t *testing.T) { t.Fatalf("a dry run may read a stale snapshot: %v %q", err, so) } } + +func TestBoardSnapshotConfigureToleratesLegacyReadyToPush(t *testing.T) { + t.Parallel() + fixture := sessionSchemaFixture(t, `[{"id":"s-werner","name":"Werner","color":"GRAY","description":"Human maintainer"}]`) + path := filepath.Join(fixture.bin, "schema.json") + data, err := os.ReadFile(path) //nolint:gosec // a test fixture path + if err != nil { + t.Fatal(err) + } + var state map[string]any + if err := json.Unmarshal(data, &state); err != nil { + t.Fatal(err) + } + for _, field := range state["fields"].([]any) { + current := field.(map[string]any) + if current["name"] == "Status" { + statuses := []map[string]any{} + for index, name := range []string{"Todo", "In progress", "Blocked", "In review", "Ready to push", "Done"} { + statuses = append(statuses, map[string]any{"id": "status-" + strconv.Itoa(index), "name": name, "color": "GRAY", "description": ""}) + } + current["options"] = statuses + } + } + updated, err := json.Marshal(state) + if err != nil { + t.Fatal(err) + } + if err := os.WriteFile(path, updated, 0o600); err != nil { + t.Fatal(err) + } + stdout, stderr, err := fixture.runEnv(t, schemaEnv(fixture), "configure", "PVT_crewbook") + if err != nil { + t.Fatalf("configure on a board that still has the option: %v %s", err, stderr) + } + if !strings.Contains(stdout, `"Ready to push"`) { + t.Fatalf("legacy option was dropped: %s", stdout) + } +}