Skip to content

Commit 7427e39

Browse files
💡 Manage Copilot Licenses (#62)
This PR closes #53 At the moment, merging is blocked due to the necessary API not functioning... Or, at least my inability to use it properly (though I did follow the docs).
1 parent 022c66d commit 7427e39

6 files changed

Lines changed: 74 additions & 14 deletions

File tree

docs/OrganizationConfiguration.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,7 @@ Teams:
3636
- Name: Some_Team
3737
- Name: Some_Team_2
3838
DisplayName: Some Team 2
39+
# An optional property that adds the given team to the list of those Copilot licenses are
40+
# granted to.
41+
CopilotEnabled: true
3942
```

src/services/gitHub.ts

Lines changed: 49 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { Config } from "../config";
44
import { GitHubClient, GitHubId, GitHubTeamId, InstalledClient, Org, OrgInvite, OrgRoles, Response } from "./gitHubTypes";
55
import { AppConfig } from "./appConfig";
66
import yaml from "js-yaml";
7-
import { throttling } from "@octokit/plugin-throttling";
7+
import { throttling } from "@octokit/plugin-throttling";
88
import { AsyncReturnType } from "../utility";
99
import { Log, LoggerToUse } from "../logging";
1010
import { GitHubClientCache } from "./gitHubCache";
@@ -36,7 +36,7 @@ async function GetOrgClient(installationId: number): Promise<InstalledClient> {
3636
// TODO: look further into this... it seems like it would be best if
3737
// installation client was generated from the original client, and not
3838
// created fresh.
39-
const MyOctokit = Octokit.plugin(throttling);
39+
const MyOctokit = Octokit.plugin(throttling);
4040

4141
const installedOctokit = new MyOctokit({
4242
authStrategy: createAppAuth,
@@ -45,7 +45,7 @@ async function GetOrgClient(installationId: number): Promise<InstalledClient> {
4545
privateKey: Config().GitHub.PrivateKey,
4646
installationId
4747
},
48-
throttle: {
48+
throttle: {
4949
onRateLimit: (retryAfter, options, octokit, retryCount) => {
5050
octokit.log.warn(
5151
`Request quota exhausted for request ${options.method} ${options.url}`
@@ -56,7 +56,7 @@ async function GetOrgClient(installationId: number): Promise<InstalledClient> {
5656
octokit.log.info(`Retrying after ${retryAfter} seconds!`);
5757
return true;
5858
}
59-
},
59+
},
6060
onSecondaryRateLimit: (retryAfter, options, octokit) => {
6161
// does not retry, only logs a warning
6262
octokit.log.warn(
@@ -74,7 +74,7 @@ async function GetOrgClient(installationId: number): Promise<InstalledClient> {
7474
// TODO: keep an eye on this as there is a good
7575
// chance login will be removed considering it
7676
// is already not present on the type...
77-
login:string
77+
login: string
7878
}
7979

8080
// HACK: gross typing nonsense
@@ -192,18 +192,18 @@ async function GetAppConfig(client: Octokit): Promise<AppConfig> {
192192
}
193193

194194
type RawAppConfig = {
195-
GitHubIdAppend?:string
196-
SecurityManagerTeams?:string[]
195+
GitHubIdAppend?: string
196+
SecurityManagerTeams?: string[]
197197
Description?: {
198198
ShortLink: string
199199
}
200-
TeamsToIgnore?:string[]
200+
TeamsToIgnore?: string[]
201201
}
202202

203203
const configuration = yaml.load(Buffer.from(contentData.content, 'base64').toString()) as RawAppConfig;
204204

205205
return {
206-
Description: configuration.Description ?? {ShortLink:"https://github.com/cloudpups/github-teams-user-sync"},
206+
Description: configuration.Description ?? { ShortLink: "https://github.com/cloudpups/github-teams-user-sync" },
207207
SecurityManagerTeams: configuration.SecurityManagerTeams ?? [],
208208
TeamsToIgnore: configuration.TeamsToIgnore ?? [],
209209
GitHubIdAppend: configuration.GitHubIdAppend ?? ""
@@ -230,6 +230,46 @@ class InstalledGitHubClient implements InstalledClient {
230230
this.orgName = orgName;
231231
}
232232

233+
async AddTeamsToCopilotSubscription(teamNames: string[]): Response<string[]> {
234+
// Such logic should not generally go in a facade, though the convenience
235+
// and lack of actual problems makes this violation of pattern more "okay."
236+
if(teamNames.length < 1) {
237+
return {
238+
// Should be "no op"
239+
successful: true,
240+
data: []
241+
}
242+
}
243+
244+
try {
245+
const response = await this.gitHubClient.request("POST /orgs/{org}/copilot/billing/selected_teams", {
246+
org: this.orgName,
247+
selected_teams: teamNames,
248+
headers: {
249+
'X-GitHub-Api-Version': '2022-11-28'
250+
}
251+
});
252+
253+
if (response.status < 200 || response.status > 299) {
254+
return {
255+
successful: false
256+
}
257+
}
258+
259+
return {
260+
successful: true,
261+
data: teamNames
262+
}
263+
}
264+
catch(e) {
265+
console.log(e);
266+
// TODO: actually catch exception and investigate...
267+
return {
268+
successful: false
269+
}
270+
}
271+
}
272+
233273
async ListPendingInvitesForTeam(teamName: string): Response<OrgInvite[]> {
234274
const safeName = MakeTeamNameSafeAndApiFriendly(teamName);
235275

src/services/gitHubCache.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { CacheClient } from "../app";
22
import { ILogger } from "../logging";
33
import { GitHubTeamId, InstalledClient, OrgInvite, OrgRoles, Response } from "./gitHubTypes";
4-
import { OrgConfig, OrgConfigurationOptions } from "./orgConfig";
4+
import { OrgConfig } from "./orgConfig";
55

66
export class GitHubClientCache implements InstalledClient {
77
client: InstalledClient;
@@ -13,6 +13,9 @@ export class GitHubClientCache implements InstalledClient {
1313
this.cacheClient = cacheClient;
1414
this.logger = logger;
1515
}
16+
AddTeamsToCopilotSubscription(teamNames: string[]): Response<string[]> {
17+
return this.client.AddTeamsToCopilotSubscription(teamNames);
18+
}
1619

1720
ListPendingInvitesForTeam(teamName: string): Response<OrgInvite[]> {
1821
return this.client.ListPendingInvitesForTeam(teamName);

src/services/gitHubTypes.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { AppConfig } from "./appConfig"
2-
import { GitHubTeamName, OrgConfig, OrgConfigurationOptions } from "./orgConfig"
2+
import { GitHubTeamName, OrgConfig } from "./orgConfig"
33

44
export interface Org {
55
id: number,
@@ -38,6 +38,7 @@ export interface InstalledClient {
3838
GetPendingOrgInvites():Response<OrgInvite[]>
3939
CancelOrgInvite(invite:OrgInvite): Response
4040
ListPendingInvitesForTeam(teamName: GitHubTeamName):Response<OrgInvite[]>
41+
AddTeamsToCopilotSubscription(teamNames: GitHubTeamName[]):Response<string[]>
4142
}
4243

4344

src/services/githubSync.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,7 @@ type ReturnTypeOfSyncOrg = {
216216
syncedSecurityManagerTeams: string[];
217217
orgOwnersGroup: string;
218218
ignoredTeams: string[];
219+
copilotTeams: string[];
219220
}
220221

221222
type FailedSecSync = {
@@ -280,7 +281,8 @@ async function syncOrg(installedGitHubClient: InstalledClient, appConfig: AppCon
280281
status: "failed",
281282
syncedSecurityManagerTeams: [] as string[],
282283
orgOwnersGroup: "",
283-
ignoredTeams: [] as string[]
284+
ignoredTeams: [] as string[],
285+
copilotTeams: [] as string[]
284286
}
285287

286288
// TODO: add this back once these APIs make sense
@@ -434,9 +436,12 @@ async function syncOrg(installedGitHubClient: InstalledClient, appConfig: AppCon
434436
}
435437
}
436438

439+
const copilotResult = await installedGitHubClient.AddTeamsToCopilotSubscription(orgConfig.CopilotTeams);
440+
437441
return {
438442
...response,
439-
status: "completed"
443+
status: "completed",
444+
copilotTeams: copilotResult.successful ? copilotResult.data : []
440445
}
441446
}
442447

@@ -480,7 +485,8 @@ export async function SyncOrg(installedGitHubClient: InstalledClient, config: Ap
480485
status: "failed",
481486
syncedSecurityManagerTeams: [],
482487
orgOwnersGroup: "",
483-
ignoredTeams: []
488+
ignoredTeams: [],
489+
copilotTeams: []
484490
}
485491

486492
Log(JSON.stringify(

src/services/orgConfig.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ export type GitHubTeamName = string;
33
export type ManagedGitHubTeam = {
44
Name: GitHubTeamName,
55
DisplayName?: string
6+
CopilotEnabled?: boolean
67
}
78

89
export type OrgConfigurationOptions = {
@@ -21,6 +22,7 @@ export class OrgConfig {
2122
public OrgMembersGroupName: string | undefined;
2223
public TeamsToManage: string[];
2324
public DisplayNameToSourceMap: Map<string,string>;
25+
public CopilotTeams: string[];
2426

2527
constructor(options: OrgConfigurationOptions) {
2628
this.options = options;
@@ -29,6 +31,11 @@ export class OrgConfig {
2931
this.TeamsToManage = this.GetTeams();
3032
this.DisplayNameToSourceMap = this.GetSourceTeamMap();
3133
this.AdditionalSecurityManagerGroups = this.GetAdditionalSecurityManagerGroupNames();
34+
this.CopilotTeams = this.GetCopilotTeams();
35+
}
36+
37+
private GetCopilotTeams(): string[] {
38+
return this.options.Teams?.filter(t => t.CopilotEnabled == true).map(t => t.DisplayName ?? t.Name) ?? [];
3239
}
3340

3441
private GetAdditionalSecurityManagerGroupNames(): string[] {

0 commit comments

Comments
 (0)