From 86c24ba6c95502403f4175938e02a1ec8390dd58 Mon Sep 17 00:00:00 2001 From: JamBalaya56562 Date: Wed, 24 Jun 2026 00:43:42 +0000 Subject: [PATCH 01/10] feat: Add remaining Projects v2 endpoints Add the eight Projects v2 REST endpoints that were present in the OpenAPI description but not yet implemented, completing the ProjectsService surface: - Create draft item (organization and user owned projects) - Add field (organization and user owned projects) - Create view (organization and user owned projects) - List items for a project view (organization and user owned projects) Drafts and view items reuse ProjectV2Item, fields reuse ProjectV2Field, and a new ProjectV2View type plus request types for the create/add operations are added. Request bodies are passed by value with a Request suffix per the paramcheck convention. Note the user-owned draft and view endpoints identify the user by the numeric user_id in the path (/user/{user_id}/... and /users/{user_id}/...), mirroring the upstream REST API, unlike the username-based paths used by the other user endpoints. Fixes #3715 --- github/github-accessors.go | 296 +++++++++++++++++++++++ github/github-accessors_test.go | 401 ++++++++++++++++++++++++++++++++ github/github-iterators.go | 62 +++++ github/github-iterators_test.go | 144 ++++++++++++ github/projects.go | 329 ++++++++++++++++++++++++++ github/projects_test.go | 359 ++++++++++++++++++++++++++++ 6 files changed, 1591 insertions(+) diff --git a/github/github-accessors.go b/github/github-accessors.go index 0a6cc1e6b53..74b2cb556e5 100644 --- a/github/github-accessors.go +++ b/github/github-accessors.go @@ -590,6 +590,46 @@ func (a *AddProjectItemOptions) GetType() *ProjectV2ItemContentType { return a.Type } +// GetDataType returns the DataType field if it's non-nil, zero value otherwise. +func (a *AddProjectV2FieldRequest) GetDataType() string { + if a == nil || a.DataType == nil { + return "" + } + return *a.DataType +} + +// GetIssueFieldID returns the IssueFieldID field if it's non-nil, zero value otherwise. +func (a *AddProjectV2FieldRequest) GetIssueFieldID() int64 { + if a == nil || a.IssueFieldID == nil { + return 0 + } + return *a.IssueFieldID +} + +// GetIterationConfiguration returns the IterationConfiguration field. +func (a *AddProjectV2FieldRequest) GetIterationConfiguration() *ProjectV2FieldIterationConfiguration { + if a == nil { + return nil + } + return a.IterationConfiguration +} + +// GetName returns the Name field if it's non-nil, zero value otherwise. +func (a *AddProjectV2FieldRequest) GetName() string { + if a == nil || a.Name == nil { + return "" + } + return *a.Name +} + +// GetSingleSelectOptions returns the SingleSelectOptions slice if it's non-nil, nil otherwise. +func (a *AddProjectV2FieldRequest) GetSingleSelectOptions() []*ProjectV2FieldSingleSelectOption { + if a == nil || a.SingleSelectOptions == nil { + return nil + } + return a.SingleSelectOptions +} + // GetMessage returns the Message field if it's non-nil, zero value otherwise. func (a *AddResourcesToCostCenterResponse) GetMessage() string { if a == nil || a.Message == nil { @@ -11078,6 +11118,54 @@ func (c *CreateOrUpdateIssueTypesOptions) GetName() string { return c.Name } +// GetBody returns the Body field if it's non-nil, zero value otherwise. +func (c *CreateProjectV2DraftItemRequest) GetBody() string { + if c == nil || c.Body == nil { + return "" + } + return *c.Body +} + +// GetTitle returns the Title field if it's non-nil, zero value otherwise. +func (c *CreateProjectV2DraftItemRequest) GetTitle() string { + if c == nil || c.Title == nil { + return "" + } + return *c.Title +} + +// GetFilter returns the Filter field if it's non-nil, zero value otherwise. +func (c *CreateProjectV2ViewRequest) GetFilter() string { + if c == nil || c.Filter == nil { + return "" + } + return *c.Filter +} + +// GetLayout returns the Layout field if it's non-nil, zero value otherwise. +func (c *CreateProjectV2ViewRequest) GetLayout() string { + if c == nil || c.Layout == nil { + return "" + } + return *c.Layout +} + +// GetName returns the Name field if it's non-nil, zero value otherwise. +func (c *CreateProjectV2ViewRequest) GetName() string { + if c == nil || c.Name == nil { + return "" + } + return *c.Name +} + +// GetVisibleFields returns the VisibleFields slice if it's non-nil, nil otherwise. +func (c *CreateProjectV2ViewRequest) GetVisibleFields() []int64 { + if c == nil || c.VisibleFields == nil { + return nil + } + return c.VisibleFields +} + // GetFrom returns the From field if it's non-nil, zero value otherwise. func (c *CreateProtectedChanges) GetFrom() bool { if c == nil || c.From == nil { @@ -28870,6 +28958,54 @@ func (p *ProjectV2FieldIteration) GetTitle() *ProjectV2TextContent { return p.Title } +// GetDuration returns the Duration field if it's non-nil, zero value otherwise. +func (p *ProjectV2FieldIterationConfiguration) GetDuration() int { + if p == nil || p.Duration == nil { + return 0 + } + return *p.Duration +} + +// GetIterations returns the Iterations slice if it's non-nil, nil otherwise. +func (p *ProjectV2FieldIterationConfiguration) GetIterations() []*ProjectV2FieldIterationConfigurationIteration { + if p == nil || p.Iterations == nil { + return nil + } + return p.Iterations +} + +// GetStartDate returns the StartDate field if it's non-nil, zero value otherwise. +func (p *ProjectV2FieldIterationConfiguration) GetStartDate() string { + if p == nil || p.StartDate == nil { + return "" + } + return *p.StartDate +} + +// GetDuration returns the Duration field if it's non-nil, zero value otherwise. +func (p *ProjectV2FieldIterationConfigurationIteration) GetDuration() int { + if p == nil || p.Duration == nil { + return 0 + } + return *p.Duration +} + +// GetStartDate returns the StartDate field if it's non-nil, zero value otherwise. +func (p *ProjectV2FieldIterationConfigurationIteration) GetStartDate() string { + if p == nil || p.StartDate == nil { + return "" + } + return *p.StartDate +} + +// GetTitle returns the Title field if it's non-nil, zero value otherwise. +func (p *ProjectV2FieldIterationConfigurationIteration) GetTitle() string { + if p == nil || p.Title == nil { + return "" + } + return *p.Title +} + // GetColor returns the Color field if it's non-nil, zero value otherwise. func (p *ProjectV2FieldOption) GetColor() string { if p == nil || p.Color == nil { @@ -28902,6 +29038,30 @@ func (p *ProjectV2FieldOption) GetName() *ProjectV2TextContent { return p.Name } +// GetColor returns the Color field if it's non-nil, zero value otherwise. +func (p *ProjectV2FieldSingleSelectOption) GetColor() string { + if p == nil || p.Color == nil { + return "" + } + return *p.Color +} + +// GetDescription returns the Description field if it's non-nil, zero value otherwise. +func (p *ProjectV2FieldSingleSelectOption) GetDescription() string { + if p == nil || p.Description == nil { + return "" + } + return *p.Description +} + +// GetName returns the Name field if it's non-nil, zero value otherwise. +func (p *ProjectV2FieldSingleSelectOption) GetName() string { + if p == nil || p.Name == nil { + return "" + } + return *p.Name +} + // GetArchivedAt returns the ArchivedAt field if it's non-nil, zero value otherwise. func (p *ProjectV2Item) GetArchivedAt() Timestamp { if p == nil || p.ArchivedAt == nil { @@ -29222,6 +29382,142 @@ func (p *ProjectV2TextContent) GetRaw() string { return *p.Raw } +// GetCreatedAt returns the CreatedAt field if it's non-nil, zero value otherwise. +func (p *ProjectV2View) GetCreatedAt() Timestamp { + if p == nil || p.CreatedAt == nil { + return Timestamp{} + } + return *p.CreatedAt +} + +// GetCreator returns the Creator field. +func (p *ProjectV2View) GetCreator() *User { + if p == nil { + return nil + } + return p.Creator +} + +// GetFilter returns the Filter field if it's non-nil, zero value otherwise. +func (p *ProjectV2View) GetFilter() string { + if p == nil || p.Filter == nil { + return "" + } + return *p.Filter +} + +// GetGroupBy returns the GroupBy slice if it's non-nil, nil otherwise. +func (p *ProjectV2View) GetGroupBy() []int64 { + if p == nil || p.GroupBy == nil { + return nil + } + return p.GroupBy +} + +// GetHTMLURL returns the HTMLURL field if it's non-nil, zero value otherwise. +func (p *ProjectV2View) GetHTMLURL() string { + if p == nil || p.HTMLURL == nil { + return "" + } + return *p.HTMLURL +} + +// GetID returns the ID field if it's non-nil, zero value otherwise. +func (p *ProjectV2View) GetID() int64 { + if p == nil || p.ID == nil { + return 0 + } + return *p.ID +} + +// GetLayout returns the Layout field if it's non-nil, zero value otherwise. +func (p *ProjectV2View) GetLayout() string { + if p == nil || p.Layout == nil { + return "" + } + return *p.Layout +} + +// GetName returns the Name field if it's non-nil, zero value otherwise. +func (p *ProjectV2View) GetName() string { + if p == nil || p.Name == nil { + return "" + } + return *p.Name +} + +// GetNodeID returns the NodeID field if it's non-nil, zero value otherwise. +func (p *ProjectV2View) GetNodeID() string { + if p == nil || p.NodeID == nil { + return "" + } + return *p.NodeID +} + +// GetNumber returns the Number field if it's non-nil, zero value otherwise. +func (p *ProjectV2View) GetNumber() int { + if p == nil || p.Number == nil { + return 0 + } + return *p.Number +} + +// GetProjectURL returns the ProjectURL field if it's non-nil, zero value otherwise. +func (p *ProjectV2View) GetProjectURL() string { + if p == nil || p.ProjectURL == nil { + return "" + } + return *p.ProjectURL +} + +// GetSortBy returns the SortBy slice if it's non-nil, nil otherwise. +func (p *ProjectV2View) GetSortBy() []*ProjectV2ViewSortBy { + if p == nil || p.SortBy == nil { + return nil + } + return p.SortBy +} + +// GetUpdatedAt returns the UpdatedAt field if it's non-nil, zero value otherwise. +func (p *ProjectV2View) GetUpdatedAt() Timestamp { + if p == nil || p.UpdatedAt == nil { + return Timestamp{} + } + return *p.UpdatedAt +} + +// GetVerticalGroupBy returns the VerticalGroupBy slice if it's non-nil, nil otherwise. +func (p *ProjectV2View) GetVerticalGroupBy() []int64 { + if p == nil || p.VerticalGroupBy == nil { + return nil + } + return p.VerticalGroupBy +} + +// GetVisibleFields returns the VisibleFields slice if it's non-nil, nil otherwise. +func (p *ProjectV2View) GetVisibleFields() []int64 { + if p == nil || p.VisibleFields == nil { + return nil + } + return p.VisibleFields +} + +// GetDirection returns the Direction field if it's non-nil, zero value otherwise. +func (p *ProjectV2ViewSortBy) GetDirection() string { + if p == nil || p.Direction == nil { + return "" + } + return *p.Direction +} + +// GetFieldID returns the FieldID field if it's non-nil, zero value otherwise. +func (p *ProjectV2ViewSortBy) GetFieldID() int64 { + if p == nil || p.FieldID == nil { + return 0 + } + return *p.FieldID +} + // GetAllowDeletions returns the AllowDeletions field. func (p *Protection) GetAllowDeletions() *AllowDeletions { if p == nil { diff --git a/github/github-accessors_test.go b/github/github-accessors_test.go index 7433e4499e8..bfcc36203dc 100644 --- a/github/github-accessors_test.go +++ b/github/github-accessors_test.go @@ -747,6 +747,58 @@ func TestAddProjectItemOptions_GetType(tt *testing.T) { a.GetType() } +func TestAddProjectV2FieldRequest_GetDataType(tt *testing.T) { + tt.Parallel() + var zeroValue string + a := &AddProjectV2FieldRequest{DataType: &zeroValue} + a.GetDataType() + a = &AddProjectV2FieldRequest{} + a.GetDataType() + a = nil + a.GetDataType() +} + +func TestAddProjectV2FieldRequest_GetIssueFieldID(tt *testing.T) { + tt.Parallel() + var zeroValue int64 + a := &AddProjectV2FieldRequest{IssueFieldID: &zeroValue} + a.GetIssueFieldID() + a = &AddProjectV2FieldRequest{} + a.GetIssueFieldID() + a = nil + a.GetIssueFieldID() +} + +func TestAddProjectV2FieldRequest_GetIterationConfiguration(tt *testing.T) { + tt.Parallel() + a := &AddProjectV2FieldRequest{} + a.GetIterationConfiguration() + a = nil + a.GetIterationConfiguration() +} + +func TestAddProjectV2FieldRequest_GetName(tt *testing.T) { + tt.Parallel() + var zeroValue string + a := &AddProjectV2FieldRequest{Name: &zeroValue} + a.GetName() + a = &AddProjectV2FieldRequest{} + a.GetName() + a = nil + a.GetName() +} + +func TestAddProjectV2FieldRequest_GetSingleSelectOptions(tt *testing.T) { + tt.Parallel() + zeroValue := []*ProjectV2FieldSingleSelectOption{} + a := &AddProjectV2FieldRequest{SingleSelectOptions: zeroValue} + a.GetSingleSelectOptions() + a = &AddProjectV2FieldRequest{} + a.GetSingleSelectOptions() + a = nil + a.GetSingleSelectOptions() +} + func TestAddResourcesToCostCenterResponse_GetMessage(tt *testing.T) { tt.Parallel() var zeroValue string @@ -14091,6 +14143,72 @@ func TestCreateOrUpdateIssueTypesOptions_GetName(tt *testing.T) { c.GetName() } +func TestCreateProjectV2DraftItemRequest_GetBody(tt *testing.T) { + tt.Parallel() + var zeroValue string + c := &CreateProjectV2DraftItemRequest{Body: &zeroValue} + c.GetBody() + c = &CreateProjectV2DraftItemRequest{} + c.GetBody() + c = nil + c.GetBody() +} + +func TestCreateProjectV2DraftItemRequest_GetTitle(tt *testing.T) { + tt.Parallel() + var zeroValue string + c := &CreateProjectV2DraftItemRequest{Title: &zeroValue} + c.GetTitle() + c = &CreateProjectV2DraftItemRequest{} + c.GetTitle() + c = nil + c.GetTitle() +} + +func TestCreateProjectV2ViewRequest_GetFilter(tt *testing.T) { + tt.Parallel() + var zeroValue string + c := &CreateProjectV2ViewRequest{Filter: &zeroValue} + c.GetFilter() + c = &CreateProjectV2ViewRequest{} + c.GetFilter() + c = nil + c.GetFilter() +} + +func TestCreateProjectV2ViewRequest_GetLayout(tt *testing.T) { + tt.Parallel() + var zeroValue string + c := &CreateProjectV2ViewRequest{Layout: &zeroValue} + c.GetLayout() + c = &CreateProjectV2ViewRequest{} + c.GetLayout() + c = nil + c.GetLayout() +} + +func TestCreateProjectV2ViewRequest_GetName(tt *testing.T) { + tt.Parallel() + var zeroValue string + c := &CreateProjectV2ViewRequest{Name: &zeroValue} + c.GetName() + c = &CreateProjectV2ViewRequest{} + c.GetName() + c = nil + c.GetName() +} + +func TestCreateProjectV2ViewRequest_GetVisibleFields(tt *testing.T) { + tt.Parallel() + zeroValue := []int64{} + c := &CreateProjectV2ViewRequest{VisibleFields: zeroValue} + c.GetVisibleFields() + c = &CreateProjectV2ViewRequest{} + c.GetVisibleFields() + c = nil + c.GetVisibleFields() +} + func TestCreateProtectedChanges_GetFrom(tt *testing.T) { tt.Parallel() var zeroValue bool @@ -36401,6 +36519,72 @@ func TestProjectV2FieldIteration_GetTitle(tt *testing.T) { p.GetTitle() } +func TestProjectV2FieldIterationConfiguration_GetDuration(tt *testing.T) { + tt.Parallel() + var zeroValue int + p := &ProjectV2FieldIterationConfiguration{Duration: &zeroValue} + p.GetDuration() + p = &ProjectV2FieldIterationConfiguration{} + p.GetDuration() + p = nil + p.GetDuration() +} + +func TestProjectV2FieldIterationConfiguration_GetIterations(tt *testing.T) { + tt.Parallel() + zeroValue := []*ProjectV2FieldIterationConfigurationIteration{} + p := &ProjectV2FieldIterationConfiguration{Iterations: zeroValue} + p.GetIterations() + p = &ProjectV2FieldIterationConfiguration{} + p.GetIterations() + p = nil + p.GetIterations() +} + +func TestProjectV2FieldIterationConfiguration_GetStartDate(tt *testing.T) { + tt.Parallel() + var zeroValue string + p := &ProjectV2FieldIterationConfiguration{StartDate: &zeroValue} + p.GetStartDate() + p = &ProjectV2FieldIterationConfiguration{} + p.GetStartDate() + p = nil + p.GetStartDate() +} + +func TestProjectV2FieldIterationConfigurationIteration_GetDuration(tt *testing.T) { + tt.Parallel() + var zeroValue int + p := &ProjectV2FieldIterationConfigurationIteration{Duration: &zeroValue} + p.GetDuration() + p = &ProjectV2FieldIterationConfigurationIteration{} + p.GetDuration() + p = nil + p.GetDuration() +} + +func TestProjectV2FieldIterationConfigurationIteration_GetStartDate(tt *testing.T) { + tt.Parallel() + var zeroValue string + p := &ProjectV2FieldIterationConfigurationIteration{StartDate: &zeroValue} + p.GetStartDate() + p = &ProjectV2FieldIterationConfigurationIteration{} + p.GetStartDate() + p = nil + p.GetStartDate() +} + +func TestProjectV2FieldIterationConfigurationIteration_GetTitle(tt *testing.T) { + tt.Parallel() + var zeroValue string + p := &ProjectV2FieldIterationConfigurationIteration{Title: &zeroValue} + p.GetTitle() + p = &ProjectV2FieldIterationConfigurationIteration{} + p.GetTitle() + p = nil + p.GetTitle() +} + func TestProjectV2FieldOption_GetColor(tt *testing.T) { tt.Parallel() var zeroValue string @@ -36439,6 +36623,39 @@ func TestProjectV2FieldOption_GetName(tt *testing.T) { p.GetName() } +func TestProjectV2FieldSingleSelectOption_GetColor(tt *testing.T) { + tt.Parallel() + var zeroValue string + p := &ProjectV2FieldSingleSelectOption{Color: &zeroValue} + p.GetColor() + p = &ProjectV2FieldSingleSelectOption{} + p.GetColor() + p = nil + p.GetColor() +} + +func TestProjectV2FieldSingleSelectOption_GetDescription(tt *testing.T) { + tt.Parallel() + var zeroValue string + p := &ProjectV2FieldSingleSelectOption{Description: &zeroValue} + p.GetDescription() + p = &ProjectV2FieldSingleSelectOption{} + p.GetDescription() + p = nil + p.GetDescription() +} + +func TestProjectV2FieldSingleSelectOption_GetName(tt *testing.T) { + tt.Parallel() + var zeroValue string + p := &ProjectV2FieldSingleSelectOption{Name: &zeroValue} + p.GetName() + p = &ProjectV2FieldSingleSelectOption{} + p.GetName() + p = nil + p.GetName() +} + func TestProjectV2Item_GetArchivedAt(tt *testing.T) { tt.Parallel() var zeroValue Timestamp @@ -36834,6 +37051,190 @@ func TestProjectV2TextContent_GetRaw(tt *testing.T) { p.GetRaw() } +func TestProjectV2View_GetCreatedAt(tt *testing.T) { + tt.Parallel() + var zeroValue Timestamp + p := &ProjectV2View{CreatedAt: &zeroValue} + p.GetCreatedAt() + p = &ProjectV2View{} + p.GetCreatedAt() + p = nil + p.GetCreatedAt() +} + +func TestProjectV2View_GetCreator(tt *testing.T) { + tt.Parallel() + p := &ProjectV2View{} + p.GetCreator() + p = nil + p.GetCreator() +} + +func TestProjectV2View_GetFilter(tt *testing.T) { + tt.Parallel() + var zeroValue string + p := &ProjectV2View{Filter: &zeroValue} + p.GetFilter() + p = &ProjectV2View{} + p.GetFilter() + p = nil + p.GetFilter() +} + +func TestProjectV2View_GetGroupBy(tt *testing.T) { + tt.Parallel() + zeroValue := []int64{} + p := &ProjectV2View{GroupBy: zeroValue} + p.GetGroupBy() + p = &ProjectV2View{} + p.GetGroupBy() + p = nil + p.GetGroupBy() +} + +func TestProjectV2View_GetHTMLURL(tt *testing.T) { + tt.Parallel() + var zeroValue string + p := &ProjectV2View{HTMLURL: &zeroValue} + p.GetHTMLURL() + p = &ProjectV2View{} + p.GetHTMLURL() + p = nil + p.GetHTMLURL() +} + +func TestProjectV2View_GetID(tt *testing.T) { + tt.Parallel() + var zeroValue int64 + p := &ProjectV2View{ID: &zeroValue} + p.GetID() + p = &ProjectV2View{} + p.GetID() + p = nil + p.GetID() +} + +func TestProjectV2View_GetLayout(tt *testing.T) { + tt.Parallel() + var zeroValue string + p := &ProjectV2View{Layout: &zeroValue} + p.GetLayout() + p = &ProjectV2View{} + p.GetLayout() + p = nil + p.GetLayout() +} + +func TestProjectV2View_GetName(tt *testing.T) { + tt.Parallel() + var zeroValue string + p := &ProjectV2View{Name: &zeroValue} + p.GetName() + p = &ProjectV2View{} + p.GetName() + p = nil + p.GetName() +} + +func TestProjectV2View_GetNodeID(tt *testing.T) { + tt.Parallel() + var zeroValue string + p := &ProjectV2View{NodeID: &zeroValue} + p.GetNodeID() + p = &ProjectV2View{} + p.GetNodeID() + p = nil + p.GetNodeID() +} + +func TestProjectV2View_GetNumber(tt *testing.T) { + tt.Parallel() + var zeroValue int + p := &ProjectV2View{Number: &zeroValue} + p.GetNumber() + p = &ProjectV2View{} + p.GetNumber() + p = nil + p.GetNumber() +} + +func TestProjectV2View_GetProjectURL(tt *testing.T) { + tt.Parallel() + var zeroValue string + p := &ProjectV2View{ProjectURL: &zeroValue} + p.GetProjectURL() + p = &ProjectV2View{} + p.GetProjectURL() + p = nil + p.GetProjectURL() +} + +func TestProjectV2View_GetSortBy(tt *testing.T) { + tt.Parallel() + zeroValue := []*ProjectV2ViewSortBy{} + p := &ProjectV2View{SortBy: zeroValue} + p.GetSortBy() + p = &ProjectV2View{} + p.GetSortBy() + p = nil + p.GetSortBy() +} + +func TestProjectV2View_GetUpdatedAt(tt *testing.T) { + tt.Parallel() + var zeroValue Timestamp + p := &ProjectV2View{UpdatedAt: &zeroValue} + p.GetUpdatedAt() + p = &ProjectV2View{} + p.GetUpdatedAt() + p = nil + p.GetUpdatedAt() +} + +func TestProjectV2View_GetVerticalGroupBy(tt *testing.T) { + tt.Parallel() + zeroValue := []int64{} + p := &ProjectV2View{VerticalGroupBy: zeroValue} + p.GetVerticalGroupBy() + p = &ProjectV2View{} + p.GetVerticalGroupBy() + p = nil + p.GetVerticalGroupBy() +} + +func TestProjectV2View_GetVisibleFields(tt *testing.T) { + tt.Parallel() + zeroValue := []int64{} + p := &ProjectV2View{VisibleFields: zeroValue} + p.GetVisibleFields() + p = &ProjectV2View{} + p.GetVisibleFields() + p = nil + p.GetVisibleFields() +} + +func TestProjectV2ViewSortBy_GetDirection(tt *testing.T) { + tt.Parallel() + var zeroValue string + p := &ProjectV2ViewSortBy{Direction: &zeroValue} + p.GetDirection() + p = &ProjectV2ViewSortBy{} + p.GetDirection() + p = nil + p.GetDirection() +} + +func TestProjectV2ViewSortBy_GetFieldID(tt *testing.T) { + tt.Parallel() + var zeroValue int64 + p := &ProjectV2ViewSortBy{FieldID: &zeroValue} + p.GetFieldID() + p = &ProjectV2ViewSortBy{} + p.GetFieldID() + p = nil + p.GetFieldID() +} + func TestProtection_GetAllowDeletions(tt *testing.T) { tt.Parallel() p := &Protection{} diff --git a/github/github-iterators.go b/github/github-iterators.go index 7d0a13f0919..e59597afd90 100644 --- a/github/github-iterators.go +++ b/github/github-iterators.go @@ -4921,6 +4921,37 @@ func (s *ProjectsService) ListOrganizationProjectItemsIter(ctx context.Context, } } +// ListOrganizationProjectViewItemsIter returns an iterator that paginates through all results of ListOrganizationProjectViewItems. +func (s *ProjectsService) ListOrganizationProjectViewItemsIter(ctx context.Context, org string, projectNumber int, viewNumber int, opts *ListProjectItemsOptions) iter.Seq2[*ProjectV2Item, error] { + return func(yield func(*ProjectV2Item, error) bool) { + // Create a copy of opts to avoid mutating the caller's struct + if opts == nil { + opts = &ListProjectItemsOptions{} + } else { + opts = Ptr(*opts) + } + + for { + results, resp, err := s.ListOrganizationProjectViewItems(ctx, org, projectNumber, viewNumber, opts) + if err != nil { + yield(nil, err) + return + } + + for _, item := range results { + if !yield(item, nil) { + return + } + } + + if resp.After == "" { + break + } + opts.After = resp.After + } + } +} + // ListOrganizationProjectsIter returns an iterator that paginates through all results of ListOrganizationProjects. func (s *ProjectsService) ListOrganizationProjectsIter(ctx context.Context, org string, opts *ListProjectsOptions) iter.Seq2[*ProjectV2, error] { return func(yield func(*ProjectV2, error) bool) { @@ -5014,6 +5045,37 @@ func (s *ProjectsService) ListUserProjectItemsIter(ctx context.Context, username } } +// ListUserProjectViewItemsIter returns an iterator that paginates through all results of ListUserProjectViewItems. +func (s *ProjectsService) ListUserProjectViewItemsIter(ctx context.Context, username string, projectNumber int, viewNumber int, opts *ListProjectItemsOptions) iter.Seq2[*ProjectV2Item, error] { + return func(yield func(*ProjectV2Item, error) bool) { + // Create a copy of opts to avoid mutating the caller's struct + if opts == nil { + opts = &ListProjectItemsOptions{} + } else { + opts = Ptr(*opts) + } + + for { + results, resp, err := s.ListUserProjectViewItems(ctx, username, projectNumber, viewNumber, opts) + if err != nil { + yield(nil, err) + return + } + + for _, item := range results { + if !yield(item, nil) { + return + } + } + + if resp.After == "" { + break + } + opts.After = resp.After + } + } +} + // ListUserProjectsIter returns an iterator that paginates through all results of ListUserProjects. func (s *ProjectsService) ListUserProjectsIter(ctx context.Context, username string, opts *ListProjectsOptions) iter.Seq2[*ProjectV2, error] { return func(yield func(*ProjectV2, error) bool) { diff --git a/github/github-iterators_test.go b/github/github-iterators_test.go index 071516b9264..abee7d43d42 100644 --- a/github/github-iterators_test.go +++ b/github/github-iterators_test.go @@ -10815,6 +10815,78 @@ func TestProjectsService_ListOrganizationProjectItemsIter(t *testing.T) { } } +func TestProjectsService_ListOrganizationProjectViewItemsIter(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + var callNum int + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + callNum++ + switch callNum { + case 1: + w.Header().Set("Link", `; rel="next"`) + fmt.Fprint(w, `[{},{},{}]`) + case 2: + fmt.Fprint(w, `[{},{},{},{}]`) + case 3: + fmt.Fprint(w, `[{},{}]`) + case 4: + w.WriteHeader(http.StatusNotFound) + case 5: + fmt.Fprint(w, `[{},{}]`) + } + }) + + iter := client.Projects.ListOrganizationProjectViewItemsIter(t.Context(), "", 0, 0, nil) + var gotItems int + for _, err := range iter { + gotItems++ + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + } + if want := 7; gotItems != want { + t.Errorf("client.Projects.ListOrganizationProjectViewItemsIter call 1 got %v items; want %v", gotItems, want) + } + + opts := &ListProjectItemsOptions{} + iter = client.Projects.ListOrganizationProjectViewItemsIter(t.Context(), "", 0, 0, opts) + gotItems = 0 + for _, err := range iter { + gotItems++ + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + } + if want := 2; gotItems != want { + t.Errorf("client.Projects.ListOrganizationProjectViewItemsIter call 2 got %v items; want %v", gotItems, want) + } + + iter = client.Projects.ListOrganizationProjectViewItemsIter(t.Context(), "", 0, 0, nil) + gotItems = 0 + for _, err := range iter { + gotItems++ + if err == nil { + t.Error("expected error; got nil") + } + } + if gotItems != 1 { + t.Errorf("client.Projects.ListOrganizationProjectViewItemsIter call 3 got %v items; want 1 (an error)", gotItems) + } + + iter = client.Projects.ListOrganizationProjectViewItemsIter(t.Context(), "", 0, 0, nil) + gotItems = 0 + iter(func(item *ProjectV2Item, err error) bool { + gotItems++ + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + return false + }) + if gotItems != 1 { + t.Errorf("client.Projects.ListOrganizationProjectViewItemsIter call 4 got %v items; want 1 (an error)", gotItems) + } +} + func TestProjectsService_ListOrganizationProjectsIter(t *testing.T) { t.Parallel() client, mux, _ := setup(t) @@ -11031,6 +11103,78 @@ func TestProjectsService_ListUserProjectItemsIter(t *testing.T) { } } +func TestProjectsService_ListUserProjectViewItemsIter(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + var callNum int + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + callNum++ + switch callNum { + case 1: + w.Header().Set("Link", `; rel="next"`) + fmt.Fprint(w, `[{},{},{}]`) + case 2: + fmt.Fprint(w, `[{},{},{},{}]`) + case 3: + fmt.Fprint(w, `[{},{}]`) + case 4: + w.WriteHeader(http.StatusNotFound) + case 5: + fmt.Fprint(w, `[{},{}]`) + } + }) + + iter := client.Projects.ListUserProjectViewItemsIter(t.Context(), "", 0, 0, nil) + var gotItems int + for _, err := range iter { + gotItems++ + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + } + if want := 7; gotItems != want { + t.Errorf("client.Projects.ListUserProjectViewItemsIter call 1 got %v items; want %v", gotItems, want) + } + + opts := &ListProjectItemsOptions{} + iter = client.Projects.ListUserProjectViewItemsIter(t.Context(), "", 0, 0, opts) + gotItems = 0 + for _, err := range iter { + gotItems++ + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + } + if want := 2; gotItems != want { + t.Errorf("client.Projects.ListUserProjectViewItemsIter call 2 got %v items; want %v", gotItems, want) + } + + iter = client.Projects.ListUserProjectViewItemsIter(t.Context(), "", 0, 0, nil) + gotItems = 0 + for _, err := range iter { + gotItems++ + if err == nil { + t.Error("expected error; got nil") + } + } + if gotItems != 1 { + t.Errorf("client.Projects.ListUserProjectViewItemsIter call 3 got %v items; want 1 (an error)", gotItems) + } + + iter = client.Projects.ListUserProjectViewItemsIter(t.Context(), "", 0, 0, nil) + gotItems = 0 + iter(func(item *ProjectV2Item, err error) bool { + gotItems++ + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + return false + }) + if gotItems != 1 { + t.Errorf("client.Projects.ListUserProjectViewItemsIter call 4 got %v items; want 1 (an error)", gotItems) + } +} + func TestProjectsService_ListUserProjectsIter(t *testing.T) { t.Parallel() client, mux, _ := setup(t) diff --git a/github/projects.go b/github/projects.go index 79d6262db2d..22839e12a97 100644 --- a/github/projects.go +++ b/github/projects.go @@ -9,6 +9,7 @@ import ( "context" "encoding/json" "fmt" + "strconv" ) // ProjectsService handles communication with the project V2 @@ -728,3 +729,331 @@ func (s *ProjectsService) DeleteUserProjectItem(ctx context.Context, username st return s.client.Do(req, nil) } + +// CreateProjectV2DraftItemRequest specifies the parameters to create a draft item in a project. +type CreateProjectV2DraftItemRequest struct { + // Title is the title of the draft issue item to create. (Required.) + Title *string `json:"title,omitempty"` + // Body is the body content of the draft issue item to create. (Optional.) + Body *string `json:"body,omitempty"` +} + +// CreateOrganizationProjectDraftItem creates a draft item in an organization owned project. +// +// GitHub API docs: https://docs.github.com/rest/projects/drafts?apiVersion=2022-11-28#create-draft-item-for-organization-owned-project +// +//meta:operation POST /orgs/{org}/projectsV2/{project_number}/drafts +func (s *ProjectsService) CreateOrganizationProjectDraftItem(ctx context.Context, org string, projectNumber int, body CreateProjectV2DraftItemRequest) (*ProjectV2Item, *Response, error) { + u := fmt.Sprintf("orgs/%v/projectsV2/%v/drafts", org, projectNumber) + req, err := s.client.NewRequest(ctx, "POST", u, body) + if err != nil { + return nil, nil, err + } + + var item *ProjectV2Item + resp, err := s.client.Do(req, &item) + if err != nil { + return nil, resp, err + } + + return item, resp, nil +} + +// CreateUserProjectDraftItem creates a draft item in a user owned project. +// +// Note: this endpoint identifies the user by their unique numeric ID (user_id) +// in the path, not by username, mirroring the GitHub REST API. +// +// GitHub API docs: https://docs.github.com/rest/projects/drafts?apiVersion=2022-11-28#create-draft-item-for-user-owned-project +// +//meta:operation POST /user/{user_id}/projectsV2/{project_number}/drafts +func (s *ProjectsService) CreateUserProjectDraftItem(ctx context.Context, userID int64, projectNumber int, body CreateProjectV2DraftItemRequest) (*ProjectV2Item, *Response, error) { + u := fmt.Sprintf("user/%v/projectsV2/%v/drafts", userID, projectNumber) + req, err := s.client.NewRequest(ctx, "POST", u, body) + if err != nil { + return nil, nil, err + } + + var item *ProjectV2Item + resp, err := s.client.Do(req, &item) + if err != nil { + return nil, resp, err + } + + return item, resp, nil +} + +// ProjectV2FieldSingleSelectOption represents an option to create for a single_select project field. +type ProjectV2FieldSingleSelectOption struct { + // Name is the display name of the option. (Required.) + Name *string `json:"name,omitempty"` + // Color is the color associated with the option. + // One of: BLUE, GRAY, GREEN, ORANGE, PINK, PURPLE, RED, YELLOW. + Color *string `json:"color,omitempty"` + // Description is an optional description of the option. + Description *string `json:"description,omitempty"` +} + +// ProjectV2FieldIterationConfiguration represents the configuration to create an iteration project field. +type ProjectV2FieldIterationConfiguration struct { + // StartDate is the start date of the first iteration in ISO 8601 (YYYY-MM-DD) format. + StartDate *string `json:"start_date,omitempty"` + // Duration is the default duration for iterations in days. + Duration *int `json:"duration,omitempty"` + // Iterations holds zero or more iterations for the field. + Iterations []*ProjectV2FieldIterationConfigurationIteration `json:"iterations,omitempty"` +} + +// ProjectV2FieldIterationConfigurationIteration represents a single iteration within a ProjectV2FieldIterationConfiguration. +type ProjectV2FieldIterationConfigurationIteration struct { + // Title is the title of the iteration. + Title *string `json:"title,omitempty"` + // StartDate is the start date of the iteration in ISO 8601 (YYYY-MM-DD) format. + StartDate *string `json:"start_date,omitempty"` + // Duration is the duration of the iteration in days. + Duration *int `json:"duration,omitempty"` +} + +// AddProjectV2FieldRequest specifies the parameters to add a field to a project. +// +// The GitHub API models the body as a one-of: set IssueFieldID to link a +// built-in issue field, or set Name and DataType to create a new field. When +// DataType is "single_select", SingleSelectOptions is required; when DataType +// is "iteration", IterationConfiguration is required. +type AddProjectV2FieldRequest struct { + // Name is the name of the field. + Name *string `json:"name,omitempty"` + // DataType is the field's data type. + // One of: text, number, date, single_select, iteration. + DataType *string `json:"data_type,omitempty"` + // SingleSelectOptions holds the options for a single_select field. (Required for single_select.) + SingleSelectOptions []*ProjectV2FieldSingleSelectOption `json:"single_select_options,omitempty"` + // IterationConfiguration holds the configuration for an iteration field. (Required for iteration.) + IterationConfiguration *ProjectV2FieldIterationConfiguration `json:"iteration_configuration,omitempty"` + // IssueFieldID links an existing built-in issue field to the project. (Organization owned projects only.) + IssueFieldID *int64 `json:"issue_field_id,omitempty"` +} + +// AddOrganizationProjectField adds a field to an organization owned project. +// +// GitHub API docs: https://docs.github.com/rest/projects/fields?apiVersion=2022-11-28#add-a-field-to-an-organization-owned-project +// +//meta:operation POST /orgs/{org}/projectsV2/{project_number}/fields +func (s *ProjectsService) AddOrganizationProjectField(ctx context.Context, org string, projectNumber int, body AddProjectV2FieldRequest) (*ProjectV2Field, *Response, error) { + u := fmt.Sprintf("orgs/%v/projectsV2/%v/fields", org, projectNumber) + req, err := s.client.NewRequest(ctx, "POST", u, body) + if err != nil { + return nil, nil, err + } + + var field *ProjectV2Field + resp, err := s.client.Do(req, &field) + if err != nil { + return nil, resp, err + } + + return field, resp, nil +} + +// AddUserProjectField adds a field to a user owned project. +// +// GitHub API docs: https://docs.github.com/rest/projects/fields?apiVersion=2022-11-28#add-field-to-user-owned-project +// +//meta:operation POST /users/{username}/projectsV2/{project_number}/fields +func (s *ProjectsService) AddUserProjectField(ctx context.Context, username string, projectNumber int, body AddProjectV2FieldRequest) (*ProjectV2Field, *Response, error) { + u := fmt.Sprintf("users/%v/projectsV2/%v/fields", username, projectNumber) + req, err := s.client.NewRequest(ctx, "POST", u, body) + if err != nil { + return nil, nil, err + } + + var field *ProjectV2Field + resp, err := s.client.Do(req, &field) + if err != nil { + return nil, resp, err + } + + return field, resp, nil +} + +// ProjectV2View represents a view in a project. +type ProjectV2View struct { + ID *int64 `json:"id,omitempty"` + Number *int `json:"number,omitempty"` + Name *string `json:"name,omitempty"` + Layout *string `json:"layout,omitempty"` + NodeID *string `json:"node_id,omitempty"` + ProjectURL *string `json:"project_url,omitempty"` + HTMLURL *string `json:"html_url,omitempty"` + Creator *User `json:"creator,omitempty"` + Filter *string `json:"filter,omitempty"` + // VisibleFields holds the IDs of the fields displayed in the view. + VisibleFields []int64 `json:"visible_fields,omitempty"` + // SortBy holds the view's sorting configuration, in priority order. + SortBy []*ProjectV2ViewSortBy `json:"sort_by,omitempty"` + // GroupBy holds the IDs of the fields the view is grouped by. + GroupBy []int64 `json:"group_by,omitempty"` + // VerticalGroupBy holds the IDs of the fields the view is vertically grouped by. + VerticalGroupBy []int64 `json:"vertical_group_by,omitempty"` + CreatedAt *Timestamp `json:"created_at,omitempty"` + UpdatedAt *Timestamp `json:"updated_at,omitempty"` +} + +// ProjectV2ViewSortBy represents a single sort criterion of a project view. +// +// On the wire it is encoded as a [field_id, direction] tuple rather than an +// object, so it has custom JSON (un)marshaling. +type ProjectV2ViewSortBy struct { + // FieldID is the ID of the field to sort by. + FieldID *int64 + // Direction is the sort direction, one of "asc" or "desc". + Direction *string +} + +// UnmarshalJSON implements custom unmarshaling for the [field_id, direction] +// tuple. The field_id may be encoded as either a number or a string. +func (s *ProjectV2ViewSortBy) UnmarshalJSON(data []byte) error { + var tuple []json.RawMessage + if err := json.Unmarshal(data, &tuple); err != nil { + return err + } + if len(tuple) != 2 { + return fmt.Errorf("ProjectV2ViewSortBy: expected a [field_id, direction] tuple, got %v elements", len(tuple)) + } + + var fieldID int64 + if err := json.Unmarshal(tuple[0], &fieldID); err != nil { + // The OpenAPI schema allows the field_id to be a string as well. + var str string + if err2 := json.Unmarshal(tuple[0], &str); err2 != nil { + return fmt.Errorf("ProjectV2ViewSortBy: invalid field_id: %w", err) + } + fieldID, err = strconv.ParseInt(str, 10, 64) + if err != nil { + return fmt.Errorf("ProjectV2ViewSortBy: invalid field_id %q: %w", str, err) + } + } + + var direction string + if err := json.Unmarshal(tuple[1], &direction); err != nil { + return fmt.Errorf("ProjectV2ViewSortBy: invalid direction: %w", err) + } + + s.FieldID = &fieldID + s.Direction = &direction + return nil +} + +// MarshalJSON implements custom marshaling to the [field_id, direction] tuple. +func (s ProjectV2ViewSortBy) MarshalJSON() ([]byte, error) { + return json.Marshal([]any{s.FieldID, s.Direction}) +} + +// CreateProjectV2ViewRequest specifies the parameters to create a project view. +type CreateProjectV2ViewRequest struct { + // Name is the view's display name. (Required.) + Name *string `json:"name,omitempty"` + // Layout is the view's layout. One of: table, board, roadmap. (Required.) + Layout *string `json:"layout,omitempty"` + // Filter is an optional query string to filter the items shown in the view. + Filter *string `json:"filter,omitempty"` + // VisibleFields lists the field IDs to display. Not applicable to the roadmap layout. + VisibleFields []int64 `json:"visible_fields,omitempty"` +} + +// CreateOrganizationProjectView creates a view for an organization owned project. +// +// GitHub API docs: https://docs.github.com/rest/projects/views?apiVersion=2022-11-28#create-a-view-for-an-organization-owned-project +// +//meta:operation POST /orgs/{org}/projectsV2/{project_number}/views +func (s *ProjectsService) CreateOrganizationProjectView(ctx context.Context, org string, projectNumber int, body CreateProjectV2ViewRequest) (*ProjectV2View, *Response, error) { + u := fmt.Sprintf("orgs/%v/projectsV2/%v/views", org, projectNumber) + req, err := s.client.NewRequest(ctx, "POST", u, body) + if err != nil { + return nil, nil, err + } + + var view *ProjectV2View + resp, err := s.client.Do(req, &view) + if err != nil { + return nil, resp, err + } + + return view, resp, nil +} + +// CreateUserProjectView creates a view for a user owned project. +// +// Note: this endpoint identifies the user by their unique numeric ID (user_id) +// in the path, not by username, mirroring the GitHub REST API. +// +// GitHub API docs: https://docs.github.com/rest/projects/views?apiVersion=2022-11-28#create-a-view-for-a-user-owned-project +// +//meta:operation POST /users/{user_id}/projectsV2/{project_number}/views +func (s *ProjectsService) CreateUserProjectView(ctx context.Context, userID int64, projectNumber int, body CreateProjectV2ViewRequest) (*ProjectV2View, *Response, error) { + u := fmt.Sprintf("users/%v/projectsV2/%v/views", userID, projectNumber) + req, err := s.client.NewRequest(ctx, "POST", u, body) + if err != nil { + return nil, nil, err + } + + var view *ProjectV2View + resp, err := s.client.Do(req, &view) + if err != nil { + return nil, resp, err + } + + return view, resp, nil +} + +// ListOrganizationProjectViewItems lists the items of a view in an organization owned project. +// +// GitHub API docs: https://docs.github.com/rest/projects/items?apiVersion=2022-11-28#list-items-for-an-organization-project-view +// +//meta:operation GET /orgs/{org}/projectsV2/{project_number}/views/{view_number}/items +func (s *ProjectsService) ListOrganizationProjectViewItems(ctx context.Context, org string, projectNumber, viewNumber int, opts *ListProjectItemsOptions) ([]*ProjectV2Item, *Response, error) { + u := fmt.Sprintf("orgs/%v/projectsV2/%v/views/%v/items", org, projectNumber, viewNumber) + u, err := addOptions(u, opts) + if err != nil { + return nil, nil, err + } + + req, err := s.client.NewRequest(ctx, "GET", u, nil) + if err != nil { + return nil, nil, err + } + + var items []*ProjectV2Item + resp, err := s.client.Do(req, &items) + if err != nil { + return nil, resp, err + } + + return items, resp, nil +} + +// ListUserProjectViewItems lists the items of a view in a user owned project. +// +// GitHub API docs: https://docs.github.com/rest/projects/items?apiVersion=2022-11-28#list-items-for-a-user-project-view +// +//meta:operation GET /users/{username}/projectsV2/{project_number}/views/{view_number}/items +func (s *ProjectsService) ListUserProjectViewItems(ctx context.Context, username string, projectNumber, viewNumber int, opts *ListProjectItemsOptions) ([]*ProjectV2Item, *Response, error) { + u := fmt.Sprintf("users/%v/projectsV2/%v/views/%v/items", username, projectNumber, viewNumber) + u, err := addOptions(u, opts) + if err != nil { + return nil, nil, err + } + + req, err := s.client.NewRequest(ctx, "GET", u, nil) + if err != nil { + return nil, nil, err + } + + var items []*ProjectV2Item + resp, err := s.client.Do(req, &items) + if err != nil { + return nil, resp, err + } + + return items, resp, nil +} diff --git a/github/projects_test.go b/github/projects_test.go index 296b4bf8dba..022dc400628 100644 --- a/github/projects_test.go +++ b/github/projects_test.go @@ -1432,3 +1432,362 @@ func TestProjectV2ItemContent_Marshal(t *testing.T) { testJSONMarshalOnly(t, &content, want) }) } + +func TestProjectsService_CreateOrganizationProjectDraftItem(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + input := CreateProjectV2DraftItemRequest{Title: Ptr("My draft"), Body: Ptr("Draft body")} + + mux.HandleFunc("/orgs/o/projectsV2/1/drafts", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + testJSONBody(t, r, input) + fmt.Fprint(w, `{"id":42,"node_id":"PVTI_draft","content_type":"DraftIssue"}`) + }) + + ctx := t.Context() + item, _, err := client.Projects.CreateOrganizationProjectDraftItem(ctx, "o", 1, input) + if err != nil { + t.Fatalf("Projects.CreateOrganizationProjectDraftItem returned error: %v", err) + } + if item.GetID() != 42 { + t.Fatalf("unexpected item: %+v", item) + } + + const methodName = "CreateOrganizationProjectDraftItem" + testBadOptions(t, methodName, func() (err error) { + _, _, err = client.Projects.CreateOrganizationProjectDraftItem(ctx, "\n", 1, input) + return err + }) + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Projects.CreateOrganizationProjectDraftItem(ctx, "o", 1, input) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + +func TestProjectsService_CreateUserProjectDraftItem(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + input := CreateProjectV2DraftItemRequest{Title: Ptr("My draft")} + + mux.HandleFunc("/user/12345/projectsV2/1/drafts", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + testJSONBody(t, r, input) + fmt.Fprint(w, `{"id":43,"node_id":"PVTI_draft_u","content_type":"DraftIssue"}`) + }) + + ctx := t.Context() + item, _, err := client.Projects.CreateUserProjectDraftItem(ctx, 12345, 1, input) + if err != nil { + t.Fatalf("Projects.CreateUserProjectDraftItem returned error: %v", err) + } + if item.GetID() != 43 { + t.Fatalf("unexpected item: %+v", item) + } + + const methodName = "CreateUserProjectDraftItem" + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Projects.CreateUserProjectDraftItem(ctx, 12345, 1, input) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + +func TestProjectsService_AddOrganizationProjectField(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + input := AddProjectV2FieldRequest{ + Name: Ptr("Priority"), + DataType: Ptr("single_select"), + SingleSelectOptions: []*ProjectV2FieldSingleSelectOption{ + {Name: Ptr("High"), Color: Ptr("RED"), Description: Ptr("Urgent")}, + {Name: Ptr("Low"), Color: Ptr("GRAY")}, + }, + } + + mux.HandleFunc("/orgs/o/projectsV2/1/fields", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + testJSONBody(t, r, input) + fmt.Fprint(w, `{"id":7,"name":"Priority","data_type":"single_select"}`) + }) + + ctx := t.Context() + field, _, err := client.Projects.AddOrganizationProjectField(ctx, "o", 1, input) + if err != nil { + t.Fatalf("Projects.AddOrganizationProjectField returned error: %v", err) + } + if field.GetID() != 7 || field.GetDataType() != "single_select" { + t.Fatalf("unexpected field: %+v", field) + } + + const methodName = "AddOrganizationProjectField" + testBadOptions(t, methodName, func() (err error) { + _, _, err = client.Projects.AddOrganizationProjectField(ctx, "\n", 1, input) + return err + }) + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Projects.AddOrganizationProjectField(ctx, "o", 1, input) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + +func TestProjectsService_AddUserProjectField(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + input := AddProjectV2FieldRequest{ + Name: Ptr("Sprint"), + DataType: Ptr("iteration"), + IterationConfiguration: &ProjectV2FieldIterationConfiguration{ + StartDate: Ptr("2026-01-01"), + Duration: Ptr(14), + Iterations: []*ProjectV2FieldIterationConfigurationIteration{ + {Title: Ptr("Sprint 1"), StartDate: Ptr("2026-01-01"), Duration: Ptr(14)}, + }, + }, + } + + mux.HandleFunc("/users/u/projectsV2/1/fields", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + testJSONBody(t, r, input) + fmt.Fprint(w, `{"id":8,"name":"Sprint","data_type":"iteration"}`) + }) + + ctx := t.Context() + field, _, err := client.Projects.AddUserProjectField(ctx, "u", 1, input) + if err != nil { + t.Fatalf("Projects.AddUserProjectField returned error: %v", err) + } + if field.GetID() != 8 { + t.Fatalf("unexpected field: %+v", field) + } + + const methodName = "AddUserProjectField" + testBadOptions(t, methodName, func() (err error) { + _, _, err = client.Projects.AddUserProjectField(ctx, "\n", 1, input) + return err + }) + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Projects.AddUserProjectField(ctx, "u", 1, input) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + +func TestProjectsService_CreateOrganizationProjectView(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + input := CreateProjectV2ViewRequest{ + Name: Ptr("My board"), + Layout: Ptr("board"), + Filter: Ptr("is:open"), + VisibleFields: []int64{1, 2, 3}, + } + + mux.HandleFunc("/orgs/o/projectsV2/1/views", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + testJSONBody(t, r, input) + // The first field_id is larger than 2^53 to ensure it is decoded as an + // int64 without the precision loss that a float64 would introduce. + fmt.Fprint(w, `{"id":5,"number":2,"name":"My board","layout":"board","sort_by":[[9007199254740993,"asc"],[456,"desc"]]}`) + }) + + ctx := t.Context() + view, _, err := client.Projects.CreateOrganizationProjectView(ctx, "o", 1, input) + if err != nil { + t.Fatalf("Projects.CreateOrganizationProjectView returned error: %v", err) + } + if view.GetID() != 5 || view.GetNumber() != 2 || view.GetLayout() != "board" { + t.Fatalf("unexpected view: %+v", view) + } + if len(view.SortBy) != 2 { + t.Fatalf("unexpected sort_by: %+v", view.SortBy) + } + if got, want := view.SortBy[0].GetFieldID(), int64(9007199254740993); got != want { + t.Errorf("view.SortBy[0].FieldID = %v, want %v", got, want) + } + if got, want := view.SortBy[0].GetDirection(), "asc"; got != want { + t.Errorf("view.SortBy[0].Direction = %q, want %q", got, want) + } + if got, want := view.SortBy[1].GetFieldID(), int64(456); got != want { + t.Errorf("view.SortBy[1].FieldID = %v, want %v", got, want) + } + if got, want := view.SortBy[1].GetDirection(), "desc"; got != want { + t.Errorf("view.SortBy[1].Direction = %q, want %q", got, want) + } + + const methodName = "CreateOrganizationProjectView" + testBadOptions(t, methodName, func() (err error) { + _, _, err = client.Projects.CreateOrganizationProjectView(ctx, "\n", 1, input) + return err + }) + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Projects.CreateOrganizationProjectView(ctx, "o", 1, input) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + +func TestProjectsService_CreateUserProjectView(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + input := CreateProjectV2ViewRequest{Name: Ptr("My table"), Layout: Ptr("table")} + + mux.HandleFunc("/users/12345/projectsV2/1/views", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + testJSONBody(t, r, input) + fmt.Fprint(w, `{"id":6,"number":3,"name":"My table","layout":"table"}`) + }) + + ctx := t.Context() + view, _, err := client.Projects.CreateUserProjectView(ctx, 12345, 1, input) + if err != nil { + t.Fatalf("Projects.CreateUserProjectView returned error: %v", err) + } + if view.GetID() != 6 || view.GetLayout() != "table" { + t.Fatalf("unexpected view: %+v", view) + } + + const methodName = "CreateUserProjectView" + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Projects.CreateUserProjectView(ctx, 12345, 1, input) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + +func TestProjectsService_ListOrganizationProjectViewItems(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/orgs/o/projectsV2/1/views/2/items", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + testFormValues(t, r, values{"after": "2", "before": "1", "per_page": "50", "fields": "10,11", "q": "status:open"}) + fmt.Fprint(w, `[{"id":21,"node_id":"PVTI_view_item"}]`) + }) + + opts := &ListProjectItemsOptions{ListProjectsOptions: ListProjectsOptions{ListProjectsPaginationOptions: ListProjectsPaginationOptions{After: "2", Before: "1", PerPage: 50}, Query: "status:open"}, Fields: []int64{10, 11}} + ctx := t.Context() + items, _, err := client.Projects.ListOrganizationProjectViewItems(ctx, "o", 1, 2, opts) + if err != nil { + t.Fatalf("Projects.ListOrganizationProjectViewItems returned error: %v", err) + } + if len(items) != 1 || items[0].GetID() != 21 { + t.Fatalf("Projects.ListOrganizationProjectViewItems returned %+v", items) + } + + const methodName = "ListOrganizationProjectViewItems" + testBadOptions(t, methodName, func() (err error) { + _, _, err = client.Projects.ListOrganizationProjectViewItems(ctx, "\n", 1, 2, opts) + return err + }) + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Projects.ListOrganizationProjectViewItems(ctx, "o", 1, 2, opts) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + +func TestProjectsService_ListUserProjectViewItems(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/users/u/projectsV2/1/views/2/items", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + testFormValues(t, r, values{"per_page": "30"}) + fmt.Fprint(w, `[{"id":22,"node_id":"PVTI_view_item_u"}]`) + }) + + opts := &ListProjectItemsOptions{ListProjectsOptions: ListProjectsOptions{ListProjectsPaginationOptions: ListProjectsPaginationOptions{PerPage: 30}}} + ctx := t.Context() + items, _, err := client.Projects.ListUserProjectViewItems(ctx, "u", 1, 2, opts) + if err != nil { + t.Fatalf("Projects.ListUserProjectViewItems returned error: %v", err) + } + if len(items) != 1 || items[0].GetID() != 22 { + t.Fatalf("Projects.ListUserProjectViewItems returned %+v", items) + } + + const methodName = "ListUserProjectViewItems" + testBadOptions(t, methodName, func() (err error) { + _, _, err = client.Projects.ListUserProjectViewItems(ctx, "\n", 1, 2, opts) + return err + }) + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Projects.ListUserProjectViewItems(ctx, "u", 1, 2, opts) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + +func TestProjectV2ViewSortBy_UnmarshalJSON(t *testing.T) { + t.Parallel() + + t.Run("numeric and string field_id", func(t *testing.T) { + t.Parallel() + // The second element uses a string field_id, which the OpenAPI schema + // permits. The first exceeds 2^53 to confirm int64 (not float64) decoding. + var got []*ProjectV2ViewSortBy + if err := json.Unmarshal([]byte(`[[9007199254740993,"asc"],["456","desc"]]`), &got); err != nil { + t.Fatalf("Unmarshal returned error: %v", err) + } + if len(got) != 2 { + t.Fatalf("len(got) = %d, want 2", len(got)) + } + if got[0].GetFieldID() != 9007199254740993 || got[0].GetDirection() != "asc" { + t.Errorf("got[0] = %+v, want {9007199254740993 asc}", got[0]) + } + if got[1].GetFieldID() != 456 || got[1].GetDirection() != "desc" { + t.Errorf("got[1] = %+v, want {456 desc}", got[1]) + } + }) + + t.Run("wrong tuple length is an error", func(t *testing.T) { + t.Parallel() + var got ProjectV2ViewSortBy + if err := json.Unmarshal([]byte(`[1]`), &got); err == nil { + t.Error("Unmarshal of a 1-element tuple = nil error, want error") + } + }) + + t.Run("round trips through MarshalJSON", func(t *testing.T) { + t.Parallel() + in := &ProjectV2ViewSortBy{FieldID: Ptr(int64(9007199254740993)), Direction: Ptr("asc")} + b, err := json.Marshal(in) + if err != nil { + t.Fatalf("Marshal returned error: %v", err) + } + if got, want := string(b), `[9007199254740993,"asc"]`; got != want { + t.Errorf("Marshal = %v, want %v", got, want) + } + var out ProjectV2ViewSortBy + if err := json.Unmarshal(b, &out); err != nil { + t.Fatalf("Unmarshal returned error: %v", err) + } + if out.GetFieldID() != 9007199254740993 || out.GetDirection() != "asc" { + t.Errorf("round trip = %+v, want {9007199254740993 asc}", out) + } + }) +} From d4f3a947d2035abfdb8d3e7b3818f345cb5a133a Mon Sep 17 00:00:00 2001 From: JamBalaya56562 Date: Wed, 24 Jun 2026 00:43:42 +0000 Subject: [PATCH 02/10] test: Cover ProjectV2ViewSortBy unmarshal errors Add subtests for the non-array, unsupported and non-numeric string field_id, and non-string direction branches of ProjectV2ViewSortBy.UnmarshalJSON, bringing the method (and MarshalJSON) to 100% coverage. --- github/projects_test.go | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/github/projects_test.go b/github/projects_test.go index 022dc400628..c23b1553241 100644 --- a/github/projects_test.go +++ b/github/projects_test.go @@ -1772,6 +1772,38 @@ func TestProjectV2ViewSortBy_UnmarshalJSON(t *testing.T) { } }) + t.Run("non-array is an error", func(t *testing.T) { + t.Parallel() + var got ProjectV2ViewSortBy + if err := json.Unmarshal([]byte(`{"a":1}`), &got); err == nil { + t.Error("Unmarshal of a non-array = nil error, want error") + } + }) + + t.Run("field_id of an unsupported type is an error", func(t *testing.T) { + t.Parallel() + var got ProjectV2ViewSortBy + if err := json.Unmarshal([]byte(`[true,"asc"]`), &got); err == nil { + t.Error("Unmarshal of a boolean field_id = nil error, want error") + } + }) + + t.Run("non-numeric string field_id is an error", func(t *testing.T) { + t.Parallel() + var got ProjectV2ViewSortBy + if err := json.Unmarshal([]byte(`["abc","asc"]`), &got); err == nil { + t.Error(`Unmarshal of field_id "abc" = nil error, want error`) + } + }) + + t.Run("non-string direction is an error", func(t *testing.T) { + t.Parallel() + var got ProjectV2ViewSortBy + if err := json.Unmarshal([]byte(`[123,456]`), &got); err == nil { + t.Error("Unmarshal of a numeric direction = nil error, want error") + } + }) + t.Run("round trips through MarshalJSON", func(t *testing.T) { t.Parallel() in := &ProjectV2ViewSortBy{FieldID: Ptr(int64(9007199254740993)), Direction: Ptr("asc")} From 0ec6b3721d0edf289d401572e7ff3ac82bf4e1b1 Mon Sep 17 00:00:00 2001 From: JamBalaya56562 Date: Wed, 24 Jun 2026 00:43:42 +0000 Subject: [PATCH 03/10] refactor: Pass required Projects v2 fields by value Per review, required request-body fields should not be pointers. The OpenAPI required arrays mark title (draft create) and name/layout (view create) as required, and a single-select option's name is its required display label, so make those non-pointer values without omitempty. AddProjectV2FieldRequest stays all-pointer because its fields are required only conditionally (the body is a oneOf), as do the optional fields and the response types. --- github/github-accessors.go | 24 ++++++++++++------------ github/github-accessors_test.go | 20 ++++---------------- github/projects.go | 8 ++++---- github/projects_test.go | 14 +++++++------- 4 files changed, 27 insertions(+), 39 deletions(-) diff --git a/github/github-accessors.go b/github/github-accessors.go index 74b2cb556e5..eb8b17089e3 100644 --- a/github/github-accessors.go +++ b/github/github-accessors.go @@ -11126,12 +11126,12 @@ func (c *CreateProjectV2DraftItemRequest) GetBody() string { return *c.Body } -// GetTitle returns the Title field if it's non-nil, zero value otherwise. +// GetTitle returns the Title field. func (c *CreateProjectV2DraftItemRequest) GetTitle() string { - if c == nil || c.Title == nil { + if c == nil { return "" } - return *c.Title + return c.Title } // GetFilter returns the Filter field if it's non-nil, zero value otherwise. @@ -11142,20 +11142,20 @@ func (c *CreateProjectV2ViewRequest) GetFilter() string { return *c.Filter } -// GetLayout returns the Layout field if it's non-nil, zero value otherwise. +// GetLayout returns the Layout field. func (c *CreateProjectV2ViewRequest) GetLayout() string { - if c == nil || c.Layout == nil { + if c == nil { return "" } - return *c.Layout + return c.Layout } -// GetName returns the Name field if it's non-nil, zero value otherwise. +// GetName returns the Name field. func (c *CreateProjectV2ViewRequest) GetName() string { - if c == nil || c.Name == nil { + if c == nil { return "" } - return *c.Name + return c.Name } // GetVisibleFields returns the VisibleFields slice if it's non-nil, nil otherwise. @@ -29054,12 +29054,12 @@ func (p *ProjectV2FieldSingleSelectOption) GetDescription() string { return *p.Description } -// GetName returns the Name field if it's non-nil, zero value otherwise. +// GetName returns the Name field. func (p *ProjectV2FieldSingleSelectOption) GetName() string { - if p == nil || p.Name == nil { + if p == nil { return "" } - return *p.Name + return p.Name } // GetArchivedAt returns the ArchivedAt field if it's non-nil, zero value otherwise. diff --git a/github/github-accessors_test.go b/github/github-accessors_test.go index bfcc36203dc..fdbd5e560d3 100644 --- a/github/github-accessors_test.go +++ b/github/github-accessors_test.go @@ -14156,10 +14156,7 @@ func TestCreateProjectV2DraftItemRequest_GetBody(tt *testing.T) { func TestCreateProjectV2DraftItemRequest_GetTitle(tt *testing.T) { tt.Parallel() - var zeroValue string - c := &CreateProjectV2DraftItemRequest{Title: &zeroValue} - c.GetTitle() - c = &CreateProjectV2DraftItemRequest{} + c := &CreateProjectV2DraftItemRequest{} c.GetTitle() c = nil c.GetTitle() @@ -14178,10 +14175,7 @@ func TestCreateProjectV2ViewRequest_GetFilter(tt *testing.T) { func TestCreateProjectV2ViewRequest_GetLayout(tt *testing.T) { tt.Parallel() - var zeroValue string - c := &CreateProjectV2ViewRequest{Layout: &zeroValue} - c.GetLayout() - c = &CreateProjectV2ViewRequest{} + c := &CreateProjectV2ViewRequest{} c.GetLayout() c = nil c.GetLayout() @@ -14189,10 +14183,7 @@ func TestCreateProjectV2ViewRequest_GetLayout(tt *testing.T) { func TestCreateProjectV2ViewRequest_GetName(tt *testing.T) { tt.Parallel() - var zeroValue string - c := &CreateProjectV2ViewRequest{Name: &zeroValue} - c.GetName() - c = &CreateProjectV2ViewRequest{} + c := &CreateProjectV2ViewRequest{} c.GetName() c = nil c.GetName() @@ -36647,10 +36638,7 @@ func TestProjectV2FieldSingleSelectOption_GetDescription(tt *testing.T) { func TestProjectV2FieldSingleSelectOption_GetName(tt *testing.T) { tt.Parallel() - var zeroValue string - p := &ProjectV2FieldSingleSelectOption{Name: &zeroValue} - p.GetName() - p = &ProjectV2FieldSingleSelectOption{} + p := &ProjectV2FieldSingleSelectOption{} p.GetName() p = nil p.GetName() diff --git a/github/projects.go b/github/projects.go index 22839e12a97..f2cddb31f62 100644 --- a/github/projects.go +++ b/github/projects.go @@ -733,7 +733,7 @@ func (s *ProjectsService) DeleteUserProjectItem(ctx context.Context, username st // CreateProjectV2DraftItemRequest specifies the parameters to create a draft item in a project. type CreateProjectV2DraftItemRequest struct { // Title is the title of the draft issue item to create. (Required.) - Title *string `json:"title,omitempty"` + Title string `json:"title"` // Body is the body content of the draft issue item to create. (Optional.) Body *string `json:"body,omitempty"` } @@ -786,7 +786,7 @@ func (s *ProjectsService) CreateUserProjectDraftItem(ctx context.Context, userID // ProjectV2FieldSingleSelectOption represents an option to create for a single_select project field. type ProjectV2FieldSingleSelectOption struct { // Name is the display name of the option. (Required.) - Name *string `json:"name,omitempty"` + Name string `json:"name"` // Color is the color associated with the option. // One of: BLUE, GRAY, GREEN, ORANGE, PINK, PURPLE, RED, YELLOW. Color *string `json:"color,omitempty"` @@ -952,9 +952,9 @@ func (s ProjectV2ViewSortBy) MarshalJSON() ([]byte, error) { // CreateProjectV2ViewRequest specifies the parameters to create a project view. type CreateProjectV2ViewRequest struct { // Name is the view's display name. (Required.) - Name *string `json:"name,omitempty"` + Name string `json:"name"` // Layout is the view's layout. One of: table, board, roadmap. (Required.) - Layout *string `json:"layout,omitempty"` + Layout string `json:"layout"` // Filter is an optional query string to filter the items shown in the view. Filter *string `json:"filter,omitempty"` // VisibleFields lists the field IDs to display. Not applicable to the roadmap layout. diff --git a/github/projects_test.go b/github/projects_test.go index c23b1553241..e638bc59a38 100644 --- a/github/projects_test.go +++ b/github/projects_test.go @@ -1437,7 +1437,7 @@ func TestProjectsService_CreateOrganizationProjectDraftItem(t *testing.T) { t.Parallel() client, mux, _ := setup(t) - input := CreateProjectV2DraftItemRequest{Title: Ptr("My draft"), Body: Ptr("Draft body")} + input := CreateProjectV2DraftItemRequest{Title: "My draft", Body: Ptr("Draft body")} mux.HandleFunc("/orgs/o/projectsV2/1/drafts", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, "POST") @@ -1472,7 +1472,7 @@ func TestProjectsService_CreateUserProjectDraftItem(t *testing.T) { t.Parallel() client, mux, _ := setup(t) - input := CreateProjectV2DraftItemRequest{Title: Ptr("My draft")} + input := CreateProjectV2DraftItemRequest{Title: "My draft"} mux.HandleFunc("/user/12345/projectsV2/1/drafts", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, "POST") @@ -1507,8 +1507,8 @@ func TestProjectsService_AddOrganizationProjectField(t *testing.T) { Name: Ptr("Priority"), DataType: Ptr("single_select"), SingleSelectOptions: []*ProjectV2FieldSingleSelectOption{ - {Name: Ptr("High"), Color: Ptr("RED"), Description: Ptr("Urgent")}, - {Name: Ptr("Low"), Color: Ptr("GRAY")}, + {Name: "High", Color: Ptr("RED"), Description: Ptr("Urgent")}, + {Name: "Low", Color: Ptr("GRAY")}, }, } @@ -1591,8 +1591,8 @@ func TestProjectsService_CreateOrganizationProjectView(t *testing.T) { client, mux, _ := setup(t) input := CreateProjectV2ViewRequest{ - Name: Ptr("My board"), - Layout: Ptr("board"), + Name: "My board", + Layout: "board", Filter: Ptr("is:open"), VisibleFields: []int64{1, 2, 3}, } @@ -1647,7 +1647,7 @@ func TestProjectsService_CreateUserProjectView(t *testing.T) { t.Parallel() client, mux, _ := setup(t) - input := CreateProjectV2ViewRequest{Name: Ptr("My table"), Layout: Ptr("table")} + input := CreateProjectV2ViewRequest{Name: "My table", Layout: "table"} mux.HandleFunc("/users/12345/projectsV2/1/views", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, "POST") From 1dff802cb09d47b86e8eccec998fad2ee7967a26 Mon Sep 17 00:00:00 2001 From: JamBalaya56562 Date: Wed, 24 Jun 2026 00:45:01 +0000 Subject: [PATCH 04/10] test: Simplify Projects v2 tests with cmp.Diff Replace the hand-written field-by-field assertions in the new draft, field, view and view-item method tests with a single cmp.Diff against a fully-populated want struct, which also verifies every decoded field. --- github/projects_test.go | 64 ++++++++++++++++++++++------------------- 1 file changed, 34 insertions(+), 30 deletions(-) diff --git a/github/projects_test.go b/github/projects_test.go index e638bc59a38..5b7b5802654 100644 --- a/github/projects_test.go +++ b/github/projects_test.go @@ -12,6 +12,8 @@ import ( "net/http" "testing" "time" + + "github.com/google/go-cmp/cmp" ) func TestProjectsService_ListOrganizationProjects(t *testing.T) { @@ -1450,8 +1452,9 @@ func TestProjectsService_CreateOrganizationProjectDraftItem(t *testing.T) { if err != nil { t.Fatalf("Projects.CreateOrganizationProjectDraftItem returned error: %v", err) } - if item.GetID() != 42 { - t.Fatalf("unexpected item: %+v", item) + want := &ProjectV2Item{ID: Ptr(int64(42)), NodeID: Ptr("PVTI_draft"), ContentType: Ptr(ProjectV2ItemContentTypeDraftIssue)} + if diff := cmp.Diff(want, item); diff != "" { + t.Errorf("Projects.CreateOrganizationProjectDraftItem mismatch (-want +got):\n%v", diff) } const methodName = "CreateOrganizationProjectDraftItem" @@ -1485,8 +1488,9 @@ func TestProjectsService_CreateUserProjectDraftItem(t *testing.T) { if err != nil { t.Fatalf("Projects.CreateUserProjectDraftItem returned error: %v", err) } - if item.GetID() != 43 { - t.Fatalf("unexpected item: %+v", item) + want := &ProjectV2Item{ID: Ptr(int64(43)), NodeID: Ptr("PVTI_draft_u"), ContentType: Ptr(ProjectV2ItemContentTypeDraftIssue)} + if diff := cmp.Diff(want, item); diff != "" { + t.Errorf("Projects.CreateUserProjectDraftItem mismatch (-want +got):\n%v", diff) } const methodName = "CreateUserProjectDraftItem" @@ -1523,8 +1527,9 @@ func TestProjectsService_AddOrganizationProjectField(t *testing.T) { if err != nil { t.Fatalf("Projects.AddOrganizationProjectField returned error: %v", err) } - if field.GetID() != 7 || field.GetDataType() != "single_select" { - t.Fatalf("unexpected field: %+v", field) + want := &ProjectV2Field{ID: Ptr(int64(7)), Name: Ptr("Priority"), DataType: Ptr("single_select")} + if diff := cmp.Diff(want, field); diff != "" { + t.Errorf("Projects.AddOrganizationProjectField mismatch (-want +got):\n%v", diff) } const methodName = "AddOrganizationProjectField" @@ -1568,8 +1573,9 @@ func TestProjectsService_AddUserProjectField(t *testing.T) { if err != nil { t.Fatalf("Projects.AddUserProjectField returned error: %v", err) } - if field.GetID() != 8 { - t.Fatalf("unexpected field: %+v", field) + want := &ProjectV2Field{ID: Ptr(int64(8)), Name: Ptr("Sprint"), DataType: Ptr("iteration")} + if diff := cmp.Diff(want, field); diff != "" { + t.Errorf("Projects.AddUserProjectField mismatch (-want +got):\n%v", diff) } const methodName = "AddUserProjectField" @@ -1610,23 +1616,18 @@ func TestProjectsService_CreateOrganizationProjectView(t *testing.T) { if err != nil { t.Fatalf("Projects.CreateOrganizationProjectView returned error: %v", err) } - if view.GetID() != 5 || view.GetNumber() != 2 || view.GetLayout() != "board" { - t.Fatalf("unexpected view: %+v", view) - } - if len(view.SortBy) != 2 { - t.Fatalf("unexpected sort_by: %+v", view.SortBy) - } - if got, want := view.SortBy[0].GetFieldID(), int64(9007199254740993); got != want { - t.Errorf("view.SortBy[0].FieldID = %v, want %v", got, want) - } - if got, want := view.SortBy[0].GetDirection(), "asc"; got != want { - t.Errorf("view.SortBy[0].Direction = %q, want %q", got, want) - } - if got, want := view.SortBy[1].GetFieldID(), int64(456); got != want { - t.Errorf("view.SortBy[1].FieldID = %v, want %v", got, want) + want := &ProjectV2View{ + ID: Ptr(int64(5)), + Number: Ptr(2), + Name: Ptr("My board"), + Layout: Ptr("board"), + SortBy: []*ProjectV2ViewSortBy{ + {FieldID: Ptr(int64(9007199254740993)), Direction: Ptr("asc")}, + {FieldID: Ptr(int64(456)), Direction: Ptr("desc")}, + }, } - if got, want := view.SortBy[1].GetDirection(), "desc"; got != want { - t.Errorf("view.SortBy[1].Direction = %q, want %q", got, want) + if diff := cmp.Diff(want, view); diff != "" { + t.Errorf("Projects.CreateOrganizationProjectView mismatch (-want +got):\n%v", diff) } const methodName = "CreateOrganizationProjectView" @@ -1660,8 +1661,9 @@ func TestProjectsService_CreateUserProjectView(t *testing.T) { if err != nil { t.Fatalf("Projects.CreateUserProjectView returned error: %v", err) } - if view.GetID() != 6 || view.GetLayout() != "table" { - t.Fatalf("unexpected view: %+v", view) + want := &ProjectV2View{ID: Ptr(int64(6)), Number: Ptr(3), Name: Ptr("My table"), Layout: Ptr("table")} + if diff := cmp.Diff(want, view); diff != "" { + t.Errorf("Projects.CreateUserProjectView mismatch (-want +got):\n%v", diff) } const methodName = "CreateUserProjectView" @@ -1690,8 +1692,9 @@ func TestProjectsService_ListOrganizationProjectViewItems(t *testing.T) { if err != nil { t.Fatalf("Projects.ListOrganizationProjectViewItems returned error: %v", err) } - if len(items) != 1 || items[0].GetID() != 21 { - t.Fatalf("Projects.ListOrganizationProjectViewItems returned %+v", items) + want := []*ProjectV2Item{{ID: Ptr(int64(21)), NodeID: Ptr("PVTI_view_item")}} + if diff := cmp.Diff(want, items); diff != "" { + t.Errorf("Projects.ListOrganizationProjectViewItems mismatch (-want +got):\n%v", diff) } const methodName = "ListOrganizationProjectViewItems" @@ -1724,8 +1727,9 @@ func TestProjectsService_ListUserProjectViewItems(t *testing.T) { if err != nil { t.Fatalf("Projects.ListUserProjectViewItems returned error: %v", err) } - if len(items) != 1 || items[0].GetID() != 22 { - t.Fatalf("Projects.ListUserProjectViewItems returned %+v", items) + want := []*ProjectV2Item{{ID: Ptr(int64(22)), NodeID: Ptr("PVTI_view_item_u")}} + if diff := cmp.Diff(want, items); diff != "" { + t.Errorf("Projects.ListUserProjectViewItems mismatch (-want +got):\n%v", diff) } const methodName = "ListUserProjectViewItems" From ef5df88bf6549d735b8bb1657d4846f153ab17d3 Mon Sep 17 00:00:00 2001 From: JamBalaya56562 Date: Wed, 24 Jun 2026 00:45:01 +0000 Subject: [PATCH 05/10] refactor: ProjectV2View required fields by value Per review (the consensus from #4202 that required response fields should be non-pointer), make ProjectV2View's required scalar fields non-pointer: id, number, name, layout, node_id, project_url and html_url. Optional (filter), nested struct (creator), timestamp and slice fields keep their existing pointer/slice forms. --- github/github-accessors.go | 42 ++++++++++++++++----------------- github/github-accessors_test.go | 35 ++++++--------------------- github/projects.go | 14 +++++------ github/projects_test.go | 10 ++++---- 4 files changed, 40 insertions(+), 61 deletions(-) diff --git a/github/github-accessors.go b/github/github-accessors.go index eb8b17089e3..6848eb28616 100644 --- a/github/github-accessors.go +++ b/github/github-accessors.go @@ -29414,60 +29414,60 @@ func (p *ProjectV2View) GetGroupBy() []int64 { return p.GroupBy } -// GetHTMLURL returns the HTMLURL field if it's non-nil, zero value otherwise. +// GetHTMLURL returns the HTMLURL field. func (p *ProjectV2View) GetHTMLURL() string { - if p == nil || p.HTMLURL == nil { + if p == nil { return "" } - return *p.HTMLURL + return p.HTMLURL } -// GetID returns the ID field if it's non-nil, zero value otherwise. +// GetID returns the ID field. func (p *ProjectV2View) GetID() int64 { - if p == nil || p.ID == nil { + if p == nil { return 0 } - return *p.ID + return p.ID } -// GetLayout returns the Layout field if it's non-nil, zero value otherwise. +// GetLayout returns the Layout field. func (p *ProjectV2View) GetLayout() string { - if p == nil || p.Layout == nil { + if p == nil { return "" } - return *p.Layout + return p.Layout } -// GetName returns the Name field if it's non-nil, zero value otherwise. +// GetName returns the Name field. func (p *ProjectV2View) GetName() string { - if p == nil || p.Name == nil { + if p == nil { return "" } - return *p.Name + return p.Name } -// GetNodeID returns the NodeID field if it's non-nil, zero value otherwise. +// GetNodeID returns the NodeID field. func (p *ProjectV2View) GetNodeID() string { - if p == nil || p.NodeID == nil { + if p == nil { return "" } - return *p.NodeID + return p.NodeID } -// GetNumber returns the Number field if it's non-nil, zero value otherwise. +// GetNumber returns the Number field. func (p *ProjectV2View) GetNumber() int { - if p == nil || p.Number == nil { + if p == nil { return 0 } - return *p.Number + return p.Number } -// GetProjectURL returns the ProjectURL field if it's non-nil, zero value otherwise. +// GetProjectURL returns the ProjectURL field. func (p *ProjectV2View) GetProjectURL() string { - if p == nil || p.ProjectURL == nil { + if p == nil { return "" } - return *p.ProjectURL + return p.ProjectURL } // GetSortBy returns the SortBy slice if it's non-nil, nil otherwise. diff --git a/github/github-accessors_test.go b/github/github-accessors_test.go index fdbd5e560d3..3c00e7358ba 100644 --- a/github/github-accessors_test.go +++ b/github/github-accessors_test.go @@ -37082,10 +37082,7 @@ func TestProjectV2View_GetGroupBy(tt *testing.T) { func TestProjectV2View_GetHTMLURL(tt *testing.T) { tt.Parallel() - var zeroValue string - p := &ProjectV2View{HTMLURL: &zeroValue} - p.GetHTMLURL() - p = &ProjectV2View{} + p := &ProjectV2View{} p.GetHTMLURL() p = nil p.GetHTMLURL() @@ -37093,10 +37090,7 @@ func TestProjectV2View_GetHTMLURL(tt *testing.T) { func TestProjectV2View_GetID(tt *testing.T) { tt.Parallel() - var zeroValue int64 - p := &ProjectV2View{ID: &zeroValue} - p.GetID() - p = &ProjectV2View{} + p := &ProjectV2View{} p.GetID() p = nil p.GetID() @@ -37104,10 +37098,7 @@ func TestProjectV2View_GetID(tt *testing.T) { func TestProjectV2View_GetLayout(tt *testing.T) { tt.Parallel() - var zeroValue string - p := &ProjectV2View{Layout: &zeroValue} - p.GetLayout() - p = &ProjectV2View{} + p := &ProjectV2View{} p.GetLayout() p = nil p.GetLayout() @@ -37115,10 +37106,7 @@ func TestProjectV2View_GetLayout(tt *testing.T) { func TestProjectV2View_GetName(tt *testing.T) { tt.Parallel() - var zeroValue string - p := &ProjectV2View{Name: &zeroValue} - p.GetName() - p = &ProjectV2View{} + p := &ProjectV2View{} p.GetName() p = nil p.GetName() @@ -37126,10 +37114,7 @@ func TestProjectV2View_GetName(tt *testing.T) { func TestProjectV2View_GetNodeID(tt *testing.T) { tt.Parallel() - var zeroValue string - p := &ProjectV2View{NodeID: &zeroValue} - p.GetNodeID() - p = &ProjectV2View{} + p := &ProjectV2View{} p.GetNodeID() p = nil p.GetNodeID() @@ -37137,10 +37122,7 @@ func TestProjectV2View_GetNodeID(tt *testing.T) { func TestProjectV2View_GetNumber(tt *testing.T) { tt.Parallel() - var zeroValue int - p := &ProjectV2View{Number: &zeroValue} - p.GetNumber() - p = &ProjectV2View{} + p := &ProjectV2View{} p.GetNumber() p = nil p.GetNumber() @@ -37148,10 +37130,7 @@ func TestProjectV2View_GetNumber(tt *testing.T) { func TestProjectV2View_GetProjectURL(tt *testing.T) { tt.Parallel() - var zeroValue string - p := &ProjectV2View{ProjectURL: &zeroValue} - p.GetProjectURL() - p = &ProjectV2View{} + p := &ProjectV2View{} p.GetProjectURL() p = nil p.GetProjectURL() diff --git a/github/projects.go b/github/projects.go index f2cddb31f62..ebac6fd0342 100644 --- a/github/projects.go +++ b/github/projects.go @@ -878,13 +878,13 @@ func (s *ProjectsService) AddUserProjectField(ctx context.Context, username stri // ProjectV2View represents a view in a project. type ProjectV2View struct { - ID *int64 `json:"id,omitempty"` - Number *int `json:"number,omitempty"` - Name *string `json:"name,omitempty"` - Layout *string `json:"layout,omitempty"` - NodeID *string `json:"node_id,omitempty"` - ProjectURL *string `json:"project_url,omitempty"` - HTMLURL *string `json:"html_url,omitempty"` + ID int64 `json:"id"` + Number int `json:"number"` + Name string `json:"name"` + Layout string `json:"layout"` + NodeID string `json:"node_id"` + ProjectURL string `json:"project_url"` + HTMLURL string `json:"html_url"` Creator *User `json:"creator,omitempty"` Filter *string `json:"filter,omitempty"` // VisibleFields holds the IDs of the fields displayed in the view. diff --git a/github/projects_test.go b/github/projects_test.go index 5b7b5802654..b0e363993ea 100644 --- a/github/projects_test.go +++ b/github/projects_test.go @@ -1617,10 +1617,10 @@ func TestProjectsService_CreateOrganizationProjectView(t *testing.T) { t.Fatalf("Projects.CreateOrganizationProjectView returned error: %v", err) } want := &ProjectV2View{ - ID: Ptr(int64(5)), - Number: Ptr(2), - Name: Ptr("My board"), - Layout: Ptr("board"), + ID: 5, + Number: 2, + Name: "My board", + Layout: "board", SortBy: []*ProjectV2ViewSortBy{ {FieldID: Ptr(int64(9007199254740993)), Direction: Ptr("asc")}, {FieldID: Ptr(int64(456)), Direction: Ptr("desc")}, @@ -1661,7 +1661,7 @@ func TestProjectsService_CreateUserProjectView(t *testing.T) { if err != nil { t.Fatalf("Projects.CreateUserProjectView returned error: %v", err) } - want := &ProjectV2View{ID: Ptr(int64(6)), Number: Ptr(3), Name: Ptr("My table"), Layout: Ptr("table")} + want := &ProjectV2View{ID: 6, Number: 3, Name: "My table", Layout: "table"} if diff := cmp.Diff(want, view); diff != "" { t.Errorf("Projects.CreateUserProjectView mismatch (-want +got):\n%v", diff) } From 38fa4d7f5fb4830e08ab2f06420870372fccbe72 Mon Sep 17 00:00:00 2001 From: JamBalaya56562 Date: Wed, 24 Jun 2026 00:45:01 +0000 Subject: [PATCH 06/10] refactor: Tidy view error messages and comments Per review, drop the redundant "ProjectV2ViewSortBy: " prefix from the UnmarshalJSON error messages (and wrap err2, the actual string-parse error, in that branch), and remove the "(Required.)" notes from the CreateProjectV2ViewRequest Name and Layout field comments. --- github/projects.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/github/projects.go b/github/projects.go index ebac6fd0342..d27a77e9279 100644 --- a/github/projects.go +++ b/github/projects.go @@ -918,7 +918,7 @@ func (s *ProjectV2ViewSortBy) UnmarshalJSON(data []byte) error { return err } if len(tuple) != 2 { - return fmt.Errorf("ProjectV2ViewSortBy: expected a [field_id, direction] tuple, got %v elements", len(tuple)) + return fmt.Errorf("expected a [field_id, direction] tuple, got %v elements", len(tuple)) } var fieldID int64 @@ -926,17 +926,17 @@ func (s *ProjectV2ViewSortBy) UnmarshalJSON(data []byte) error { // The OpenAPI schema allows the field_id to be a string as well. var str string if err2 := json.Unmarshal(tuple[0], &str); err2 != nil { - return fmt.Errorf("ProjectV2ViewSortBy: invalid field_id: %w", err) + return fmt.Errorf("invalid field_id: %w", err2) } fieldID, err = strconv.ParseInt(str, 10, 64) if err != nil { - return fmt.Errorf("ProjectV2ViewSortBy: invalid field_id %q: %w", str, err) + return fmt.Errorf("invalid field_id %q: %w", str, err) } } var direction string if err := json.Unmarshal(tuple[1], &direction); err != nil { - return fmt.Errorf("ProjectV2ViewSortBy: invalid direction: %w", err) + return fmt.Errorf("invalid direction: %w", err) } s.FieldID = &fieldID @@ -951,9 +951,9 @@ func (s ProjectV2ViewSortBy) MarshalJSON() ([]byte, error) { // CreateProjectV2ViewRequest specifies the parameters to create a project view. type CreateProjectV2ViewRequest struct { - // Name is the view's display name. (Required.) + // Name is the view's display name. Name string `json:"name"` - // Layout is the view's layout. One of: table, board, roadmap. (Required.) + // Layout is the view's layout. One of: table, board, roadmap. Layout string `json:"layout"` // Filter is an optional query string to filter the items shown in the view. Filter *string `json:"filter,omitempty"` From 572fc7088e8495f91d213abd571ba522dc81688a Mon Sep 17 00:00:00 2001 From: JamBalaya56562 Date: Wed, 24 Jun 2026 00:45:01 +0000 Subject: [PATCH 07/10] refactor: Polish ProjectV2View and SortBy structs Per review, drop the inconsistent per-field comments from ProjectV2View so the struct matches the sibling ProjectV2 (no per-field comments), and add an explicit json:"-" tag to ProjectV2ViewSortBy's FieldID and Direction fields, which are (de)serialized through custom MarshalJSON / UnmarshalJSON rather than struct tags. --- github/projects.go | 38 +++++++++++++++++--------------------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/github/projects.go b/github/projects.go index d27a77e9279..6c01a1b6f13 100644 --- a/github/projects.go +++ b/github/projects.go @@ -878,25 +878,21 @@ func (s *ProjectsService) AddUserProjectField(ctx context.Context, username stri // ProjectV2View represents a view in a project. type ProjectV2View struct { - ID int64 `json:"id"` - Number int `json:"number"` - Name string `json:"name"` - Layout string `json:"layout"` - NodeID string `json:"node_id"` - ProjectURL string `json:"project_url"` - HTMLURL string `json:"html_url"` - Creator *User `json:"creator,omitempty"` - Filter *string `json:"filter,omitempty"` - // VisibleFields holds the IDs of the fields displayed in the view. - VisibleFields []int64 `json:"visible_fields,omitempty"` - // SortBy holds the view's sorting configuration, in priority order. - SortBy []*ProjectV2ViewSortBy `json:"sort_by,omitempty"` - // GroupBy holds the IDs of the fields the view is grouped by. - GroupBy []int64 `json:"group_by,omitempty"` - // VerticalGroupBy holds the IDs of the fields the view is vertically grouped by. - VerticalGroupBy []int64 `json:"vertical_group_by,omitempty"` - CreatedAt *Timestamp `json:"created_at,omitempty"` - UpdatedAt *Timestamp `json:"updated_at,omitempty"` + ID int64 `json:"id"` + Number int `json:"number"` + Name string `json:"name"` + Layout string `json:"layout"` + NodeID string `json:"node_id"` + ProjectURL string `json:"project_url"` + HTMLURL string `json:"html_url"` + Creator *User `json:"creator,omitempty"` + Filter *string `json:"filter,omitempty"` + VisibleFields []int64 `json:"visible_fields,omitempty"` + SortBy []*ProjectV2ViewSortBy `json:"sort_by,omitempty"` + GroupBy []int64 `json:"group_by,omitempty"` + VerticalGroupBy []int64 `json:"vertical_group_by,omitempty"` + CreatedAt *Timestamp `json:"created_at,omitempty"` + UpdatedAt *Timestamp `json:"updated_at,omitempty"` } // ProjectV2ViewSortBy represents a single sort criterion of a project view. @@ -905,9 +901,9 @@ type ProjectV2View struct { // object, so it has custom JSON (un)marshaling. type ProjectV2ViewSortBy struct { // FieldID is the ID of the field to sort by. - FieldID *int64 + FieldID *int64 `json:"-"` // Direction is the sort direction, one of "asc" or "desc". - Direction *string + Direction *string `json:"-"` } // UnmarshalJSON implements custom unmarshaling for the [field_id, direction] From 21ef628d47798b6122aac5ccdef51f23d66bedb8 Mon Sep 17 00:00:00 2001 From: JamBalaya56562 Date: Wed, 24 Jun 2026 00:45:01 +0000 Subject: [PATCH 08/10] refactor: Drop redundant (Required.)/(Optional.) field notes Per review, remove the parenthetical (Required.)/(Optional.) annotations from CreateProjectV2DraftItemRequest.Title/Body and the single-select option Name comments; the value/pointer type already conveys this. The conditional notes on AddProjectV2FieldRequest (required for single_select / iteration) are kept since they describe the one-of constraint. --- github/projects.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/github/projects.go b/github/projects.go index 6c01a1b6f13..33b7573cb6e 100644 --- a/github/projects.go +++ b/github/projects.go @@ -732,9 +732,9 @@ func (s *ProjectsService) DeleteUserProjectItem(ctx context.Context, username st // CreateProjectV2DraftItemRequest specifies the parameters to create a draft item in a project. type CreateProjectV2DraftItemRequest struct { - // Title is the title of the draft issue item to create. (Required.) + // Title is the title of the draft issue item to create. Title string `json:"title"` - // Body is the body content of the draft issue item to create. (Optional.) + // Body is the body content of the draft issue item to create. Body *string `json:"body,omitempty"` } @@ -785,7 +785,7 @@ func (s *ProjectsService) CreateUserProjectDraftItem(ctx context.Context, userID // ProjectV2FieldSingleSelectOption represents an option to create for a single_select project field. type ProjectV2FieldSingleSelectOption struct { - // Name is the display name of the option. (Required.) + // Name is the display name of the option. Name string `json:"name"` // Color is the color associated with the option. // One of: BLUE, GRAY, GREEN, ORANGE, PINK, PURPLE, RED, YELLOW. From 10bea9e262ce925d9d4cddd369bf968a6b9c9c17 Mon Sep 17 00:00:00 2001 From: JamBalaya56562 Date: Wed, 24 Jun 2026 00:48:41 +0000 Subject: [PATCH 09/10] refactor: Drop pointers from remaining ProjectV2View required fields Per the OpenAPI schema, creator, created_at, updated_at, visible_fields, sort_by, group_by and vertical_group_by are required on the project view response, so they should be non-pointer values without omitempty (matching the convention that only optional fields are pointers). The scalar fields were already converted; this does the same for the object, timestamp and slice fields. Filter stays a pointer as it is optional. Accessors are regenerated accordingly. --- github/github-accessors.go | 16 ++++++++-------- github/github-accessors_test.go | 10 ++-------- github/projects.go | 14 +++++++------- 3 files changed, 17 insertions(+), 23 deletions(-) diff --git a/github/github-accessors.go b/github/github-accessors.go index 6848eb28616..99c123a1ea3 100644 --- a/github/github-accessors.go +++ b/github/github-accessors.go @@ -29382,18 +29382,18 @@ func (p *ProjectV2TextContent) GetRaw() string { return *p.Raw } -// GetCreatedAt returns the CreatedAt field if it's non-nil, zero value otherwise. +// GetCreatedAt returns the CreatedAt field. func (p *ProjectV2View) GetCreatedAt() Timestamp { - if p == nil || p.CreatedAt == nil { + if p == nil { return Timestamp{} } - return *p.CreatedAt + return p.CreatedAt } // GetCreator returns the Creator field. -func (p *ProjectV2View) GetCreator() *User { +func (p *ProjectV2View) GetCreator() User { if p == nil { - return nil + return User{} } return p.Creator } @@ -29478,12 +29478,12 @@ func (p *ProjectV2View) GetSortBy() []*ProjectV2ViewSortBy { return p.SortBy } -// GetUpdatedAt returns the UpdatedAt field if it's non-nil, zero value otherwise. +// GetUpdatedAt returns the UpdatedAt field. func (p *ProjectV2View) GetUpdatedAt() Timestamp { - if p == nil || p.UpdatedAt == nil { + if p == nil { return Timestamp{} } - return *p.UpdatedAt + return p.UpdatedAt } // GetVerticalGroupBy returns the VerticalGroupBy slice if it's non-nil, nil otherwise. diff --git a/github/github-accessors_test.go b/github/github-accessors_test.go index 3c00e7358ba..9392ba021ac 100644 --- a/github/github-accessors_test.go +++ b/github/github-accessors_test.go @@ -37041,10 +37041,7 @@ func TestProjectV2TextContent_GetRaw(tt *testing.T) { func TestProjectV2View_GetCreatedAt(tt *testing.T) { tt.Parallel() - var zeroValue Timestamp - p := &ProjectV2View{CreatedAt: &zeroValue} - p.GetCreatedAt() - p = &ProjectV2View{} + p := &ProjectV2View{} p.GetCreatedAt() p = nil p.GetCreatedAt() @@ -37149,10 +37146,7 @@ func TestProjectV2View_GetSortBy(tt *testing.T) { func TestProjectV2View_GetUpdatedAt(tt *testing.T) { tt.Parallel() - var zeroValue Timestamp - p := &ProjectV2View{UpdatedAt: &zeroValue} - p.GetUpdatedAt() - p = &ProjectV2View{} + p := &ProjectV2View{} p.GetUpdatedAt() p = nil p.GetUpdatedAt() diff --git a/github/projects.go b/github/projects.go index 33b7573cb6e..53c999121ee 100644 --- a/github/projects.go +++ b/github/projects.go @@ -885,14 +885,14 @@ type ProjectV2View struct { NodeID string `json:"node_id"` ProjectURL string `json:"project_url"` HTMLURL string `json:"html_url"` - Creator *User `json:"creator,omitempty"` + Creator User `json:"creator"` Filter *string `json:"filter,omitempty"` - VisibleFields []int64 `json:"visible_fields,omitempty"` - SortBy []*ProjectV2ViewSortBy `json:"sort_by,omitempty"` - GroupBy []int64 `json:"group_by,omitempty"` - VerticalGroupBy []int64 `json:"vertical_group_by,omitempty"` - CreatedAt *Timestamp `json:"created_at,omitempty"` - UpdatedAt *Timestamp `json:"updated_at,omitempty"` + VisibleFields []int64 `json:"visible_fields"` + SortBy []*ProjectV2ViewSortBy `json:"sort_by"` + GroupBy []int64 `json:"group_by"` + VerticalGroupBy []int64 `json:"vertical_group_by"` + CreatedAt Timestamp `json:"created_at"` + UpdatedAt Timestamp `json:"updated_at"` } // ProjectV2ViewSortBy represents a single sort criterion of a project view. From ac5bf327894bd93d04d54a3b5af7fc0097d3ba00 Mon Sep 17 00:00:00 2001 From: JamBalaya56562 Date: Wed, 24 Jun 2026 18:19:17 +0900 Subject: [PATCH 10/10] test: Refactor SortBy unmarshal test like #4323 Use the testJSONUnmarshalOnly helper for the success case of TestProjectV2ViewSortBy_UnmarshalJSON instead of unmarshaling by hand and asserting field by field, matching the style introduced in #4323. The error-case subtests stay manual since the helper expects a successful unmarshal. --- github/projects_test.go | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/github/projects_test.go b/github/projects_test.go index b0e363993ea..4206a44c15c 100644 --- a/github/projects_test.go +++ b/github/projects_test.go @@ -1753,19 +1753,11 @@ func TestProjectV2ViewSortBy_UnmarshalJSON(t *testing.T) { t.Parallel() // The second element uses a string field_id, which the OpenAPI schema // permits. The first exceeds 2^53 to confirm int64 (not float64) decoding. - var got []*ProjectV2ViewSortBy - if err := json.Unmarshal([]byte(`[[9007199254740993,"asc"],["456","desc"]]`), &got); err != nil { - t.Fatalf("Unmarshal returned error: %v", err) - } - if len(got) != 2 { - t.Fatalf("len(got) = %d, want 2", len(got)) - } - if got[0].GetFieldID() != 9007199254740993 || got[0].GetDirection() != "asc" { - t.Errorf("got[0] = %+v, want {9007199254740993 asc}", got[0]) - } - if got[1].GetFieldID() != 456 || got[1].GetDirection() != "desc" { - t.Errorf("got[1] = %+v, want {456 desc}", got[1]) + want := []*ProjectV2ViewSortBy{ + {FieldID: Ptr(int64(9007199254740993)), Direction: Ptr("asc")}, + {FieldID: Ptr(int64(456)), Direction: Ptr("desc")}, } + testJSONUnmarshalOnly(t, want, `[[9007199254740993,"asc"],["456","desc"]]`) }) t.Run("wrong tuple length is an error", func(t *testing.T) {