Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
775f0f6
Add support for projects V2
JoannaaKL Sep 17, 2025
74712bd
Add test
JoannaaKL Sep 17, 2025
3a8cf0c
Address feedback
JoannaaKL Sep 22, 2025
b754eeb
Merge branch 'master' into add-projects
JoannaaKL Sep 22, 2025
e93ed36
Remove header and rename functions
JoannaaKL Sep 22, 2025
99852cf
Update comments
JoannaaKL Sep 22, 2025
7664215
Copyright update
JoannaaKL Sep 22, 2025
89016d9
Merge branch 'master' into add-projects
JoannaaKL Sep 23, 2025
2b94765
Update comments
JoannaaKL Sep 23, 2025
a4b0a56
Generate docs
JoannaaKL Sep 23, 2025
696102b
Add list projects option
JoannaaKL Sep 23, 2025
469e03a
Generate openapi docs
JoannaaKL Sep 23, 2025
2dbe5b4
Results of generate
JoannaaKL Sep 23, 2025
478ef5a
Merge branch 'master' into add-projects
JoannaaKL Sep 23, 2025
c34802d
Merge branch 'master' into add-projects
JoannaaKL Sep 23, 2025
053a9e1
Update github/projects.go
JoannaaKL Sep 23, 2025
680a0d1
Rename url in test
JoannaaKL Sep 23, 2025
d8696b4
Generate again
JoannaaKL Sep 23, 2025
93dd34f
Merge branch 'master' into add-projects
JoannaaKL Sep 23, 2025
060aaae
Shorten functions for orgs
JoannaaKL Sep 24, 2025
65e64f7
Change ID type from string to int
JoannaaKL Sep 24, 2025
d300550
Extract pagination options for projects
JoannaaKL Sep 24, 2025
fc5d3cb
Remove comments
JoannaaKL Sep 24, 2025
9d6bf4b
Merge branch 'master' into add-projects
JoannaaKL Sep 24, 2025
9f4c89d
Merge branch 'master' into add-projects
JoannaaKL Sep 24, 2025
3dd69be
Merge branch 'master' into add-projects
JoannaaKL Sep 25, 2025
275f930
Update github/projects.go
JoannaaKL Sep 25, 2025
aca15c0
Sync openapi_operations.yaml
JoannaaKL Sep 26, 2025
8ee5bc4
Merge branch 'master' into add-projects
JoannaaKL Sep 26, 2025
0ca0521
Address pr comments
JoannaaKL Sep 26, 2025
fb6e7a8
Add integration test and minor tweaks
JoannaaKL Sep 26, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions github/github.go
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ type Client struct {
Meta *MetaService
Migrations *MigrationService
Organizations *OrganizationsService
Projects *ProjectsService
PullRequests *PullRequestsService
RateLimit *RateLimitService
Reactions *ReactionsService
Expand Down Expand Up @@ -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)
Expand Down
120 changes: 120 additions & 0 deletions github/projects.go
Original file line number Diff line number Diff line change
@@ -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
}
165 changes: 165 additions & 0 deletions github/projects_test.go
Original file line number Diff line number Diff line change
@@ -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)
}