Skip to content

Commit be34ad7

Browse files
committed
Add support for external user identity fetching
1 parent c01b2a9 commit be34ad7

File tree

11 files changed

+132315
-41854
lines changed

11 files changed

+132315
-41854
lines changed

.github/workflows/usage-notify.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ jobs:
1313
steps:
1414
- uses: actions/checkout@v4
1515
- uses: ./
16+
id: copilot
1617
with:
1718
github-token: ${{ secrets.TOKEN }}
1819
organization: 'myorg'

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
.DS_Store
22
node_modules
3+
.env

README.md

Lines changed: 43 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@ Run this action on a schedule to automatically remove inactive Copilot licenses.
55
In addition to this it can also deploy users from a CSV file. This is useful as you are adopting Copilot as it can help facilitate the process of adding users to your organization.
66

77
## Usage
8+
89
Create a workflow (eg: `.github/workflows/copilot-license-management.yml`). See [Creating a Workflow file](https://help.github.com/en/articles/configuring-a-workflow#creating-a-workflow-file).
910

1011
### Deploying users from a CSV file
1112

1213
If you want to deploy users from a CSV file you will need to create a CSV file with the following columns:
14+
1315
- `organization` - The organization to add the user to
1416
- `deployment_group` - An arbitrary group name used to track the deployments
1517
- `login` - The user's GitHub Login name to add
@@ -29,15 +31,18 @@ If you are using Enterprise Managed Users, it may be easier to use a group from
2931

3032
### PAT(Personal Access Token)
3133

32-
You will need to [create a PAT(Personal Access Token)](https://github.com/settings/tokens/new?scopes=manage_billing:copilot) that has `manage_billing:copilot` access. If you are specifying an 'enterprise' rather than individual organizations you must also include the `read:org` and `read:enterprise` scopes.
34+
You will need to [create a PAT(Personal Access Token)](https://github.com/settings/tokens/new?scopes=manage_billing:copilot) that has `manage_billing:copilot` access. If you are specifying an 'enterprise' rather than individual organizations you must also include the `read:org` and `read:enterprise` scopes.
35+
36+
If you are including external identities to the output must also include the `read:org` and `user:email` scopes.
37+
38+
Add this PAT as a secret `TOKEN` so we can use it for input `github-token`, see [Creating encrypted secrets for a repository](https://docs.github.com/en/enterprise-cloud@latest/actions/security-guides/encrypted-secrets#creating-encrypted-secrets-for-a-repository).
3339

34-
Add this PAT as a secret `TOKEN` so we can use it for input `github-token`, see [Creating encrypted secrets for a repository](https://docs.github.com/en/enterprise-cloud@latest/actions/security-guides/encrypted-secrets#creating-encrypted-secrets-for-a-repository).
3540
### Organizations
3641

3742
If your organization has SAML enabled you must authorize the PAT, see [Authorizing a personal access token for use with SAML single sign-on](https://docs.github.com/en/enterprise-cloud@latest/authentication/authenticating-with-saml-single-sign-on/authorizing-a-personal-access-token-for-use-with-saml-single-sign-on).
3843

39-
4044
#### Example
45+
4146
```yml
4247
name: Cleanup Copilot Licenses
4348
on:
@@ -56,6 +61,7 @@ jobs:
5661
```
5762
5863
#### Example Auto remove
64+
5965
```yml
6066
- uses: austenstone/[email protected]
6167
with:
@@ -65,6 +71,7 @@ jobs:
6571
```
6672
6773
#### Example Custom days before inactive
74+
6875
```yml
6976
- uses: austenstone/[email protected]
7077
with:
@@ -74,15 +81,17 @@ jobs:
7481
inactive-days: 10
7582
```
7683
77-
#### Example Specifying multiple organizations:
84+
#### Example Specifying multiple organizations
85+
7886
```yml
7987
- uses: austenstone/[email protected]
8088
with:
8189
github-token: ${{ secrets.TOKEN }}
8290
organization: exampleorg1, demoorg2, myorg3
8391
```
8492
85-
#### Example specifying a GitHub Enterprise (to run on all organizations in the enterprise):
93+
#### Example specifying a GitHub Enterprise (to run on all organizations in the enterprise)
94+
8695
```yml
8796
- uses: austenstone/[email protected]
8897
with:
@@ -91,6 +100,7 @@ jobs:
91100
```
92101
93102
#### Example uploading inactive users JSON artifact (same could be done with deployed-seats)
103+
94104
```yml
95105
- uses: austenstone/[email protected]
96106
id: copilot
@@ -106,7 +116,7 @@ jobs:
106116
path: inactive-seats.json
107117
```
108118
109-
#### Example deploying users from a CSV file
119+
#### Example deploying users from a CSV file
110120
111121
```yml
112122
name: Copilot License Review
@@ -140,42 +150,46 @@ jobs:
140150
141151
<details>
142152
<summary>Job summary example</summary>
143-
153+
144154
<img src="https://github.com/austenstone/copilot-license-cleanup/assets/22425467/4695fc23-e9c7-4403-ba04-2de0e2d36242"/>
145-
146-
</details>
147155
156+
</details>
148157
149158
## ➡️ Inputs
159+
150160
Various inputs are defined in [`action.yml`](action.yml):
151161

152-
| Name | Description | Default |
153-
| --- | - | - |
154-
| **github&#x2011;token** | Token to use to authorize. | ${{&nbsp;github.token&nbsp;}} |
155-
| organization | The organization(s) to use for the action (comma separated)| ${{&nbsp;github.repository_owner&nbsp;}} |
156-
| enterprise | (optional) All organizations in this enterprise (overrides organization) | null |
157-
| remove | Whether to remove inactive users | false |
158-
| remove-from-team | Whether to remove inactive users from their assigning team | false |
159-
| inactive&#x2011;days | The number of days to consider a user inactive | 90 |
160-
| job-summary | Whether to output a summary of the job | true |
161-
| csv | Whether to output a CSV of inactive users | false |
162-
| deploy-users | Whether to deploy users from a CSV file | false |
163-
| deploy-users-dry-run | Whether to perform a dry run when deploying users | true |
164-
| deploy-users-csv | CSV file location if deploying users | ./copilot-users.csv |
165-
| deploy-validation-time | The number of days to attempt to deploy the user beyond activation date | 3 |
162+
| Name | Description | Default |
163+
| ----------------------- | ------------------------------------------------------------------------ | ---------------------------------------- |
164+
| **github&#x2011;token** | Token to use to authorize. | ${{&nbsp;github.token&nbsp;}} |
165+
| organization | The organization(s) to use for the action (comma separated) | ${{&nbsp;github.repository_owner&nbsp;}} |
166+
| enterprise | (optional) All organizations in this enterprise (overrides organization) | null |
167+
| remove | Whether to remove inactive users | false |
168+
| remove-from-team | Whether to remove inactive users from their assigning team | false |
169+
| inactive&#x2011;days | The number of days to consider a user inactive | 90 |
170+
| job-summary | Whether to output a summary of the job | true |
171+
| external-identities | Weather to add external user identities to the output | false |
172+
| csv | Whether to output a CSV of inactive users | false |
173+
| deploy-users | Whether to deploy users from a CSV file | false |
174+
| deploy-users-dry-run | Whether to perform a dry run when deploying users | true |
175+
| deploy-users-csv | CSV file location if deploying users | ./copilot-users.csv |
176+
| deploy-validation-time | The number of days to attempt to deploy the user beyond activation date | 3 |
166177

167178
## ⬅️ Outputs
168-
| Name | Description |
169-
| --- | - |
170-
| inactive-seats | JSON array of inactive seats |
179+
180+
| Name | Description |
181+
| ------------------- | ---------------------------- |
182+
| inactive-seats | JSON array of inactive seats |
171183
| inactive-seat-count | The number of inactive seats |
172-
| removed-seats | The number of seats removed |
173-
| seat-count | The total number of seats |
174-
| deployed-seats | JSON array of deployed seats |
184+
| removed-seats | The number of seats removed |
185+
| seat-count | The total number of seats |
186+
| deployed-seats | JSON array of deployed seats |
175187
| deployed-seat-count | The number of deployed seats |
176188

177189
## How does it work?
190+
178191
We're simply leveraging the [GitHub Copilot API](https://docs.github.com/en/rest/copilot). First we fetch all the Copilot seats and filter them to only inactive seats. Then if the seat is assigned directly we remove it but if it's assigned through a team we remove the user from the team. Those inactive users are reported as a CSV and a job summary table.
179192

180193
## Further help
194+
181195
To get more help on the Actions see [documentation](https://docs.github.com/en/actions).

__tests__/main.test.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,11 @@ const input: any = {
1111
'github-token': process.env.GITHUB_TOKEN,
1212
'organization': process.env.ORGANIZATION || 'austenstone',
1313
'inactive-days': process.env.INACTIVE_DAYS || '30',
14+
'external-identities': process.env.EXTERNAL_IDENTITIES || false,
1415
'remove': process.env.REMOVE || false,
1516
'remove-from-team': process.env.REMOVE_FROM_TEAM || false,
17+
'deploy-users': process.env.DEPLOY_USERS || false,
18+
'deploy-users-dry-run': process.env.DEPLOY_USERS_DRY_RUN || false,
1619
'job-summary': process.env.JOB_SUMMARY || false,
1720
'csv': process.env.CSV || false,
1821
}
@@ -25,7 +28,8 @@ test('test run', () => {
2528
const options: cp.ExecFileSyncOptions = {
2629
env: process.env,
2730
};
28-
31+
2932
const spawned = cp.spawnSync(np, [ip], options);
3033
console.log(spawned.stdout.toString());
34+
console.log(spawned.stderr.toString());
3135
});

action.yml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: Copilot License Management
1+
name: Copilot License Management
22
author: Austen Stone
33
description: Automations to manage copilot licenses. Cleanup inactive, rollouts, reporting.
44
branding:
@@ -11,7 +11,7 @@ inputs:
1111
default: ${{ github.repository_owner }}
1212
required: true
1313
enterprise:
14-
description: Search for all organizations in the enterprise (overrides organization)
14+
description: Search for all organizations in the enterprise (overrides organization)
1515
default: null
1616
required: false
1717
github-token:
@@ -34,6 +34,10 @@ inputs:
3434
description: Whether to output a summary of the job
3535
default: true
3636
required: false
37+
external-identities:
38+
description: Whether to add external user identities to the output
39+
default: false
40+
required: false
3741
csv:
3842
description: Whether to output a CSV of the inactive users
3943
default: false

dist/15.index.js

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
"use strict";
2+
exports.id = 15;
3+
exports.ids = [15];
4+
exports.modules = {
5+
6+
/***/ 31015:
7+
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
8+
9+
__webpack_require__.r(__webpack_exports__);
10+
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
11+
/* harmony export */ RequestError: () => (/* binding */ RequestError)
12+
/* harmony export */ });
13+
class RequestError extends Error {
14+
name;
15+
/**
16+
* http status code
17+
*/
18+
status;
19+
/**
20+
* Request options that lead to the error.
21+
*/
22+
request;
23+
/**
24+
* Response object if a response was received
25+
*/
26+
response;
27+
constructor(message, statusCode, options) {
28+
super(message);
29+
this.name = "HttpError";
30+
this.status = Number.parseInt(statusCode);
31+
if (Number.isNaN(this.status)) {
32+
this.status = 0;
33+
}
34+
if ("response" in options) {
35+
this.response = options.response;
36+
}
37+
const requestCopy = Object.assign({}, options.request);
38+
if (options.request.headers.authorization) {
39+
requestCopy.headers = Object.assign({}, options.request.headers, {
40+
authorization: options.request.headers.authorization.replace(
41+
/ .*$/,
42+
" [REDACTED]"
43+
)
44+
});
45+
}
46+
requestCopy.url = requestCopy.url.replace(/\bclient_secret=\w+/g, "client_secret=[REDACTED]").replace(/\baccess_token=\w+/g, "access_token=[REDACTED]");
47+
this.request = requestCopy;
48+
}
49+
}
50+
51+
52+
53+
/***/ })
54+
55+
};
56+
;

0 commit comments

Comments
 (0)