Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .github/workflows/usage-notify.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- uses: ./
id: copilot
with:
github-token: ${{ secrets.TOKEN }}
organization: 'myorg'
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
.DS_Store
node_modules
.env
72 changes: 43 additions & 29 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ Run this action on a schedule to automatically remove inactive Copilot licenses.
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.

## Usage

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).

### Deploying users from a CSV file

If you want to deploy users from a CSV file you will need to create a CSV file with the following columns:

- `organization` - The organization to add the user to
- `deployment_group` - An arbitrary group name used to track the deployments
- `login` - The user's GitHub Login name to add
Expand All @@ -29,15 +31,18 @@ If you are using Enterprise Managed Users, it may be easier to use a group from

### PAT(Personal Access Token)

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.
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.

If you are including external identities to the output must also include the `read:org` and `user:email` scopes.

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).

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).
### Organizations

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).


#### Example

```yml
name: Cleanup Copilot Licenses
on:
Expand All @@ -56,6 +61,7 @@ jobs:
```

#### Example Auto remove

```yml
- uses: austenstone/[email protected]
with:
Expand All @@ -65,6 +71,7 @@ jobs:
```

#### Example Custom days before inactive

```yml
- uses: austenstone/[email protected]
with:
Expand All @@ -74,15 +81,17 @@ jobs:
inactive-days: 10
```

#### Example Specifying multiple organizations:
#### Example Specifying multiple organizations

```yml
- uses: austenstone/[email protected]
with:
github-token: ${{ secrets.TOKEN }}
organization: exampleorg1, demoorg2, myorg3
```

#### Example specifying a GitHub Enterprise (to run on all organizations in the enterprise):
#### Example specifying a GitHub Enterprise (to run on all organizations in the enterprise)

```yml
- uses: austenstone/[email protected]
with:
Expand All @@ -91,6 +100,7 @@ jobs:
```

#### Example uploading inactive users JSON artifact (same could be done with deployed-seats)

```yml
- uses: austenstone/[email protected]
id: copilot
Expand All @@ -106,7 +116,7 @@ jobs:
path: inactive-seats.json
```

#### Example deploying users from a CSV file
#### Example deploying users from a CSV file

```yml
name: Copilot License Review
Expand Down Expand Up @@ -140,42 +150,46 @@ jobs:

<details>
<summary>Job summary example</summary>

<img src="https://github.com/austenstone/copilot-license-cleanup/assets/22425467/4695fc23-e9c7-4403-ba04-2de0e2d36242"/>

</details>

</details>

## ➡️ Inputs

Various inputs are defined in [`action.yml`](action.yml):

| Name | Description | Default |
| --- | - | - |
| **github&#x2011;token** | Token to use to authorize. | ${{&nbsp;github.token&nbsp;}} |
| organization | The organization(s) to use for the action (comma separated)| ${{&nbsp;github.repository_owner&nbsp;}} |
| enterprise | (optional) All organizations in this enterprise (overrides organization) | null |
| remove | Whether to remove inactive users | false |
| remove-from-team | Whether to remove inactive users from their assigning team | false |
| inactive&#x2011;days | The number of days to consider a user inactive | 90 |
| job-summary | Whether to output a summary of the job | true |
| csv | Whether to output a CSV of inactive users | false |
| deploy-users | Whether to deploy users from a CSV file | false |
| deploy-users-dry-run | Whether to perform a dry run when deploying users | true |
| deploy-users-csv | CSV file location if deploying users | ./copilot-users.csv |
| deploy-validation-time | The number of days to attempt to deploy the user beyond activation date | 3 |
| Name | Description | Default |
| ----------------------- | ------------------------------------------------------------------------ | ---------------------------------------- |
| **github&#x2011;token** | Token to use to authorize. | ${{&nbsp;github.token&nbsp;}} |
| organization | The organization(s) to use for the action (comma separated) | ${{&nbsp;github.repository_owner&nbsp;}} |
| enterprise | (optional) All organizations in this enterprise (overrides organization) | null |
| remove | Whether to remove inactive users | false |
| remove-from-team | Whether to remove inactive users from their assigning team | false |
| inactive&#x2011;days | The number of days to consider a user inactive | 90 |
| job-summary | Whether to output a summary of the job | true |
| external-identities | Weather to add external user identities to the output | false |
| csv | Whether to output a CSV of inactive users | false |
| deploy-users | Whether to deploy users from a CSV file | false |
| deploy-users-dry-run | Whether to perform a dry run when deploying users | true |
| deploy-users-csv | CSV file location if deploying users | ./copilot-users.csv |
| deploy-validation-time | The number of days to attempt to deploy the user beyond activation date | 3 |

## ⬅️ Outputs
| Name | Description |
| --- | - |
| inactive-seats | JSON array of inactive seats |

| Name | Description |
| ------------------- | ---------------------------- |
| inactive-seats | JSON array of inactive seats |
| inactive-seat-count | The number of inactive seats |
| removed-seats | The number of seats removed |
| seat-count | The total number of seats |
| deployed-seats | JSON array of deployed seats |
| removed-seats | The number of seats removed |
| seat-count | The total number of seats |
| deployed-seats | JSON array of deployed seats |
| deployed-seat-count | The number of deployed seats |

## How does it work?

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.

## Further help

To get more help on the Actions see [documentation](https://docs.github.com/en/actions).
6 changes: 5 additions & 1 deletion __tests__/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,11 @@ const input: any = {
'github-token': process.env.GITHUB_TOKEN,
'organization': process.env.ORGANIZATION || 'austenstone',
'inactive-days': process.env.INACTIVE_DAYS || '30',
'external-identities': process.env.EXTERNAL_IDENTITIES || false,
'remove': process.env.REMOVE || false,
'remove-from-team': process.env.REMOVE_FROM_TEAM || false,
'deploy-users': process.env.DEPLOY_USERS || false,
'deploy-users-dry-run': process.env.DEPLOY_USERS_DRY_RUN || false,
'job-summary': process.env.JOB_SUMMARY || false,
'csv': process.env.CSV || false,
}
Expand All @@ -25,7 +28,8 @@ test('test run', () => {
const options: cp.ExecFileSyncOptions = {
env: process.env,
};

const spawned = cp.spawnSync(np, [ip], options);
console.log(spawned.stdout.toString());
console.log(spawned.stderr.toString());
});
8 changes: 6 additions & 2 deletions action.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Copilot License Management
name: Copilot License Management
author: Austen Stone
description: Automations to manage copilot licenses. Cleanup inactive, rollouts, reporting.
branding:
Expand All @@ -11,7 +11,7 @@ inputs:
default: ${{ github.repository_owner }}
required: true
enterprise:
description: Search for all organizations in the enterprise (overrides organization)
description: Search for all organizations in the enterprise (overrides organization)
default: null
required: false
github-token:
Expand All @@ -34,6 +34,10 @@ inputs:
description: Whether to output a summary of the job
default: true
required: false
external-identities:
description: Whether to add external user identities to the output
default: false
required: false
csv:
description: Whether to output a CSV of the inactive users
default: false
Expand Down
56 changes: 56 additions & 0 deletions dist/15.index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
"use strict";
exports.id = 15;
exports.ids = [15];
exports.modules = {

/***/ 31015:
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {

__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ RequestError: () => (/* binding */ RequestError)
/* harmony export */ });
class RequestError extends Error {
name;
/**
* http status code
*/
status;
/**
* Request options that lead to the error.
*/
request;
/**
* Response object if a response was received
*/
response;
constructor(message, statusCode, options) {
super(message);
this.name = "HttpError";
this.status = Number.parseInt(statusCode);
if (Number.isNaN(this.status)) {
this.status = 0;
}
if ("response" in options) {
this.response = options.response;
}
const requestCopy = Object.assign({}, options.request);
if (options.request.headers.authorization) {
requestCopy.headers = Object.assign({}, options.request.headers, {
authorization: options.request.headers.authorization.replace(
/ .*$/,
" [REDACTED]"
)
});
}
requestCopy.url = requestCopy.url.replace(/\bclient_secret=\w+/g, "client_secret=[REDACTED]").replace(/\baccess_token=\w+/g, "access_token=[REDACTED]");
this.request = requestCopy;
}
}



/***/ })

};
;
Loading