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
63 changes: 53 additions & 10 deletions cli/bash/commands/basectl/subcommands/gh_issue_readiness.sh
Original file line number Diff line number Diff line change
Expand Up @@ -57,21 +57,64 @@ base_gh_lines_to_csv() {
fi
}

base_gh_jq_string_literal() {
local value="$1"
base_gh_issue_readiness_project_api_prefix() {
local project_owner="$1" owner_type

value="${value//\\/\\\\}"
value="${value//\"/\\\"}"
printf '"%s"\n' "$value"
owner_type="$(base_cli_gh_run api "users/$project_owner" --jq .type)" || return $?
case "$owner_type" in
Organization)
printf 'orgs/%s\n' "$project_owner"
;;
User)
printf 'users/%s\n' "$project_owner"
;;
*)
base_gh_error "Unsupported GitHub Project owner type '$owner_type'."
return 1
;;
esac
}

base_gh_issue_readiness_project_row() {
local issue="$1" repo="$2" project_owner="$3" project_number="$4"
local repo_literal query
local api_prefix fields_json field_ids_csv items_json

api_prefix="$(base_gh_issue_readiness_project_api_prefix "$project_owner")" || return $?
fields_json="$(base_cli_gh_run api --method GET \
"$api_prefix/projectsV2/$project_number/fields" -f per_page=100)" || return $?
field_ids_csv="$(jq -r '
[ .[]
| select(.name == "Status" or .name == "Priority" or .name == "Size" or .name == "Area" or .name == "Initiative")
| .id
] | join(",")
' <<<"$fields_json")" || return $?
[[ -n "$field_ids_csv" ]] || {
base_gh_error "GitHub Project is missing one or more required readiness fields."
return 1
}

repo_literal="$(base_gh_jq_string_literal "$repo")"
query=".items[] | select((.content.number == $issue) and (.content.repository == $repo_literal)) | [.status // \"\", .priority // \"\", .size // \"\", .area // \"\", .initiative // \"\"] | join(\"\u001f\")"
base_cli_gh_run project item-list "$project_number" --owner "$project_owner" --format json --limit 1000 --jq "$query"
items_json="$(base_cli_gh_run api --method GET --paginate --slurp \
"$api_prefix/projectsV2/$project_number/items" \
-f per_page=100 -f fields="$field_ids_csv")" || return $?
jq -r --argjson issue "$issue" --arg repo "$repo" '
def field_value($name):
[ (.fields // [])[]
| select(.name == $name)
| (.value.name.raw // .value.raw // .value // "")
][0]
| if type == "string" then . else "" end;

[ .[][]
| select((.content.number == $issue) and (.content.repository.full_name == $repo))
][0]
| if . == null then
empty
else
[field_value("Status"), field_value("Priority"), field_value("Size"),
field_value("Area"), field_value("Initiative")]
| join("\u001f")
end
' <<<"$items_json"
}

base_gh_issue_readiness_format_error() {
Expand Down Expand Up @@ -301,7 +344,7 @@ base_gh_issue_readiness_collect_project_findings() {
"$_BASE_GH_ISSUE_READINESS_PROJECT_OWNER" "$_BASE_GH_ISSUE_READINESS_PROJECT_NUMBER")"
status=$?
((status == 0)) || {
base_gh_issue_readiness_upstream_error "$_BASE_GH_ISSUE_READINESS_OUTPUT_FORMAT" project_item_list "$status"
base_gh_issue_readiness_upstream_error "$_BASE_GH_ISSUE_READINESS_OUTPUT_FORMAT" project_item_lookup "$status"
return $?
}
if [[ -z "$_BASE_GH_ISSUE_READINESS_PROJECT_ROW" ]]; then
Expand Down
73 changes: 61 additions & 12 deletions cli/bash/commands/basectl/tests/gh.bats
Original file line number Diff line number Diff line change
Expand Up @@ -186,18 +186,33 @@ if [[ "$1" == "issue" && "$2" == "view" ]]; then
exit 0
fi
fi
if [[ "$1" == "project" && "$2" == "item-list" ]]; then
case "${BASE_GH_TEST_PROJECT_MODE:-complete}" in
complete)
printf 'Ready\037P2\037M\037CLI\037Agentic Coding Platform\n'
;;
missing)
printf 'Ready\037P2\037\037CLI\037\n'
;;
none)
;;
esac
exit 0
if [[ "$1" == "api" ]]; then
if [[ "${BASE_GH_TEST_FAIL_PROJECT:-0}" == "1" && "$*" == *"projectsV2"* ]]; then
printf 'Project API unavailable\n' >&2
exit 1
fi
if [[ "$*" == *"users/basefoundry"* ]]; then
printf 'Organization\n'
exit 0
fi
if [[ "$*" == *"projectsV2/10/fields"* ]]; then
printf '%s\n' '[{"id":"status-id","name":"Status"},{"id":"priority-id","name":"Priority"},{"id":"size-id","name":"Size"},{"id":"area-id","name":"Area"},{"id":"initiative-id","name":"Initiative"}]'
exit 0
fi
if [[ "$*" == *"projectsV2/10/items"* ]]; then
case "${BASE_GH_TEST_PROJECT_MODE:-complete}" in
complete)
printf '%s\n' '[[{"content":{"number":123,"repository":{"full_name":"basefoundry/base"}},"fields":[{"name":"Status","value":{"name":{"raw":"Ready"}}},{"name":"Priority","value":{"name":{"raw":"P2"}}},{"name":"Size","value":{"name":{"raw":"M"}}},{"name":"Area","value":{"name":{"raw":"CLI"}}},{"name":"Initiative","value":{"name":{"raw":"Agentic Coding Platform"}}}]}]]'
;;
missing)
printf '%s\n' '[[{"content":{"number":123,"repository":{"full_name":"basefoundry/base"}},"fields":[{"name":"Status","value":{"name":{"raw":"Ready"}}},{"name":"Priority","value":{"name":{"raw":"P2"}}},{"name":"Size","value":null},{"name":"Area","value":{"name":{"raw":"CLI"}}},{"name":"Initiative","value":null}]}]]'
;;
none)
printf '%s\n' '[[]]'
;;
esac
exit 0
fi
fi
printf 'unexpected gh args: %s\n' "$*" >&2
exit 99
Expand Down Expand Up @@ -605,6 +620,17 @@ run_gh_subcommand() {
[[ "$output" == *"Assignees: codeforester"* ]]
}

@test "basectl gh issue readiness paginates Project items through the REST API" {
write_issue_readiness_gh_mock
write_complete_issue_readiness_body

run_gh_subcommand issue readiness 123 --repo basefoundry/base --project-owner basefoundry --project-number 10

[ "$status" -eq 0 ]
[[ "$(cat "$TEST_STATE_DIR/gh-args")" == *"api --method GET --paginate --slurp orgs/basefoundry/projectsV2/10/items"* ]]
[[ "$(cat "$TEST_STATE_DIR/gh-args")" != *"project item-list"* ]]
}

@test "basectl gh issue readiness reports partial when Project validation is omitted" {
write_issue_readiness_gh_mock
write_complete_issue_readiness_body
Expand Down Expand Up @@ -656,6 +682,29 @@ run_gh_subcommand() {
[[ "$output" == *"Fix hint: set missing Project fields before assigning implementation work."* ]]
}

@test "basectl gh issue readiness distinguishes a missing Project item" {
write_issue_readiness_gh_mock
write_complete_issue_readiness_body

BASE_GH_TEST_PROJECT_MODE=none \
run_gh_subcommand issue readiness 123 --repo basefoundry/base --project-owner basefoundry --project-number 10

[ "$status" -eq 1 ]
[[ "$output" == *"Project fields: missing Project item, Status, Priority, Size, Area, Initiative"* ]]
}

@test "basectl gh issue readiness reports Project API failures" {
write_issue_readiness_gh_mock
write_complete_issue_readiness_body

BASE_GH_TEST_FAIL_PROJECT=1 \
run_gh_subcommand issue readiness 123 --repo basefoundry/base --project-owner basefoundry --project-number 10

[ "$status" -eq 1 ]
[[ "$output" == *"Project API unavailable"* ]]
[[ "$output" == *"GitHub command failed: gh api --method GET orgs/basefoundry/projectsV2/10/fields -f per_page=100"* ]]
}

@test "basectl gh issue readiness reports GitHub API failures" {
write_issue_readiness_gh_mock
write_complete_issue_readiness_body
Expand Down
22 changes: 16 additions & 6 deletions cli/bash/commands/basectl/tests/inspection-json.bats
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,23 @@ if [[ "$1" == "issue" && "$2" == "view" ]]; then
exit 0
fi
fi
if [[ "$1" == "project" && "$2" == "item-list" ]]; then
if [[ "${BASE_GH_TEST_PROJECT_MISSING:-0}" == "1" ]]; then
printf 'Ready\037P2\037\037CLI\037\n'
else
printf 'Ready\037P2\037M\037CLI\037Contract Hardening\n'
if [[ "$1" == "api" ]]; then
if [[ "$*" == *"users/basefoundry"* ]]; then
printf 'Organization\n'
exit 0
fi
if [[ "$*" == *projectsV2/*/fields* ]]; then
printf '%s\n' '[{"id":"status-id","name":"Status"},{"id":"priority-id","name":"Priority"},{"id":"size-id","name":"Size"},{"id":"area-id","name":"Area"},{"id":"initiative-id","name":"Initiative"}]'
exit 0
fi
if [[ "$*" == *projectsV2/*/items* ]]; then
if [[ "${BASE_GH_TEST_PROJECT_MISSING:-0}" == "1" ]]; then
printf '%s\n' '[[{"content":{"number":123,"repository":{"full_name":"basefoundry/base"}},"fields":[{"name":"Status","value":{"name":{"raw":"Ready"}}},{"name":"Priority","value":{"name":{"raw":"P2"}}},{"name":"Size","value":null},{"name":"Area","value":{"name":{"raw":"CLI"}}},{"name":"Initiative","value":null}]}]]'
else
printf '%s\n' '[[{"content":{"number":123,"repository":{"full_name":"basefoundry/base"}},"fields":[{"name":"Status","value":{"name":{"raw":"Ready"}}},{"name":"Priority","value":{"name":{"raw":"P2"}}},{"name":"Size","value":{"name":{"raw":"M"}}},{"name":"Area","value":{"name":{"raw":"CLI"}}},{"name":"Initiative","value":{"name":{"raw":"Contract Hardening"}}}]}]]'
fi
exit 0
fi
exit 0
fi
printf 'unexpected gh args: %s\n' "$*" >&2
exit 99
Expand Down
Loading