-
Notifications
You must be signed in to change notification settings - Fork 2.1k
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
Add Copilot endpoints #2973
Changes from all commits
aaa5b5b
67e0a68
3e32bab
8dddf5f
0864167
9d5be67
3e27021
568617d
19339c2
4f78d68
2c90d0c
e01ef53
99fe9ba
f2d3230
2ab8aca
b0f05fe
63276a8
d3fa200
78f4db0
da71a5a
766902b
76779c0
baf2515
2adca97
b0181bb
c7a9ad7
56a8c95
5390049
9f70f1f
5e63691
cec367c
1080dff
27decbb
f5b837b
f53e74d
0053173
25e042b
6e03d4e
428a0da
36bc1f4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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"` | ||
AssigningTeam *Team `json:"assigning_team,omitempty"` | ||
PendingCancellationDate *string `json:"pending_cancellation_date,omitempty"` | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. type should be |
||
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||
|
||
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 | ||
} | ||
|
||
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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How are we handling pagination here for more than 100 users? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Will be adding support for this, thanks for the callout 🙏 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
} |
There was a problem hiding this comment.
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 theType
field. @gmlewis will know what the preferred method is.There was a problem hiding this comment.
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 noLogin
field, but has aName
field, which bothUser
andOrganization
have.Login
is used for the username in the case of a user, whereasName
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.
There was a problem hiding this comment.
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.