From 775f0f6b50673104a05a028b2d6f27ae6bfad7f2 Mon Sep 17 00:00:00 2001 From: JoannaaKL Date: Wed, 17 Sep 2025 14:31:10 +0000 Subject: [PATCH 01/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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 65e64f71159517f6c4389e7d534705111e04dcca Mon Sep 17 00:00:00 2001 From: JoannaaKL Date: Wed, 24 Sep 2025 12:09:51 +0200 Subject: [PATCH 16/27] Change ID type from string to int --- github/github-accessors.go | 8 ++++++++ github/github-accessors_test.go | 11 +++++++++++ github/projects.go | 2 +- github/projects_test.go | 26 +++++++++++++------------- 4 files changed, 33 insertions(+), 14 deletions(-) diff --git a/github/github-accessors.go b/github/github-accessors.go index 55334fddea2..1cd85ab5af9 100644 --- a/github/github-accessors.go +++ b/github/github-accessors.go @@ -18646,6 +18646,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 { diff --git a/github/github-accessors_test.go b/github/github-accessors_test.go index fd2d06717b0..30bce6b54f8 100644 --- a/github/github-accessors_test.go +++ b/github/github-accessors_test.go @@ -24191,6 +24191,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 diff --git a/github/projects.go b/github/projects.go index 9d559e298fb..56cd67d95eb 100644 --- a/github/projects.go +++ b/github/projects.go @@ -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"). diff --git a/github/projects_test.go b/github/projects_test.go index 43e7dc3353e..5f78704839b 100644 --- a/github/projects_test.go +++ b/github/projects_test.go @@ -281,7 +281,7 @@ func TestProjectsService_ListProjectFieldsForOrg(t *testing.T) { testFormValues(t, r, values{"q": "text", "after": "2", "before": "1"}) fmt.Fprint(w, `[ { - "id": "field1", + "id": 1, "node_id": "node_1", "name": "Status", "dataType": "single_select", @@ -303,7 +303,7 @@ func TestProjectsService_ListProjectFieldsForOrg(t *testing.T) { "updated_at": "2012-01-02T15:04:05Z" }, { - "id": "field2", + "id": 2, "node_id": "node_2", "name": "Priority", "dataType": "text", @@ -327,8 +327,8 @@ func TestProjectsService_ListProjectFieldsForOrg(t *testing.T) { // 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", + if field1.ID == nil || *field1.ID != 1 || field1.Name != "Status" || field1.DataType != "single_select" { + t.Errorf("First field: got ID=%v, Name=%s, DataType=%s; want 1, Status, single_select", field1.ID, field1.Name, field1.DataType) } if len(field1.Options) != 2 { @@ -341,8 +341,8 @@ func TestProjectsService_ListProjectFieldsForOrg(t *testing.T) { // 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", + if field2.ID == nil || *field2.ID != 2 || field2.Name != "Priority" || field2.DataType != "text" { + t.Errorf("Second field: got ID=%v, Name=%s, DataType=%s; want 2, Priority, text", field2.ID, field2.Name, field2.DataType) } if len(field2.Options) != 0 { @@ -382,13 +382,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 +400,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 +413,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,7 +454,7 @@ func TestProjectV2Field_Marshal(t *testing.T) { testJSONMarshal(t, &ProjectV2FieldOption{}, "{}") field := &ProjectV2Field{ - ID: "field1", + ID: Ptr(int64(1)), NodeID: "node_1", Name: "Status", DataType: "single_select", @@ -472,14 +472,14 @@ func TestProjectV2Field_Marshal(t *testing.T) { } want := `{ - "id": "field1", + "id": 1, "node_id": "node_1", "name": "Status", "dataType": "single_select", "url": "https://api.github.com/projects/1/fields/field1", "options": [ { - "id": "option1", + "id": "option1", "name": "Todo", "color": "blue", "description": "Tasks to be done" From d30055078feb6bacc7708a0e4d55be74dff53884 Mon Sep 17 00:00:00 2001 From: JoannaaKL Date: Wed, 24 Sep 2025 12:33:07 +0200 Subject: [PATCH 17/27] Extract pagination options for projects --- github/projects.go | 9 +++++++-- github/projects_test.go | 18 +++++++++--------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/github/projects.go b/github/projects.go index 56cd67d95eb..f8504bf24d6 100644 --- a/github/projects.go +++ b/github/projects.go @@ -18,7 +18,7 @@ type ProjectsService service func (p ProjectV2) String() string { return Stringify(p) } -// ListProjectsOptions specifies optional parameters to list projects for user / organization. +// ListProjectsPaginationOptions 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 @@ -26,7 +26,7 @@ func (p ProjectV2) String() string { return Stringify(p) } // 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 { +type ListProjectsPaginationOptions 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"` @@ -35,6 +35,11 @@ type ListProjectsOptions struct { // For paginated result sets, the number of results to include per page. PerPage int `url:"per_page,omitempty"` +} + +// ListProjectsOptions specifies optional parameters to list projects for user / organization. +type ListProjectsOptions struct { + ListProjectsPaginationOptions // Q is an optional query string to limit results to projects of the specified type. Query string `url:"q,omitempty"` diff --git a/github/projects_test.go b/github/projects_test.go index 5f78704839b..82e967a68b1 100644 --- a/github/projects_test.go +++ b/github/projects_test.go @@ -29,7 +29,7 @@ func TestProjectsService_ListProjectsForOrg(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{Query: "alpha", After: "2", Before: "1"} + opts := &ListProjectsOptions{Query: "alpha", ListProjectsPaginationOptions: ListProjectsPaginationOptions{After: "2", Before: "1"}} ctx := context.Background() projects, _, err := client.Projects.ListProjectsForOrg(ctx, "o", opts) if err != nil { @@ -55,7 +55,7 @@ func TestProjectsService_ListProjectsForOrg(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.ListProjectsForOrg(ctxBypass, "o", &ListProjectsOptions{Before: "b", After: "a"}); err != nil { + if _, _, err = client.Projects.ListProjectsForOrg(ctxBypass, "o", &ListProjectsOptions{ListProjectsPaginationOptions: ListProjectsPaginationOptions{Before: "b", After: "a"}}); err != nil { t.Fatalf("unexpected error when both before/after set: %v", err) } } @@ -104,7 +104,7 @@ func TestProjectsService_ListUserProjects(t *testing.T) { 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} + opts := &ListProjectsOptions{Query: "beta", ListProjectsPaginationOptions: ListProjectsPaginationOptions{Before: "1", After: "2", PerPage: 2}} ctx := context.Background() var ctxBypass context.Context projects, _, err := client.Projects.ListProjectsForUser(ctx, "u", opts) @@ -131,7 +131,7 @@ func TestProjectsService_ListUserProjects(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.ListProjectsForUser(ctxBypass, "u", &ListProjectsOptions{Before: "b", After: "a"}); err != nil { + if _, _, err = client.Projects.ListProjectsForUser(ctxBypass, "u", &ListProjectsOptions{ListProjectsPaginationOptions: ListProjectsPaginationOptions{Before: "b", After: "a"}}); err != nil { t.Fatalf("unexpected error when both before/after set: %v", err) } } @@ -204,7 +204,7 @@ func TestProjectsService_ListProjectsForOrg_pagination(t *testing.T) { } // Use resp.After as opts.After for next page - opts := &ListProjectsOptions{After: resp.After} + opts := &ListProjectsOptions{ListProjectsPaginationOptions: ListProjectsPaginationOptions{After: resp.After}} second, resp2, err := client.Projects.ListProjectsForOrg(ctx, "o", opts) if err != nil { t.Fatalf("second page error: %v", err) @@ -252,7 +252,7 @@ func TestProjectsService_ListProjectsForUser_pagination(t *testing.T) { t.Fatalf("expected resp.After=ucursor2 got %q", resp.After) } - opts := &ListProjectsOptions{After: resp.After} + opts := &ListProjectsOptions{ListProjectsPaginationOptions: ListProjectsPaginationOptions{After: resp.After}} second, resp2, err := client.Projects.ListProjectsForUser(ctx, "u", opts) if err != nil { t.Fatalf("second page error: %v", err) @@ -314,7 +314,7 @@ func TestProjectsService_ListProjectFieldsForOrg(t *testing.T) { ]`) }) - opts := &ListProjectsOptions{Query: "text", After: "2", Before: "1"} + opts := &ListProjectsOptions{Query: "text", ListProjectsPaginationOptions: ListProjectsPaginationOptions{After: "2", Before: "1"}} ctx := context.Background() fields, _, err := client.Projects.ListProjectFieldsForOrg(ctx, "o", 1, opts) if err != nil { @@ -365,7 +365,7 @@ func TestProjectsService_ListProjectFieldsForOrg(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.ListProjectFieldsForOrg(ctxBypass, "o", 1, &ListProjectsOptions{Before: "b", After: "a"}); err != nil { + if _, _, err = client.Projects.ListProjectFieldsForOrg(ctxBypass, "o", 1, &ListProjectsOptions{ListProjectsPaginationOptions: ListProjectsPaginationOptions{Before: "b", After: "a"}}); err != nil { t.Fatalf("unexpected error when both before/after set: %v", err) } } @@ -408,7 +408,7 @@ func TestProjectsService_ListProjectFieldsForOrg_pagination(t *testing.T) { } // Use resp.After as opts.After for next page - opts := &ListProjectsOptions{After: resp.After} + opts := &ListProjectsOptions{ListProjectsPaginationOptions: ListProjectsPaginationOptions{After: resp.After}} second, resp2, err := client.Projects.ListProjectFieldsForOrg(ctx, "o", 1, opts) if err != nil { t.Fatalf("second page error: %v", err) From fc5d3cb49f5be4dabba901b84053a9364372e008 Mon Sep 17 00:00:00 2001 From: JoannaaKL Date: Wed, 24 Sep 2025 12:38:27 +0200 Subject: [PATCH 18/27] Remove comments --- github/projects_test.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/github/projects_test.go b/github/projects_test.go index 82e967a68b1..87f5e116512 100644 --- a/github/projects_test.go +++ b/github/projects_test.go @@ -164,8 +164,6 @@ func TestProjectsService_GetProjectForUser(t *testing.T) { }) } -// 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) @@ -217,8 +215,6 @@ func TestProjectsService_ListProjectsForOrg_pagination(t *testing.T) { } } -// 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) From 275f9308a8f637cba23f8478a5bfdef30a3c49ef Mon Sep 17 00:00:00 2001 From: JoannaaKL Date: Thu, 25 Sep 2025 16:19:09 +0200 Subject: [PATCH 19/27] Update github/projects.go Co-authored-by: Oleksandr Redko --- github/projects.go | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/github/projects.go b/github/projects.go index f8504bf24d6..9c6328ecb37 100644 --- a/github/projects.go +++ b/github/projects.go @@ -50,10 +50,14 @@ 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. - 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. + // The unique identifier for this option. + ID string `json:"id,omitempty"` + // The display name of the option. + Name string `json:"name,omitempty"` + // The color associated with this option (e.g., "blue", "red"). + Color string `json:"color,omitempty"` + // An optional description for this option. + Description string `json:"description,omitempty"` } // ProjectV2Field represents a field in a GitHub Projects V2 project. From aca15c04b77253d348e8734e5cac2e74cc6f3931 Mon Sep 17 00:00:00 2001 From: JoannaaKL Date: Fri, 26 Sep 2025 10:07:01 +0200 Subject: [PATCH 20/27] Sync openapi_operations.yaml --- openapi_operations.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openapi_operations.yaml b/openapi_operations.yaml index 053c19325b6..47e58a6c66a 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: 44dd20c107ca46d1dbcaf4aa522d9039e96e631c +openapi_commit: 5efe9a47bbe583fdc512c811f92b779b0715b95c openapi_operations: - name: GET / documentation_url: https://docs.github.com/rest/meta/meta#github-api-root @@ -2418,7 +2418,7 @@ openapi_operations: - descriptions/ghec/ghec.json - descriptions/ghes-3.17/ghes-3.17.json - name: GET /orgs/{org}/external-groups - documentation_url: https://docs.github.com/enterprise-cloud@latest//rest/teams/external-groups#list-external-groups-in-an-organization + documentation_url: https://docs.github.com/enterprise-cloud@latest//rest/teams/external-groups#list-external-groups-available-to-an-organization openapi_files: - descriptions/ghec/ghec.json - descriptions/ghes-3.17/ghes-3.17.json From 0ca0521da67186e5623465ac9df4b0a4602bbb12 Mon Sep 17 00:00:00 2001 From: JoannaaKL Date: Fri, 26 Sep 2025 10:19:04 +0200 Subject: [PATCH 21/27] Address pr comments --- github/event_types.go | 29 --------------------------- github/projects.go | 46 ++++++++++++++++++++++++++++++++++--------- 2 files changed, 37 insertions(+), 38 deletions(-) diff --git a/github/event_types.go b/github/event_types.go index 80b0cf0485b..f459c95839e 100644 --- a/github/event_types.go +++ b/github/event_types.go @@ -1111,35 +1111,6 @@ type ProjectV2Event struct { Sender *User `json:"sender,omitempty"` } -// ProjectV2 represents a v2 project. -type ProjectV2 struct { - ID *int64 `json:"id,omitempty"` - NodeID *string `json:"node_id,omitempty"` - Owner *User `json:"owner,omitempty"` - Creator *User `json:"creator,omitempty"` - Title *string `json:"title,omitempty"` - Description *string `json:"description,omitempty"` - Public *bool `json:"public,omitempty"` - ClosedAt *Timestamp `json:"closed_at,omitempty"` - CreatedAt *Timestamp `json:"created_at,omitempty"` - UpdatedAt *Timestamp `json:"updated_at,omitempty"` - DeletedAt *Timestamp `json:"deleted_at,omitempty"` - Number *int `json:"number,omitempty"` - ShortDescription *string `json:"short_description,omitempty"` - DeletedBy *User `json:"deleted_by,omitempty"` - - // Fields migrated from the Project (classic) struct: - URL *string `json:"url,omitempty"` - HTMLURL *string `json:"html_url,omitempty"` - ColumnsURL *string `json:"columns_url,omitempty"` - OwnerURL *string `json:"owner_url,omitempty"` - Name *string `json:"name,omitempty"` - Body *string `json:"body,omitempty"` - State *string `json:"state,omitempty"` - OrganizationPermission *string `json:"organization_permission,omitempty"` - Private *bool `json:"private,omitempty"` -} - // ProjectV2ItemEvent is triggered when there is activity relating to an item on an organization-level project. // The Webhook event name is "projects_v2_item". // diff --git a/github/projects.go b/github/projects.go index 9c6328ecb37..53067305fd5 100644 --- a/github/projects.go +++ b/github/projects.go @@ -16,6 +16,35 @@ import ( // GitHub API docs: https://docs.github.com/rest/projects/projects type ProjectsService service +// ProjectV2 represents a v2 project. +type ProjectV2 struct { + ID *int64 `json:"id,omitempty"` + NodeID *string `json:"node_id,omitempty"` + Owner *User `json:"owner,omitempty"` + Creator *User `json:"creator,omitempty"` + Title *string `json:"title,omitempty"` + Description *string `json:"description,omitempty"` + Public *bool `json:"public,omitempty"` + ClosedAt *Timestamp `json:"closed_at,omitempty"` + CreatedAt *Timestamp `json:"created_at,omitempty"` + UpdatedAt *Timestamp `json:"updated_at,omitempty"` + DeletedAt *Timestamp `json:"deleted_at,omitempty"` + Number *int `json:"number,omitempty"` + ShortDescription *string `json:"short_description,omitempty"` + DeletedBy *User `json:"deleted_by,omitempty"` + + // Fields migrated from the Project (classic) struct: + URL *string `json:"url,omitempty"` + HTMLURL *string `json:"html_url,omitempty"` + ColumnsURL *string `json:"columns_url,omitempty"` + OwnerURL *string `json:"owner_url,omitempty"` + Name *string `json:"name,omitempty"` + Body *string `json:"body,omitempty"` + State *string `json:"state,omitempty"` + OrganizationPermission *string `json:"organization_permission,omitempty"` + Private *bool `json:"private,omitempty"` +} + func (p ProjectV2) String() string { return Stringify(p) } // ListProjectsPaginationOptions specifies optional parameters to list projects for user / organization. @@ -50,7 +79,6 @@ type ListProjectsOptions struct { // // GitHub API docs: https://docs.github.com/rest/projects/fields type ProjectV2FieldOption struct { - // The unique identifier for this option. ID string `json:"id,omitempty"` // The display name of the option. Name string `json:"name,omitempty"` @@ -65,14 +93,14 @@ type ProjectV2FieldOption struct { // // GitHub API docs: https://docs.github.com/rest/projects/fields type ProjectV2Field struct { - 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"). - 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 *int64 `json:"id,omitempty"` + NodeID string `json:"node_id,omitempty"` + Name string `json:"name,omitempty"` + DataType string `json:"dataType,omitempty"` + URL string `json:"url,omitempty"` + Options []*ProjectV2FieldOption `json:"options,omitempty"` + CreatedAt *Timestamp `json:"created_at,omitempty"` + UpdatedAt *Timestamp `json:"updated_at,omitempty"` } // ListProjectsForOrg lists Projects V2 for an organization. From fb6e7a8494940d7e1cf10675186d2bd6eecde38a Mon Sep 17 00:00:00 2001 From: JoannaaKL Date: Fri, 26 Sep 2025 11:04:08 +0200 Subject: [PATCH 22/27] Add integration test and minor tweaks --- github/projects.go | 16 ++--- github/projects_test.go | 97 +++++++++++++------------ test/integration/projects_test.go | 113 ++++++++++++++++++++++++++++++ 3 files changed, 174 insertions(+), 52 deletions(-) create mode 100644 test/integration/projects_test.go diff --git a/github/projects.go b/github/projects.go index 53067305fd5..2226dbe7607 100644 --- a/github/projects.go +++ b/github/projects.go @@ -93,14 +93,14 @@ type ProjectV2FieldOption struct { // // GitHub API docs: https://docs.github.com/rest/projects/fields type ProjectV2Field struct { - ID *int64 `json:"id,omitempty"` - NodeID string `json:"node_id,omitempty"` - Name string `json:"name,omitempty"` - DataType string `json:"dataType,omitempty"` - URL string `json:"url,omitempty"` - Options []*ProjectV2FieldOption `json:"options,omitempty"` - CreatedAt *Timestamp `json:"created_at,omitempty"` - UpdatedAt *Timestamp `json:"updated_at,omitempty"` + ID *int64 `json:"id,omitempty"` + NodeID string `json:"node_id,omitempty"` + Name string `json:"name,omitempty"` + DataType string `json:"dataType,omitempty"` + URL string `json:"url,omitempty"` + Options []*any `json:"options,omitempty"` + CreatedAt *Timestamp `json:"created_at,omitempty"` + UpdatedAt *Timestamp `json:"updated_at,omitempty"` } // ListProjectsForOrg lists Projects V2 for an organization. diff --git a/github/projects_test.go b/github/projects_test.go index 87f5e116512..0fa2d333d5c 100644 --- a/github/projects_test.go +++ b/github/projects_test.go @@ -265,7 +265,6 @@ 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() @@ -273,7 +272,6 @@ func TestProjectsService_ListProjectFieldsForOrg(t *testing.T) { fmt.Fprint(w, `[]`) return } - // default expectation for main part of test testFormValues(t, r, values{"q": "text", "after": "2", "before": "1"}) fmt.Fprint(w, `[ { @@ -321,25 +319,37 @@ func TestProjectsService_ListProjectFieldsForOrg(t *testing.T) { t.Fatalf("Projects.ListProjectFieldsForOrg returned %d fields, want 2", len(fields)) } - // Validate first field (with options) field1 := fields[0] if field1.ID == nil || *field1.ID != 1 || field1.Name != "Status" || field1.DataType != "single_select" { - t.Errorf("First field: got ID=%v, Name=%s, DataType=%s; want 1, Status, single_select", - field1.ID, field1.Name, field1.DataType) + t.Errorf("First field: got ID=%v, Name=%s, DataType=%s; want 1, 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) + } else { + getName := func(o *any) string { + if o == nil || *o == nil { + return "" + } + switch v := (*o).(type) { + case map[string]any: + if n, ok := v["name"].(string); ok { + return n + } + default: + // fall back to fmt for debug; reflection can be added if needed. + } + return "" + } + name0, name1 := getName(field1.Options[0]), getName(field1.Options[1]) + if name0 != "Todo" || name1 != "In Progress" { + t.Errorf("First field option names: got %q, %q; want Todo, In Progress", name0, name1) + } } // Validate second field (without options) field2 := fields[1] if field2.ID == nil || *field2.ID != 2 || field2.Name != "Priority" || field2.DataType != "text" { - t.Errorf("Second field: got ID=%v, Name=%s, DataType=%s; want 2, Priority, text", - field2.ID, field2.Name, field2.DataType) + t.Errorf("Second field: got ID=%v, Name=%s, DataType=%s; want 2, 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)) @@ -403,7 +413,6 @@ func TestProjectsService_ListProjectFieldsForOrg_pagination(t *testing.T) { t.Fatalf("expected resp.After=cursor2 got %q", resp.After) } - // Use resp.After as opts.After for next page opts := &ListProjectsOptions{ListProjectsPaginationOptions: ListProjectsPaginationOptions{After: resp.After}} second, resp2, err := client.Projects.ListProjectFieldsForOrg(ctx, "o", 1, opts) if err != nil { @@ -417,7 +426,6 @@ func TestProjectsService_ListProjectFieldsForOrg_pagination(t *testing.T) { } } -// Marshal test ensures V2 fields marshal correctly. func TestProjectV2_Marshal(t *testing.T) { t.Parallel() testJSONMarshal(t, &ProjectV2{}, "{}") @@ -443,47 +451,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{}, "{}") + testJSONMarshal(t, &ProjectV2Field{}, "{}") // empty struct + testJSONMarshal(t, &ProjectV2FieldOption{}, "{}") // option struct still individually testable + + type optStruct struct { + Color string `json:"color,omitempty"` + Description string `json:"description,omitempty"` + ID string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + } + optVal := &optStruct{Color: "blue", Description: "Tasks to be done", ID: "option1", Name: "Todo"} + var optAny any = optVal field := &ProjectV2Field{ - ID: Ptr(int64(1)), - NodeID: "node_1", - Name: "Status", - DataType: "single_select", - URL: "https://api.github.com/projects/1/fields/field1", - Options: []*ProjectV2FieldOption{ - { - ID: "option1", - Name: "Todo", - Color: "blue", - Description: "Tasks to be done", - }, - }, + ID: Ptr(int64(1)), + NodeID: "node_1", + Name: "Status", + DataType: "single_select", + URL: "https://api.github.com/projects/1/fields/field1", + Options: []*any{&optAny}, CreatedAt: &Timestamp{referenceTime}, UpdatedAt: &Timestamp{referenceTime}, } want := `{ "id": 1, - "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 + ` - }` + "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) } diff --git a/test/integration/projects_test.go b/test/integration/projects_test.go new file mode 100644 index 00000000000..93d9db3bc04 --- /dev/null +++ b/test/integration/projects_test.go @@ -0,0 +1,113 @@ +// 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. + +//go:build integration + +package integration + +import ( + "context" + "os" + "testing" + + "github.com/google/go-github/v75/github" +) + +// Integration tests for Projects V2 endpoints defined in github/projects.go. +// +// These tests are intentionally defensive. They only require minimal +// environment variables identifying a target org and user. Project numbers are +// discovered dynamically by first listing projects and selecting one. For item +// CRUD operations, the test creates a temporary repository & issue (where +// possible) and adds/removes that issue as a project item. If prerequisites +// (auth, env vars, permissions, presence of at least one project) are missing, +// the relevant sub-test is skipped so other integration tests can still run. +// +// Required / optional environment variables: +// GITHUB_AUTH_TOKEN (required for any of these tests to run) +// GITHUB_TEST_ORG (org login; required for org project tests) +// GITHUB_TEST_USER (user login; required for user project tests) +// GITHUB_TEST_REPO (repo name) + +func TestProjectsV2_Org(t *testing.T) { + if !checkAuth("TestProjectsV2_Org") { // ensures client is authed + return + } + org := os.Getenv("GITHUB_TEST_ORG") + if org == "" { + t.Skip("GITHUB_TEST_ORG not set") + } + + ctx := context.Background() + + opts := &github.ListProjectsOptions{} + // List projects for org; pick the first available project we can read. + projects, _, err := client.Projects.ListProjectsForOrg(ctx, org, opts) + if err != nil { + // If listing itself fails, abort this test. + t.Fatalf("Projects.ListProjectsForOrg returned error: %v", err) + } + if len(projects) == 0 { + t.Skipf("no Projects V2 found for org %s", org) + } + project := projects[0] + if project.Number == nil { + t.Skip("selected org project has nil Number field") + } + projectNumber := int64(*project.Number) + + // Re-fetch via Get to exercise endpoint explicitly. + proj, _, err := client.Projects.GetProjectForOrg(ctx, org, projectNumber) + if err != nil { + // Permission mismatch? Skip CRUD while still reporting failure would make the test fail; + // we want correctness so treat as fatal here. + t.Fatalf("Projects.GetProjectForOrg returned error: %v", err) + } + if proj.Number == nil || int64(*proj.Number) != projectNumber { + t.Fatalf("GetProjectForOrg returned unexpected project number: got %+v want %d", proj.Number, projectNumber) + } + + // List fields (may be empty) + _, _, err = client.Projects.ListProjectFieldsForOrg(ctx, org, projectNumber, nil) + if err != nil { + // Fields listing might require extra perms; treat as fatal to surface potential regression. + t.Fatalf("Projects.ListProjectFieldsForOrg returned error: %v", err) + } +} + +func TestProjectsV2_User(t *testing.T) { + if !checkAuth("TestProjectsV2_User") { + return + } + user := os.Getenv("GITHUB_TEST_USER") + if user == "" { + t.Skip("GITHUB_TEST_USER not set") + } + + ctx := context.Background() + opts := &github.ListProjectsOptions{} + projects, _, err := client.Projects.ListProjectsForUser(ctx, user, opts) + if err != nil { + // Can't list user projects: fatal (indicates API or permission issue). + t.Fatalf("Projects.ListProjectsForUser returned error: %v", err) + } + if len(projects) == 0 { + t.Skipf("no Projects V2 found for user %s", user) + } + project := projects[0] + if project.Number == nil { + t.Skip("selected user project has nil Number field") + } + projectNumber := int64(*project.Number) + + proj, _, err := client.Projects.GetProjectForUser(ctx, user, projectNumber) + if err != nil { + // can't fetch specific project; treat as fatal + t.Fatalf("Projects.GetProjectForUser returned error: %v", err) + } + if proj.Number == nil || int64(*proj.Number) != projectNumber { + t.Fatalf("GetProjectForUser returned unexpected project number: got %+v want %d", proj.Number, projectNumber) + } +} From 5cc797e1f48324499607406cf23ba1773f1ba3d3 Mon Sep 17 00:00:00 2001 From: JoannaaKL Date: Wed, 1 Oct 2025 12:41:34 +0000 Subject: [PATCH 23/27] . --- github/projects.go | 6 +++--- test/integration/projects_test.go | 11 +++++------ 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/github/projects.go b/github/projects.go index 2226dbe7607..9f562fee865 100644 --- a/github/projects.go +++ b/github/projects.go @@ -133,7 +133,7 @@ func (s *ProjectsService) ListProjectsForOrg(ctx context.Context, org string, op // GitHub API docs: https://docs.github.com/rest/projects/projects#get-project-for-organization // //meta:operation GET /orgs/{org}/projectsV2/{project_number} -func (s *ProjectsService) GetProjectForOrg(ctx context.Context, org string, projectNumber int64) (*ProjectV2, *Response, error) { +func (s *ProjectsService) GetProjectForOrg(ctx context.Context, org string, projectNumber int) (*ProjectV2, *Response, error) { u := fmt.Sprintf("orgs/%v/projectsV2/%v", org, projectNumber) req, err := s.client.NewRequest("GET", u, nil) if err != nil { @@ -177,7 +177,7 @@ func (s *ProjectsService) ListProjectsForUser(ctx context.Context, username stri // GitHub API docs: https://docs.github.com/rest/projects/projects#get-project-for-user // //meta:operation GET /users/{username}/projectsV2/{project_number} -func (s *ProjectsService) GetProjectForUser(ctx context.Context, username string, projectNumber int64) (*ProjectV2, *Response, error) { +func (s *ProjectsService) GetProjectForUser(ctx context.Context, username string, projectNumber int) (*ProjectV2, *Response, error) { u := fmt.Sprintf("users/%v/projectsV2/%v", username, projectNumber) req, err := s.client.NewRequest("GET", u, nil) if err != nil { @@ -197,7 +197,7 @@ func (s *ProjectsService) GetProjectForUser(ctx context.Context, username string // 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) ListProjectFieldsForOrg(ctx context.Context, org string, projectNumber int64, opts *ListProjectsOptions) ([]*ProjectV2Field, *Response, error) { +func (s *ProjectsService) ListProjectFieldsForOrg(ctx context.Context, org string, projectNumber int, 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/test/integration/projects_test.go b/test/integration/projects_test.go index 93d9db3bc04..fd946ca73c6 100644 --- a/test/integration/projects_test.go +++ b/test/integration/projects_test.go @@ -56,7 +56,7 @@ func TestProjectsV2_Org(t *testing.T) { if project.Number == nil { t.Skip("selected org project has nil Number field") } - projectNumber := int64(*project.Number) + projectNumber := *project.Number // Re-fetch via Get to exercise endpoint explicitly. proj, _, err := client.Projects.GetProjectForOrg(ctx, org, projectNumber) @@ -65,7 +65,7 @@ func TestProjectsV2_Org(t *testing.T) { // we want correctness so treat as fatal here. t.Fatalf("Projects.GetProjectForOrg returned error: %v", err) } - if proj.Number == nil || int64(*proj.Number) != projectNumber { + if proj.Number == nil || *proj.Number != projectNumber { t.Fatalf("GetProjectForOrg returned unexpected project number: got %+v want %d", proj.Number, projectNumber) } @@ -100,14 +100,13 @@ func TestProjectsV2_User(t *testing.T) { if project.Number == nil { t.Skip("selected user project has nil Number field") } - projectNumber := int64(*project.Number) - proj, _, err := client.Projects.GetProjectForUser(ctx, user, projectNumber) + proj, _, err := client.Projects.GetProjectForUser(ctx, user, *project.Number) if err != nil { // can't fetch specific project; treat as fatal t.Fatalf("Projects.GetProjectForUser returned error: %v", err) } - if proj.Number == nil || int64(*proj.Number) != projectNumber { - t.Fatalf("GetProjectForUser returned unexpected project number: got %+v want %d", proj.Number, projectNumber) + if proj.Number == nil || *proj.Number != *project.Number { + t.Fatalf("GetProjectForUser returned unexpected project number: got %+v want %d", proj.Number, *project.Number) } } From c6cfe3180fd3b7133aa818c43ed2e2e3740b07c4 Mon Sep 17 00:00:00 2001 From: JoannaaKL Date: Fri, 3 Oct 2025 05:22:54 +0200 Subject: [PATCH 24/27] Update test/integration/projects_test.go Co-authored-by: Oleksandr Redko --- test/integration/projects_test.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/integration/projects_test.go b/test/integration/projects_test.go index fd946ca73c6..3f247259254 100644 --- a/test/integration/projects_test.go +++ b/test/integration/projects_test.go @@ -90,8 +90,7 @@ func TestProjectsV2_User(t *testing.T) { opts := &github.ListProjectsOptions{} projects, _, err := client.Projects.ListProjectsForUser(ctx, user, opts) if err != nil { - // Can't list user projects: fatal (indicates API or permission issue). - t.Fatalf("Projects.ListProjectsForUser returned error: %v", err) + t.Fatalf("Projects.ListProjectsForUser returned error: %v. This indicates API or permission issue", err) } if len(projects) == 0 { t.Skipf("no Projects V2 found for user %s", user) From c9feeb22df6d15dca124014446de00e85afd0bec Mon Sep 17 00:00:00 2001 From: JoannaaKL Date: Fri, 3 Oct 2025 05:23:06 +0200 Subject: [PATCH 25/27] Update test/integration/projects_test.go Co-authored-by: Oleksandr Redko --- test/integration/projects_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/test/integration/projects_test.go b/test/integration/projects_test.go index 3f247259254..4bb6ce231fd 100644 --- a/test/integration/projects_test.go +++ b/test/integration/projects_test.go @@ -69,7 +69,6 @@ func TestProjectsV2_Org(t *testing.T) { t.Fatalf("GetProjectForOrg returned unexpected project number: got %+v want %d", proj.Number, projectNumber) } - // List fields (may be empty) _, _, err = client.Projects.ListProjectFieldsForOrg(ctx, org, projectNumber, nil) if err != nil { // Fields listing might require extra perms; treat as fatal to surface potential regression. From 8c985a9465dce7e973be420f1f95f9f29fa65624 Mon Sep 17 00:00:00 2001 From: JoannaaKL Date: Fri, 3 Oct 2025 05:23:16 +0200 Subject: [PATCH 26/27] Update test/integration/projects_test.go Co-authored-by: Oleksandr Redko --- test/integration/projects_test.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/integration/projects_test.go b/test/integration/projects_test.go index 4bb6ce231fd..3880dd9609d 100644 --- a/test/integration/projects_test.go +++ b/test/integration/projects_test.go @@ -71,8 +71,7 @@ func TestProjectsV2_Org(t *testing.T) { _, _, err = client.Projects.ListProjectFieldsForOrg(ctx, org, projectNumber, nil) if err != nil { - // Fields listing might require extra perms; treat as fatal to surface potential regression. - t.Fatalf("Projects.ListProjectFieldsForOrg returned error: %v", err) + t.Fatalf("Projects.ListProjectFieldsForOrg returned error: %v. Fields listing might require extra permissions", err) } } From 1625c619f4b38c1094d515ec84f25b8e3fd43385 Mon Sep 17 00:00:00 2001 From: JoannaaKL Date: Fri, 3 Oct 2025 05:23:41 +0200 Subject: [PATCH 27/27] Update test/integration/projects_test.go Co-authored-by: Oleksandr Redko --- test/integration/projects_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/projects_test.go b/test/integration/projects_test.go index 3880dd9609d..84939a47ce6 100644 --- a/test/integration/projects_test.go +++ b/test/integration/projects_test.go @@ -32,7 +32,7 @@ import ( // GITHUB_TEST_REPO (repo name) func TestProjectsV2_Org(t *testing.T) { - if !checkAuth("TestProjectsV2_Org") { // ensures client is authed + if !checkAuth("TestProjectsV2_Org") { return } org := os.Getenv("GITHUB_TEST_ORG")