Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions docs/content/docs/manual/board-tooling.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -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.
14 changes: 8 additions & 6 deletions scripts/board-snapshot.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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)')
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
46 changes: 42 additions & 4 deletions scripts/board_snapshot_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 ;;
Expand All @@ -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
Expand Down Expand Up @@ -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)
}
}
Loading