From 775f0f6b50673104a05a028b2d6f27ae6bfad7f2 Mon Sep 17 00:00:00 2001 From: JoannaaKL Date: Wed, 17 Sep 2025 14:31:10 +0000 Subject: [PATCH 01/19] Add support for projects V2 --- github/github.go | 2 + github/projects.go | 120 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 122 insertions(+) create mode 100644 github/projects.go diff --git a/github/github.go b/github/github.go index b1e7b3ddaf1..f434fdfaa16 100644 --- a/github/github.go +++ b/github/github.go @@ -218,6 +218,7 @@ type Client struct { Meta *MetaService Migrations *MigrationService Organizations *OrganizationsService + Projects *ProjectsService PullRequests *PullRequestsService RateLimit *RateLimitService Reactions *ReactionsService @@ -456,6 +457,7 @@ func (c *Client) initialize() { c.Meta = (*MetaService)(&c.common) c.Migrations = (*MigrationService)(&c.common) c.Organizations = (*OrganizationsService)(&c.common) + c.Projects = (*ProjectsService)(&c.common) c.PullRequests = (*PullRequestsService)(&c.common) c.RateLimit = (*RateLimitService)(&c.common) c.Reactions = (*ReactionsService)(&c.common) diff --git a/github/projects.go b/github/projects.go new file mode 100644 index 00000000000..09978427eb8 --- /dev/null +++ b/github/projects.go @@ -0,0 +1,120 @@ +// Copyright 2013 The go-github AUTHORS. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package github + +import ( + "context" + "fmt" +) + +// ProjectsService handles communication with the project V2 +// methods of the GitHub API. +// +// GitHub API docs: https://docs.github.com/rest/projects/projects +type ProjectsService service + +func (p ProjectV2) String() string { return Stringify(p) } + +// ListProjectsOptions specifies optional parameters to list organization projects. +type ListProjectsOptions struct { + // Q is an optional query string to filter/search projects (when supported). + Q string `url:"q,omitempty"` + ListOptions + ListCursorOptions +} + +// ListOrganizationProjects lists Projects V2 for an organization. +// +// GitHub API docs: https://docs.github.com/rest/projects/projects#list-organization-projects +// +//meta:operation GET /orgs/{org}/projectsV2 +func (s *ProjectsService) ListOrganizationProjects(ctx context.Context, org string, opts *ListProjectsOptions) ([]*ProjectV2, *Response, error) { + u := fmt.Sprintf("orgs/%v/projectsV2", org) + u, err := addOptions(u, opts) + if err != nil { + return nil, nil, err + } + + req, err := s.client.NewRequest("GET", u, nil) + if err != nil { + return nil, nil, err + } + req.Header.Set("Accept", mediaTypeProjectsPreview) + + var projects []*ProjectV2 + resp, err := s.client.Do(ctx, req, &projects) + if err != nil { + return nil, resp, err + } + return projects, resp, nil +} + +// GetByOrg gets a Projects V2 project for an organization by ID. +// +// GitHub API docs: https://docs.github.com/rest/projects/projects#get-project-for-organization +// +//meta:operation GET /orgs/{org}/projectsV2/{project_id} +func (s *ProjectsService) GetByOrg(ctx context.Context, org string, projectID int64) (*ProjectV2, *Response, error) { + u := fmt.Sprintf("orgs/%v/projectsV2/%v", org, projectID) + req, err := s.client.NewRequest("GET", u, nil) + if err != nil { + return nil, nil, err + } + req.Header.Set("Accept", mediaTypeProjectsPreview) + + project := new(ProjectV2) + resp, err := s.client.Do(ctx, req, project) + if err != nil { + return nil, resp, err + } + return project, resp, nil +} + +// ListByUser lists Projects V2 for a user. +// +// GitHub API docs: https://docs.github.com/en/rest/projects/projects#list-projects-for-user +// +//meta:operation GET /users/{username}/projectsV2 +func (s *ProjectsService) ListByUser(ctx context.Context, username string, opts *ListProjectsOptions) ([]*ProjectV2, *Response, error) { + u := fmt.Sprintf("users/%v/projectsV2", username) + u, err := addOptions(u, opts) + if err != nil { + return nil, nil, err + } + req, err := s.client.NewRequest("GET", u, nil) + if err != nil { + return nil, nil, err + } + req.Header.Set("Accept", mediaTypeProjectsPreview) + + var projects []*ProjectV2 + resp, err := s.client.Do(ctx, req, &projects) + if err != nil { + return nil, resp, err + } + return projects, resp, nil +} + +// GetUserProject gets a Projects V2 project for a user by ID. +// +// GitHub API docs: https://docs.github.com/en/rest/projects/projects#get-project-for-user +// +//meta:operation GET /users/{username}/projectsV2/{project_id} +func (s *ProjectsService) GetUserProject(ctx context.Context, username string, projectID int64) (*ProjectV2, *Response, error) { + u := fmt.Sprintf("users/%v/projectsV2/%v", username, projectID) + req, err := s.client.NewRequest("GET", u, nil) + if err != nil { + return nil, nil, err + } + req.Header.Set("Accept", mediaTypeProjectsPreview) + + project := new(ProjectV2) + resp, err := s.client.Do(ctx, req, project) + if err != nil { + return nil, resp, err + } + return project, resp, nil +} From 74712bd3d1b6af699dc3af74208fee2e1d229c68 Mon Sep 17 00:00:00 2001 From: JoannaaKL Date: Wed, 17 Sep 2025 14:35:27 +0000 Subject: [PATCH 02/19] Add test --- github/projects_test.go | 165 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 165 insertions(+) create mode 100644 github/projects_test.go diff --git a/github/projects_test.go b/github/projects_test.go new file mode 100644 index 00000000000..2a305d83b0e --- /dev/null +++ b/github/projects_test.go @@ -0,0 +1,165 @@ +package github + +import ( + "context" + "fmt" + "net/http" + "testing" +) + +func TestProjectsService_ListOrganizationProjects(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/orgs/o/projectsV2", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + testHeader(t, r, "Accept", mediaTypeProjectsPreview) + // Expect query params q, page, per_page when provided + testFormValues(t, r, values{"q": "alpha", "page": "2", "per_page": "1"}) + fmt.Fprint(w, `[{"id":1,"title":"T1","created_at":"2011-01-02T15:04:05Z","updated_at":"2012-01-02T15:04:05Z"}]`) + }) + + opts := &ListProjectsOptions{Q: "alpha", ListOptions: ListOptions{Page: 2, PerPage: 1}} + ctx := context.Background() + projects, _, err := client.Projects.ListOrganizationProjects(ctx, "o", opts) + if err != nil { + t.Fatalf("Projects.ListOrganizationProjects returned error: %v", err) + } + if len(projects) != 1 || projects[0].GetID() != 1 || projects[0].GetTitle() != "T1" { + t.Fatalf("Projects.ListOrganizationProjects returned %+v", projects) + } + + const methodName = "ListOrganizationProjects" + testBadOptions(t, methodName, func() (err error) { + _, _, err = client.Projects.ListOrganizationProjects(ctx, "\n", opts) + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Projects.ListOrganizationProjects(ctx, "o", opts) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + +func TestProjectsService_GetOrganizationProject(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/orgs/o/projectsV2/1", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + testHeader(t, r, "Accept", mediaTypeProjectsPreview) + fmt.Fprint(w, `{"id":1,"title":"OrgProj","created_at":"2011-01-02T15:04:05Z","updated_at":"2012-01-02T15:04:05Z"}`) + }) + + ctx := context.Background() + project, _, err := client.Projects.GetByOrg(ctx, "o", 1) + if err != nil { + t.Fatalf("Projects.GetByOrg returned error: %v", err) + } + if project.GetID() != 1 || project.GetTitle() != "OrgProj" { + t.Fatalf("Projects.GetByOrg returned %+v", project) + } + + const methodName = "GetByOrg" + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Projects.GetByOrg(ctx, "o", 1) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + +func TestProjectsService_ListUserProjects(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/users/u/projectsV2", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + testHeader(t, r, "Accept", mediaTypeProjectsPreview) + testFormValues(t, r, values{"q": "beta", "page": "1", "per_page": "2"}) + fmt.Fprint(w, `[{"id":2,"title":"UProj","created_at":"2011-01-02T15:04:05Z","updated_at":"2012-01-02T15:04:05Z"}]`) + }) + + opts := &ListProjectsOptions{Q: "beta", ListOptions: ListOptions{Page: 1, PerPage: 2}} + ctx := context.Background() + projects, _, err := client.Projects.ListByUser(ctx, "u", opts) + if err != nil { + t.Fatalf("Projects.ListByUser returned error: %v", err) + } + if len(projects) != 1 || projects[0].GetID() != 2 || projects[0].GetTitle() != "UProj" { + t.Fatalf("Projects.ListByUser returned %+v", projects) + } + + const methodName = "ListByUser" + testBadOptions(t, methodName, func() (err error) { + _, _, err = client.Projects.ListByUser(ctx, "\n", opts) + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Projects.ListByUser(ctx, "u", opts) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + +func TestProjectsService_GetUserProject(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/users/u/projectsV2/2", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + testHeader(t, r, "Accept", mediaTypeProjectsPreview) + fmt.Fprint(w, `{"id":2,"title":"UProj","created_at":"2011-01-02T15:04:05Z","updated_at":"2012-01-02T15:04:05Z"}`) + }) + + ctx := context.Background() + project, _, err := client.Projects.GetUserProject(ctx, "u", 2) + if err != nil { + t.Fatalf("Projects.GetUserProject returned error: %v", err) + } + if project.GetID() != 2 || project.GetTitle() != "UProj" { + t.Fatalf("Projects.GetUserProject returned %+v", project) + } + + const methodName = "GetUserProject" + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Projects.GetUserProject(ctx, "u", 2) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + +// Marshal test ensures V2 fields marshal correctly. +func TestProjectV2_Marshal(t *testing.T) { + t.Parallel() + testJSONMarshal(t, &ProjectV2{}, "{}") + + p := &ProjectV2{ + ID: Ptr(int64(10)), + Title: Ptr("Title"), + Description: Ptr("Desc"), + Public: Ptr(true), + CreatedAt: &Timestamp{referenceTime}, + UpdatedAt: &Timestamp{referenceTime}, + } + + want := `{ + "id": 10, + "title": "Title", + "description": "Desc", + "public": true, + "created_at": ` + referenceTimeStr + `, + "updated_at": ` + referenceTimeStr + ` + }` + + testJSONMarshal(t, p, want) +} From 3a8cf0c0852698f4d715ece629aff8bb57b1c0b2 Mon Sep 17 00:00:00 2001 From: JoannaaKL Date: Mon, 22 Sep 2025 13:49:18 +0000 Subject: [PATCH 03/19] Address feedback --- github/projects.go | 15 +++++++++++---- github/projects_test.go | 6 +++--- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/github/projects.go b/github/projects.go index 09978427eb8..264a800b5b3 100644 --- a/github/projects.go +++ b/github/projects.go @@ -18,12 +18,19 @@ type ProjectsService service func (p ProjectV2) String() string { return Stringify(p) } -// ListProjectsOptions specifies optional parameters to list organization projects. +// ListProjectsOptions specifies optional parameters to list projects for user / organization. type ListProjectsOptions struct { + // A cursor, as given in the Link header. If specified, the query only searches for events before this cursor. + Before string `url:"before,omitempty"` + + // A cursor, as given in the Link header. If specified, the query only searches for events after this cursor. + After string `url:"after,omitempty"` + + // For paginated result sets, the number of results to include per page. + PerPage int `url:"per_page,omitempty"` + // Q is an optional query string to filter/search projects (when supported). - Q string `url:"q,omitempty"` - ListOptions - ListCursorOptions + Query string `url:"q,omitempty"` } // ListOrganizationProjects lists Projects V2 for an organization. diff --git a/github/projects_test.go b/github/projects_test.go index 2a305d83b0e..017b89e7577 100644 --- a/github/projects_test.go +++ b/github/projects_test.go @@ -19,7 +19,7 @@ func TestProjectsService_ListOrganizationProjects(t *testing.T) { fmt.Fprint(w, `[{"id":1,"title":"T1","created_at":"2011-01-02T15:04:05Z","updated_at":"2012-01-02T15:04:05Z"}]`) }) - opts := &ListProjectsOptions{Q: "alpha", ListOptions: ListOptions{Page: 2, PerPage: 1}} + opts := &ListProjectsOptions{Query: "alpha", After: "2", Before: "1"} ctx := context.Background() projects, _, err := client.Projects.ListOrganizationProjects(ctx, "o", opts) if err != nil { @@ -80,11 +80,11 @@ func TestProjectsService_ListUserProjects(t *testing.T) { mux.HandleFunc("/users/u/projectsV2", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, "GET") testHeader(t, r, "Accept", mediaTypeProjectsPreview) - testFormValues(t, r, values{"q": "beta", "page": "1", "per_page": "2"}) + testFormValues(t, r, values{"q": "beta", "before": "1", "after": "2", "per_page": "2"}) fmt.Fprint(w, `[{"id":2,"title":"UProj","created_at":"2011-01-02T15:04:05Z","updated_at":"2012-01-02T15:04:05Z"}]`) }) - opts := &ListProjectsOptions{Q: "beta", ListOptions: ListOptions{Page: 1, PerPage: 2}} + opts := &ListProjectsOptions{Query: "beta", Before: "1", After: "2", PerPage: 2} ctx := context.Background() projects, _, err := client.Projects.ListByUser(ctx, "u", opts) if err != nil { From e93ed36abae785f1434f1e50b4f5ac60182bb7de Mon Sep 17 00:00:00 2001 From: JoannaaKL Date: Mon, 22 Sep 2025 19:01:59 +0000 Subject: [PATCH 04/19] Remove header and rename functions --- github/projects.go | 50 ++++++++++++++++------------------- github/projects_test.go | 58 +++++++++++++++++++---------------------- 2 files changed, 50 insertions(+), 58 deletions(-) diff --git a/github/projects.go b/github/projects.go index 264a800b5b3..2631968826e 100644 --- a/github/projects.go +++ b/github/projects.go @@ -38,7 +38,7 @@ type ListProjectsOptions struct { // GitHub API docs: https://docs.github.com/rest/projects/projects#list-organization-projects // //meta:operation GET /orgs/{org}/projectsV2 -func (s *ProjectsService) ListOrganizationProjects(ctx context.Context, org string, opts *ListProjectsOptions) ([]*ProjectV2, *Response, error) { +func (s *ProjectsService) ListProjectsForOrganization(ctx context.Context, org string, opts *ListProjectsOptions) ([]*ProjectV2, *Response, error) { u := fmt.Sprintf("orgs/%v/projectsV2", org) u, err := addOptions(u, opts) if err != nil { @@ -49,7 +49,6 @@ func (s *ProjectsService) ListOrganizationProjects(ctx context.Context, org stri if err != nil { return nil, nil, err } - req.Header.Set("Accept", mediaTypeProjectsPreview) var projects []*ProjectV2 resp, err := s.client.Do(ctx, req, &projects) @@ -59,33 +58,12 @@ func (s *ProjectsService) ListOrganizationProjects(ctx context.Context, org stri return projects, resp, nil } -// GetByOrg gets a Projects V2 project for an organization by ID. -// -// GitHub API docs: https://docs.github.com/rest/projects/projects#get-project-for-organization -// -//meta:operation GET /orgs/{org}/projectsV2/{project_id} -func (s *ProjectsService) GetByOrg(ctx context.Context, org string, projectID int64) (*ProjectV2, *Response, error) { - u := fmt.Sprintf("orgs/%v/projectsV2/%v", org, projectID) - req, err := s.client.NewRequest("GET", u, nil) - if err != nil { - return nil, nil, err - } - req.Header.Set("Accept", mediaTypeProjectsPreview) - - project := new(ProjectV2) - resp, err := s.client.Do(ctx, req, project) - if err != nil { - return nil, resp, err - } - return project, resp, nil -} - // ListByUser lists Projects V2 for a user. // // GitHub API docs: https://docs.github.com/en/rest/projects/projects#list-projects-for-user // //meta:operation GET /users/{username}/projectsV2 -func (s *ProjectsService) ListByUser(ctx context.Context, username string, opts *ListProjectsOptions) ([]*ProjectV2, *Response, error) { +func (s *ProjectsService) ListProjectsForUser(ctx context.Context, username string, opts *ListProjectsOptions) ([]*ProjectV2, *Response, error) { u := fmt.Sprintf("users/%v/projectsV2", username) u, err := addOptions(u, opts) if err != nil { @@ -95,7 +73,6 @@ func (s *ProjectsService) ListByUser(ctx context.Context, username string, opts if err != nil { return nil, nil, err } - req.Header.Set("Accept", mediaTypeProjectsPreview) var projects []*ProjectV2 resp, err := s.client.Do(ctx, req, &projects) @@ -110,13 +87,32 @@ func (s *ProjectsService) ListByUser(ctx context.Context, username string, opts // GitHub API docs: https://docs.github.com/en/rest/projects/projects#get-project-for-user // //meta:operation GET /users/{username}/projectsV2/{project_id} -func (s *ProjectsService) GetUserProject(ctx context.Context, username string, projectID int64) (*ProjectV2, *Response, error) { +func (s *ProjectsService) GetProjectForUser(ctx context.Context, username string, projectID int64) (*ProjectV2, *Response, error) { u := fmt.Sprintf("users/%v/projectsV2/%v", username, projectID) req, err := s.client.NewRequest("GET", u, nil) if err != nil { return nil, nil, err } - req.Header.Set("Accept", mediaTypeProjectsPreview) + + project := new(ProjectV2) + resp, err := s.client.Do(ctx, req, project) + if err != nil { + return nil, resp, err + } + return project, resp, nil +} + +// GetByOrg gets a Projects V2 project for an organization by ID. +// +// GitHub API docs: https://docs.github.com/rest/projects/projects#get-project-for-organization +// +//meta:operation GET /orgs/{org}/projectsV2/{project_id} +func (s *ProjectsService) GetProjectForOrg(ctx context.Context, org string, projectID int64) (*ProjectV2, *Response, error) { + u := fmt.Sprintf("orgs/%v/projectsV2/%v", org, projectID) + req, err := s.client.NewRequest("GET", u, nil) + if err != nil { + return nil, nil, err + } project := new(ProjectV2) resp, err := s.client.Do(ctx, req, project) diff --git a/github/projects_test.go b/github/projects_test.go index 017b89e7577..056a47406c5 100644 --- a/github/projects_test.go +++ b/github/projects_test.go @@ -7,36 +7,35 @@ import ( "testing" ) -func TestProjectsService_ListOrganizationProjects(t *testing.T) { +func TestProjectsService_ListProjectsForOrganizations(t *testing.T) { t.Parallel() client, mux, _ := setup(t) mux.HandleFunc("/orgs/o/projectsV2", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, "GET") - testHeader(t, r, "Accept", mediaTypeProjectsPreview) - // Expect query params q, page, per_page when provided - testFormValues(t, r, values{"q": "alpha", "page": "2", "per_page": "1"}) + // Expect query params q, after, before when provided + testFormValues(t, r, values{"q": "alpha", "after": "2", "before": "1"}) fmt.Fprint(w, `[{"id":1,"title":"T1","created_at":"2011-01-02T15:04:05Z","updated_at":"2012-01-02T15:04:05Z"}]`) }) opts := &ListProjectsOptions{Query: "alpha", After: "2", Before: "1"} ctx := context.Background() - projects, _, err := client.Projects.ListOrganizationProjects(ctx, "o", opts) + projects, _, err := client.Projects.ListProjectsForOrganization(ctx, "o", opts) if err != nil { - t.Fatalf("Projects.ListOrganizationProjects returned error: %v", err) + t.Fatalf("Projects.ListProjectsForOrganization returned error: %v", err) } if len(projects) != 1 || projects[0].GetID() != 1 || projects[0].GetTitle() != "T1" { - t.Fatalf("Projects.ListOrganizationProjects returned %+v", projects) + t.Fatalf("Projects.ListProjectsForOrganization returned %+v", projects) } - const methodName = "ListOrganizationProjects" + const methodName = "ListProjectsForOrganization" testBadOptions(t, methodName, func() (err error) { - _, _, err = client.Projects.ListOrganizationProjects(ctx, "\n", opts) + _, _, err = client.Projects.ListProjectsForOrganization(ctx, "\n", opts) return err }) testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { - got, resp, err := client.Projects.ListOrganizationProjects(ctx, "o", opts) + got, resp, err := client.Projects.ListProjectsForOrganization(ctx, "o", opts) if got != nil { t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) } @@ -44,28 +43,27 @@ func TestProjectsService_ListOrganizationProjects(t *testing.T) { }) } -func TestProjectsService_GetOrganizationProject(t *testing.T) { +func TestProjectsService_GetProjectForOrg(t *testing.T) { t.Parallel() client, mux, _ := setup(t) mux.HandleFunc("/orgs/o/projectsV2/1", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, "GET") - testHeader(t, r, "Accept", mediaTypeProjectsPreview) fmt.Fprint(w, `{"id":1,"title":"OrgProj","created_at":"2011-01-02T15:04:05Z","updated_at":"2012-01-02T15:04:05Z"}`) }) ctx := context.Background() - project, _, err := client.Projects.GetByOrg(ctx, "o", 1) + project, _, err := client.Projects.GetProjectForOrg(ctx, "o", 1) if err != nil { - t.Fatalf("Projects.GetByOrg returned error: %v", err) + t.Fatalf("Projects.GetProjectForOrg returned error: %v", err) } if project.GetID() != 1 || project.GetTitle() != "OrgProj" { - t.Fatalf("Projects.GetByOrg returned %+v", project) + t.Fatalf("Projects.GetProjectForOrg returned %+v", project) } - const methodName = "GetByOrg" + const methodName = "GetProjectForOrg" testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { - got, resp, err := client.Projects.GetByOrg(ctx, "o", 1) + got, resp, err := client.Projects.GetProjectForOrg(ctx, "o", 1) if got != nil { t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) } @@ -79,29 +77,28 @@ func TestProjectsService_ListUserProjects(t *testing.T) { mux.HandleFunc("/users/u/projectsV2", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, "GET") - testHeader(t, r, "Accept", mediaTypeProjectsPreview) testFormValues(t, r, values{"q": "beta", "before": "1", "after": "2", "per_page": "2"}) fmt.Fprint(w, `[{"id":2,"title":"UProj","created_at":"2011-01-02T15:04:05Z","updated_at":"2012-01-02T15:04:05Z"}]`) }) opts := &ListProjectsOptions{Query: "beta", Before: "1", After: "2", PerPage: 2} ctx := context.Background() - projects, _, err := client.Projects.ListByUser(ctx, "u", opts) + projects, _, err := client.Projects.ListProjectsForUser(ctx, "u", opts) if err != nil { - t.Fatalf("Projects.ListByUser returned error: %v", err) + t.Fatalf("Projects.ListProjectsForUser returned error: %v", err) } if len(projects) != 1 || projects[0].GetID() != 2 || projects[0].GetTitle() != "UProj" { - t.Fatalf("Projects.ListByUser returned %+v", projects) + t.Fatalf("Projects.ListProjectsForUser returned %+v", projects) } - const methodName = "ListByUser" + const methodName = "ListProjectsForUser" testBadOptions(t, methodName, func() (err error) { - _, _, err = client.Projects.ListByUser(ctx, "\n", opts) + _, _, err = client.Projects.ListProjectsForUser(ctx, "\n", opts) return err }) testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { - got, resp, err := client.Projects.ListByUser(ctx, "u", opts) + got, resp, err := client.Projects.ListProjectsForUser(ctx, "u", opts) if got != nil { t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) } @@ -109,28 +106,27 @@ func TestProjectsService_ListUserProjects(t *testing.T) { }) } -func TestProjectsService_GetUserProject(t *testing.T) { +func TestProjectsService_GetProjectForUser(t *testing.T) { t.Parallel() client, mux, _ := setup(t) mux.HandleFunc("/users/u/projectsV2/2", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, "GET") - testHeader(t, r, "Accept", mediaTypeProjectsPreview) fmt.Fprint(w, `{"id":2,"title":"UProj","created_at":"2011-01-02T15:04:05Z","updated_at":"2012-01-02T15:04:05Z"}`) }) ctx := context.Background() - project, _, err := client.Projects.GetUserProject(ctx, "u", 2) + project, _, err := client.Projects.GetProjectForUser(ctx, "u", 2) if err != nil { - t.Fatalf("Projects.GetUserProject returned error: %v", err) + t.Fatalf("Projects.GetProjectForUser returned error: %v", err) } if project.GetID() != 2 || project.GetTitle() != "UProj" { - t.Fatalf("Projects.GetUserProject returned %+v", project) + t.Fatalf("Projects.GetProjectForUser returned %+v", project) } - const methodName = "GetUserProject" + const methodName = "GetProjectForUser" testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { - got, resp, err := client.Projects.GetUserProject(ctx, "u", 2) + got, resp, err := client.Projects.GetProjectForUser(ctx, "u", 2) if got != nil { t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) } From 99852cff4cf7c605d9a8b2d568a43fe76500fdae Mon Sep 17 00:00:00 2001 From: JoannaaKL Date: Mon, 22 Sep 2025 19:56:32 +0000 Subject: [PATCH 05/19] Update comments --- github/projects.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/github/projects.go b/github/projects.go index 2631968826e..c341bee33ac 100644 --- a/github/projects.go +++ b/github/projects.go @@ -33,7 +33,7 @@ type ListProjectsOptions struct { Query string `url:"q,omitempty"` } -// ListOrganizationProjects lists Projects V2 for an organization. +// ListProjectsForOrganization lists Projects V2 for an organization. // // GitHub API docs: https://docs.github.com/rest/projects/projects#list-organization-projects // @@ -58,7 +58,7 @@ func (s *ProjectsService) ListProjectsForOrganization(ctx context.Context, org s return projects, resp, nil } -// ListByUser lists Projects V2 for a user. +// ListProjectsForUser lists Projects V2 for a user. // // GitHub API docs: https://docs.github.com/en/rest/projects/projects#list-projects-for-user // @@ -82,7 +82,7 @@ func (s *ProjectsService) ListProjectsForUser(ctx context.Context, username stri return projects, resp, nil } -// GetUserProject gets a Projects V2 project for a user by ID. +// GetProjectForUser gets a Projects V2 project for a user by ID. // // GitHub API docs: https://docs.github.com/en/rest/projects/projects#get-project-for-user // @@ -102,7 +102,7 @@ func (s *ProjectsService) GetProjectForUser(ctx context.Context, username string return project, resp, nil } -// GetByOrg gets a Projects V2 project for an organization by ID. +// GetProjectForOrg gets a Projects V2 project for an organization by ID. // // GitHub API docs: https://docs.github.com/rest/projects/projects#get-project-for-organization // From 76642151d013908d077440ecf20ffe91f5b78470 Mon Sep 17 00:00:00 2001 From: JoannaaKL Date: Mon, 22 Sep 2025 20:03:17 +0000 Subject: [PATCH 06/19] Copyright update --- github/projects.go | 2 +- github/projects_test.go | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/github/projects.go b/github/projects.go index c341bee33ac..52960a1d4db 100644 --- a/github/projects.go +++ b/github/projects.go @@ -1,4 +1,4 @@ -// Copyright 2013 The go-github AUTHORS. All rights reserved. +// Copyright 2025 The go-github AUTHORS. All rights reserved. // // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. diff --git a/github/projects_test.go b/github/projects_test.go index 056a47406c5..a0cd13aed91 100644 --- a/github/projects_test.go +++ b/github/projects_test.go @@ -1,3 +1,8 @@ +// Copyright 2025 The go-github AUTHORS. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + package github import ( From 2b9476576779c6315d3270717e50ae951bd3d498 Mon Sep 17 00:00:00 2001 From: JoannaaKL Date: Tue, 23 Sep 2025 08:13:19 +0000 Subject: [PATCH 07/19] Update comments --- github/projects.go | 53 +++++++++-------- github/projects_test.go | 128 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 157 insertions(+), 24 deletions(-) diff --git a/github/projects.go b/github/projects.go index 52960a1d4db..be14f1b2ad0 100644 --- a/github/projects.go +++ b/github/projects.go @@ -19,6 +19,13 @@ type ProjectsService service func (p ProjectV2) String() string { return Stringify(p) } // ListProjectsOptions specifies optional parameters to list projects for user / organization. +// +// Note: Pagination is powered by before/after cursor-style pagination. After the initial call, +// inspect the returned *Response. Use resp.After as the opts.After value to request +// the next page, and resp.Before as the opts.Before value to request the previous +// page. Set either Before or After for a request; if both are +// supplied GitHub API will return an error. PerPage controls the number of items +// per page (max 100 per GitHub API docs). type ListProjectsOptions struct { // A cursor, as given in the Link header. If specified, the query only searches for events before this cursor. Before string `url:"before,omitempty"` @@ -29,13 +36,13 @@ type ListProjectsOptions struct { // For paginated result sets, the number of results to include per page. PerPage int `url:"per_page,omitempty"` - // Q is an optional query string to filter/search projects (when supported). + // Q is an optional query string to limit results to projects of the specified type. Query string `url:"q,omitempty"` } // ListProjectsForOrganization lists Projects V2 for an organization. // -// GitHub API docs: https://docs.github.com/rest/projects/projects#list-organization-projects +// GitHub API docs: https://docs.github.com/rest/projects/projects#list-projects-for-organization // //meta:operation GET /orgs/{org}/projectsV2 func (s *ProjectsService) ListProjectsForOrganization(ctx context.Context, org string, opts *ListProjectsOptions) ([]*ProjectV2, *Response, error) { @@ -58,9 +65,29 @@ func (s *ProjectsService) ListProjectsForOrganization(ctx context.Context, org s return projects, resp, nil } +// GetProjectForOrg gets a Projects V2 project for an organization by ID. +// +// GitHub API docs: https://docs.github.com/rest/projects/projects#get-project-for-organization +// +//meta:operation GET /orgs/{org}/projectsV2/{project_id} +func (s *ProjectsService) GetProjectForOrg(ctx context.Context, org string, projectID int64) (*ProjectV2, *Response, error) { + u := fmt.Sprintf("orgs/%v/projectsV2/%v", org, projectID) + req, err := s.client.NewRequest("GET", u, nil) + if err != nil { + return nil, nil, err + } + + project := new(ProjectV2) + resp, err := s.client.Do(ctx, req, project) + if err != nil { + return nil, resp, err + } + return project, resp, nil +} + // ListProjectsForUser lists Projects V2 for a user. // -// GitHub API docs: https://docs.github.com/en/rest/projects/projects#list-projects-for-user +// GitHub API docs: https://docs.github.com/rest/projects/projects#list-projects-for-user // //meta:operation GET /users/{username}/projectsV2 func (s *ProjectsService) ListProjectsForUser(ctx context.Context, username string, opts *ListProjectsOptions) ([]*ProjectV2, *Response, error) { @@ -101,23 +128,3 @@ func (s *ProjectsService) GetProjectForUser(ctx context.Context, username string } return project, resp, nil } - -// GetProjectForOrg gets a Projects V2 project for an organization by ID. -// -// GitHub API docs: https://docs.github.com/rest/projects/projects#get-project-for-organization -// -//meta:operation GET /orgs/{org}/projectsV2/{project_id} -func (s *ProjectsService) GetProjectForOrg(ctx context.Context, org string, projectID int64) (*ProjectV2, *Response, error) { - u := fmt.Sprintf("orgs/%v/projectsV2/%v", org, projectID) - req, err := s.client.NewRequest("GET", u, nil) - if err != nil { - return nil, nil, err - } - - project := new(ProjectV2) - resp, err := s.client.Do(ctx, req, project) - if err != nil { - return nil, resp, err - } - return project, resp, nil -} diff --git a/github/projects_test.go b/github/projects_test.go index a0cd13aed91..f12a44aa2b6 100644 --- a/github/projects_test.go +++ b/github/projects_test.go @@ -16,9 +16,15 @@ func TestProjectsService_ListProjectsForOrganizations(t *testing.T) { t.Parallel() client, mux, _ := setup(t) + // Combined handler: supports initial test case and dual before/after validation scenario. mux.HandleFunc("/orgs/o/projectsV2", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, "GET") - // Expect query params q, after, before when provided + q := r.URL.Query() + if q.Get("before") == "b" && q.Get("after") == "a" { + fmt.Fprint(w, `[]`) + return + } + // default expectation for main part of test testFormValues(t, r, values{"q": "alpha", "after": "2", "before": "1"}) fmt.Fprint(w, `[{"id":1,"title":"T1","created_at":"2011-01-02T15:04:05Z","updated_at":"2012-01-02T15:04:05Z"}]`) }) @@ -46,6 +52,12 @@ func TestProjectsService_ListProjectsForOrganizations(t *testing.T) { } return resp, err }) + + // still allow both set (no validation enforced) – ensure it does not error + ctxBypass := context.WithValue(context.Background(), BypassRateLimitCheck, true) + if _, _, err = client.Projects.ListProjectsForOrganization(ctxBypass, "o", &ListProjectsOptions{Before: "b", After: "a"}); err != nil { + t.Fatalf("unexpected error when both before/after set: %v", err) + } } func TestProjectsService_GetProjectForOrg(t *testing.T) { @@ -80,14 +92,21 @@ func TestProjectsService_ListUserProjects(t *testing.T) { t.Parallel() client, mux, _ := setup(t) + // Combined handler: supports initial test case and dual before/after scenario. mux.HandleFunc("/users/u/projectsV2", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, "GET") + q := r.URL.Query() + if q.Get("before") == "b" && q.Get("after") == "a" { + fmt.Fprint(w, `[]`) + return + } testFormValues(t, r, values{"q": "beta", "before": "1", "after": "2", "per_page": "2"}) fmt.Fprint(w, `[{"id":2,"title":"UProj","created_at":"2011-01-02T15:04:05Z","updated_at":"2012-01-02T15:04:05Z"}]`) }) opts := &ListProjectsOptions{Query: "beta", Before: "1", After: "2", PerPage: 2} ctx := context.Background() + var ctxBypass context.Context projects, _, err := client.Projects.ListProjectsForUser(ctx, "u", opts) if err != nil { t.Fatalf("Projects.ListProjectsForUser returned error: %v", err) @@ -109,6 +128,12 @@ func TestProjectsService_ListUserProjects(t *testing.T) { } return resp, err }) + + // still allow both set (no validation enforced) – ensure it does not error + ctxBypass = context.WithValue(context.Background(), BypassRateLimitCheck, true) + if _, _, err = client.Projects.ListProjectsForUser(ctxBypass, "u", &ListProjectsOptions{Before: "b", After: "a"}); err != nil { + t.Fatalf("unexpected error when both before/after set: %v", err) + } } func TestProjectsService_GetProjectForUser(t *testing.T) { @@ -139,6 +164,107 @@ func TestProjectsService_GetProjectForUser(t *testing.T) { }) } +// TestProjectsService_ListProjectsForOrganization_pagination clarifies how callers should +// use resp.After to request the next page and resp.Before for previous pages when supported. +func TestProjectsService_ListProjectsForOrganization_pagination(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + // First page returns a Link header with rel="next" containing an after cursor (after=cursor2) + mux.HandleFunc("/orgs/o/projectsV2", func(w http.ResponseWriter, r *http.Request) { + q := r.URL.Query() + after := q.Get("after") + before := q.Get("before") + if after == "" && before == "" { + // first request + w.Header().Set("Link", "; rel=\"next\"") + fmt.Fprint(w, `[{"id":1,"title":"P1","created_at":"2011-01-02T15:04:05Z","updated_at":"2012-01-02T15:04:05Z"}]`) + return + } + if after == "cursor2" { + // second request simulates a previous link + w.Header().Set("Link", "; rel=\"prev\"") + fmt.Fprint(w, `[{"id":2,"title":"P2","created_at":"2011-01-02T15:04:05Z","updated_at":"2012-01-02T15:04:05Z"}]`) + return + } + // unexpected state + http.Error(w, "unexpected query", http.StatusBadRequest) + }) + + ctx := context.Background() + first, resp, err := client.Projects.ListProjectsForOrganization(ctx, "o", nil) + if err != nil { + t.Fatalf("first page error: %v", err) + } + if len(first) != 1 || first[0].GetID() != 1 { + t.Fatalf("unexpected first page %+v", first) + } + if resp.After != "cursor2" { + t.Fatalf("expected resp.After=cursor2 got %q", resp.After) + } + + // Use resp.After as opts.After for next page + opts := &ListProjectsOptions{After: resp.After} + second, resp2, err := client.Projects.ListProjectsForOrganization(ctx, "o", opts) + if err != nil { + t.Fatalf("second page error: %v", err) + } + if len(second) != 1 || second[0].GetID() != 2 { + t.Fatalf("unexpected second page %+v", second) + } + if resp2.Before != "cursor2" { + t.Fatalf("expected resp2.Before=cursor2 got %q", resp2.Before) + } +} + +// TestProjectsService_ListProjectsForUser_pagination mirrors the org pagination test +// but exercises the user endpoint to ensure Before/After cursor handling works identically. +func TestProjectsService_ListProjectsForUser_pagination(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/users/u/projectsV2", func(w http.ResponseWriter, r *http.Request) { + q := r.URL.Query() + after := q.Get("after") + before := q.Get("before") + if after == "" && before == "" { // first page + w.Header().Set("Link", "; rel=\"next\"") + fmt.Fprint(w, `[{"id":10,"title":"UP1","created_at":"2011-01-02T15:04:05Z","updated_at":"2012-01-02T15:04:05Z"}]`) + return + } + if after == "ucursor2" { // second page provides prev + w.Header().Set("Link", "; rel=\"prev\"") + fmt.Fprint(w, `[{"id":11,"title":"UP2","created_at":"2011-01-02T15:04:05Z","updated_at":"2012-01-02T15:04:05Z"}]`) + return + } + http.Error(w, "unexpected query", http.StatusBadRequest) + }) + + ctx := context.Background() + first, resp, err := client.Projects.ListProjectsForUser(ctx, "u", nil) + if err != nil { + t.Fatalf("first page error: %v", err) + } + if len(first) != 1 || first[0].GetID() != 10 { + t.Fatalf("unexpected first page %+v", first) + } + if resp.After != "ucursor2" { + t.Fatalf("expected resp.After=ucursor2 got %q", resp.After) + } + + opts := &ListProjectsOptions{After: resp.After} + second, resp2, err := client.Projects.ListProjectsForUser(ctx, "u", opts) + if err != nil { + t.Fatalf("second page error: %v", err) + } + if len(second) != 1 || second[0].GetID() != 11 { + t.Fatalf("unexpected second page %+v", second) + } + if resp2.Before != "ucursor2" { + t.Fatalf("expected resp2.Before=ucursor2 got %q", resp2.Before) + } +} + // Marshal test ensures V2 fields marshal correctly. func TestProjectV2_Marshal(t *testing.T) { t.Parallel() From a4b0a56bdf47c8e28aebf52bd3a09ebf321e4fdc Mon Sep 17 00:00:00 2001 From: JoannaaKL Date: Tue, 23 Sep 2025 10:37:26 +0200 Subject: [PATCH 08/19] Generate docs --- github/github-stringify_test.go | 33 ++++++++++++++++++++ github/projects.go | 14 ++++----- openapi_operations.yaml | 55 +++++++++++++++++++++++++++++++++ 3 files changed, 95 insertions(+), 7 deletions(-) diff --git a/github/github-stringify_test.go b/github/github-stringify_test.go index b246f0bc35a..79998b56960 100644 --- a/github/github-stringify_test.go +++ b/github/github-stringify_test.go @@ -1477,6 +1477,39 @@ func TestPreReceiveHook_String(t *testing.T) { } } +func TestProjectV2_String(t *testing.T) { + t.Parallel() + v := ProjectV2{ + ID: Ptr(int64(0)), + NodeID: Ptr(""), + Owner: &User{}, + Creator: &User{}, + Title: Ptr(""), + Description: Ptr(""), + Public: Ptr(false), + ClosedAt: &Timestamp{}, + CreatedAt: &Timestamp{}, + UpdatedAt: &Timestamp{}, + DeletedAt: &Timestamp{}, + Number: Ptr(0), + ShortDescription: Ptr(""), + DeletedBy: &User{}, + URL: Ptr(""), + HTMLURL: Ptr(""), + ColumnsURL: Ptr(""), + OwnerURL: Ptr(""), + Name: Ptr(""), + Body: Ptr(""), + State: Ptr(""), + OrganizationPermission: Ptr(""), + Private: Ptr(false), + } + want := `github.ProjectV2{ID:0, NodeID:"", Owner:github.User{}, Creator:github.User{}, Title:"", Description:"", Public:false, ClosedAt:github.Timestamp{0001-01-01 00:00:00 +0000 UTC}, CreatedAt:github.Timestamp{0001-01-01 00:00:00 +0000 UTC}, UpdatedAt:github.Timestamp{0001-01-01 00:00:00 +0000 UTC}, DeletedAt:github.Timestamp{0001-01-01 00:00:00 +0000 UTC}, Number:0, ShortDescription:"", DeletedBy:github.User{}, URL:"", HTMLURL:"", ColumnsURL:"", OwnerURL:"", Name:"", Body:"", State:"", OrganizationPermission:"", Private:false}` + if got := v.String(); got != want { + t.Errorf("ProjectV2.String = %v, want %v", got, want) + } +} + func TestPullRequest_String(t *testing.T) { t.Parallel() v := PullRequest{ diff --git a/github/projects.go b/github/projects.go index be14f1b2ad0..b2aab5e5399 100644 --- a/github/projects.go +++ b/github/projects.go @@ -69,9 +69,9 @@ func (s *ProjectsService) ListProjectsForOrganization(ctx context.Context, org s // // GitHub API docs: https://docs.github.com/rest/projects/projects#get-project-for-organization // -//meta:operation GET /orgs/{org}/projectsV2/{project_id} -func (s *ProjectsService) GetProjectForOrg(ctx context.Context, org string, projectID int64) (*ProjectV2, *Response, error) { - u := fmt.Sprintf("orgs/%v/projectsV2/%v", org, projectID) +//meta:operation GET /orgs/{org}/projectsV2/{project_number} +func (s *ProjectsService) GetProjectForOrg(ctx context.Context, org string, projectNumber int64) (*ProjectV2, *Response, error) { + u := fmt.Sprintf("orgs/%v/projectsV2/%v", org, projectNumber) req, err := s.client.NewRequest("GET", u, nil) if err != nil { return nil, nil, err @@ -111,11 +111,11 @@ func (s *ProjectsService) ListProjectsForUser(ctx context.Context, username stri // GetProjectForUser gets a Projects V2 project for a user by ID. // -// GitHub API docs: https://docs.github.com/en/rest/projects/projects#get-project-for-user +// GitHub API docs: https://docs.github.com/rest/projects/projects#get-project-for-user // -//meta:operation GET /users/{username}/projectsV2/{project_id} -func (s *ProjectsService) GetProjectForUser(ctx context.Context, username string, projectID int64) (*ProjectV2, *Response, error) { - u := fmt.Sprintf("users/%v/projectsV2/%v", username, projectID) +//meta:operation GET /users/{username}/projectsV2/{project_number} +func (s *ProjectsService) GetProjectForUser(ctx context.Context, username string, projectNumber int64) (*ProjectV2, *Response, error) { + u := fmt.Sprintf("users/%v/projectsV2/%v", username, projectNumber) req, err := s.client.NewRequest("GET", u, nil) if err != nil { return nil, nil, err diff --git a/openapi_operations.yaml b/openapi_operations.yaml index d817be5af5a..3f433cc0a33 100644 --- a/openapi_operations.yaml +++ b/openapi_operations.yaml @@ -2877,6 +2877,61 @@ openapi_operations: - descriptions/api.github.com/api.github.com.json - descriptions/ghec/ghec.json - descriptions/ghes-3.17/ghes-3.17.json + - name: GET /orgs/{org}/projectsV2 + documentation_url: https://docs.github.com/rest/projects/projects#list-projects-for-organization + openapi_files: + - descriptions/api.github.com/api.github.com.json + - descriptions/ghec/ghec.json + - name: GET /users/{username}/projectsV2 + documentation_url: https://docs.github.com/rest/projects/projects#list-projects-for-user + openapi_files: + - descriptions/api.github.com/api.github.com.json + - descriptions/ghec/ghec.json + - name: GET /orgs/{org}/projectsV2/{project_number} + documentation_url: https://docs.github.com/rest/projects/projects#get-project-for-organization + openapi_files: + - descriptions/api.github.com/api.github.com.json + - descriptions/ghec/ghec.json + - name: GET /users/{username}/projectsV2/{project_number} + documentation_url: https://docs.github.com/rest/projects/projects#get-project-for-user + openapi_files: + - descriptions/api.github.com/api.github.com.json + - descriptions/ghec/ghec.json + - name: GET /orgs/{org}/projectsV2/{project_number}/fields + documentation_url: https://docs.github.com/rest/projects/fields#list-project-fields-for-organization + openapi_files: + - descriptions/api.github.com/api.github.com.json + - descriptions/ghec/ghec.json + - name: GET /orgs/{org}/projectsV2/{project_number}/fields/{field_id} + documentation_url: https://docs.github.com/rest/projects/fields#get-project-field-for-organization + openapi_files: + - descriptions/api.github.com/api.github.com.json + - descriptions/ghec/ghec.json + - name: GET /orgs/{org}/projectsV2/{project_number}/items + documentation_url: https://docs.github.com/rest/projects/items#list-items-for-an-organization-owned-project + openapi_files: + - descriptions/api.github.com/api.github.com.json + - descriptions/ghec/ghec.json + - name: POST /orgs/{org}/projectsV2/{project_number}/items + documentation_url: https://docs.github.com/rest/projects/items#add-item-to-organization-owned-project + openapi_files: + - descriptions/api.github.com/api.github.com.json + - descriptions/ghec/ghec.json + - name: DELETE /orgs/{org}/projectsV2/{project_number}/items/{item_id} + documentation_url: https://docs.github.com/rest/projects/items#delete-project-item-for-organization + openapi_files: + - descriptions/api.github.com/api.github.com.json + - descriptions/ghec/ghec.json + - name: GET /orgs/{org}/projectsV2/{project_number}/items/{item_id} + documentation_url: https://docs.github.com/rest/projects/items#get-an-item-for-an-organization-owned-project + openapi_files: + - descriptions/api.github.com/api.github.com.json + - descriptions/ghec/ghec.json + - name: PATCH /orgs/{org}/projectsV2/{project_number}/items/{item_id} + documentation_url: https://docs.github.com/rest/projects/items#update-project-item-for-organization + openapi_files: + - descriptions/api.github.com/api.github.com.json + - descriptions/ghec/ghec.json - name: GET /orgs/{org}/properties/schema documentation_url: https://docs.github.com/rest/orgs/custom-properties#get-all-custom-properties-for-an-organization openapi_files: From 696102bd7b89fc0fa87766b79bf95618d2569195 Mon Sep 17 00:00:00 2001 From: JoannaaKL Date: Tue, 23 Sep 2025 11:30:01 +0200 Subject: [PATCH 09/19] Add list projects option --- github/github-accessors.go | 16 +++ github/github-accessors_test.go | 22 ++++ github/projects.go | 51 ++++++++ github/projects_test.go | 201 ++++++++++++++++++++++++++++++++ 4 files changed, 290 insertions(+) diff --git a/github/github-accessors.go b/github/github-accessors.go index b470ba80255..55334fddea2 100644 --- a/github/github-accessors.go +++ b/github/github-accessors.go @@ -18638,6 +18638,22 @@ func (p *ProjectV2Event) GetSender() *User { return p.Sender } +// GetCreatedAt returns the CreatedAt field if it's non-nil, zero value otherwise. +func (p *ProjectV2Field) GetCreatedAt() Timestamp { + if p == nil || p.CreatedAt == nil { + return Timestamp{} + } + return *p.CreatedAt +} + +// GetUpdatedAt returns the UpdatedAt field if it's non-nil, zero value otherwise. +func (p *ProjectV2Field) GetUpdatedAt() Timestamp { + if p == nil || p.UpdatedAt == nil { + return Timestamp{} + } + return *p.UpdatedAt +} + // GetArchivedAt returns the ArchivedAt field if it's non-nil, zero value otherwise. func (p *ProjectV2Item) GetArchivedAt() Timestamp { if p == nil || p.ArchivedAt == nil { diff --git a/github/github-accessors_test.go b/github/github-accessors_test.go index d61b00e5f69..fd2d06717b0 100644 --- a/github/github-accessors_test.go +++ b/github/github-accessors_test.go @@ -24180,6 +24180,28 @@ func TestProjectV2Event_GetSender(tt *testing.T) { p.GetSender() } +func TestProjectV2Field_GetCreatedAt(tt *testing.T) { + tt.Parallel() + var zeroValue Timestamp + p := &ProjectV2Field{CreatedAt: &zeroValue} + p.GetCreatedAt() + p = &ProjectV2Field{} + p.GetCreatedAt() + p = nil + p.GetCreatedAt() +} + +func TestProjectV2Field_GetUpdatedAt(tt *testing.T) { + tt.Parallel() + var zeroValue Timestamp + p := &ProjectV2Field{UpdatedAt: &zeroValue} + p.GetUpdatedAt() + p = &ProjectV2Field{} + p.GetUpdatedAt() + p = nil + p.GetUpdatedAt() +} + func TestProjectV2Item_GetArchivedAt(tt *testing.T) { tt.Parallel() var zeroValue Timestamp diff --git a/github/projects.go b/github/projects.go index b2aab5e5399..be932a597a2 100644 --- a/github/projects.go +++ b/github/projects.go @@ -40,6 +40,32 @@ type ListProjectsOptions struct { Query string `url:"q,omitempty"` } +// ProjectV2FieldOption represents an option for a project field of type single_select or multi_select. +// It defines the available choices that can be selected for dropdown-style fields. +// +// GitHub API docs: https://docs.github.com/rest/projects/fields +type ProjectV2FieldOption struct { + ID string `json:"id,omitempty"` // The unique identifier for this option. + Name string `json:"name,omitempty"` // The display name of the option. + Color string `json:"color,omitempty"` // The color associated with this option (e.g., "blue", "red"). + Description string `json:"description,omitempty"` // An optional description for this option. +} + +// ProjectV2Field represents a field in a GitHub Projects V2 project. +// Fields define the structure and data types for project items. +// +// GitHub API docs: https://docs.github.com/rest/projects/fields +type ProjectV2Field struct { + ID string `json:"id,omitempty"` // The unique identifier for this field. + NodeID string `json:"node_id,omitempty"` // The GraphQL node ID for this field. + Name string `json:"name,omitempty"` // The display name of the field. + DataType string `json:"dataType,omitempty"` // The data type of the field (e.g., "text", "number", "date", "single_select", "multi_select"). + ProjectURL string `json:"url,omitempty"` // The API URL for this field. + Options []*ProjectV2FieldOption `json:"options,omitempty"` // Available options for single_select and multi_select fields. + CreatedAt *Timestamp `json:"created_at,omitempty"` // The time when this field was created. + UpdatedAt *Timestamp `json:"updated_at,omitempty"` // The time when this field was last updated. +} + // ListProjectsForOrganization lists Projects V2 for an organization. // // GitHub API docs: https://docs.github.com/rest/projects/projects#list-projects-for-organization @@ -128,3 +154,28 @@ func (s *ProjectsService) GetProjectForUser(ctx context.Context, username string } return project, resp, nil } + +// ListProjectFieldsForOrganization lists Projects V2 for an organization. +// +// GitHub API docs: https://docs.github.com/rest/projects/fields#list-project-fields-for-organization +// +//meta:operation GET /orgs/{org}/projectsV2/{project_number}/fields +func (s *ProjectsService) ListProjectFieldsForOrganization(ctx context.Context, org string, projectNumber int64, opts *ListProjectsOptions) ([]*ProjectV2Field, *Response, error) { + u := fmt.Sprintf("orgs/%v/projectsV2/%v/fields", org, projectNumber) + u, err := addOptions(u, opts) + if err != nil { + return nil, nil, err + } + + req, err := s.client.NewRequest("GET", u, nil) + if err != nil { + return nil, nil, err + } + + var fields []*ProjectV2Field + resp, err := s.client.Do(ctx, req, &fields) + if err != nil { + return nil, resp, err + } + return fields, resp, nil +} diff --git a/github/projects_test.go b/github/projects_test.go index f12a44aa2b6..8fcb20f43ad 100644 --- a/github/projects_test.go +++ b/github/projects_test.go @@ -265,6 +265,162 @@ func TestProjectsService_ListProjectsForUser_pagination(t *testing.T) { } } +func TestProjectsService_ListProjectFieldsForOrganization(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + // Combined handler: supports initial test case and dual before/after validation scenario. + mux.HandleFunc("/orgs/o/projectsV2/1/fields", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + q := r.URL.Query() + if q.Get("before") == "b" && q.Get("after") == "a" { + fmt.Fprint(w, `[]`) + return + } + // default expectation for main part of test + testFormValues(t, r, values{"q": "text", "after": "2", "before": "1"}) + fmt.Fprint(w, `[ + { + "id": "field1", + "node_id": "node_1", + "name": "Status", + "dataType": "single_select", + "url": "https://api.github.com/projects/1/fields/field1", + "options": [ + { + "id": "option1", + "name": "Todo", + "color": "blue", + "description": "Tasks to be done" + }, + { + "id": "option2", + "name": "In Progress", + "color": "yellow" + } + ], + "created_at": "2011-01-02T15:04:05Z", + "updated_at": "2012-01-02T15:04:05Z" + }, + { + "id": "field2", + "node_id": "node_2", + "name": "Priority", + "dataType": "text", + "url": "https://api.github.com/projects/1/fields/field2", + "created_at": "2011-01-02T15:04:05Z", + "updated_at": "2012-01-02T15:04:05Z" + } + ]`) + }) + + opts := &ListProjectsOptions{Query: "text", After: "2", Before: "1"} + ctx := context.Background() + fields, _, err := client.Projects.ListProjectFieldsForOrganization(ctx, "o", 1, opts) + if err != nil { + t.Fatalf("Projects.ListProjectFieldsForOrganization returned error: %v", err) + } + + if len(fields) != 2 { + t.Fatalf("Projects.ListProjectFieldsForOrganization returned %d fields, want 2", len(fields)) + } + + // Validate first field (with options) + field1 := fields[0] + if field1.ID != "field1" || field1.Name != "Status" || field1.DataType != "single_select" { + t.Errorf("First field: got ID=%s, Name=%s, DataType=%s; want field1, Status, single_select", + field1.ID, field1.Name, field1.DataType) + } + if len(field1.Options) != 2 { + t.Errorf("First field options: got %d, want 2", len(field1.Options)) + } + if field1.Options[0].Name != "Todo" || field1.Options[1].Name != "In Progress" { + t.Errorf("First field option names: got %s, %s; want Todo, In Progress", + field1.Options[0].Name, field1.Options[1].Name) + } + + // Validate second field (without options) + field2 := fields[1] + if field2.ID != "field2" || field2.Name != "Priority" || field2.DataType != "text" { + t.Errorf("Second field: got ID=%s, Name=%s, DataType=%s; want field2, Priority, text", + field2.ID, field2.Name, field2.DataType) + } + if len(field2.Options) != 0 { + t.Errorf("Second field options: got %d, want 0", len(field2.Options)) + } + + const methodName = "ListProjectFieldsForOrganization" + testBadOptions(t, methodName, func() (err error) { + _, _, err = client.Projects.ListProjectFieldsForOrganization(ctx, "\n", 1, opts) + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Projects.ListProjectFieldsForOrganization(ctx, "o", 1, opts) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) + + // still allow both set (no validation enforced) – ensure it does not error + ctxBypass := context.WithValue(context.Background(), BypassRateLimitCheck, true) + if _, _, err = client.Projects.ListProjectFieldsForOrganization(ctxBypass, "o", 1, &ListProjectsOptions{Before: "b", After: "a"}); err != nil { + t.Fatalf("unexpected error when both before/after set: %v", err) + } +} + +func TestProjectsService_ListProjectFieldsForOrganization_pagination(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + // First page returns a Link header with rel="next" containing an after cursor + mux.HandleFunc("/orgs/o/projectsV2/1/fields", func(w http.ResponseWriter, r *http.Request) { + q := r.URL.Query() + after := q.Get("after") + before := q.Get("before") + if after == "" && before == "" { + // first request + w.Header().Set("Link", "; rel=\"next\"") + fmt.Fprint(w, `[{"id":"field1","name":"Status","dataType":"single_select","created_at":"2011-01-02T15:04:05Z","updated_at":"2012-01-02T15:04:05Z"}]`) + return + } + if after == "cursor2" { + // second request simulates a previous link + w.Header().Set("Link", "; rel=\"prev\"") + fmt.Fprint(w, `[{"id":"field2","name":"Priority","dataType":"text","created_at":"2011-01-02T15:04:05Z","updated_at":"2012-01-02T15:04:05Z"}]`) + return + } + // unexpected state + http.Error(w, "unexpected query", http.StatusBadRequest) + }) + + ctx := context.Background() + first, resp, err := client.Projects.ListProjectFieldsForOrganization(ctx, "o", 1, nil) + if err != nil { + t.Fatalf("first page error: %v", err) + } + if len(first) != 1 || first[0].ID != "field1" { + t.Fatalf("unexpected first page %+v", first) + } + if resp.After != "cursor2" { + t.Fatalf("expected resp.After=cursor2 got %q", resp.After) + } + + // Use resp.After as opts.After for next page + opts := &ListProjectsOptions{After: resp.After} + second, resp2, err := client.Projects.ListProjectFieldsForOrganization(ctx, "o", 1, opts) + if err != nil { + t.Fatalf("second page error: %v", err) + } + if len(second) != 1 || second[0].ID != "field2" { + t.Fatalf("unexpected second page %+v", second) + } + if resp2.Before != "cursor2" { + t.Fatalf("expected resp2.Before=cursor2 got %q", resp2.Before) + } +} + // Marshal test ensures V2 fields marshal correctly. func TestProjectV2_Marshal(t *testing.T) { t.Parallel() @@ -290,3 +446,48 @@ func TestProjectV2_Marshal(t *testing.T) { testJSONMarshal(t, p, want) } + +// Marshal test ensures V2 field structures marshal correctly. +func TestProjectV2Field_Marshal(t *testing.T) { + t.Parallel() + testJSONMarshal(t, &ProjectV2Field{}, "{}") + testJSONMarshal(t, &ProjectV2FieldOption{}, "{}") + + field := &ProjectV2Field{ + ID: "field1", + NodeID: "node_1", + Name: "Status", + DataType: "single_select", + ProjectURL: "https://api.github.com/projects/1/fields/field1", + Options: []*ProjectV2FieldOption{ + { + ID: "option1", + Name: "Todo", + Color: "blue", + Description: "Tasks to be done", + }, + }, + CreatedAt: &Timestamp{referenceTime}, + UpdatedAt: &Timestamp{referenceTime}, + } + + want := `{ + "id": "field1", + "node_id": "node_1", + "name": "Status", + "dataType": "single_select", + "url": "https://api.github.com/projects/1/fields/field1", + "options": [ + { + "id": "option1", + "name": "Todo", + "color": "blue", + "description": "Tasks to be done" + } + ], + "created_at": ` + referenceTimeStr + `, + "updated_at": ` + referenceTimeStr + ` + }` + + testJSONMarshal(t, field, want) +} From 469e03a0a0719f8dc26a406bc2f2b8d9f31e7fdf Mon Sep 17 00:00:00 2001 From: JoannaaKL Date: Tue, 23 Sep 2025 12:16:06 +0200 Subject: [PATCH 10/19] Generate openapi docs --- openapi_operations.yaml | 189 +++++++++++++++++++++++++++++++++------- 1 file changed, 158 insertions(+), 31 deletions(-) diff --git a/openapi_operations.yaml b/openapi_operations.yaml index 3f433cc0a33..053c19325b6 100644 --- a/openapi_operations.yaml +++ b/openapi_operations.yaml @@ -27,7 +27,7 @@ operation_overrides: documentation_url: https://docs.github.com/rest/pages/pages#request-a-github-pages-build - name: GET /repos/{owner}/{repo}/pages/builds/{build_id} documentation_url: https://docs.github.com/rest/pages/pages#get-github-pages-build -openapi_commit: 30ab35c5db4a05519ceed2e41292cdb7af182f1c +openapi_commit: 44dd20c107ca46d1dbcaf4aa522d9039e96e631c openapi_operations: - name: GET / documentation_url: https://docs.github.com/rest/meta/meta#github-api-root @@ -476,6 +476,14 @@ openapi_operations: documentation_url: https://docs.github.com/enterprise-server@3.17/rest/enterprise-admin/admin-stats#get-users-statistics openapi_files: - descriptions/ghes-3.17/ghes-3.17.json + - name: POST /enterprises/{enterprise}/access-restrictions/disable + documentation_url: https://docs.github.com/enterprise-cloud@latest//rest/enterprise-admin/enterprises#disable-access-restrictions-for-an-enterprise + openapi_files: + - descriptions/ghec/ghec.json + - name: POST /enterprises/{enterprise}/access-restrictions/enable + documentation_url: https://docs.github.com/enterprise-cloud@latest//rest/enterprise-admin/enterprises#enable-access-restrictions-for-an-enterprise + openapi_files: + - descriptions/ghec/ghec.json - name: GET /enterprises/{enterprise}/actions/cache/usage documentation_url: https://docs.github.com/enterprise-cloud@latest//rest/actions/cache#get-github-actions-cache-usage-for-an-enterprise openapi_files: @@ -827,6 +835,10 @@ openapi_operations: documentation_url: https://docs.github.com/enterprise-cloud@latest//rest/enterprise-admin/bypass-requests#list-push-rule-bypass-requests-within-an-enterprise openapi_files: - descriptions/ghec/ghec.json + - name: GET /enterprises/{enterprise}/bypass-requests/secret-scanning + documentation_url: https://docs.github.com/enterprise-cloud@latest//rest/secret-scanning/delegated-bypass#list-bypass-requests-for-secret-scanning-for-an-enterprise + openapi_files: + - descriptions/ghec/ghec.json - name: GET /enterprises/{enterprise}/code-scanning/alerts documentation_url: https://docs.github.com/enterprise-cloud@latest//rest/code-scanning/code-scanning#list-code-scanning-alerts-for-an-enterprise openapi_files: @@ -904,6 +916,22 @@ openapi_operations: documentation_url: https://docs.github.com/enterprise-cloud@latest//rest/copilot/copilot-user-management#list-all-copilot-seat-assignments-for-an-enterprise openapi_files: - descriptions/ghec/ghec.json + - name: DELETE /enterprises/{enterprise}/copilot/billing/selected_enterprise_teams + documentation_url: https://docs.github.com/enterprise-cloud@latest//rest/copilot/copilot-user-management#remove-enterprise-teams-from-the-copilot-subscription-for-an-enterprise + openapi_files: + - descriptions/ghec/ghec.json + - name: POST /enterprises/{enterprise}/copilot/billing/selected_enterprise_teams + documentation_url: https://docs.github.com/enterprise-cloud@latest//rest/copilot/copilot-user-management#add-enterprise-teams-to-the-copilot-subscription-for-an-enterprise + openapi_files: + - descriptions/ghec/ghec.json + - name: DELETE /enterprises/{enterprise}/copilot/billing/selected_users + documentation_url: https://docs.github.com/enterprise-cloud@latest//rest/copilot/copilot-user-management#remove-users-from-the-copilot-subscription-for-an-enterprise + openapi_files: + - descriptions/ghec/ghec.json + - name: POST /enterprises/{enterprise}/copilot/billing/selected_users + documentation_url: https://docs.github.com/enterprise-cloud@latest//rest/copilot/copilot-user-management#add-users-to-the-copilot-subscription-for-an-enterprise + openapi_files: + - descriptions/ghec/ghec.json - name: GET /enterprises/{enterprise}/copilot/metrics documentation_url: https://docs.github.com/enterprise-cloud@latest//rest/copilot/copilot-metrics#get-copilot-metrics-for-an-enterprise openapi_files: @@ -1061,6 +1089,61 @@ openapi_operations: documentation_url: https://docs.github.com/enterprise-cloud@latest//rest/copilot/copilot-metrics#get-copilot-metrics-for-an-enterprise-team openapi_files: - descriptions/ghec/ghec.json + - name: GET /enterprises/{enterprise}/teams + documentation_url: https://docs.github.com/rest/enterprise-teams/enterprise-teams#list-enterprise-teams + openapi_files: + - descriptions/api.github.com/api.github.com.json + - descriptions/ghec/ghec.json + - name: POST /enterprises/{enterprise}/teams + documentation_url: https://docs.github.com/rest/enterprise-teams/enterprise-teams#create-an-enterprise-team + openapi_files: + - descriptions/api.github.com/api.github.com.json + - descriptions/ghec/ghec.json + - name: GET /enterprises/{enterprise}/teams/{enterprise-team}/memberships + documentation_url: https://docs.github.com/rest/enterprise-teams/enterprise-team-members#list-members-in-an-enterprise-team + openapi_files: + - descriptions/api.github.com/api.github.com.json + - descriptions/ghec/ghec.json + - name: POST /enterprises/{enterprise}/teams/{enterprise-team}/memberships/add + documentation_url: https://docs.github.com/rest/enterprise-teams/enterprise-team-members#bulk-add-team-members + openapi_files: + - descriptions/api.github.com/api.github.com.json + - descriptions/ghec/ghec.json + - name: POST /enterprises/{enterprise}/teams/{enterprise-team}/memberships/remove + documentation_url: https://docs.github.com/rest/enterprise-teams/enterprise-team-members#bulk-remove-team-members + openapi_files: + - descriptions/api.github.com/api.github.com.json + - descriptions/ghec/ghec.json + - name: DELETE /enterprises/{enterprise}/teams/{enterprise-team}/memberships/{username} + documentation_url: https://docs.github.com/rest/enterprise-teams/enterprise-team-members#remove-team-membership + openapi_files: + - descriptions/api.github.com/api.github.com.json + - descriptions/ghec/ghec.json + - name: GET /enterprises/{enterprise}/teams/{enterprise-team}/memberships/{username} + documentation_url: https://docs.github.com/rest/enterprise-teams/enterprise-team-members#get-enterprise-team-membership + openapi_files: + - descriptions/api.github.com/api.github.com.json + - descriptions/ghec/ghec.json + - name: PUT /enterprises/{enterprise}/teams/{enterprise-team}/memberships/{username} + documentation_url: https://docs.github.com/rest/enterprise-teams/enterprise-team-members#add-team-member + openapi_files: + - descriptions/api.github.com/api.github.com.json + - descriptions/ghec/ghec.json + - name: DELETE /enterprises/{enterprise}/teams/{team_slug} + documentation_url: https://docs.github.com/rest/enterprise-teams/enterprise-teams#delete-an-enterprise-team + openapi_files: + - descriptions/api.github.com/api.github.com.json + - descriptions/ghec/ghec.json + - name: GET /enterprises/{enterprise}/teams/{team_slug} + documentation_url: https://docs.github.com/rest/enterprise-teams/enterprise-teams#get-an-enterprise-team + openapi_files: + - descriptions/api.github.com/api.github.com.json + - descriptions/ghec/ghec.json + - name: PATCH /enterprises/{enterprise}/teams/{team_slug} + documentation_url: https://docs.github.com/rest/enterprise-teams/enterprise-teams#update-an-enterprise-team + openapi_files: + - descriptions/api.github.com/api.github.com.json + - descriptions/ghec/ghec.json - name: POST /enterprises/{enterprise}/{security_product}/{enablement} documentation_url: https://docs.github.com/enterprise-cloud@latest//rest/enterprise-admin/code-security-and-analysis#enable-or-disable-a-security-feature openapi_files: @@ -1939,8 +2022,18 @@ openapi_operations: openapi_files: - descriptions/ghec/ghec.json - descriptions/ghes-3.17/ghes-3.17.json + - name: POST /orgs/{org}/artifacts/metadata/storage-record + documentation_url: https://docs.github.com/rest/orgs/artifact-metadata#create-artifact-metadata-storage-record + openapi_files: + - descriptions/api.github.com/api.github.com.json + - descriptions/ghec/ghec.json + - name: GET /orgs/{org}/artifacts/{subject_digest}/metadata/storage-records + documentation_url: https://docs.github.com/rest/orgs/artifact-metadata#list-artifact-storage-records + openapi_files: + - descriptions/api.github.com/api.github.com.json + - descriptions/ghec/ghec.json - name: POST /orgs/{org}/attestations/bulk-list - documentation_url: https://docs.github.com/rest/orgs/orgs#list-attestations-by-bulk-subject-digests + documentation_url: https://docs.github.com/rest/orgs/attestations#list-attestations-by-bulk-subject-digests openapi_files: - descriptions/api.github.com/api.github.com.json - descriptions/ghec/ghec.json @@ -1960,7 +2053,7 @@ openapi_operations: - descriptions/api.github.com/api.github.com.json - descriptions/ghec/ghec.json - name: GET /orgs/{org}/attestations/{subject_digest} - documentation_url: https://docs.github.com/rest/orgs/orgs#list-attestations + documentation_url: https://docs.github.com/rest/orgs/attestations#list-attestations openapi_files: - descriptions/api.github.com/api.github.com.json - descriptions/ghec/ghec.json @@ -2882,21 +2975,11 @@ openapi_operations: openapi_files: - descriptions/api.github.com/api.github.com.json - descriptions/ghec/ghec.json - - name: GET /users/{username}/projectsV2 - documentation_url: https://docs.github.com/rest/projects/projects#list-projects-for-user - openapi_files: - - descriptions/api.github.com/api.github.com.json - - descriptions/ghec/ghec.json - name: GET /orgs/{org}/projectsV2/{project_number} documentation_url: https://docs.github.com/rest/projects/projects#get-project-for-organization openapi_files: - descriptions/api.github.com/api.github.com.json - descriptions/ghec/ghec.json - - name: GET /users/{username}/projectsV2/{project_number} - documentation_url: https://docs.github.com/rest/projects/projects#get-project-for-user - openapi_files: - - descriptions/api.github.com/api.github.com.json - - descriptions/ghec/ghec.json - name: GET /orgs/{org}/projectsV2/{project_number}/fields documentation_url: https://docs.github.com/rest/projects/fields#list-project-fields-for-organization openapi_files: @@ -3404,29 +3487,25 @@ openapi_operations: - descriptions/ghec/ghec.json - descriptions/ghes-3.17/ghes-3.17.json - name: DELETE /projects/columns/cards/{card_id} - documentation_url: https://docs.github.com/rest/projects-classic/cards#delete-a-project-card + documentation_url: https://docs.github.com/enterprise-cloud@latest//rest/projects-classic/cards#delete-a-project-card openapi_files: - - descriptions/api.github.com/api.github.com.json - descriptions/ghec/ghec.json - - descriptions/ghes-3.17/ghes-3.17.json + - descriptions/ghes-3.16/ghes-3.16.json - name: GET /projects/columns/cards/{card_id} - documentation_url: https://docs.github.com/rest/projects-classic/cards#get-a-project-card + documentation_url: https://docs.github.com/enterprise-cloud@latest//rest/projects-classic/cards#get-a-project-card openapi_files: - - descriptions/api.github.com/api.github.com.json - descriptions/ghec/ghec.json - - descriptions/ghes-3.17/ghes-3.17.json + - descriptions/ghes-3.16/ghes-3.16.json - name: PATCH /projects/columns/cards/{card_id} - documentation_url: https://docs.github.com/rest/projects-classic/cards#update-an-existing-project-card + documentation_url: https://docs.github.com/enterprise-cloud@latest//rest/projects-classic/cards#update-an-existing-project-card openapi_files: - - descriptions/api.github.com/api.github.com.json - descriptions/ghec/ghec.json - - descriptions/ghes-3.17/ghes-3.17.json + - descriptions/ghes-3.16/ghes-3.16.json - name: POST /projects/columns/cards/{card_id}/moves - documentation_url: https://docs.github.com/rest/projects-classic/cards#move-a-project-card + documentation_url: https://docs.github.com/enterprise-cloud@latest//rest/projects-classic/cards#move-a-project-card openapi_files: - - descriptions/api.github.com/api.github.com.json - descriptions/ghec/ghec.json - - descriptions/ghes-3.17/ghes-3.17.json + - descriptions/ghes-3.16/ghes-3.16.json - name: DELETE /projects/columns/{column_id} documentation_url: https://docs.github.com/rest/projects-classic/columns#delete-a-project-column openapi_files: @@ -3446,17 +3525,15 @@ openapi_operations: - descriptions/ghec/ghec.json - descriptions/ghes-3.17/ghes-3.17.json - name: GET /projects/columns/{column_id}/cards - documentation_url: https://docs.github.com/rest/projects-classic/cards#list-project-cards + documentation_url: https://docs.github.com/enterprise-cloud@latest//rest/projects-classic/cards#list-project-cards openapi_files: - - descriptions/api.github.com/api.github.com.json - descriptions/ghec/ghec.json - - descriptions/ghes-3.17/ghes-3.17.json + - descriptions/ghes-3.16/ghes-3.16.json - name: POST /projects/columns/{column_id}/cards - documentation_url: https://docs.github.com/rest/projects-classic/cards#create-a-project-card + documentation_url: https://docs.github.com/enterprise-cloud@latest//rest/projects-classic/cards#create-a-project-card openapi_files: - - descriptions/api.github.com/api.github.com.json - descriptions/ghec/ghec.json - - descriptions/ghes-3.17/ghes-3.17.json + - descriptions/ghes-3.16/ghes-3.16.json - name: POST /projects/columns/{column_id}/moves documentation_url: https://docs.github.com/rest/projects-classic/columns#move-a-project-column openapi_files: @@ -5440,6 +5517,11 @@ openapi_operations: - descriptions/api.github.com/api.github.com.json - descriptions/ghec/ghec.json - descriptions/ghes-3.17/ghes-3.17.json + - name: GET /repos/{owner}/{repo}/issues/{issue_number}/parent + documentation_url: https://docs.github.com/rest/issues/sub-issues#get-parent-issue + openapi_files: + - descriptions/api.github.com/api.github.com.json + - descriptions/ghec/ghec.json - name: GET /repos/{owner}/{repo}/issues/{issue_number}/reactions documentation_url: https://docs.github.com/rest/reactions/reactions#list-reactions-for-an-issue openapi_files: @@ -7501,6 +7583,51 @@ openapi_operations: - descriptions/api.github.com/api.github.com.json - descriptions/ghec/ghec.json - descriptions/ghes-3.17/ghes-3.17.json + - name: GET /users/{username}/projectsV2 + documentation_url: https://docs.github.com/rest/projects/projects#list-projects-for-user + openapi_files: + - descriptions/api.github.com/api.github.com.json + - descriptions/ghec/ghec.json + - name: GET /users/{username}/projectsV2/{project_number} + documentation_url: https://docs.github.com/rest/projects/projects#get-project-for-user + openapi_files: + - descriptions/api.github.com/api.github.com.json + - descriptions/ghec/ghec.json + - name: GET /users/{username}/projectsV2/{project_number}/fields + documentation_url: https://docs.github.com/rest/projects/fields#list-project-fields-for-user + openapi_files: + - descriptions/api.github.com/api.github.com.json + - descriptions/ghec/ghec.json + - name: GET /users/{username}/projectsV2/{project_number}/fields/{field_id} + documentation_url: https://docs.github.com/rest/projects/fields#get-project-field-for-user + openapi_files: + - descriptions/api.github.com/api.github.com.json + - descriptions/ghec/ghec.json + - name: GET /users/{username}/projectsV2/{project_number}/items + documentation_url: https://docs.github.com/rest/projects/items#list-items-for-a-user-owned-project + openapi_files: + - descriptions/api.github.com/api.github.com.json + - descriptions/ghec/ghec.json + - name: POST /users/{username}/projectsV2/{project_number}/items + documentation_url: https://docs.github.com/rest/projects/items#add-item-to-user-owned-project + openapi_files: + - descriptions/api.github.com/api.github.com.json + - descriptions/ghec/ghec.json + - name: DELETE /users/{username}/projectsV2/{project_number}/items/{item_id} + documentation_url: https://docs.github.com/rest/projects/items#delete-project-item-for-user + openapi_files: + - descriptions/api.github.com/api.github.com.json + - descriptions/ghec/ghec.json + - name: GET /users/{username}/projectsV2/{project_number}/items/{item_id} + documentation_url: https://docs.github.com/rest/projects/items#get-an-item-for-a-user-owned-project + openapi_files: + - descriptions/api.github.com/api.github.com.json + - descriptions/ghec/ghec.json + - name: PATCH /users/{username}/projectsV2/{project_number}/items/{item_id} + documentation_url: https://docs.github.com/rest/projects/items#update-project-item-for-user + openapi_files: + - descriptions/api.github.com/api.github.com.json + - descriptions/ghec/ghec.json - name: GET /users/{username}/received_events documentation_url: https://docs.github.com/rest/activity/events#list-events-received-by-the-authenticated-user openapi_files: From 2dbe5b45ea6c776a5255f8a8cefd71b6cc613e6c Mon Sep 17 00:00:00 2001 From: JoannaaKL Date: Tue, 23 Sep 2025 12:19:38 +0200 Subject: [PATCH 11/19] Results of generate --- github/orgs_attestations.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/github/orgs_attestations.go b/github/orgs_attestations.go index d0ac123e2cf..1a7a1d5c966 100644 --- a/github/orgs_attestations.go +++ b/github/orgs_attestations.go @@ -14,7 +14,7 @@ import ( // with a given subject digest that are associated with repositories // owned by an organization. // -// GitHub API docs: https://docs.github.com/rest/orgs/orgs#list-attestations +// GitHub API docs: https://docs.github.com/rest/orgs/attestations#list-attestations // //meta:operation GET /orgs/{org}/attestations/{subject_digest} func (s *OrganizationsService) ListAttestations(ctx context.Context, org, subjectDigest string, opts *ListOptions) (*AttestationsResponse, *Response, error) { From 053a9e171d07fc56c390218fd7b0dd4ca6dc109e Mon Sep 17 00:00:00 2001 From: JoannaaKL Date: Tue, 23 Sep 2025 19:22:26 +0200 Subject: [PATCH 12/19] Update github/projects.go Co-authored-by: Glenn Lewis <6598971+gmlewis@users.noreply.github.com> --- github/projects.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/github/projects.go b/github/projects.go index be932a597a2..afebf1dab1a 100644 --- a/github/projects.go +++ b/github/projects.go @@ -60,7 +60,7 @@ type ProjectV2Field struct { NodeID string `json:"node_id,omitempty"` // The GraphQL node ID for this field. Name string `json:"name,omitempty"` // The display name of the field. DataType string `json:"dataType,omitempty"` // The data type of the field (e.g., "text", "number", "date", "single_select", "multi_select"). - ProjectURL string `json:"url,omitempty"` // The API URL for this field. + URL string `json:"url,omitempty"` // The API URL for this field. Options []*ProjectV2FieldOption `json:"options,omitempty"` // Available options for single_select and multi_select fields. CreatedAt *Timestamp `json:"created_at,omitempty"` // The time when this field was created. UpdatedAt *Timestamp `json:"updated_at,omitempty"` // The time when this field was last updated. From 680a0d1217eeb13d8e09f6c4f4b4678619e437e7 Mon Sep 17 00:00:00 2001 From: JoannaaKL Date: Tue, 23 Sep 2025 19:23:52 +0200 Subject: [PATCH 13/19] Rename url in test --- github/projects_test.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/github/projects_test.go b/github/projects_test.go index 8fcb20f43ad..ccbdc65601a 100644 --- a/github/projects_test.go +++ b/github/projects_test.go @@ -454,11 +454,11 @@ func TestProjectV2Field_Marshal(t *testing.T) { testJSONMarshal(t, &ProjectV2FieldOption{}, "{}") field := &ProjectV2Field{ - ID: "field1", - NodeID: "node_1", - Name: "Status", - DataType: "single_select", - ProjectURL: "https://api.github.com/projects/1/fields/field1", + ID: "field1", + NodeID: "node_1", + Name: "Status", + DataType: "single_select", + URL: "https://api.github.com/projects/1/fields/field1", Options: []*ProjectV2FieldOption{ { ID: "option1", From d8696b44026369d53f5cfbe533dbe2908caeb560 Mon Sep 17 00:00:00 2001 From: JoannaaKL Date: Tue, 23 Sep 2025 19:30:47 +0200 Subject: [PATCH 14/19] Generate again --- github/projects.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/github/projects.go b/github/projects.go index afebf1dab1a..4359db07ef6 100644 --- a/github/projects.go +++ b/github/projects.go @@ -56,14 +56,14 @@ type ProjectV2FieldOption struct { // // GitHub API docs: https://docs.github.com/rest/projects/fields type ProjectV2Field struct { - ID string `json:"id,omitempty"` // The unique identifier for this field. - NodeID string `json:"node_id,omitempty"` // The GraphQL node ID for this field. - Name string `json:"name,omitempty"` // The display name of the field. - DataType string `json:"dataType,omitempty"` // The data type of the field (e.g., "text", "number", "date", "single_select", "multi_select"). - URL string `json:"url,omitempty"` // The API URL for this field. - Options []*ProjectV2FieldOption `json:"options,omitempty"` // Available options for single_select and multi_select fields. - CreatedAt *Timestamp `json:"created_at,omitempty"` // The time when this field was created. - UpdatedAt *Timestamp `json:"updated_at,omitempty"` // The time when this field was last updated. + ID string `json:"id,omitempty"` // The unique identifier for this field. + NodeID string `json:"node_id,omitempty"` // The GraphQL node ID for this field. + Name string `json:"name,omitempty"` // The display name of the field. + DataType string `json:"dataType,omitempty"` // The data type of the field (e.g., "text", "number", "date", "single_select", "multi_select"). + URL string `json:"url,omitempty"` // The API URL for this field. + Options []*ProjectV2FieldOption `json:"options,omitempty"` // Available options for single_select and multi_select fields. + CreatedAt *Timestamp `json:"created_at,omitempty"` // The time when this field was created. + UpdatedAt *Timestamp `json:"updated_at,omitempty"` // The time when this field was last updated. } // ListProjectsForOrganization lists Projects V2 for an organization. From 060aaae6143e3e9cbfc8a386ff1c53baaf78dca0 Mon Sep 17 00:00:00 2001 From: JoannaaKL Date: Wed, 24 Sep 2025 09:58:38 +0200 Subject: [PATCH 15/19] Shorten functions for orgs --- github/projects.go | 8 +++---- github/projects_test.go | 46 ++++++++++++++++++++--------------------- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/github/projects.go b/github/projects.go index 4359db07ef6..9d559e298fb 100644 --- a/github/projects.go +++ b/github/projects.go @@ -66,12 +66,12 @@ type ProjectV2Field struct { UpdatedAt *Timestamp `json:"updated_at,omitempty"` // The time when this field was last updated. } -// ListProjectsForOrganization lists Projects V2 for an organization. +// ListProjectsForOrg lists Projects V2 for an organization. // // GitHub API docs: https://docs.github.com/rest/projects/projects#list-projects-for-organization // //meta:operation GET /orgs/{org}/projectsV2 -func (s *ProjectsService) ListProjectsForOrganization(ctx context.Context, org string, opts *ListProjectsOptions) ([]*ProjectV2, *Response, error) { +func (s *ProjectsService) ListProjectsForOrg(ctx context.Context, org string, opts *ListProjectsOptions) ([]*ProjectV2, *Response, error) { u := fmt.Sprintf("orgs/%v/projectsV2", org) u, err := addOptions(u, opts) if err != nil { @@ -155,12 +155,12 @@ func (s *ProjectsService) GetProjectForUser(ctx context.Context, username string return project, resp, nil } -// ListProjectFieldsForOrganization lists Projects V2 for an organization. +// ListProjectFieldsForOrg lists Projects V2 for an organization. // // GitHub API docs: https://docs.github.com/rest/projects/fields#list-project-fields-for-organization // //meta:operation GET /orgs/{org}/projectsV2/{project_number}/fields -func (s *ProjectsService) ListProjectFieldsForOrganization(ctx context.Context, org string, projectNumber int64, opts *ListProjectsOptions) ([]*ProjectV2Field, *Response, error) { +func (s *ProjectsService) ListProjectFieldsForOrg(ctx context.Context, org string, projectNumber int64, opts *ListProjectsOptions) ([]*ProjectV2Field, *Response, error) { u := fmt.Sprintf("orgs/%v/projectsV2/%v/fields", org, projectNumber) u, err := addOptions(u, opts) if err != nil { diff --git a/github/projects_test.go b/github/projects_test.go index ccbdc65601a..43e7dc3353e 100644 --- a/github/projects_test.go +++ b/github/projects_test.go @@ -12,7 +12,7 @@ import ( "testing" ) -func TestProjectsService_ListProjectsForOrganizations(t *testing.T) { +func TestProjectsService_ListProjectsForOrg(t *testing.T) { t.Parallel() client, mux, _ := setup(t) @@ -31,22 +31,22 @@ func TestProjectsService_ListProjectsForOrganizations(t *testing.T) { opts := &ListProjectsOptions{Query: "alpha", After: "2", Before: "1"} ctx := context.Background() - projects, _, err := client.Projects.ListProjectsForOrganization(ctx, "o", opts) + projects, _, err := client.Projects.ListProjectsForOrg(ctx, "o", opts) if err != nil { - t.Fatalf("Projects.ListProjectsForOrganization returned error: %v", err) + t.Fatalf("Projects.ListProjectsForOrg returned error: %v", err) } if len(projects) != 1 || projects[0].GetID() != 1 || projects[0].GetTitle() != "T1" { - t.Fatalf("Projects.ListProjectsForOrganization returned %+v", projects) + t.Fatalf("Projects.ListProjectsForOrg returned %+v", projects) } - const methodName = "ListProjectsForOrganization" + const methodName = "ListProjectsForOrg" testBadOptions(t, methodName, func() (err error) { - _, _, err = client.Projects.ListProjectsForOrganization(ctx, "\n", opts) + _, _, err = client.Projects.ListProjectsForOrg(ctx, "\n", opts) return err }) testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { - got, resp, err := client.Projects.ListProjectsForOrganization(ctx, "o", opts) + got, resp, err := client.Projects.ListProjectsForOrg(ctx, "o", opts) if got != nil { t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) } @@ -55,7 +55,7 @@ func TestProjectsService_ListProjectsForOrganizations(t *testing.T) { // still allow both set (no validation enforced) – ensure it does not error ctxBypass := context.WithValue(context.Background(), BypassRateLimitCheck, true) - if _, _, err = client.Projects.ListProjectsForOrganization(ctxBypass, "o", &ListProjectsOptions{Before: "b", After: "a"}); err != nil { + if _, _, err = client.Projects.ListProjectsForOrg(ctxBypass, "o", &ListProjectsOptions{Before: "b", After: "a"}); err != nil { t.Fatalf("unexpected error when both before/after set: %v", err) } } @@ -164,9 +164,9 @@ func TestProjectsService_GetProjectForUser(t *testing.T) { }) } -// TestProjectsService_ListProjectsForOrganization_pagination clarifies how callers should +// TestProjectsService_ListProjectsForOrg_pagination clarifies how callers should // use resp.After to request the next page and resp.Before for previous pages when supported. -func TestProjectsService_ListProjectsForOrganization_pagination(t *testing.T) { +func TestProjectsService_ListProjectsForOrg_pagination(t *testing.T) { t.Parallel() client, mux, _ := setup(t) @@ -192,7 +192,7 @@ func TestProjectsService_ListProjectsForOrganization_pagination(t *testing.T) { }) ctx := context.Background() - first, resp, err := client.Projects.ListProjectsForOrganization(ctx, "o", nil) + first, resp, err := client.Projects.ListProjectsForOrg(ctx, "o", nil) if err != nil { t.Fatalf("first page error: %v", err) } @@ -205,7 +205,7 @@ func TestProjectsService_ListProjectsForOrganization_pagination(t *testing.T) { // Use resp.After as opts.After for next page opts := &ListProjectsOptions{After: resp.After} - second, resp2, err := client.Projects.ListProjectsForOrganization(ctx, "o", opts) + second, resp2, err := client.Projects.ListProjectsForOrg(ctx, "o", opts) if err != nil { t.Fatalf("second page error: %v", err) } @@ -265,7 +265,7 @@ func TestProjectsService_ListProjectsForUser_pagination(t *testing.T) { } } -func TestProjectsService_ListProjectFieldsForOrganization(t *testing.T) { +func TestProjectsService_ListProjectFieldsForOrg(t *testing.T) { t.Parallel() client, mux, _ := setup(t) @@ -316,13 +316,13 @@ func TestProjectsService_ListProjectFieldsForOrganization(t *testing.T) { opts := &ListProjectsOptions{Query: "text", After: "2", Before: "1"} ctx := context.Background() - fields, _, err := client.Projects.ListProjectFieldsForOrganization(ctx, "o", 1, opts) + fields, _, err := client.Projects.ListProjectFieldsForOrg(ctx, "o", 1, opts) if err != nil { - t.Fatalf("Projects.ListProjectFieldsForOrganization returned error: %v", err) + t.Fatalf("Projects.ListProjectFieldsForOrg returned error: %v", err) } if len(fields) != 2 { - t.Fatalf("Projects.ListProjectFieldsForOrganization returned %d fields, want 2", len(fields)) + t.Fatalf("Projects.ListProjectFieldsForOrg returned %d fields, want 2", len(fields)) } // Validate first field (with options) @@ -349,14 +349,14 @@ func TestProjectsService_ListProjectFieldsForOrganization(t *testing.T) { t.Errorf("Second field options: got %d, want 0", len(field2.Options)) } - const methodName = "ListProjectFieldsForOrganization" + const methodName = "ListProjectFieldsForOrg" testBadOptions(t, methodName, func() (err error) { - _, _, err = client.Projects.ListProjectFieldsForOrganization(ctx, "\n", 1, opts) + _, _, err = client.Projects.ListProjectFieldsForOrg(ctx, "\n", 1, opts) return err }) testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { - got, resp, err := client.Projects.ListProjectFieldsForOrganization(ctx, "o", 1, opts) + got, resp, err := client.Projects.ListProjectFieldsForOrg(ctx, "o", 1, opts) if got != nil { t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) } @@ -365,12 +365,12 @@ func TestProjectsService_ListProjectFieldsForOrganization(t *testing.T) { // still allow both set (no validation enforced) – ensure it does not error ctxBypass := context.WithValue(context.Background(), BypassRateLimitCheck, true) - if _, _, err = client.Projects.ListProjectFieldsForOrganization(ctxBypass, "o", 1, &ListProjectsOptions{Before: "b", After: "a"}); err != nil { + if _, _, err = client.Projects.ListProjectFieldsForOrg(ctxBypass, "o", 1, &ListProjectsOptions{Before: "b", After: "a"}); err != nil { t.Fatalf("unexpected error when both before/after set: %v", err) } } -func TestProjectsService_ListProjectFieldsForOrganization_pagination(t *testing.T) { +func TestProjectsService_ListProjectFieldsForOrg_pagination(t *testing.T) { t.Parallel() client, mux, _ := setup(t) @@ -396,7 +396,7 @@ func TestProjectsService_ListProjectFieldsForOrganization_pagination(t *testing. }) ctx := context.Background() - first, resp, err := client.Projects.ListProjectFieldsForOrganization(ctx, "o", 1, nil) + first, resp, err := client.Projects.ListProjectFieldsForOrg(ctx, "o", 1, nil) if err != nil { t.Fatalf("first page error: %v", err) } @@ -409,7 +409,7 @@ func TestProjectsService_ListProjectFieldsForOrganization_pagination(t *testing. // Use resp.After as opts.After for next page opts := &ListProjectsOptions{After: resp.After} - second, resp2, err := client.Projects.ListProjectFieldsForOrganization(ctx, "o", 1, opts) + second, resp2, err := client.Projects.ListProjectFieldsForOrg(ctx, "o", 1, opts) if err != nil { t.Fatalf("second page error: %v", err) } From 98c28fa2018d2c34f3c44f984003b54cd24de3fc Mon Sep 17 00:00:00 2001 From: JoannaaKL Date: Wed, 24 Sep 2025 11:17:49 +0200 Subject: [PATCH 16/19] Implemented all endpoints for project items --- github/event_types.go | 21 +- github/github-accessors.go | 48 ++++ github/github-accessors_test.go | 66 +++++ github/projects.go | 235 ++++++++++++++- github/projects_test.go | 491 ++++++++++++++++++-------------- 5 files changed, 629 insertions(+), 232 deletions(-) diff --git a/github/event_types.go b/github/event_types.go index 480ed8dfb96..a4dc7daaa63 100644 --- a/github/event_types.go +++ b/github/event_types.go @@ -1179,15 +1179,18 @@ type FieldValue struct { // ProjectV2Item represents an item belonging to a project. type ProjectV2Item struct { - ID *int64 `json:"id,omitempty"` - NodeID *string `json:"node_id,omitempty"` - ProjectNodeID *string `json:"project_node_id,omitempty"` - ContentNodeID *string `json:"content_node_id,omitempty"` - ContentType *string `json:"content_type,omitempty"` - Creator *User `json:"creator,omitempty"` - CreatedAt *Timestamp `json:"created_at,omitempty"` - UpdatedAt *Timestamp `json:"updated_at,omitempty"` - ArchivedAt *Timestamp `json:"archived_at,omitempty"` + ID *int64 `json:"id,omitempty"` + NodeID *string `json:"node_id,omitempty"` + ProjectNodeID *string `json:"project_node_id,omitempty"` + ContentNodeID *string `json:"content_node_id,omitempty"` + ProjectURL *string `json:"project_url,omitempty"` + ContentType *string `json:"content_type,omitempty"` + Creator *User `json:"creator,omitempty"` + CreatedAt *Timestamp `json:"created_at,omitempty"` + UpdatedAt *Timestamp `json:"updated_at,omitempty"` + ArchivedAt *Timestamp `json:"archived_at,omitempty"` + ItemURL *string `json:"item_url,omitempty"` + Fields []*ProjectV2Field `json:"fields,omitempty"` } // PublicEvent is triggered when a private repository is open sourced. diff --git a/github/github-accessors.go b/github/github-accessors.go index 55334fddea2..fdbfe6a4984 100644 --- a/github/github-accessors.go +++ b/github/github-accessors.go @@ -15198,6 +15198,14 @@ func (n *NetworkSettingsResource) GetSubnetID() string { return *n.SubnetID } +// GetID returns the ID field if it's non-nil, zero value otherwise. +func (n *NewProjectV2Field) GetID() int64 { + if n == nil || n.ID == nil { + return 0 + } + return *n.ID +} + // GetBase returns the Base field if it's non-nil, zero value otherwise. func (n *NewPullRequest) GetBase() string { if n == nil || n.Base == nil { @@ -18646,6 +18654,14 @@ func (p *ProjectV2Field) GetCreatedAt() Timestamp { return *p.CreatedAt } +// GetID returns the ID field if it's non-nil, zero value otherwise. +func (p *ProjectV2Field) GetID() int64 { + if p == nil || p.ID == nil { + return 0 + } + return *p.ID +} + // GetUpdatedAt returns the UpdatedAt field if it's non-nil, zero value otherwise. func (p *ProjectV2Field) GetUpdatedAt() Timestamp { if p == nil || p.UpdatedAt == nil { @@ -18654,6 +18670,14 @@ func (p *ProjectV2Field) GetUpdatedAt() Timestamp { return *p.UpdatedAt } +// GetID returns the ID field if it's non-nil, zero value otherwise. +func (p *ProjectV2FieldOption) GetID() int64 { + if p == nil || p.ID == nil { + return 0 + } + return *p.ID +} + // GetArchivedAt returns the ArchivedAt field if it's non-nil, zero value otherwise. func (p *ProjectV2Item) GetArchivedAt() Timestamp { if p == nil || p.ArchivedAt == nil { @@ -18702,6 +18726,14 @@ func (p *ProjectV2Item) GetID() int64 { return *p.ID } +// GetItemURL returns the ItemURL field if it's non-nil, zero value otherwise. +func (p *ProjectV2Item) GetItemURL() string { + if p == nil || p.ItemURL == nil { + return "" + } + return *p.ItemURL +} + // GetNodeID returns the NodeID field if it's non-nil, zero value otherwise. func (p *ProjectV2Item) GetNodeID() string { if p == nil || p.NodeID == nil { @@ -18718,6 +18750,14 @@ func (p *ProjectV2Item) GetProjectNodeID() string { return *p.ProjectNodeID } +// GetProjectURL returns the ProjectURL field if it's non-nil, zero value otherwise. +func (p *ProjectV2Item) GetProjectURL() string { + if p == nil || p.ProjectURL == nil { + return "" + } + return *p.ProjectURL +} + // GetUpdatedAt returns the UpdatedAt field if it's non-nil, zero value otherwise. func (p *ProjectV2Item) GetUpdatedAt() Timestamp { if p == nil || p.UpdatedAt == nil { @@ -28334,6 +28374,14 @@ func (u *UpdateEnterpriseRunnerGroupRequest) GetVisibility() string { return *u.Visibility } +// GetArchived returns the Archived field if it's non-nil, zero value otherwise. +func (u *UpdateProjectItemOptions) GetArchived() bool { + if u == nil || u.Archived == nil { + return false + } + return *u.Archived +} + // GetForce returns the Force field if it's non-nil, zero value otherwise. func (u *UpdateRef) GetForce() bool { if u == nil || u.Force == nil { diff --git a/github/github-accessors_test.go b/github/github-accessors_test.go index fd2d06717b0..dab5705bb43 100644 --- a/github/github-accessors_test.go +++ b/github/github-accessors_test.go @@ -19699,6 +19699,17 @@ func TestNetworkSettingsResource_GetSubnetID(tt *testing.T) { n.GetSubnetID() } +func TestNewProjectV2Field_GetID(tt *testing.T) { + tt.Parallel() + var zeroValue int64 + n := &NewProjectV2Field{ID: &zeroValue} + n.GetID() + n = &NewProjectV2Field{} + n.GetID() + n = nil + n.GetID() +} + func TestNewPullRequest_GetBase(tt *testing.T) { tt.Parallel() var zeroValue string @@ -24191,6 +24202,17 @@ func TestProjectV2Field_GetCreatedAt(tt *testing.T) { p.GetCreatedAt() } +func TestProjectV2Field_GetID(tt *testing.T) { + tt.Parallel() + var zeroValue int64 + p := &ProjectV2Field{ID: &zeroValue} + p.GetID() + p = &ProjectV2Field{} + p.GetID() + p = nil + p.GetID() +} + func TestProjectV2Field_GetUpdatedAt(tt *testing.T) { tt.Parallel() var zeroValue Timestamp @@ -24202,6 +24224,17 @@ func TestProjectV2Field_GetUpdatedAt(tt *testing.T) { p.GetUpdatedAt() } +func TestProjectV2FieldOption_GetID(tt *testing.T) { + tt.Parallel() + var zeroValue int64 + p := &ProjectV2FieldOption{ID: &zeroValue} + p.GetID() + p = &ProjectV2FieldOption{} + p.GetID() + p = nil + p.GetID() +} + func TestProjectV2Item_GetArchivedAt(tt *testing.T) { tt.Parallel() var zeroValue Timestamp @@ -24265,6 +24298,17 @@ func TestProjectV2Item_GetID(tt *testing.T) { p.GetID() } +func TestProjectV2Item_GetItemURL(tt *testing.T) { + tt.Parallel() + var zeroValue string + p := &ProjectV2Item{ItemURL: &zeroValue} + p.GetItemURL() + p = &ProjectV2Item{} + p.GetItemURL() + p = nil + p.GetItemURL() +} + func TestProjectV2Item_GetNodeID(tt *testing.T) { tt.Parallel() var zeroValue string @@ -24287,6 +24331,17 @@ func TestProjectV2Item_GetProjectNodeID(tt *testing.T) { p.GetProjectNodeID() } +func TestProjectV2Item_GetProjectURL(tt *testing.T) { + tt.Parallel() + var zeroValue string + p := &ProjectV2Item{ProjectURL: &zeroValue} + p.GetProjectURL() + p = &ProjectV2Item{} + p.GetProjectURL() + p = nil + p.GetProjectURL() +} + func TestProjectV2Item_GetUpdatedAt(tt *testing.T) { tt.Parallel() var zeroValue Timestamp @@ -36477,6 +36532,17 @@ func TestUpdateEnterpriseRunnerGroupRequest_GetVisibility(tt *testing.T) { u.GetVisibility() } +func TestUpdateProjectItemOptions_GetArchived(tt *testing.T) { + tt.Parallel() + var zeroValue bool + u := &UpdateProjectItemOptions{Archived: &zeroValue} + u.GetArchived() + u = &UpdateProjectItemOptions{} + u.GetArchived() + u = nil + u.GetArchived() +} + func TestUpdateRef_GetForce(tt *testing.T) { tt.Parallel() var zeroValue bool diff --git a/github/projects.go b/github/projects.go index 9d559e298fb..178d18b54d1 100644 --- a/github/projects.go +++ b/github/projects.go @@ -45,7 +45,7 @@ type ListProjectsOptions struct { // // GitHub API docs: https://docs.github.com/rest/projects/fields type ProjectV2FieldOption struct { - ID string `json:"id,omitempty"` // The unique identifier for this option. + ID *int64 `json:"id,omitempty"` // The unique identifier for this option. Name string `json:"name,omitempty"` // The display name of the option. Color string `json:"color,omitempty"` // The color associated with this option (e.g., "blue", "red"). Description string `json:"description,omitempty"` // An optional description for this option. @@ -56,7 +56,7 @@ type ProjectV2FieldOption struct { // // GitHub API docs: https://docs.github.com/rest/projects/fields type ProjectV2Field struct { - ID string `json:"id,omitempty"` // The unique identifier for this field. + ID *int64 `json:"id,omitempty"` // The unique identifier for this field. NodeID string `json:"node_id,omitempty"` // The GraphQL node ID for this field. Name string `json:"name,omitempty"` // The display name of the field. DataType string `json:"dataType,omitempty"` // The data type of the field (e.g., "text", "number", "date", "single_select", "multi_select"). @@ -66,6 +66,12 @@ type ProjectV2Field struct { UpdatedAt *Timestamp `json:"updated_at,omitempty"` // The time when this field was last updated. } +// NewProjectV2Field represents a new field to be added to a GitHub Projects V2. +type NewProjectV2Field struct { + ID *int64 `json:"id,omitempty"` // The unique identifier for this field. + Value any `json:"value,omitempty"` // The value of the field. +} + // ListProjectsForOrg lists Projects V2 for an organization. // // GitHub API docs: https://docs.github.com/rest/projects/projects#list-projects-for-organization @@ -179,3 +185,228 @@ func (s *ProjectsService) ListProjectFieldsForOrg(ctx context.Context, org strin } return fields, resp, nil } + +// ListProjectItemsOptions specifies optional parameters when listing project items. +// Note: Pagination uses before/after cursor-style pagination similar to ListProjectsOptions. +// "Fields" can be used to restrict which field values are returned (by their numeric IDs). +type ListProjectItemsOptions struct { + // Embed ListProjectsOptions to reuse pagination and query parameters. + ListProjectsOptions + // Fields restricts which field values are returned by numeric field IDs. + Fields []int64 `url:"fields,omitempty,comma"` +} + +// GetProjectItemOptions specifies optional parameters when getting a project item. +type GetProjectItemOptions struct { + // Fields restricts which field values are returned by numeric field IDs. + Fields []int64 `url:"fields,omitempty,comma"` +} + +// AddProjectItemOptions represents the payload to add an item (issue or pull request) +// to a project. The Type must be either "Issue" or "PullRequest" (as per API docs) and +// ID is the numerical ID of that issue or pull request. +type AddProjectItemOptions struct { + Type string `json:"type,omitempty"` + ID int64 `json:"id,omitempty"` +} + +// UpdateProjectItemOptions represents fields that can be modified for a project item. +// Currently the REST API allows archiving/unarchiving an item (archived boolean). +// This struct can be expanded in the future as the API grows. +type UpdateProjectItemOptions struct { + // Archived indicates whether the item should be archived (true) or unarchived (false). + Archived *bool `json:"archived,omitempty"` + // Fields allows updating field values for the item. Each entry supplies a field ID and a value. + Fields []*NewProjectV2Field `json:"fields,omitempty"` +} + +// ListProjectItemsForOrg lists items for an organization owned project. +// +// GitHub API docs: https://docs.github.com/rest/projects/items#list-items-for-an-organization-owned-project +// +//meta:operation GET /orgs/{org}/projectsV2/{project_number}/items +func (s *ProjectsService) ListProjectItemsForOrg(ctx context.Context, org string, projectNumber int64, opts *ListProjectItemsOptions) ([]*ProjectV2Item, *Response, error) { + u := fmt.Sprintf("orgs/%v/projectsV2/%v/items", org, projectNumber) + u, err := addOptions(u, opts) + if err != nil { + return nil, nil, err + } + + req, err := s.client.NewRequest("GET", u, nil) + if err != nil { + return nil, nil, err + } + + var items []*ProjectV2Item + resp, err := s.client.Do(ctx, req, &items) + if err != nil { + return nil, resp, err + } + return items, resp, nil +} + +// AddProjectItemForOrg adds an issue or pull request item to an organization owned project. +// +// GitHub API docs: https://docs.github.com/rest/projects/items#add-item-to-organization-owned-project +// +//meta:operation POST /orgs/{org}/projectsV2/{project_number}/items +func (s *ProjectsService) AddProjectItemForOrg(ctx context.Context, org string, projectNumber int64, opts *AddProjectItemOptions) (*ProjectV2Item, *Response, error) { + u := fmt.Sprintf("orgs/%v/projectsV2/%v/items", org, projectNumber) + req, err := s.client.NewRequest("POST", u, opts) + if err != nil { + return nil, nil, err + } + + item := new(ProjectV2Item) + resp, err := s.client.Do(ctx, req, item) + if err != nil { + return nil, resp, err + } + return item, resp, nil +} + +// GetProjectItemForOrg gets a single item from an organization owned project. +// +// GitHub API docs: https://docs.github.com/rest/projects/items#get-an-item-for-an-organization-owned-project +// +//meta:operation GET /orgs/{org}/projectsV2/{project_number}/items/{item_id} +func (s *ProjectsService) GetProjectItemForOrg(ctx context.Context, org string, projectNumber, itemID int64, opts *GetProjectItemOptions) (*ProjectV2Item, *Response, error) { + u := fmt.Sprintf("orgs/%v/projectsV2/%v/items/%v", org, projectNumber, itemID) + req, err := s.client.NewRequest("GET", u, opts) + if err != nil { + return nil, nil, err + } + item := new(ProjectV2Item) + resp, err := s.client.Do(ctx, req, item) + if err != nil { + return nil, resp, err + } + return item, resp, nil +} + +// UpdateProjectItemForOrg updates an item in an organization owned project. +// +// GitHub API docs: https://docs.github.com/rest/projects/items#update-project-item-for-organization +// +//meta:operation PATCH /orgs/{org}/projectsV2/{project_number}/items/{item_id} +func (s *ProjectsService) UpdateProjectItemForOrg(ctx context.Context, org string, projectNumber, itemID int64, opts *UpdateProjectItemOptions) (*ProjectV2Item, *Response, error) { + u := fmt.Sprintf("orgs/%v/projectsV2/%v/items/%v", org, projectNumber, itemID) + req, err := s.client.NewRequest("PATCH", u, opts) + if err != nil { + return nil, nil, err + } + item := new(ProjectV2Item) + resp, err := s.client.Do(ctx, req, item) + if err != nil { + return nil, resp, err + } + return item, resp, nil +} + +// DeleteProjectItemForOrg deletes an item from an organization owned project. +// +// GitHub API docs: https://docs.github.com/rest/projects/items#delete-project-item-for-organization +// +//meta:operation DELETE /orgs/{org}/projectsV2/{project_number}/items/{item_id} +func (s *ProjectsService) DeleteProjectItemForOrg(ctx context.Context, org string, projectNumber, itemID int64) (*Response, error) { + u := fmt.Sprintf("orgs/%v/projectsV2/%v/items/%v", org, projectNumber, itemID) + req, err := s.client.NewRequest("DELETE", u, nil) + if err != nil { + return nil, err + } + return s.client.Do(ctx, req, nil) +} + +// ListProjectItemsForUser lists items for a user owned project. +// +// GitHub API docs: https://docs.github.com/rest/projects/items#list-items-for-a-user-owned-project +// +//meta:operation GET /users/{username}/projectsV2/{project_number}/items +func (s *ProjectsService) ListProjectItemsForUser(ctx context.Context, username string, projectNumber int64, opts *ListProjectItemsOptions) ([]*ProjectV2Item, *Response, error) { + u := fmt.Sprintf("users/%v/projectsV2/%v/items", username, projectNumber) + u, err := addOptions(u, opts) + if err != nil { + return nil, nil, err + } + req, err := s.client.NewRequest("GET", u, nil) + if err != nil { + return nil, nil, err + } + var items []*ProjectV2Item + resp, err := s.client.Do(ctx, req, &items) + if err != nil { + return nil, resp, err + } + return items, resp, nil +} + +// AddProjectItemForUser adds an issue or pull request item to a user owned project. +// +// GitHub API docs: https://docs.github.com/rest/projects/items#add-item-to-user-owned-project +// +//meta:operation POST /users/{username}/projectsV2/{project_number}/items +func (s *ProjectsService) AddProjectItemForUser(ctx context.Context, username string, projectNumber int64, opts *AddProjectItemOptions) (*ProjectV2Item, *Response, error) { + u := fmt.Sprintf("users/%v/projectsV2/%v/items", username, projectNumber) + req, err := s.client.NewRequest("POST", u, opts) + if err != nil { + return nil, nil, err + } + item := new(ProjectV2Item) + resp, err := s.client.Do(ctx, req, item) + if err != nil { + return nil, resp, err + } + return item, resp, nil +} + +// GetProjectItemForUser gets a single item from a user owned project. +// +// GitHub API docs: https://docs.github.com/rest/projects/items#get-an-item-for-a-user-owned-project +// +//meta:operation GET /users/{username}/projectsV2/{project_number}/items/{item_id} +func (s *ProjectsService) GetProjectItemForUser(ctx context.Context, username string, projectNumber, itemID int64, opts *GetProjectItemOptions) (*ProjectV2Item, *Response, error) { + u := fmt.Sprintf("users/%v/projectsV2/%v/items/%v", username, projectNumber, itemID) + req, err := s.client.NewRequest("GET", u, opts) + if err != nil { + return nil, nil, err + } + item := new(ProjectV2Item) + resp, err := s.client.Do(ctx, req, item) + if err != nil { + return nil, resp, err + } + return item, resp, nil +} + +// UpdateProjectItemForUser updates an item in a user owned project. +// +// GitHub API docs: https://docs.github.com/rest/projects/items#update-project-item-for-user +// +//meta:operation PATCH /users/{username}/projectsV2/{project_number}/items/{item_id} +func (s *ProjectsService) UpdateProjectItemForUser(ctx context.Context, username string, projectNumber, itemID int64, opts *UpdateProjectItemOptions) (*ProjectV2Item, *Response, error) { + u := fmt.Sprintf("users/%v/projectsV2/%v/items/%v", username, projectNumber, itemID) + req, err := s.client.NewRequest("PATCH", u, opts) + if err != nil { + return nil, nil, err + } + item := new(ProjectV2Item) + resp, err := s.client.Do(ctx, req, item) + if err != nil { + return nil, resp, err + } + return item, resp, nil +} + +// DeleteProjectItemForUser deletes an item from a user owned project. +// +// GitHub API docs: https://docs.github.com/rest/projects/items#delete-project-item-for-user +// +//meta:operation DELETE /users/{username}/projectsV2/{project_number}/items/{item_id} +func (s *ProjectsService) DeleteProjectItemForUser(ctx context.Context, username string, projectNumber, itemID int64) (*Response, error) { + u := fmt.Sprintf("users/%v/projectsV2/%v/items/%v", username, projectNumber, itemID) + req, err := s.client.NewRequest("DELETE", u, nil) + if err != nil { + return nil, err + } + return s.client.Do(ctx, req, nil) +} diff --git a/github/projects_test.go b/github/projects_test.go index 43e7dc3353e..be2b9070017 100644 --- a/github/projects_test.go +++ b/github/projects_test.go @@ -8,6 +8,7 @@ package github import ( "context" "fmt" + "io" "net/http" "testing" ) @@ -88,158 +89,95 @@ func TestProjectsService_GetProjectForOrg(t *testing.T) { }) } -func TestProjectsService_ListUserProjects(t *testing.T) { +// TestProjectsService_ListProjectFieldsForOrg lists fields for an org project. +func TestProjectsService_ListProjectFieldsForOrg(t *testing.T) { t.Parallel() client, mux, _ := setup(t) - // Combined handler: supports initial test case and dual before/after scenario. - mux.HandleFunc("/users/u/projectsV2", func(w http.ResponseWriter, r *http.Request) { + mux.HandleFunc("/orgs/o/projectsV2/1/fields", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, "GET") q := r.URL.Query() - if q.Get("before") == "b" && q.Get("after") == "a" { + if q.Get("before") == "b" && q.Get("after") == "a" { // bypass scenario fmt.Fprint(w, `[]`) return } - testFormValues(t, r, values{"q": "beta", "before": "1", "after": "2", "per_page": "2"}) - fmt.Fprint(w, `[{"id":2,"title":"UProj","created_at":"2011-01-02T15:04:05Z","updated_at":"2012-01-02T15:04:05Z"}]`) + testFormValues(t, r, values{"after": "2", "before": "1", "q": "text"}) + fmt.Fprint(w, `[ + { + "id": 1, + "node_id": "node_1", + "name": "Status", + "dataType": "single_select", + "url": "https://api.github.com/projects/1/fields/field1", + "options": [ + {"id": 1, "name": "Todo", "color": "blue", "description": "Tasks to be done"}, + {"id": 2, "name": "In Progress", "color": "yellow"} + ], + "created_at": "2011-01-02T15:04:05Z", + "updated_at": "2012-01-02T15:04:05Z" + }, + { + "id": 2, + "node_id": "node_2", + "name": "Priority", + "dataType": "text", + "url": "https://api.github.com/projects/1/fields/field2", + "created_at": "2011-01-02T15:04:05Z", + "updated_at": "2012-01-02T15:04:05Z" + } + ]`) }) - opts := &ListProjectsOptions{Query: "beta", Before: "1", After: "2", PerPage: 2} + opts := &ListProjectsOptions{Query: "text", After: "2", Before: "1"} ctx := context.Background() - var ctxBypass context.Context - projects, _, err := client.Projects.ListProjectsForUser(ctx, "u", opts) + fields, _, err := client.Projects.ListProjectFieldsForOrg(ctx, "o", 1, opts) if err != nil { - t.Fatalf("Projects.ListProjectsForUser returned error: %v", err) + t.Fatalf("Projects.ListProjectFieldsForOrg returned error: %v", err) + } + if len(fields) != 2 { + t.Fatalf("Projects.ListProjectFieldsForOrg returned %d fields, want 2", len(fields)) } - if len(projects) != 1 || projects[0].GetID() != 2 || projects[0].GetTitle() != "UProj" { - t.Fatalf("Projects.ListProjectsForUser returned %+v", projects) + if fields[0].ID == nil || *fields[0].ID != 1 || fields[1].ID == nil || *fields[1].ID != 2 { + t.Fatalf("unexpected field IDs: %+v", fields) } - const methodName = "ListProjectsForUser" + const methodName = "ListProjectFieldsForOrg" testBadOptions(t, methodName, func() (err error) { - _, _, err = client.Projects.ListProjectsForUser(ctx, "\n", opts) + _, _, err = client.Projects.ListProjectFieldsForOrg(ctx, "\n", 1, opts) return err }) - testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { - got, resp, err := client.Projects.ListProjectsForUser(ctx, "u", opts) + got, resp, err := client.Projects.ListProjectFieldsForOrg(ctx, "o", 1, opts) if got != nil { t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) } return resp, err }) - - // still allow both set (no validation enforced) – ensure it does not error - ctxBypass = context.WithValue(context.Background(), BypassRateLimitCheck, true) - if _, _, err = client.Projects.ListProjectsForUser(ctxBypass, "u", &ListProjectsOptions{Before: "b", After: "a"}); err != nil { + ctxBypass := context.WithValue(context.Background(), BypassRateLimitCheck, true) + if _, _, err = client.Projects.ListProjectFieldsForOrg(ctxBypass, "o", 1, &ListProjectsOptions{Before: "b", After: "a"}); err != nil { t.Fatalf("unexpected error when both before/after set: %v", err) } } -func TestProjectsService_GetProjectForUser(t *testing.T) { - t.Parallel() - client, mux, _ := setup(t) - - mux.HandleFunc("/users/u/projectsV2/2", func(w http.ResponseWriter, r *http.Request) { - testMethod(t, r, "GET") - fmt.Fprint(w, `{"id":2,"title":"UProj","created_at":"2011-01-02T15:04:05Z","updated_at":"2012-01-02T15:04:05Z"}`) - }) - - ctx := context.Background() - project, _, err := client.Projects.GetProjectForUser(ctx, "u", 2) - if err != nil { - t.Fatalf("Projects.GetProjectForUser returned error: %v", err) - } - if project.GetID() != 2 || project.GetTitle() != "UProj" { - t.Fatalf("Projects.GetProjectForUser returned %+v", project) - } - - const methodName = "GetProjectForUser" - testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { - got, resp, err := client.Projects.GetProjectForUser(ctx, "u", 2) - if got != nil { - t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) - } - return resp, err - }) -} - -// TestProjectsService_ListProjectsForOrg_pagination clarifies how callers should -// use resp.After to request the next page and resp.Before for previous pages when supported. -func TestProjectsService_ListProjectsForOrg_pagination(t *testing.T) { - t.Parallel() - client, mux, _ := setup(t) - - // First page returns a Link header with rel="next" containing an after cursor (after=cursor2) - mux.HandleFunc("/orgs/o/projectsV2", func(w http.ResponseWriter, r *http.Request) { - q := r.URL.Query() - after := q.Get("after") - before := q.Get("before") - if after == "" && before == "" { - // first request - w.Header().Set("Link", "; rel=\"next\"") - fmt.Fprint(w, `[{"id":1,"title":"P1","created_at":"2011-01-02T15:04:05Z","updated_at":"2012-01-02T15:04:05Z"}]`) - return - } - if after == "cursor2" { - // second request simulates a previous link - w.Header().Set("Link", "; rel=\"prev\"") - fmt.Fprint(w, `[{"id":2,"title":"P2","created_at":"2011-01-02T15:04:05Z","updated_at":"2012-01-02T15:04:05Z"}]`) - return - } - // unexpected state - http.Error(w, "unexpected query", http.StatusBadRequest) - }) - - ctx := context.Background() - first, resp, err := client.Projects.ListProjectsForOrg(ctx, "o", nil) - if err != nil { - t.Fatalf("first page error: %v", err) - } - if len(first) != 1 || first[0].GetID() != 1 { - t.Fatalf("unexpected first page %+v", first) - } - if resp.After != "cursor2" { - t.Fatalf("expected resp.After=cursor2 got %q", resp.After) - } - - // Use resp.After as opts.After for next page - opts := &ListProjectsOptions{After: resp.After} - second, resp2, err := client.Projects.ListProjectsForOrg(ctx, "o", opts) - if err != nil { - t.Fatalf("second page error: %v", err) - } - if len(second) != 1 || second[0].GetID() != 2 { - t.Fatalf("unexpected second page %+v", second) - } - if resp2.Before != "cursor2" { - t.Fatalf("expected resp2.Before=cursor2 got %q", resp2.Before) - } -} - -// TestProjectsService_ListProjectsForUser_pagination mirrors the org pagination test -// but exercises the user endpoint to ensure Before/After cursor handling works identically. func TestProjectsService_ListProjectsForUser_pagination(t *testing.T) { t.Parallel() client, mux, _ := setup(t) - mux.HandleFunc("/users/u/projectsV2", func(w http.ResponseWriter, r *http.Request) { q := r.URL.Query() after := q.Get("after") before := q.Get("before") - if after == "" && before == "" { // first page + if after == "" && before == "" { w.Header().Set("Link", "; rel=\"next\"") fmt.Fprint(w, `[{"id":10,"title":"UP1","created_at":"2011-01-02T15:04:05Z","updated_at":"2012-01-02T15:04:05Z"}]`) return } - if after == "ucursor2" { // second page provides prev + if after == "ucursor2" { w.Header().Set("Link", "; rel=\"prev\"") fmt.Fprint(w, `[{"id":11,"title":"UP2","created_at":"2011-01-02T15:04:05Z","updated_at":"2012-01-02T15:04:05Z"}]`) return } http.Error(w, "unexpected query", http.StatusBadRequest) }) - ctx := context.Background() first, resp, err := client.Projects.ListProjectsForUser(ctx, "u", nil) if err != nil { @@ -251,7 +189,6 @@ func TestProjectsService_ListProjectsForUser_pagination(t *testing.T) { if resp.After != "ucursor2" { t.Fatalf("expected resp.After=ucursor2 got %q", resp.After) } - opts := &ListProjectsOptions{After: resp.After} second, resp2, err := client.Projects.ListProjectsForUser(ctx, "u", opts) if err != nil { @@ -265,111 +202,6 @@ func TestProjectsService_ListProjectsForUser_pagination(t *testing.T) { } } -func TestProjectsService_ListProjectFieldsForOrg(t *testing.T) { - t.Parallel() - client, mux, _ := setup(t) - - // Combined handler: supports initial test case and dual before/after validation scenario. - mux.HandleFunc("/orgs/o/projectsV2/1/fields", func(w http.ResponseWriter, r *http.Request) { - testMethod(t, r, "GET") - q := r.URL.Query() - if q.Get("before") == "b" && q.Get("after") == "a" { - fmt.Fprint(w, `[]`) - return - } - // default expectation for main part of test - testFormValues(t, r, values{"q": "text", "after": "2", "before": "1"}) - fmt.Fprint(w, `[ - { - "id": "field1", - "node_id": "node_1", - "name": "Status", - "dataType": "single_select", - "url": "https://api.github.com/projects/1/fields/field1", - "options": [ - { - "id": "option1", - "name": "Todo", - "color": "blue", - "description": "Tasks to be done" - }, - { - "id": "option2", - "name": "In Progress", - "color": "yellow" - } - ], - "created_at": "2011-01-02T15:04:05Z", - "updated_at": "2012-01-02T15:04:05Z" - }, - { - "id": "field2", - "node_id": "node_2", - "name": "Priority", - "dataType": "text", - "url": "https://api.github.com/projects/1/fields/field2", - "created_at": "2011-01-02T15:04:05Z", - "updated_at": "2012-01-02T15:04:05Z" - } - ]`) - }) - - opts := &ListProjectsOptions{Query: "text", After: "2", Before: "1"} - ctx := context.Background() - fields, _, err := client.Projects.ListProjectFieldsForOrg(ctx, "o", 1, opts) - if err != nil { - t.Fatalf("Projects.ListProjectFieldsForOrg returned error: %v", err) - } - - if len(fields) != 2 { - t.Fatalf("Projects.ListProjectFieldsForOrg returned %d fields, want 2", len(fields)) - } - - // Validate first field (with options) - field1 := fields[0] - if field1.ID != "field1" || field1.Name != "Status" || field1.DataType != "single_select" { - t.Errorf("First field: got ID=%s, Name=%s, DataType=%s; want field1, Status, single_select", - field1.ID, field1.Name, field1.DataType) - } - if len(field1.Options) != 2 { - t.Errorf("First field options: got %d, want 2", len(field1.Options)) - } - if field1.Options[0].Name != "Todo" || field1.Options[1].Name != "In Progress" { - t.Errorf("First field option names: got %s, %s; want Todo, In Progress", - field1.Options[0].Name, field1.Options[1].Name) - } - - // Validate second field (without options) - field2 := fields[1] - if field2.ID != "field2" || field2.Name != "Priority" || field2.DataType != "text" { - t.Errorf("Second field: got ID=%s, Name=%s, DataType=%s; want field2, Priority, text", - field2.ID, field2.Name, field2.DataType) - } - if len(field2.Options) != 0 { - t.Errorf("Second field options: got %d, want 0", len(field2.Options)) - } - - const methodName = "ListProjectFieldsForOrg" - testBadOptions(t, methodName, func() (err error) { - _, _, err = client.Projects.ListProjectFieldsForOrg(ctx, "\n", 1, opts) - return err - }) - - testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { - got, resp, err := client.Projects.ListProjectFieldsForOrg(ctx, "o", 1, opts) - if got != nil { - t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) - } - return resp, err - }) - - // still allow both set (no validation enforced) – ensure it does not error - ctxBypass := context.WithValue(context.Background(), BypassRateLimitCheck, true) - if _, _, err = client.Projects.ListProjectFieldsForOrg(ctxBypass, "o", 1, &ListProjectsOptions{Before: "b", After: "a"}); err != nil { - t.Fatalf("unexpected error when both before/after set: %v", err) - } -} - func TestProjectsService_ListProjectFieldsForOrg_pagination(t *testing.T) { t.Parallel() client, mux, _ := setup(t) @@ -382,13 +214,13 @@ func TestProjectsService_ListProjectFieldsForOrg_pagination(t *testing.T) { if after == "" && before == "" { // first request w.Header().Set("Link", "; rel=\"next\"") - fmt.Fprint(w, `[{"id":"field1","name":"Status","dataType":"single_select","created_at":"2011-01-02T15:04:05Z","updated_at":"2012-01-02T15:04:05Z"}]`) + fmt.Fprint(w, `[{"id":1,"name":"Status","dataType":"single_select","created_at":"2011-01-02T15:04:05Z","updated_at":"2012-01-02T15:04:05Z"}]`) return } if after == "cursor2" { // second request simulates a previous link w.Header().Set("Link", "; rel=\"prev\"") - fmt.Fprint(w, `[{"id":"field2","name":"Priority","dataType":"text","created_at":"2011-01-02T15:04:05Z","updated_at":"2012-01-02T15:04:05Z"}]`) + fmt.Fprint(w, `[{"id":2,"name":"Priority","dataType":"text","created_at":"2011-01-02T15:04:05Z","updated_at":"2012-01-02T15:04:05Z"}]`) return } // unexpected state @@ -400,7 +232,7 @@ func TestProjectsService_ListProjectFieldsForOrg_pagination(t *testing.T) { if err != nil { t.Fatalf("first page error: %v", err) } - if len(first) != 1 || first[0].ID != "field1" { + if len(first) != 1 || first[0].ID == nil || *first[0].ID != 1 { t.Fatalf("unexpected first page %+v", first) } if resp.After != "cursor2" { @@ -413,7 +245,7 @@ func TestProjectsService_ListProjectFieldsForOrg_pagination(t *testing.T) { if err != nil { t.Fatalf("second page error: %v", err) } - if len(second) != 1 || second[0].ID != "field2" { + if len(second) != 1 || second[0].ID == nil || *second[0].ID != 2 { t.Fatalf("unexpected second page %+v", second) } if resp2.Before != "cursor2" { @@ -454,14 +286,14 @@ func TestProjectV2Field_Marshal(t *testing.T) { testJSONMarshal(t, &ProjectV2FieldOption{}, "{}") field := &ProjectV2Field{ - ID: "field1", + ID: Ptr(int64(2)), NodeID: "node_1", Name: "Status", DataType: "single_select", URL: "https://api.github.com/projects/1/fields/field1", Options: []*ProjectV2FieldOption{ { - ID: "option1", + ID: Ptr(int64(1)), Name: "Todo", Color: "blue", Description: "Tasks to be done", @@ -472,14 +304,14 @@ func TestProjectV2Field_Marshal(t *testing.T) { } want := `{ - "id": "field1", + "id": 2, "node_id": "node_1", "name": "Status", "dataType": "single_select", "url": "https://api.github.com/projects/1/fields/field1", "options": [ { - "id": "option1", + "id": 1, "name": "Todo", "color": "blue", "description": "Tasks to be done" @@ -491,3 +323,220 @@ func TestProjectV2Field_Marshal(t *testing.T) { testJSONMarshal(t, field, want) } + +func TestProjectsService_ListProjectItemsForOrg(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/orgs/o/projectsV2/1/items", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + q := r.URL.Query() + if q.Get("before") == "b" && q.Get("after") == "a" { // bypass scenario + fmt.Fprint(w, `[]`) + return + } + testFormValues(t, r, values{"after": "2", "before": "1", "per_page": "50", "fields": "10,11", "q": "status:open"}) + fmt.Fprint(w, `[{"id":17,"node_id":"PVTI_node"}]`) + }) + + opts := &ListProjectItemsOptions{ListProjectsOptions: ListProjectsOptions{After: "2", Before: "1", PerPage: 50, Query: "status:open"}, Fields: []int64{10, 11}} + ctx := context.Background() + items, _, err := client.Projects.ListProjectItemsForOrg(ctx, "o", 1, opts) + if err != nil { + t.Fatalf("Projects.ListProjectItemsForOrg returned error: %v", err) + } + if len(items) != 1 || items[0].GetID() != 17 { + t.Fatalf("Projects.ListProjectItemsForOrg returned %+v", items) + } + + const methodName = "ListProjectItemsForOrg" + testBadOptions(t, methodName, func() (err error) { + _, _, err = client.Projects.ListProjectItemsForOrg(ctx, "\n", 1, opts) + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Projects.ListProjectItemsForOrg(ctx, "o", 1, opts) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) + + ctxBypass := context.WithValue(context.Background(), BypassRateLimitCheck, true) + if _, _, err = client.Projects.ListProjectItemsForOrg(ctxBypass, "o", 1, &ListProjectItemsOptions{ListProjectsOptions: ListProjectsOptions{Before: "b", After: "a"}}); err != nil { + t.Fatalf("unexpected error when both before/after set: %v", err) + } +} + +func TestProjectsService_AddProjectItemForOrg(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/orgs/o/projectsV2/1/items", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + b, _ := io.ReadAll(r.Body) + body := string(b) + if body != `{"type":"Issue","id":99}`+"\n" { // encoder adds newline + t.Fatalf("unexpected body: %s", body) + } + fmt.Fprint(w, `{"id":99,"node_id":"PVTI_new"}`) + }) + + ctx := context.Background() + item, _, err := client.Projects.AddProjectItemForOrg(ctx, "o", 1, &AddProjectItemOptions{Type: "Issue", ID: 99}) + if err != nil { + t.Fatalf("Projects.AddProjectItemForOrg returned error: %v", err) + } + if item.GetID() != 99 { + t.Fatalf("unexpected item: %+v", item) + } +} + +func TestProjectsService_GetProjectItemForOrg(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + mux.HandleFunc("/orgs/o/projectsV2/1/items/17", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `{"id":17,"node_id":"PVTI_node"}`) + }) + ctx := context.Background() + opts := &GetProjectItemOptions{} + item, _, err := client.Projects.GetProjectItemForOrg(ctx, "o", 1, 17, opts) + if err != nil { + t.Fatalf("GetProjectItemForOrg error: %v", err) + } + if item.GetID() != 17 { + t.Fatalf("unexpected item: %+v", item) + } +} + +func TestProjectsService_UpdateProjectItemForOrg(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + mux.HandleFunc("/orgs/o/projectsV2/1/items/17", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PATCH") + b, _ := io.ReadAll(r.Body) + body := string(b) + if body != `{"archived":true}`+"\n" { + t.Fatalf("unexpected body: %s", body) + } + fmt.Fprint(w, `{"id":17}`) + }) + archived := true + ctx := context.Background() + item, _, err := client.Projects.UpdateProjectItemForOrg(ctx, "o", 1, 17, &UpdateProjectItemOptions{Archived: &archived}) + if err != nil { + t.Fatalf("UpdateProjectItemForOrg error: %v", err) + } + if item.GetID() != 17 { + t.Fatalf("unexpected item: %+v", item) + } +} + +func TestProjectsService_DeleteProjectItemForOrg(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + mux.HandleFunc("/orgs/o/projectsV2/1/items/17", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "DELETE") + w.WriteHeader(http.StatusNoContent) + }) + ctx := context.Background() + if _, err := client.Projects.DeleteProjectItemForOrg(ctx, "o", 1, 17); err != nil { + t.Fatalf("DeleteProjectItemForOrg error: %v", err) + } +} + +func TestProjectsService_ListProjectItemsForUser(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + mux.HandleFunc("/users/u/projectsV2/2/items", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + testFormValues(t, r, values{"per_page": "20", "q": "type:issue"}) + fmt.Fprint(w, `[{"id":7,"node_id":"PVTI_user"}]`) + }) + ctx := context.Background() + items, _, err := client.Projects.ListProjectItemsForUser(ctx, "u", 2, &ListProjectItemsOptions{ListProjectsOptions: ListProjectsOptions{PerPage: 20, Query: "type:issue"}}) + if err != nil { + t.Fatalf("ListProjectItemsForUser error: %v", err) + } + if len(items) != 1 || items[0].GetID() != 7 { + t.Fatalf("unexpected items: %+v", items) + } +} + +func TestProjectsService_AddProjectItemForUser(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + mux.HandleFunc("/users/u/projectsV2/2/items", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + b, _ := io.ReadAll(r.Body) + body := string(b) + if body != `{"type":"PullRequest","id":123}`+"\n" { + t.Fatalf("unexpected body: %s", body) + } + fmt.Fprint(w, `{"id":123,"node_id":"PVTI_new_user"}`) + }) + ctx := context.Background() + item, _, err := client.Projects.AddProjectItemForUser(ctx, "u", 2, &AddProjectItemOptions{Type: "PullRequest", ID: 123}) + if err != nil { + t.Fatalf("AddProjectItemForUser error: %v", err) + } + if item.GetID() != 123 { + t.Fatalf("unexpected item: %+v", item) + } +} + +func TestProjectsService_GetProjectItemForUser(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + mux.HandleFunc("/users/u/projectsV2/2/items/55", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `{"id":55,"node_id":"PVTI_user_item"}`) + }) + ctx := context.Background() + opts := &GetProjectItemOptions{} + item, _, err := client.Projects.GetProjectItemForUser(ctx, "u", 2, 55, opts) + if err != nil { + t.Fatalf("GetProjectItemForUser error: %v", err) + } + if item.GetID() != 55 { + t.Fatalf("unexpected item: %+v", item) + } +} + +func TestProjectsService_UpdateProjectItemForUser(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + mux.HandleFunc("/users/u/projectsV2/2/items/55", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PATCH") + b, _ := io.ReadAll(r.Body) + body := string(b) + if body != `{"archived":false}`+"\n" { + t.Fatalf("unexpected body: %s", body) + } + fmt.Fprint(w, `{"id":55}`) + }) + archived := false + ctx := context.Background() + item, _, err := client.Projects.UpdateProjectItemForUser(ctx, "u", 2, 55, &UpdateProjectItemOptions{Archived: &archived}) + if err != nil { + t.Fatalf("UpdateProjectItemForUser error: %v", err) + } + if item.GetID() != 55 { + t.Fatalf("unexpected item: %+v", item) + } +} + +func TestProjectsService_DeleteProjectItemForUser(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + mux.HandleFunc("/users/u/projectsV2/2/items/55", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "DELETE") + w.WriteHeader(http.StatusNoContent) + }) + ctx := context.Background() + if _, err := client.Projects.DeleteProjectItemForUser(ctx, "u", 2, 55); err != nil { + t.Fatalf("DeleteProjectItemForUser error: %v", err) + } +} From a4b1014c1d2eca39cd425480ad5b44f644f62246 Mon Sep 17 00:00:00 2001 From: JoannaaKL Date: Wed, 24 Sep 2025 12:39:19 +0200 Subject: [PATCH 17/19] Remove comments --- github/projects_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/github/projects_test.go b/github/projects_test.go index be2b9070017..1c4cd627b9e 100644 --- a/github/projects_test.go +++ b/github/projects_test.go @@ -89,7 +89,6 @@ func TestProjectsService_GetProjectForOrg(t *testing.T) { }) } -// TestProjectsService_ListProjectFieldsForOrg lists fields for an org project. func TestProjectsService_ListProjectFieldsForOrg(t *testing.T) { t.Parallel() client, mux, _ := setup(t) From 4e935dd6e67affd6b77107432f698c90bac38c53 Mon Sep 17 00:00:00 2001 From: JoannaaKL Date: Wed, 24 Sep 2025 18:29:21 +0200 Subject: [PATCH 18/19] Add tests --- github/projects_test.go | 74 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/github/projects_test.go b/github/projects_test.go index 1c4cd627b9e..6659949b478 100644 --- a/github/projects_test.go +++ b/github/projects_test.go @@ -89,6 +89,34 @@ func TestProjectsService_GetProjectForOrg(t *testing.T) { }) } +func TestProjectsService_GetProjectForUser(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/users/u/projectsV2/3", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `{"id":3,"title":"UserProj","created_at":"2011-01-02T15:04:05Z","updated_at":"2012-01-02T15:04:05Z"}`) + }) + + ctx := context.Background() + project, _, err := client.Projects.GetProjectForUser(ctx, "u", 3) + if err != nil { + t.Fatalf("Projects.GetProjectForUser returned error: %v", err) + } + if project.GetID() != 3 || project.GetTitle() != "UserProj" { + t.Fatalf("Projects.GetProjectForUser returned %+v", project) + } + + const methodName = "GetProjectForUser" + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Projects.GetProjectForUser(ctx, "u", 3) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + func TestProjectsService_ListProjectFieldsForOrg(t *testing.T) { t.Parallel() client, mux, _ := setup(t) @@ -252,6 +280,52 @@ func TestProjectsService_ListProjectFieldsForOrg_pagination(t *testing.T) { } } +func TestProjectsService_ListProjectsForOrg_pagination(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/orgs/o/projectsV2", func(w http.ResponseWriter, r *http.Request) { + q := r.URL.Query() + after := q.Get("after") + before := q.Get("before") + if after == "" && before == "" { + w.Header().Set("Link", "; rel=\"next\"") + fmt.Fprint(w, `[{"id":20,"title":"OP1","created_at":"2011-01-02T15:04:05Z","updated_at":"2012-01-02T15:04:05Z"}]`) + return + } + if after == "ocursor2" { + w.Header().Set("Link", "; rel=\"prev\"") + fmt.Fprint(w, `[{"id":21,"title":"OP2","created_at":"2011-01-02T15:04:05Z","updated_at":"2012-01-02T15:04:05Z"}]`) + return + } + http.Error(w, "unexpected query", http.StatusBadRequest) + }) + + ctx := context.Background() + first, resp, err := client.Projects.ListProjectsForOrg(ctx, "o", nil) + if err != nil { + t.Fatalf("first page error: %v", err) + } + if len(first) != 1 || first[0].GetID() != 20 { + t.Fatalf("unexpected first page %+v", first) + } + if resp.After != "ocursor2" { + t.Fatalf("expected resp.After=ocursor2 got %q", resp.After) + } + + opts := &ListProjectsOptions{After: resp.After} + second, resp2, err := client.Projects.ListProjectsForOrg(ctx, "o", opts) + if err != nil { + t.Fatalf("second page error: %v", err) + } + if len(second) != 1 || second[0].GetID() != 21 { + t.Fatalf("unexpected second page %+v", second) + } + if resp2.Before != "ocursor2" { + t.Fatalf("expected resp2.Before=ocursor2 got %q", resp2.Before) + } +} + // Marshal test ensures V2 fields marshal correctly. func TestProjectV2_Marshal(t *testing.T) { t.Parallel() From 0c5356d08b9b16a591925f05cf6d6caa387e2e87 Mon Sep 17 00:00:00 2001 From: JoannaaKL Date: Wed, 24 Sep 2025 19:49:07 +0200 Subject: [PATCH 19/19] More tests --- github/projects_test.go | 184 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 184 insertions(+) diff --git a/github/projects_test.go b/github/projects_test.go index 6659949b478..91427ef1b4e 100644 --- a/github/projects_test.go +++ b/github/projects_test.go @@ -229,6 +229,29 @@ func TestProjectsService_ListProjectsForUser_pagination(t *testing.T) { } } +func TestProjectsService_ListProjectsForUser_error(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + mux.HandleFunc("/users/u/projectsV2", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `[]`) + }) + ctx := context.Background() + const methodName = "ListProjectsForUser" + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Projects.ListProjectsForUser(ctx, "u", nil) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) + // bad options (bad username) should error + testBadOptions(t, methodName, func() (err error) { + _, _, err = client.Projects.ListProjectsForUser(ctx, "\n", nil) + return err + }) +} + func TestProjectsService_ListProjectFieldsForOrg_pagination(t *testing.T) { t.Parallel() client, mux, _ := setup(t) @@ -466,6 +489,25 @@ func TestProjectsService_AddProjectItemForOrg(t *testing.T) { } } +func TestProjectsService_AddProjectItemForOrg_error(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + mux.HandleFunc("/orgs/o/projectsV2/1/items", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + w.WriteHeader(http.StatusCreated) + fmt.Fprint(w, `{"id":1}`) + }) + ctx := context.Background() + const methodName = "AddProjectItemForOrg" + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Projects.AddProjectItemForOrg(ctx, "o", 1, &AddProjectItemOptions{Type: "Issue", ID: 1}) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + func TestProjectsService_GetProjectItemForOrg(t *testing.T) { t.Parallel() client, mux, _ := setup(t) @@ -484,6 +526,24 @@ func TestProjectsService_GetProjectItemForOrg(t *testing.T) { } } +func TestProjectsService_GetProjectItemForOrg_error(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + mux.HandleFunc("/orgs/o/projectsV2/1/items/17", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `{"id":17}`) + }) + ctx := context.Background() + const methodName = "GetProjectItemForOrg" + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Projects.GetProjectItemForOrg(ctx, "o", 1, 17, &GetProjectItemOptions{}) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + func TestProjectsService_UpdateProjectItemForOrg(t *testing.T) { t.Parallel() client, mux, _ := setup(t) @@ -507,6 +567,25 @@ func TestProjectsService_UpdateProjectItemForOrg(t *testing.T) { } } +func TestProjectsService_UpdateProjectItemForOrg_error(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + mux.HandleFunc("/orgs/o/projectsV2/1/items/17", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PATCH") + fmt.Fprint(w, `{"id":17}`) + }) + archived := true + ctx := context.Background() + const methodName = "UpdateProjectItemForOrg" + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Projects.UpdateProjectItemForOrg(ctx, "o", 1, 17, &UpdateProjectItemOptions{Archived: &archived}) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + func TestProjectsService_DeleteProjectItemForOrg(t *testing.T) { t.Parallel() client, mux, _ := setup(t) @@ -520,6 +599,20 @@ func TestProjectsService_DeleteProjectItemForOrg(t *testing.T) { } } +func TestProjectsService_DeleteProjectItemForOrg_error(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + mux.HandleFunc("/orgs/o/projectsV2/1/items/17", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "DELETE") + w.WriteHeader(http.StatusNoContent) + }) + ctx := context.Background() + const methodName = "DeleteProjectItemForOrg" + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + return client.Projects.DeleteProjectItemForOrg(ctx, "o", 1, 17) + }) +} + func TestProjectsService_ListProjectItemsForUser(t *testing.T) { t.Parallel() client, mux, _ := setup(t) @@ -538,6 +631,28 @@ func TestProjectsService_ListProjectItemsForUser(t *testing.T) { } } +func TestProjectsService_ListProjectItemsForUser_error(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + mux.HandleFunc("/users/u/projectsV2/2/items", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `[]`) + }) + ctx := context.Background() + const methodName = "ListProjectItemsForUser" + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Projects.ListProjectItemsForUser(ctx, "u", 2, nil) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) + testBadOptions(t, methodName, func() (err error) { + _, _, err = client.Projects.ListProjectItemsForUser(ctx, "\n", 2, nil) + return err + }) +} + func TestProjectsService_AddProjectItemForUser(t *testing.T) { t.Parallel() client, mux, _ := setup(t) @@ -560,6 +675,24 @@ func TestProjectsService_AddProjectItemForUser(t *testing.T) { } } +func TestProjectsService_AddProjectItemForUser_error(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + mux.HandleFunc("/users/u/projectsV2/2/items", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + fmt.Fprint(w, `{"id":5}`) + }) + ctx := context.Background() + const methodName = "AddProjectItemForUser" + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Projects.AddProjectItemForUser(ctx, "u", 2, &AddProjectItemOptions{Type: "Issue", ID: 5}) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + func TestProjectsService_GetProjectItemForUser(t *testing.T) { t.Parallel() client, mux, _ := setup(t) @@ -578,6 +711,24 @@ func TestProjectsService_GetProjectItemForUser(t *testing.T) { } } +func TestProjectsService_GetProjectItemForUser_error(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + mux.HandleFunc("/users/u/projectsV2/2/items/55", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `{"id":55}`) + }) + ctx := context.Background() + const methodName = "GetProjectItemForUser" + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Projects.GetProjectItemForUser(ctx, "u", 2, 55, &GetProjectItemOptions{}) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + func TestProjectsService_UpdateProjectItemForUser(t *testing.T) { t.Parallel() client, mux, _ := setup(t) @@ -601,6 +752,25 @@ func TestProjectsService_UpdateProjectItemForUser(t *testing.T) { } } +func TestProjectsService_UpdateProjectItemForUser_error(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + mux.HandleFunc("/users/u/projectsV2/2/items/55", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PATCH") + fmt.Fprint(w, `{"id":55}`) + }) + archived := false + ctx := context.Background() + const methodName = "UpdateProjectItemForUser" + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Projects.UpdateProjectItemForUser(ctx, "u", 2, 55, &UpdateProjectItemOptions{Archived: &archived}) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + func TestProjectsService_DeleteProjectItemForUser(t *testing.T) { t.Parallel() client, mux, _ := setup(t) @@ -613,3 +783,17 @@ func TestProjectsService_DeleteProjectItemForUser(t *testing.T) { t.Fatalf("DeleteProjectItemForUser error: %v", err) } } + +func TestProjectsService_DeleteProjectItemForUser_error(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + mux.HandleFunc("/users/u/projectsV2/2/items/55", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "DELETE") + w.WriteHeader(http.StatusNoContent) + }) + ctx := context.Background() + const methodName = "DeleteProjectItemForUser" + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + return client.Projects.DeleteProjectItemForUser(ctx, "u", 2, 55) + }) +}