chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:37:57 +08:00
commit e30f8ba47c
533 changed files with 115926 additions and 0 deletions
+389
View File
@@ -0,0 +1,389 @@
package github
import (
"context"
"fmt"
"strconv"
"strings"
ghErrors "github.com/github/github-mcp-server/pkg/errors"
"github.com/shurcooL/githubv4"
)
// resolverFieldsPageSize is the GraphQL ProjectV2 max page size; covers most
// projects in a single round-trip.
const resolverFieldsPageSize = 100
// ResolvedFieldOption is one option on a SINGLE_SELECT project field.
type ResolvedFieldOption struct {
ID string
Name string
}
// ResolvedField is a project field resolved by name; Options is only set when
// DataType == "SINGLE_SELECT".
type ResolvedField struct {
ID string
Name string
DataType string
Options []ResolvedFieldOption
}
// projectFieldsQueryOrg fetches all fields on an org-owned project (paginated).
type projectFieldsQueryOrg struct {
Organization struct {
ProjectV2 struct {
Fields projectFieldsConnection `graphql:"fields(first: $first, after: $after)"`
} `graphql:"projectV2(number: $projectNumber)"`
} `graphql:"organization(login: $owner)"`
}
// projectFieldsQueryUser fetches all fields on a user-owned project (paginated).
type projectFieldsQueryUser struct {
User struct {
ProjectV2 struct {
Fields projectFieldsConnection `graphql:"fields(first: $first, after: $after)"`
} `graphql:"projectV2(number: $projectNumber)"`
} `graphql:"user(login: $owner)"`
}
// projectFieldsConnection is a paginated list of project fields. We select `id`
// to discriminate the union variant and `databaseId` for the numeric ID REST needs.
type projectFieldsConnection struct {
Nodes []struct {
ProjectV2Field struct {
ID githubv4.ID
DatabaseID githubv4.Int `graphql:"databaseId"`
Name githubv4.String
DataType githubv4.String
} `graphql:"... on ProjectV2Field"`
ProjectV2IterationField struct {
ID githubv4.ID
DatabaseID githubv4.Int `graphql:"databaseId"`
Name githubv4.String
DataType githubv4.String
} `graphql:"... on ProjectV2IterationField"`
ProjectV2SingleSelectField struct {
ID githubv4.ID
DatabaseID githubv4.Int `graphql:"databaseId"`
Name githubv4.String
DataType githubv4.String
Options []struct {
ID githubv4.String
Name githubv4.String
}
} `graphql:"... on ProjectV2SingleSelectField"`
}
PageInfo PageInfoFragment
}
// listAllProjectFields fetches every field on a project, paginating as needed.
func listAllProjectFields(ctx context.Context, gqlClient *githubv4.Client, owner, ownerType string, projectNumber int) ([]ResolvedField, error) {
all := []ResolvedField{}
var after *githubv4.String
for {
vars := map[string]any{
"owner": githubv4.String(owner),
"projectNumber": githubv4.Int(int32(projectNumber)), //nolint:gosec // Project numbers are small
"first": githubv4.Int(resolverFieldsPageSize),
"after": (*githubv4.String)(nil),
}
if after != nil {
vars["after"] = after
}
var conn projectFieldsConnection
if ownerType == "org" {
var q projectFieldsQueryOrg
if err := gqlClient.Query(ctx, &q, vars); err != nil {
return nil, fmt.Errorf("failed to list project fields: %w", err)
}
conn = q.Organization.ProjectV2.Fields
} else {
var q projectFieldsQueryUser
if err := gqlClient.Query(ctx, &q, vars); err != nil {
return nil, fmt.Errorf("failed to list project fields: %w", err)
}
conn = q.User.ProjectV2.Fields
}
for _, n := range conn.Nodes {
switch {
case n.ProjectV2SingleSelectField.ID != nil:
opts := make([]ResolvedFieldOption, 0, len(n.ProjectV2SingleSelectField.Options))
for _, o := range n.ProjectV2SingleSelectField.Options {
opts = append(opts, ResolvedFieldOption{ID: string(o.ID), Name: string(o.Name)})
}
all = append(all, ResolvedField{
ID: fmt.Sprintf("%d", n.ProjectV2SingleSelectField.DatabaseID),
Name: string(n.ProjectV2SingleSelectField.Name),
DataType: string(n.ProjectV2SingleSelectField.DataType),
Options: opts,
})
case n.ProjectV2IterationField.ID != nil:
all = append(all, ResolvedField{
ID: fmt.Sprintf("%d", n.ProjectV2IterationField.DatabaseID),
Name: string(n.ProjectV2IterationField.Name),
DataType: string(n.ProjectV2IterationField.DataType),
})
case n.ProjectV2Field.ID != nil:
all = append(all, ResolvedField{
ID: fmt.Sprintf("%d", n.ProjectV2Field.DatabaseID),
Name: string(n.ProjectV2Field.Name),
DataType: string(n.ProjectV2Field.DataType),
})
}
}
if !bool(conn.PageInfo.HasNextPage) {
break
}
end := conn.PageInfo.EndCursor
after = &end
}
return all, nil
}
// resolveProjectFieldByName resolves a field by display name. Returns a
// structured error on not-found, ambiguous, or wrong-data-type (when
// expectedDataType is set) so the agent can self-correct.
func resolveProjectFieldByName(ctx context.Context, gqlClient *githubv4.Client, owner, ownerType string, projectNumber int, fieldName, expectedDataType string) (*ResolvedField, error) {
if fieldName == "" {
return nil, fmt.Errorf("field name must not be empty")
}
all, err := listAllProjectFields(ctx, gqlClient, owner, ownerType, projectNumber)
if err != nil {
return nil, err
}
var matches []ResolvedField
for _, f := range all {
if strings.EqualFold(f.Name, fieldName) {
matches = append(matches, f)
}
}
if len(matches) == 0 {
candidates := make([]any, 0, len(all))
for _, f := range all {
candidates = append(candidates, map[string]any{
"name": f.Name,
"data_type": f.DataType,
})
}
return nil, ghErrors.NewStructuredResolutionError(
"field_not_found",
fieldName,
fmt.Sprintf("no project field named %q on project %s#%d; see candidates for available names", fieldName, owner, projectNumber),
candidates,
)
}
if len(matches) > 1 {
candidates := make([]any, 0, len(matches))
for _, f := range matches {
candidates = append(candidates, map[string]any{
"id": f.ID,
"data_type": f.DataType,
})
}
return nil, ghErrors.NewStructuredResolutionError(
"field_ambiguous",
fieldName,
"multiple fields share this name; pass updated_field.id to disambiguate",
candidates,
)
}
field := matches[0]
if expectedDataType != "" && field.DataType != expectedDataType {
return nil, ghErrors.NewStructuredResolutionError(
"wrong_field_type",
fieldName,
fmt.Sprintf("field %q has data type %q but %q was expected", fieldName, field.DataType, expectedDataType),
[]any{map[string]any{"id": field.ID, "data_type": field.DataType}},
)
}
return &field, nil
}
// resolveSingleSelectOptionByName resolves an option name to its ID on a
// SINGLE_SELECT field. Returns a structured error if not found or ambiguous.
func resolveSingleSelectOptionByName(field *ResolvedField, optionName string) (string, error) {
if field == nil {
return "", fmt.Errorf("field must not be nil")
}
if field.DataType != "SINGLE_SELECT" {
return "", ghErrors.NewStructuredResolutionError(
"wrong_field_type",
field.Name,
fmt.Sprintf("cannot resolve option name on non-SINGLE_SELECT field %q (data type %q)", field.Name, field.DataType),
nil,
)
}
var matchIDs []string
for _, o := range field.Options {
if strings.EqualFold(o.Name, optionName) {
matchIDs = append(matchIDs, o.ID)
}
}
switch len(matchIDs) {
case 0:
candidates := make([]any, 0, len(field.Options))
for _, o := range field.Options {
candidates = append(candidates, map[string]any{"name": o.Name})
}
return "", ghErrors.NewStructuredResolutionError(
"option_not_found",
optionName,
fmt.Sprintf("no option named %q on field %q; see candidates for available options", optionName, field.Name),
candidates,
)
case 1:
return matchIDs[0], nil
default:
candidates := make([]any, 0, len(matchIDs))
for _, id := range matchIDs {
candidates = append(candidates, map[string]any{"id": id})
}
return "", ghErrors.NewStructuredResolutionError(
"option_ambiguous",
optionName,
fmt.Sprintf("multiple options on field %q share the name %q", field.Name, optionName),
candidates,
)
}
}
// resolveProjectItemIDByIssueNumber resolves a (project, issue) pair to the
// project item's full database ID in one GraphQL hop. Returns a structured
// error if the issue is not an item on the project.
func resolveProjectItemIDByIssueNumber(ctx context.Context, gqlClient *githubv4.Client, owner, ownerType string, projectNumber int, issueOwner, issueRepo string, issueNumber int) (int64, error) {
projectID, err := resolveProjectNodeID(ctx, gqlClient, owner, ownerType, projectNumber)
if err != nil {
return 0, err
}
var query struct {
Repository struct {
Issue struct {
ProjectItems struct {
Nodes []struct {
FullDatabaseID githubv4.String `graphql:"fullDatabaseId"`
Project struct {
ID githubv4.ID
}
}
PageInfo PageInfoFragment
} `graphql:"projectItems(first: 50, includeArchived: true)"`
} `graphql:"issue(number: $issueNumber)"`
} `graphql:"repository(owner: $issueOwner, name: $issueRepo)"`
}
vars := map[string]any{
"issueOwner": githubv4.String(issueOwner),
"issueRepo": githubv4.String(issueRepo),
"issueNumber": githubv4.Int(int32(issueNumber)), //nolint:gosec // Issue numbers are small
}
if err := gqlClient.Query(ctx, &query, vars); err != nil {
return 0, fmt.Errorf("failed to resolve project item for %s/%s#%d: %w", issueOwner, issueRepo, issueNumber, err)
}
for _, item := range query.Repository.Issue.ProjectItems.Nodes {
if item.Project.ID == projectID {
itemID, parseErr := parseInt64(string(item.FullDatabaseID))
if parseErr != nil {
return 0, fmt.Errorf("project item ID %q is not an integer: %w", string(item.FullDatabaseID), parseErr)
}
return itemID, nil
}
}
return 0, ghErrors.NewStructuredResolutionError(
"item_not_in_project",
fmt.Sprintf("%s/%s#%d", issueOwner, issueRepo, issueNumber),
"the issue exists but is not an item on the named project; add it first via add_project_item",
nil,
)
}
// resolveItemIDFromIssueArgs reads (item_owner, item_repo, issue_number) from args
// and resolves them to a project item ID. Returns a single friendly error if any input is missing.
func resolveItemIDFromIssueArgs(ctx context.Context, gqlClient *githubv4.Client, owner, ownerType string, projectNumber int, args map[string]any) (int64, error) {
issueOwner, ownerErr := RequiredParam[string](args, "item_owner")
issueRepo, repoErr := RequiredParam[string](args, "item_repo")
issueNumber, numErr := RequiredInt(args, "issue_number")
if ownerErr != nil || repoErr != nil || numErr != nil {
return 0, fmt.Errorf("update_project_item requires either item_id, or item_owner + item_repo + issue_number to resolve the item by issue")
}
return resolveProjectItemIDByIssueNumber(ctx, gqlClient, owner, ownerType, projectNumber, issueOwner, issueRepo, issueNumber)
}
func parseInt64(s string) (int64, error) {
return strconv.ParseInt(s, 10, 64)
}
// resolveFieldNamesToIDs resolves field names to numeric IDs in one GraphQL
// hop. Fails fast with a structured error on any unresolved or ambiguous name.
func resolveFieldNamesToIDs(ctx context.Context, gqlClient *githubv4.Client, owner, ownerType string, projectNumber int, names []string) ([]int64, error) {
if len(names) == 0 {
return nil, nil
}
all, err := listAllProjectFields(ctx, gqlClient, owner, ownerType, projectNumber)
if err != nil {
return nil, err
}
// Build a name -> []ResolvedField map so we can detect duplicates per name.
// Matching is case-insensitive to align with the GraphQL API's behaviour.
byName := make(map[string][]ResolvedField, len(all))
for _, f := range all {
key := strings.ToLower(f.Name)
byName[key] = append(byName[key], f)
}
out := make([]int64, 0, len(names))
for _, name := range names {
matches := byName[strings.ToLower(name)]
switch len(matches) {
case 0:
candidates := make([]any, 0, len(all))
for _, f := range all {
candidates = append(candidates, map[string]any{"name": f.Name, "data_type": f.DataType})
}
return nil, ghErrors.NewStructuredResolutionError(
"field_not_found",
name,
fmt.Sprintf("no project field named %q on project %s#%d", name, owner, projectNumber),
candidates,
)
case 1:
id, parseErr := parseInt64(matches[0].ID)
if parseErr != nil {
return nil, fmt.Errorf("resolved field %q has non-numeric ID %q; pass it via 'fields' instead", name, matches[0].ID)
}
out = append(out, id)
default:
candidates := make([]any, 0, len(matches))
for _, f := range matches {
candidates = append(candidates, map[string]any{"id": f.ID, "data_type": f.DataType})
}
return nil, ghErrors.NewStructuredResolutionError(
"field_ambiguous",
name,
"multiple fields share this name; pass numeric IDs via 'fields' to disambiguate",
candidates,
)
}
}
return out, nil
}