Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Fix list_project_fields JSON unmarshal error for single_select fields#1490

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Draft
Copilot wants to merge2 commits intomain
base:main
Choose a base branch
Loading
fromcopilot/fix-list-project-fields-bug
Draft
Show file tree
Hide file tree
Changes fromall 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
162 changes: 149 additions & 13 deletionspkg/github/projects.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -23,6 +23,73 @@ const (
MaxProjectsPerPage = 50
)

// FlexibleString handles JSON unmarshaling of fields that can be either
// a plain string or an object with "raw" and "html" fields.
// This is needed because the GitHub API returns option names as strings,
// while go-github v79 expects them to be ProjectV2TextContent objects.
type FlexibleString struct {
Raw string `json:"raw,omitempty"`
HTML string `json:"html,omitempty"`
}

// UnmarshalJSON implements custom unmarshaling for FlexibleString
func (f *FlexibleString) UnmarshalJSON(data []byte) error {
// Try to unmarshal as a plain string first
var s string
if err := json.Unmarshal(data, &s); err == nil {
f.Raw = s
f.HTML = s
return nil
}

// If that fails, try to unmarshal as an object
type flexibleStringAlias FlexibleString
var obj flexibleStringAlias
if err := json.Unmarshal(data, &obj); err != nil {
return err
}
*f = FlexibleString(obj)
return nil
}

// ProjectFieldOption represents an option for single_select or iteration fields.
// This is a custom type that handles the flexible name format from the GitHub API.
type ProjectFieldOption struct {
ID string `json:"id,omitempty"`
Name *FlexibleString `json:"name,omitempty"`
Color string `json:"color,omitempty"`
Description *FlexibleString `json:"description,omitempty"`
}

// ProjectFieldIteration represents an iteration within a project field.
type ProjectFieldIteration struct {
ID string `json:"id,omitempty"`
Title *FlexibleString `json:"title,omitempty"`
StartDate string `json:"start_date,omitempty"`
Duration int `json:"duration,omitempty"`
}

// ProjectFieldConfiguration represents the configuration for iteration fields.
type ProjectFieldConfiguration struct {
Duration int `json:"duration,omitempty"`
StartDay int `json:"start_day,omitempty"`
Iterations []*ProjectFieldIteration `json:"iterations,omitempty"`
}

// ProjectField represents a field in a GitHub Project V2.
// This is a custom type that properly handles the options array format from the GitHub API.
type ProjectField struct {
ID int64 `json:"id,omitempty"`
NodeID string `json:"node_id,omitempty"`
Name string `json:"name,omitempty"`
DataType string `json:"data_type,omitempty"`
ProjectURL string `json:"project_url,omitempty"`
Options []*ProjectFieldOption `json:"options,omitempty"`
Configuration *ProjectFieldConfiguration `json:"configuration,omitempty"`
CreatedAt string `json:"created_at,omitempty"`
UpdatedAt string `json:"updated_at,omitempty"`
}

func ListProjects(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
return mcp.NewTool("list_projects",
mcp.WithDescription(t("TOOL_LIST_PROJECTS_DESCRIPTION", `List Projects for a user or organization`)),
Expand DownExpand Up@@ -253,19 +320,22 @@ func ListProjectFields(getClient GetClientFn, t translations.TranslationHelperFu
return mcp.NewToolResultError(err.Error()), nil
}

var resp *github.Response
var projectFields []*github.ProjectV2Field
// Build the URL for the API request
var urlPath string
if ownerType == "org" {
urlPath = fmt.Sprintf("orgs/%s/projectsV2/%d/fields", owner, projectNumber)
} else {
urlPath = fmt.Sprintf("users/%s/projectsV2/%d/fields", owner, projectNumber)
}

// Create options for the request
opts := &github.ListProjectsOptions{
ListProjectsPaginationOptions: pagination,
}

if ownerType == "org" {
projectFields, resp, err = client.Projects.ListOrganizationProjectFields(ctx, owner, projectNumber, opts)
} else {
projectFields, resp, err = client.Projects.ListUserProjectFields(ctx, owner, projectNumber, opts)
}

// Make the raw API request using go-github's client
// We use our custom ProjectField type which handles flexible name format
projectFields, resp, err := listProjectFieldsRaw(ctx, client, urlPath, opts)
if err != nil {
return ghErrors.NewGitHubAPIErrorResponse(ctx,
"failed to list project fields",
Expand All@@ -289,6 +359,70 @@ func ListProjectFields(getClient GetClientFn, t translations.TranslationHelperFu
}
}

// listProjectFieldsRaw makes a raw API request to list project fields and parses
// the response using our custom ProjectField type that handles flexible name formats.
func listProjectFieldsRaw(ctx context.Context, client *github.Client, urlPath string, opts *github.ListProjectsOptions) ([]*ProjectField, *github.Response, error) {
u, err := addProjectOptions(urlPath, opts)
if err != nil {
return nil, nil, err
}

req, err := client.NewRequest("GET", u, nil)
if err != nil {
return nil, nil, err
}

var fields []*ProjectField
resp, err := client.Do(ctx, req, &fields)
if err != nil {
return nil, resp, err
}
return fields, resp, nil
}

// addProjectOptions adds query parameters to a URL for project API requests.
func addProjectOptions(s string, opts *github.ListProjectsOptions) (string, error) {
if opts == nil {
return s, nil
}

// Build query parameters manually
params := make([]string, 0)
if opts.PerPage != nil && *opts.PerPage > 0 {
params = append(params, fmt.Sprintf("per_page=%d", *opts.PerPage))
}
if opts.After != nil && *opts.After != "" {
params = append(params, fmt.Sprintf("after=%s", *opts.After))
}
if opts.Before != nil && *opts.Before != "" {
params = append(params, fmt.Sprintf("before=%s", *opts.Before))
}
if opts.Query != nil && *opts.Query != "" {
params = append(params, fmt.Sprintf("q=%s", *opts.Query))
}

if len(params) > 0 {
s = s + "?" + strings.Join(params, "&")
}
return s, nil
}

// getProjectFieldRaw makes a raw API request to get a single project field and parses
// the response using our custom ProjectField type that handles flexible name formats.
func getProjectFieldRaw(ctx context.Context, client *github.Client, urlPath string) (*ProjectField, *github.Response, error) {
req, err := client.NewRequest("GET", urlPath, nil)
if err != nil {
return nil, nil, err
}

var field ProjectField
resp, err := client.Do(ctx, req, &field)
if err != nil {
return nil, resp, err
}
return &field, resp, nil
}

func GetProjectField(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
return mcp.NewTool("get_project_field",
mcp.WithDescription(t("TOOL_GET_PROJECT_FIELD_DESCRIPTION", "Get Project field for a user or org")),
Expand DownExpand Up@@ -332,15 +466,17 @@ func GetProjectField(getClient GetClientFn, t translations.TranslationHelperFunc
return mcp.NewToolResultError(err.Error()), nil
}

var resp *github.Response
var projectField *github.ProjectV2Field

// Build the URL for the API request
var urlPath string
if ownerType == "org" {
projectField, resp, err = client.Projects.GetOrganizationProjectField(ctx, owner, projectNumber, fieldID)
urlPath = fmt.Sprintf("orgs/%s/projectsV2/%d/fields/%d", owner, projectNumber, fieldID)
} else {
projectField, resp, err = client.Projects.GetUserProjectField(ctx, owner, projectNumber, fieldID)
urlPath = fmt.Sprintf("users/%s/projectsV2/%d/fields/%d", owner, projectNumber, fieldID)
}

// Make the raw API request using go-github's client
// We use our custom ProjectField type which handles flexible name format
projectField, resp, err := getProjectFieldRaw(ctx, client, urlPath)
if err != nil {
return ghErrors.NewGitHubAPIErrorResponse(ctx,
"failed to get project field",
Expand Down
59 changes: 59 additions & 0 deletionspkg/github/projects_test.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -320,6 +320,29 @@ func Test_ListProjectFields(t *testing.T) {
orgFields := []map[string]any{{"id": 101, "name": "Status", "data_type": "single_select"}}
userFields := []map[string]any{{"id": 201, "name": "Priority", "data_type": "single_select"}}

// Test data with single_select options using string names (as GitHub API returns)
fieldsWithStringOptions := []map[string]any{{
"id": 102,
"name": "Status",
"data_type": "single_select",
"options": []map[string]any{
{"id": "aeba538c", "name": "Backlog", "color": "GREEN"},
{"id": "f75ad846", "name": "Ready", "color": "YELLOW"},
{"id": "47fc9ee4", "name": "In Progress", "color": "ORANGE"},
},
}}

// Test data with single_select options using object names (alternative format)
fieldsWithObjectOptions := []map[string]any{{
"id": 103,
"name": "Priority",
"data_type": "single_select",
"options": []map[string]any{
{"id": "opt1", "name": map[string]string{"raw": "High", "html": "High"}, "color": "RED"},
{"id": "opt2", "name": map[string]string{"raw": "Low", "html": "Low"}, "color": "GREEN"},
},
}}

tests := []struct {
name string
mockedClient *http.Client
Expand All@@ -346,6 +369,42 @@ func Test_ListProjectFields(t *testing.T) {
},
expectedLength: 1,
},
{
name: "success with single_select options using string names",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
mock.EndpointPattern{Pattern: "/orgs/{org}/projectsV2/{project}/fields", Method: http.MethodGet},
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write(mock.MustMarshal(fieldsWithStringOptions))
}),
),
),
requestArgs: map[string]interface{}{
"owner": "octo-org",
"owner_type": "org",
"project_number": float64(124),
},
expectedLength: 1,
},
{
name: "success with single_select options using object names",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
mock.EndpointPattern{Pattern: "/orgs/{org}/projectsV2/{project}/fields", Method: http.MethodGet},
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write(mock.MustMarshal(fieldsWithObjectOptions))
}),
),
),
requestArgs: map[string]interface{}{
"owner": "octo-org",
"owner_type": "org",
"project_number": float64(125),
},
expectedLength: 1,
},
{
name: "success user fields with per_page override",
mockedClient: mock.NewMockedHTTPClient(
Expand Down

[8]ページ先頭

©2009-2025 Movatter.jp