Skip to content
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

copilot: Introduced support for organizations and team metrics visualization #1261

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
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
7 changes: 7 additions & 0 deletions workspaces/copilot/.changeset/six-cats-beg.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@backstage-community/plugin-copilot-backend': major
'@backstage-community/plugin-copilot-common': major
'@backstage-community/plugin-copilot': major
---

Introduced support for organizations and team metrics visualization in the Copilot plugin.
5 changes: 2 additions & 3 deletions workspaces/copilot/packages/app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ import { AppRouter, FlatRoutes } from '@backstage/core-app-api';
import { CatalogGraphPage } from '@backstage/plugin-catalog-graph';
import { RequirePermission } from '@backstage/plugin-permission-react';
import { catalogEntityCreatePermission } from '@backstage/plugin-catalog-common/alpha';
import { CopilotPage } from '@backstage-community/plugin-copilot';
import { CopilotIndexPage } from '@backstage-community/plugin-copilot';

const app = createApp({
apis,
Expand Down Expand Up @@ -111,10 +111,9 @@ const routes = (
</Route>
<Route path="/settings" element={<UserSettingsPage />} />
<Route path="/catalog-graph" element={<CatalogGraphPage />} />
<Route path="/copilot" element={<CopilotPage />} />
<Route path="/copilot" element={<CopilotIndexPage />} />
</FlatRoutes>
);

export default app.createRoot(
<>
<AlertDisplay />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import HomeIcon from '@material-ui/icons/Home';
import ExtensionIcon from '@material-ui/icons/Extension';
import LibraryBooks from '@material-ui/icons/LibraryBooks';
import CreateComponentIcon from '@material-ui/icons/AddCircleOutline';
import GithubIcon from '@material-ui/icons/GitHub';
import LogoFull from './LogoFull';
import LogoIcon from './LogoIcon';
import {
Expand All @@ -41,6 +40,7 @@ import {
} from '@backstage/core-components';
import MenuIcon from '@material-ui/icons/Menu';
import SearchIcon from '@material-ui/icons/Search';
import { CopilotSidebar } from '@backstage-community/plugin-copilot';

const useSidebarLogoStyles = makeStyles({
root: {
Expand Down Expand Up @@ -87,7 +87,7 @@ export const Root = ({ children }: PropsWithChildren<{}>) => (
{/* End global nav */}
<SidebarDivider />
<SidebarScrollWrapper>
<SidebarItem icon={GithubIcon} to="copilot" text="Copilot" />
<CopilotSidebar />
</SidebarScrollWrapper>
</SidebarGroup>
<SidebarSpace />
Expand Down
54 changes: 37 additions & 17 deletions workspaces/copilot/plugins/copilot-backend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ To configure the plugin using the new backend system:

const backend = createBackend();

backend.add(import('@backstage-community/plugin-copilot'));
backend.add(import('@backstage-community/plugin-copilot-backend'));

backend.start();
```
Expand All @@ -24,17 +24,17 @@ To configure the plugin using the new backend system:

To install the plugin using the old method:

1. Add the `@backstage-community/plugin-copilot` package to your backend:
1. Add the `@backstage-community/plugin-copilot-backend` package to your backend:

```sh
yarn --cwd packages/backend add @backstage-community/plugin-copilot
yarn --cwd packages/backend add @backstage-community/plugin-copilot-backend
```

2. In your `packages/backend/src/plugins/copilot.ts` file, add the following code:

```typescript
import { TaskScheduleDefinition } from '@backstage/backend-tasks';
import { createRouterFromConfig } from '@backstage-community/plugin-copilot';
import { createRouterFromConfig } from '@backstage-community/plugin-copilot-backend';

export default async function createPlugin(): Promise<void> {
const schedule: TaskScheduleDefinition = {
Expand All @@ -53,9 +53,7 @@ To install the plugin using the old method:
import { createRouterFromConfig } from './plugins/copilot';

async function main() {
// Backend setup
const env = createEnv('copilot');
// Plugin registration
apiRouter.use('/copilot', await createRouterFromConfig(env));
}
```
Expand All @@ -68,14 +66,36 @@ To configure the GitHub Copilot plugin, you need to set the following environmen

- **`copilot.host`**: The host URL for your GitHub Copilot instance (e.g., `github.com` or `github.enterprise.com`).
- **`copilot.enterprise`**: The name of your GitHub Enterprise instance (e.g., `my-enterprise`).
- **`copilot.organization`**: The name of your GitHub Organization (e.g., `my-organization`).

These variables are used to configure the plugin and ensure it communicates with the correct GitHub instance.

### GitHub Credentials

**Important:** The GitHub token, which is necessary for authentication, should be managed within your Backstage integrations configuration. The token must be added to your GitHub integration settings, and the plugin will retrieve it through the `GithubCredentialsProvider`.
**Important:** The GitHub token, necessary for authentication, should be managed within your Backstage integrations configuration. Ensure that your GitHub integration in the Backstage configuration includes the necessary token for the `GithubCredentialsProvider` to function correctly.

Ensure that your GitHub integration in the Backstage configuration includes the necessary token for the `GithubCredentialsProvider` to work correctly.
### GitHub Token Scopes

To ensure the GitHub Copilot plugin operates correctly within your organization or enterprise, your GitHub access token must include specific scopes. These scopes grant the plugin the necessary permissions to interact with your GitHub organization and manage Copilot usage.

#### Required Scopes

1. **List Teams Endpoint**

- **Scope Required:** `read:org`
- **Purpose:** Allows the plugin to list all teams within your GitHub organization.

2. **Copilot Usage**
- **Scopes Required - enterprise:** `manage_billing:copilot`, `read:enterprise`
- **Scopes Required - organization:** `manage_billing:copilot`, `read:org`, or `read:enterprise`
- **Purpose:** Enables the plugin to manage and monitor GitHub Copilot usage within your organization or/and enterprise.

#### How to Configure Token Scopes

1. **Generate a Personal Access Token (PAT):**
- Navigate to [GitHub Personal Access Tokens](https://github.com/settings/tokens).
- Click on **Generate new token**.
- Select the scopes according to your needs

### YAML Configuration Example

Expand All @@ -90,20 +110,20 @@ copilot:
seconds: 15
host: YOUR_GITHUB_HOST_HERE
enterprise: YOUR_ENTERPRISE_NAME_HERE
```

### Generating GitHub Copilot Token
organization: YOUR_ORGANIZATION_NAME_HERE
esw-afabiano marked this conversation as resolved.
Show resolved Hide resolved

To generate an access token for using GitHub Copilot:

- Visit [Generate GitHub Access Token](https://github.com/settings/tokens).
- Follow the instructions to create a new token with the `read:enterprise` scope.
integrations:
github:
- host: YOUR_GITHUB_HOST_HERE
token: YOUR_GENERATED_TOKEN
```

### API Documentation

For more details on using the GitHub Copilot API:
For more details on using the GitHub Copilot and Teams APIs, refer to the following documentation:

- Refer to the [API documentation](https://docs.github.com/en/rest/copilot/copilot-usage?apiVersion=2022-11-28) for comprehensive information on available functionalities.
- [GitHub Teams API - List Teams](https://docs.github.com/en/rest/teams/teams?apiVersion=2022-11-28#list-teams)
- [GitHub Copilot API - Usage](https://docs.github.com/en/rest/copilot/copilot-usage?apiVersion=2022-11-28)

## Run

Expand Down
6 changes: 5 additions & 1 deletion workspaces/copilot/plugins/copilot-backend/config.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2023 The Backstage Authors
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -30,6 +30,10 @@ export interface Config {
* The name of the GitHub enterprise.
*/
enterprise?: string;
/**
* The name of the GitHub organization.
*/
organization?: string;
/**
* The host for GitHub Copilot integration.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @param {import('knex').Knex} knex
*/
exports.up = async function up(knex) {
esw-afabiano marked this conversation as resolved.
Show resolved Hide resolved
await knex.schema.table('metrics', table => {
table
.string('type', 50)
.defaultTo('enterprise')
.notNullable()
.comment('Type of the metrics data: enterprise, organization');

table.string('team_name', 255).nullable().comment('Name of the team');

table.dropPrimary();

table.unique(['day', 'type', 'team_name'], 'uk_day_type_team_name');
});
};

/**
* @param {import('knex').Knex} knex
*/
exports.down = async function down(knex) {
await knex.schema.table('metrics', table => {
table.dropUnique(['day', 'type', 'team_name']);

table.dropColumn('type');
table.dropColumn('team_name');

table.primary('day');
table.index('day', 'idx_metrics_day');
});
};
3 changes: 2 additions & 1 deletion workspaces/copilot/plugins/copilot-backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@
"luxon": "^3.5.0",
"node-fetch": "^2.6.7",
"winston": "^3.2.1",
"yn": "^4.0.0"
"yn": "^4.0.0",
"zod": "^3.23.8"
},
"devDependencies": {
"@backstage/backend-test-utils": "^1.0.2",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,17 @@

import { ResponseError } from '@backstage/errors';
import { Config } from '@backstage/config';
import { Metric } from '@backstage-community/plugin-copilot-common';
import { Metric, TeamInfo } from '@backstage-community/plugin-copilot-common';
import fetch from 'node-fetch';
import { getGithubInfo, GithubInfo } from '../utils/GithubUtils';
import { getGithubInfo, GithubInfo } from '../utils/githubUtils';

interface GithubApi {
getCopilotUsageDataForEnterprise: () => Promise<Metric[]>;
fetchEnterpriseCopilotUsage: () => Promise<Metric[]>;
fetchEnterpriseTeamCopilotUsage: (teamId: string) => Promise<Metric[]>;
fetchEnterpriseTeams: () => Promise<TeamInfo[]>;
fetchOrganizationCopilotUsage: () => Promise<Metric[]>;
fetchOrganizationTeamCopilotUsage: (teamId: string) => Promise<Metric[]>;
fetchOrganizationTeams: () => Promise<TeamInfo[]>;
}

export class GithubClient implements GithubApi {
Expand All @@ -32,11 +37,36 @@ export class GithubClient implements GithubApi {
return new GithubClient(info);
}

async getCopilotUsageDataForEnterprise(): Promise<Metric[]> {
async fetchEnterpriseCopilotUsage(): Promise<Metric[]> {
const path = `/enterprises/${this.props.enterprise}/copilot/usage`;
return this.get(path);
}

async fetchEnterpriseTeamCopilotUsage(teamId: string): Promise<Metric[]> {
const path = `/enterprises/${this.props.enterprise}/team/${teamId}/copilot/usage`;
return this.get(path);
}

async fetchEnterpriseTeams(): Promise<TeamInfo[]> {
const path = `/enterprises/${this.props.enterprise}/teams`;
return this.get(path);
}

async fetchOrganizationCopilotUsage(): Promise<Metric[]> {
const path = `/orgs/${this.props.organization}/copilot/usage`;
return this.get(path);
}

async fetchOrganizationTeamCopilotUsage(teamId: string): Promise<Metric[]> {
const path = `/orgs/${this.props.organization}/team/${teamId}/copilot/usage`;
return this.get(path);
}

async fetchOrganizationTeams(): Promise<TeamInfo[]> {
const path = `/orgs/${this.props.organization}/teams`;
return this.get(path);
}

private async get<T>(path: string): Promise<T> {
const response = await fetch(`${this.props.apiBaseUrl}${path}`, {
headers: {
Expand Down
Loading
Loading