Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 2.5k
Add Copilot endpoints#2973
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
Uh oh!
There was an error while loading. Please reload this page.
Add Copilot endpoints #2973
Changes from all commits
aaa5b5b67e0a683e32bab8dddf5f08641679d5be673e27021568617d19339c24f78d682c90d0ce01ef5399fe9baf2d32302ab8acab0f05fe63276a8d3fa20078f4db0da71a5a766902b76779c0baf25152adca97b0181bbc7a9ad756a8c9553900499f70f1f5e63691cec367c1080dff27decbbf5b837bf53e74d005317325e042b6e03d4e428a0da36bc1f4File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
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"` | ||
Contributor 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
Collaborator 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 ContributorAuthor 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? ContributorAuthor 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 🙏 Contributor 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 | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.
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
*Userhere. There are a few other places where we use*Userand let users distinguish using theTypefield. @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.
Teamhas noLoginfield, but has aNamefield, which bothUserandOrganizationhave.Loginis used for the username in the case of a user, whereasNameis 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.