Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Copilot endpoints #2973

Merged
merged 40 commits into from
Dec 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
aaa5b5b
Add Copilot Endpoints
o-sama Oct 24, 2023
67e0a68
clean up unmarshal func
o-sama Oct 24, 2023
3e32bab
Add coverage and adddress linting issues
o-sama Oct 24, 2023
8dddf5f
undo wrong file committed
o-sama Oct 24, 2023
0864167
Add json unmarshal tests for seat details
o-sama Oct 24, 2023
9d5be67
update seat details unmarshal tests
o-sama Oct 24, 2023
3e27021
Apply suggestions from code review
Oct 24, 2023
568617d
code review comments
o-sama Oct 24, 2023
19339c2
Add coverage for Assignee helper methods
o-sama Oct 25, 2023
4f78d68
Add coverage for Assignee helper methods error cases
o-sama Oct 25, 2023
2c90d0c
Add requested test case
o-sama Oct 25, 2023
e01ef53
Fix unmarshal test cases
o-sama Oct 25, 2023
99fe9ba
Update tests and time types
o-sama Oct 25, 2023
f2d3230
Apply suggestions from code review
Oct 26, 2023
2ab8aca
Address review comments
o-sama Oct 26, 2023
b0f05fe
Merge branch 'master' into add-copilot-endpoints
o-sama Oct 26, 2023
63276a8
Address review comments
o-sama Nov 17, 2023
d3fa200
Merge branch 'master' into add-copilot-endpoints
o-sama Nov 17, 2023
78f4db0
Add operations metadata
o-sama Nov 17, 2023
da71a5a
Update generated code
o-sama Nov 17, 2023
766902b
Add default branch to repository edit event (#2995)
caseyduquettesc Nov 22, 2023
76779c0
Add `Draft` to `Issue` type (#2997)
caseyduquettesc Nov 23, 2023
baf2515
Fix secondary rate limits URL (#3001)
Letiste Nov 27, 2023
2adca97
Bump golang.org/x/net from 0.18.0 to 0.19.0 in /scrape (#3003)
dependabot[bot] Nov 27, 2023
b0181bb
Implement global security advisories API (#2993)
cpanato Nov 29, 2023
c7a9ad7
Change `PushEvent.Pusher` type to `CommitAuthor` (#2999)
caseyduquettesc Nov 30, 2023
56a8c95
Bump version of go-github to v57.0.0 (#3009)
gmlewis Dec 1, 2023
5390049
Bump go-github from v56 to v57 in /scrape (#3010)
gmlewis Dec 1, 2023
9f70f1f
Update metadata (#3012)
WillAbides Dec 2, 2023
5e63691
Fix broken CreateOrUpdateRepoCustomPropertyValues (#3023)
peter-aglen Dec 8, 2023
cec367c
Bump actions/setup-go from 4 to 5 (#3027)
dependabot[bot] Dec 11, 2023
1080dff
Add scanning validity checks (#3026)
tomasz-adam-skrzypczak Dec 14, 2023
27decbb
Add Referrer field to AuditEntry (#3032)
ngoduykhanh Dec 15, 2023
f5b837b
Add code_search and dependency_snapshots for RateLimits (#3019)
rufusnufus Dec 15, 2023
f53e74d
Support temporary private fork creation via API (#3025)
Kiyo510 Dec 15, 2023
0053173
Escape package names to support names which include a slash (#3002)
bn4t Dec 16, 2023
25e042b
Don't update httpClient passed to NewClient (#3011)
WillAbides Dec 16, 2023
6e03d4e
Add GetAllCustomPropertyValues for repositories (#3020)
liaodaniel Dec 17, 2023
428a0da
Remove ambiguous fields from AuditEntry (#3017)
WillAbides Dec 18, 2023
36bc1f4
Merge branch 'master' into add-copilot-endpoints
gmlewis Dec 19, 2023
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
311 changes: 311 additions & 0 deletions github/copilot.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,311 @@
// Copyright 2023 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"
"encoding/json"
"fmt"
)

// CopilotService provides access to the Copilot-related functions
// in the GitHub API.
//
// GitHub API docs: https://docs.github.com/en/rest/copilot/
type CopilotService service

// CopilotOrganizationDetails represents the details of an organization's Copilot for Business subscription.
type CopilotOrganizationDetails struct {
SeatBreakdown *CopilotSeatBreakdown `json:"seat_breakdown"`
PublicCodeSuggestions string `json:"public_code_suggestions"`
CopilotChat string `json:"copilot_chat"`
SeatManagementSetting string `json:"seat_management_setting"`
}

// CopilotSeatBreakdown represents the breakdown of Copilot for Business seats for the organization.
type CopilotSeatBreakdown struct {
Total int `json:"total"`
AddedThisCycle int `json:"added_this_cycle"`
PendingCancellation int `json:"pending_cancellation"`
PendingInvitation int `json:"pending_invitation"`
ActiveThisCycle int `json:"active_this_cycle"`
InactiveThisCycle int `json:"inactive_this_cycle"`
}

// ListCopilotSeatsResponse represents the Copilot for Business seat assignments for an organization.
type ListCopilotSeatsResponse struct {
TotalSeats int64 `json:"total_seats"`
Seats []*CopilotSeatDetails `json:"seats"`
}

// CopilotSeatDetails represents the details of a Copilot for Business seat.
type CopilotSeatDetails struct {
// Assignee can either be a User, Team, or Organization.
Assignee interface{} `json:"assignee"`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It may be ok to use *User here. There are a few other places where we use *User and let users distinguish using the Type field. @gmlewis will know what the preferred method is.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like this might be a better option since we're explicitly setting the correct type for the field, but both approaches leave type-checking to the end user.

There are minor differences as far as I'm aware between the types, e.g. Team has no Login field, but has a Name field, which both User and Organization have. Login is used for the username in the case of a user, whereas Name is used for team name.

I'll leave you guys with these thoughts and will be happy to make changes based on what you think.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good point about Team being different. I think this might be the best way to handle it after all.

AssigningTeam *Team `json:"assigning_team,omitempty"`
PendingCancellationDate *string `json:"pending_cancellation_date,omitempty"`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

type should be *Timestamp instead that it works in similar way than other time fields.

LastActivityAt *Timestamp `json:"last_activity_at,omitempty"`
LastActivityEditor *string `json:"last_activity_editor,omitempty"`
CreatedAt *Timestamp `json:"created_at"`
UpdatedAt *Timestamp `json:"updated_at,omitempty"`
}

// SeatAssignments represents the number of seats assigned.
type SeatAssignments struct {
SeatsCreated int `json:"seats_created"`
}

// SeatCancellations represents the number of seats cancelled.
type SeatCancellations struct {
SeatsCancelled int `json:"seats_cancelled"`
}

func (cp *CopilotSeatDetails) UnmarshalJSON(data []byte) error {
// Using an alias to avoid infinite recursion when calling json.Unmarshal
type alias CopilotSeatDetails
var seatDetail alias
Comment on lines +68 to +69
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I understand the need for line 67 or why we can't have var seatDetails CopilotSeatDetails.
Can you please either add comments as to why these are necessary or just remove them if they are not needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The alias type has the CopilotSeatDetails struct fields, but not its methods, this way when we call json.Unmarshal inside this function we don't have an infinite recursion. I'll add the comment and make the other requested changes tomorrow 🙂


if err := json.Unmarshal(data, &seatDetail); err != nil {
return err
}

cp.AssigningTeam = seatDetail.AssigningTeam
cp.PendingCancellationDate = seatDetail.PendingCancellationDate
cp.LastActivityAt = seatDetail.LastActivityAt
cp.LastActivityEditor = seatDetail.LastActivityEditor
cp.CreatedAt = seatDetail.CreatedAt
cp.UpdatedAt = seatDetail.UpdatedAt

switch v := seatDetail.Assignee.(type) {
case map[string]interface{}:
jsonData, err := json.Marshal(seatDetail.Assignee)
if err != nil {
return err
}

Check warning on line 87 in github/copilot.go

View check run for this annotation

Codecov / codecov/patch

github/copilot.go#L86-L87

Added lines #L86 - L87 were not covered by tests

if v["type"] == nil {
return fmt.Errorf("assignee type field is not set")
}

if t, ok := v["type"].(string); ok && t == "User" {
user := &User{}
if err := json.Unmarshal(jsonData, user); err != nil {
return err
}
cp.Assignee = user
} else if t, ok := v["type"].(string); ok && t == "Team" {
team := &Team{}
if err := json.Unmarshal(jsonData, team); err != nil {
return err
}
cp.Assignee = team
} else if t, ok := v["type"].(string); ok && t == "Organization" {
organization := &Organization{}
if err := json.Unmarshal(jsonData, organization); err != nil {
return err
}
cp.Assignee = organization
} else {
return fmt.Errorf("unsupported assignee type %v", v["type"])
}
default:
return fmt.Errorf("unsupported assignee type %T", v)
}

return nil
}

// GetUser gets the User from the CopilotSeatDetails if the assignee is a user.
func (cp *CopilotSeatDetails) GetUser() (*User, bool) { u, ok := cp.Assignee.(*User); return u, ok }

// GetTeam gets the Team from the CopilotSeatDetails if the assignee is a team.
func (cp *CopilotSeatDetails) GetTeam() (*Team, bool) { t, ok := cp.Assignee.(*Team); return t, ok }

// GetOrganization gets the Organization from the CopilotSeatDetails if the assignee is an organization.
func (cp *CopilotSeatDetails) GetOrganization() (*Organization, bool) {
o, ok := cp.Assignee.(*Organization)
return o, ok
}

// GetCopilotBilling gets Copilot for Business billing information and settings for an organization.
//
// GitHub API docs: https://docs.github.com/rest/copilot/copilot-business#get-copilot-business-seat-information-and-settings-for-an-organization
//
//meta:operation GET /orgs/{org}/copilot/billing
func (s *CopilotService) GetCopilotBilling(ctx context.Context, org string) (*CopilotOrganizationDetails, *Response, error) {
u := fmt.Sprintf("orgs/%v/copilot/billing", org)

req, err := s.client.NewRequest("GET", u, nil)
if err != nil {
return nil, nil, err
}

var copilotDetails *CopilotOrganizationDetails
resp, err := s.client.Do(ctx, req, &copilotDetails)
if err != nil {
return nil, resp, err
}

return copilotDetails, resp, nil
}

// ListCopilotSeats lists Copilot for Business seat assignments for an organization.
//
// To paginate through all seats, populate 'Page' with the number of the last page.
//
// GitHub API docs: https://docs.github.com/rest/copilot/copilot-business#list-all-copilot-business-seat-assignments-for-an-organization
//
//meta:operation GET /orgs/{org}/copilot/billing/seats
func (s *CopilotService) ListCopilotSeats(ctx context.Context, org string, opts *ListOptions) (*ListCopilotSeatsResponse, *Response, error) {
u := fmt.Sprintf("orgs/%v/copilot/billing/seats", org)

req, err := s.client.NewRequest("GET", u, opts)
if err != nil {
return nil, nil, err
}

var copilotSeats *ListCopilotSeatsResponse
resp, err := s.client.Do(ctx, req, &copilotSeats)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How are we handling pagination here for more than 100 users?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will be adding support for this, thanks for the callout 🙏

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pagination works in current version

if err != nil {
return nil, resp, err
}

return copilotSeats, resp, nil
}

// AddCopilotTeams adds teams to the Copilot for Business subscription for an organization.
//
// GitHub API docs: https://docs.github.com/rest/copilot/copilot-business#add-teams-to-the-copilot-business-subscription-for-an-organization
//
//meta:operation POST /orgs/{org}/copilot/billing/selected_teams
func (s *CopilotService) AddCopilotTeams(ctx context.Context, org string, teamNames []string) (*SeatAssignments, *Response, error) {
u := fmt.Sprintf("orgs/%v/copilot/billing/selected_teams", org)

body := struct {
SelectedTeams []string `json:"selected_teams"`
}{
SelectedTeams: teamNames,
}

req, err := s.client.NewRequest("POST", u, body)
if err != nil {
return nil, nil, err
}

var seatAssignments *SeatAssignments
resp, err := s.client.Do(ctx, req, &seatAssignments)
if err != nil {
return nil, resp, err
}

return seatAssignments, resp, nil
}

// RemoveCopilotTeams removes teams from the Copilot for Business subscription for an organization.
//
// GitHub API docs: https://docs.github.com/rest/copilot/copilot-business#remove-teams-from-the-copilot-business-subscription-for-an-organization
//
//meta:operation DELETE /orgs/{org}/copilot/billing/selected_teams
func (s *CopilotService) RemoveCopilotTeams(ctx context.Context, org string, teamNames []string) (*SeatCancellations, *Response, error) {
u := fmt.Sprintf("orgs/%v/copilot/billing/selected_teams", org)

body := struct {
SelectedTeams []string `json:"selected_teams"`
}{
SelectedTeams: teamNames,
}

req, err := s.client.NewRequest("DELETE", u, body)
if err != nil {
return nil, nil, err
}

var seatCancellations *SeatCancellations
resp, err := s.client.Do(ctx, req, &seatCancellations)
if err != nil {
return nil, resp, err
}

return seatCancellations, resp, nil
}

// AddCopilotUsers adds users to the Copilot for Business subscription for an organization
//
// GitHub API docs: https://docs.github.com/rest/copilot/copilot-business#add-users-to-the-copilot-business-subscription-for-an-organization
//
//meta:operation POST /orgs/{org}/copilot/billing/selected_users
func (s *CopilotService) AddCopilotUsers(ctx context.Context, org string, users []string) (*SeatAssignments, *Response, error) {
u := fmt.Sprintf("orgs/%v/copilot/billing/selected_users", org)

body := struct {
SelectedUsers []string `json:"selected_users"`
}{
SelectedUsers: users,
}

req, err := s.client.NewRequest("POST", u, body)
if err != nil {
return nil, nil, err
}

var seatAssignments *SeatAssignments
resp, err := s.client.Do(ctx, req, &seatAssignments)
if err != nil {
return nil, resp, err
}

return seatAssignments, resp, nil
}

// RemoveCopilotUsers removes users from the Copilot for Business subscription for an organization.
//
// GitHub API docs: https://docs.github.com/rest/copilot/copilot-business#remove-users-from-the-copilot-business-subscription-for-an-organization
//
//meta:operation DELETE /orgs/{org}/copilot/billing/selected_users
func (s *CopilotService) RemoveCopilotUsers(ctx context.Context, org string, users []string) (*SeatCancellations, *Response, error) {
u := fmt.Sprintf("orgs/%v/copilot/billing/selected_users", org)

body := struct {
SelectedUsers []string `json:"selected_users"`
}{
SelectedUsers: users,
}

req, err := s.client.NewRequest("DELETE", u, body)
if err != nil {
return nil, nil, err
}

var seatCancellations *SeatCancellations
resp, err := s.client.Do(ctx, req, &seatCancellations)
if err != nil {
return nil, resp, err
}

return seatCancellations, resp, nil
}

// GetSeatDetails gets Copilot for Business seat assignment details for a user.
//
// GitHub API docs: https://docs.github.com/rest/copilot/copilot-business#get-copilot-business-seat-assignment-details-for-a-user
//
//meta:operation GET /orgs/{org}/members/{username}/copilot
func (s *CopilotService) GetSeatDetails(ctx context.Context, org, user string) (*CopilotSeatDetails, *Response, error) {
u := fmt.Sprintf("orgs/%v/members/%v/copilot", org, user)

req, err := s.client.NewRequest("GET", u, nil)
if err != nil {
return nil, nil, err
}

var seatDetails *CopilotSeatDetails
resp, err := s.client.Do(ctx, req, &seatDetails)
if err != nil {
return nil, resp, err
}

return seatDetails, resp, nil
}
Loading
Loading