@backstage-community/plugin-copilot-backend 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +121 -0
- package/config.d.ts +38 -0
- package/dist/index.cjs.js +266 -0
- package/dist/index.cjs.js.map +1 -0
- package/dist/index.d.ts +60 -0
- package/migrations/202410131705_init.js +75 -0
- package/package.json +72 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# @backstage-community/plugin-copilot-backend
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 2d5f011: Introduced the GitHub Copilot plugin, checkout the plugin's [`README.md`](https://github.com/backstage/community-plugins/tree/main/workspaces/copilot/plugins/copilot) for more details!
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- Updated dependencies [2d5f011]
|
|
12
|
+
- @backstage-community/plugin-copilot-common@0.1.0
|
package/README.md
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# GitHub Copilot Plugin
|
|
2
|
+
|
|
3
|
+
This GitHub Copilot plugin integrates with Backstage to provide metrics and insights for members of your organization or enterprise.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
### New Backend System
|
|
8
|
+
|
|
9
|
+
To configure the plugin using the new backend system:
|
|
10
|
+
|
|
11
|
+
1. In the `packages/backend/src/index.ts` file, add the following:
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { createBackend } from '@backstage/backend-defaults';
|
|
15
|
+
|
|
16
|
+
const backend = createBackend();
|
|
17
|
+
|
|
18
|
+
backend.add(import('@backstage-community/plugin-copilot'));
|
|
19
|
+
|
|
20
|
+
backend.start();
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Old System
|
|
24
|
+
|
|
25
|
+
To install the plugin using the old method:
|
|
26
|
+
|
|
27
|
+
1. Add the `@backstage-community/plugin-copilot` package to your backend:
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
yarn --cwd packages/backend add @backstage-community/plugin-copilot
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
2. In your `packages/backend/src/plugins/copilot.ts` file, add the following code:
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
import { TaskScheduleDefinition } from '@backstage/backend-tasks';
|
|
37
|
+
import { createRouterFromConfig } from '@backstage-community/plugin-copilot';
|
|
38
|
+
|
|
39
|
+
export default async function createPlugin(): Promise<void> {
|
|
40
|
+
const schedule: TaskScheduleDefinition = {
|
|
41
|
+
frequency: { cron: '0 2 * * *' },
|
|
42
|
+
timeout: { minutes: 15 },
|
|
43
|
+
initialDelay: { seconds: 15 },
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
return createRouterFromConfig({ schedule });
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
3. Integrate the plugin into the main backend router in `packages/backend/src/index.ts`:
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
import { createRouterFromConfig } from './plugins/copilot';
|
|
54
|
+
|
|
55
|
+
async function main() {
|
|
56
|
+
// Backend setup
|
|
57
|
+
const env = createEnv('copilot');
|
|
58
|
+
// Plugin registration
|
|
59
|
+
apiRouter.use('/copilot', await createRouterFromConfig(env));
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Configuration
|
|
64
|
+
|
|
65
|
+
### Environment Variables
|
|
66
|
+
|
|
67
|
+
To configure the GitHub Copilot plugin, you need to set the following environment variables:
|
|
68
|
+
|
|
69
|
+
- **`copilot.host`**: The host URL for your GitHub Copilot instance (e.g., `github.com` or `github.enterprise.com`).
|
|
70
|
+
- **`copilot.enterprise`**: The name of your GitHub Enterprise instance (e.g., `my-enterprise`).
|
|
71
|
+
|
|
72
|
+
These variables are used to configure the plugin and ensure it communicates with the correct GitHub instance.
|
|
73
|
+
|
|
74
|
+
### GitHub Credentials
|
|
75
|
+
|
|
76
|
+
**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`.
|
|
77
|
+
|
|
78
|
+
Ensure that your GitHub integration in the Backstage configuration includes the necessary token for the `GithubCredentialsProvider` to work correctly.
|
|
79
|
+
|
|
80
|
+
### YAML Configuration Example
|
|
81
|
+
|
|
82
|
+
```yaml
|
|
83
|
+
copilot:
|
|
84
|
+
scheduler:
|
|
85
|
+
frequency:
|
|
86
|
+
hours: 2
|
|
87
|
+
timeout:
|
|
88
|
+
minutes: 2
|
|
89
|
+
initialDelay:
|
|
90
|
+
seconds: 15
|
|
91
|
+
host: YOUR_GITHUB_HOST_HERE
|
|
92
|
+
enterprise: YOUR_ENTERPRISE_NAME_HERE
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Generating GitHub Copilot Token
|
|
96
|
+
|
|
97
|
+
To generate an access token for using GitHub Copilot:
|
|
98
|
+
|
|
99
|
+
- Visit [Generate GitHub Access Token](https://github.com/settings/tokens).
|
|
100
|
+
- Follow the instructions to create a new token with the `read:enterprise` scope.
|
|
101
|
+
|
|
102
|
+
### API Documentation
|
|
103
|
+
|
|
104
|
+
For more details on using the GitHub Copilot API:
|
|
105
|
+
|
|
106
|
+
- Refer to the [API documentation](https://docs.github.com/en/rest/copilot/copilot-usage?apiVersion=2022-11-28) for comprehensive information on available functionalities.
|
|
107
|
+
|
|
108
|
+
## Run
|
|
109
|
+
|
|
110
|
+
1. Start the backend:
|
|
111
|
+
|
|
112
|
+
```sh
|
|
113
|
+
yarn start-backend
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
2. Verify the plugin is running by accessing `http://localhost:7007/api/copilot/health`.
|
|
117
|
+
|
|
118
|
+
## Links
|
|
119
|
+
|
|
120
|
+
- [GitHub Copilot Plugin Frontend](https://github.com/backstage/backstage/tree/master/plugins/copilot)
|
|
121
|
+
- [Backstage Homepage](https://backstage.io)
|
package/config.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2023 The Backstage Authors
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { SchedulerServiceTaskScheduleDefinition } from '@backstage/backend-plugin-api';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Configuration interface for the GitHub Copilot Plugin.
|
|
21
|
+
*/
|
|
22
|
+
export interface Config {
|
|
23
|
+
/** Configuration options for the GitHub Copilot Plugin */
|
|
24
|
+
copilot?: {
|
|
25
|
+
/**
|
|
26
|
+
* Schedule definition for the GitHub Copilot Plugin tasks.
|
|
27
|
+
*/
|
|
28
|
+
schedule?: SchedulerServiceTaskScheduleDefinition;
|
|
29
|
+
/**
|
|
30
|
+
* The name of the GitHub enterprise.
|
|
31
|
+
*/
|
|
32
|
+
enterprise?: string;
|
|
33
|
+
/**
|
|
34
|
+
* The host for GitHub Copilot integration.
|
|
35
|
+
*/
|
|
36
|
+
host?: string;
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
|
+
|
|
5
|
+
var express = require('express');
|
|
6
|
+
var Router = require('express-promise-router');
|
|
7
|
+
var rootHttpRouter = require('@backstage/backend-defaults/rootHttpRouter');
|
|
8
|
+
var backendPluginApi = require('@backstage/backend-plugin-api');
|
|
9
|
+
var luxon = require('luxon');
|
|
10
|
+
var errors = require('@backstage/errors');
|
|
11
|
+
var fetch = require('node-fetch');
|
|
12
|
+
var integration = require('@backstage/integration');
|
|
13
|
+
|
|
14
|
+
function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
|
|
15
|
+
|
|
16
|
+
var express__default = /*#__PURE__*/_interopDefaultCompat(express);
|
|
17
|
+
var Router__default = /*#__PURE__*/_interopDefaultCompat(Router);
|
|
18
|
+
var fetch__default = /*#__PURE__*/_interopDefaultCompat(fetch);
|
|
19
|
+
|
|
20
|
+
const migrationsDir = backendPluginApi.resolvePackagePath(
|
|
21
|
+
"@backstage-community/plugin-copilot-backend",
|
|
22
|
+
"migrations"
|
|
23
|
+
);
|
|
24
|
+
class DatabaseHandler {
|
|
25
|
+
constructor(db) {
|
|
26
|
+
this.db = db;
|
|
27
|
+
}
|
|
28
|
+
static async create(options) {
|
|
29
|
+
const { database } = options;
|
|
30
|
+
const client = await database.getClient();
|
|
31
|
+
if (!database.migrations?.skip) {
|
|
32
|
+
await client.migrate.latest({
|
|
33
|
+
directory: migrationsDir
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
return new DatabaseHandler(client);
|
|
37
|
+
}
|
|
38
|
+
async getByPeriod(startDate, endDate) {
|
|
39
|
+
const records = await this.db("metrics").whereBetween("day", [
|
|
40
|
+
startDate,
|
|
41
|
+
endDate
|
|
42
|
+
]);
|
|
43
|
+
return records ?? [];
|
|
44
|
+
}
|
|
45
|
+
async getPeriodRange() {
|
|
46
|
+
const minDate = await this.db("metrics").orderBy("day", "asc").first("day");
|
|
47
|
+
const maxDate = await this.db("metrics").orderBy("day", "desc").first("day");
|
|
48
|
+
if (!minDate?.day || !maxDate?.day) return void 0;
|
|
49
|
+
return { minDate: minDate.day, maxDate: maxDate.day };
|
|
50
|
+
}
|
|
51
|
+
async batchInsert(metrics) {
|
|
52
|
+
await this.db("metrics").insert(metrics).onConflict("day").ignore();
|
|
53
|
+
}
|
|
54
|
+
async getMostRecentDayFromMetrics() {
|
|
55
|
+
try {
|
|
56
|
+
const mostRecent = await this.db("metrics").orderBy("day", "desc").first("day");
|
|
57
|
+
return mostRecent ? mostRecent.day : void 0;
|
|
58
|
+
} catch (e) {
|
|
59
|
+
return void 0;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
class Scheduler {
|
|
65
|
+
constructor(options) {
|
|
66
|
+
this.options = options;
|
|
67
|
+
}
|
|
68
|
+
static create(options) {
|
|
69
|
+
return new Scheduler(options);
|
|
70
|
+
}
|
|
71
|
+
async run() {
|
|
72
|
+
try {
|
|
73
|
+
this.options.logger.info("Starting Github Copilot Processor");
|
|
74
|
+
const copilotMetrics = await this.options.api.getCopilotUsageDataForEnterprise();
|
|
75
|
+
this.options.logger.info(`Fetched ${copilotMetrics.length} metrics`);
|
|
76
|
+
const lastDay = await this.options.db.getMostRecentDayFromMetrics();
|
|
77
|
+
this.options.logger.info(`Found last day: ${lastDay}`);
|
|
78
|
+
const diff = copilotMetrics.sort(
|
|
79
|
+
(a, b) => luxon.DateTime.fromISO(a.day).toMillis() - luxon.DateTime.fromISO(b.day).toMillis()
|
|
80
|
+
).filter((metric) => {
|
|
81
|
+
const metricDate = luxon.DateTime.fromISO(metric.day);
|
|
82
|
+
const lastDayDate = lastDay ? luxon.DateTime.fromISO(lastDay) : null;
|
|
83
|
+
return !lastDayDate || metricDate > lastDayDate;
|
|
84
|
+
}).map(({ breakdown, ...rest }) => ({
|
|
85
|
+
...rest,
|
|
86
|
+
breakdown: JSON.stringify(breakdown)
|
|
87
|
+
}));
|
|
88
|
+
this.options.logger.info(`Found ${diff.length} new metrics to insert`);
|
|
89
|
+
if (diff.length > 0) {
|
|
90
|
+
await batchInsertInChunks(
|
|
91
|
+
diff,
|
|
92
|
+
30,
|
|
93
|
+
async (chunk) => {
|
|
94
|
+
await this.options.db.batchInsert(chunk);
|
|
95
|
+
}
|
|
96
|
+
);
|
|
97
|
+
this.options.logger.info("Inserted new metrics into the database");
|
|
98
|
+
} else {
|
|
99
|
+
this.options.logger.info("No new metrics found to insert");
|
|
100
|
+
}
|
|
101
|
+
} catch (error) {
|
|
102
|
+
this.options.logger.error(
|
|
103
|
+
`An error occurred while processing Github Copilot metrics: ${error}`
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
async function batchInsertInChunks(data, chunkSize, batchInsertFunc) {
|
|
109
|
+
for (let i = 0; i < data.length; i += chunkSize) {
|
|
110
|
+
const chunk = data.slice(i, i + chunkSize);
|
|
111
|
+
await batchInsertFunc(chunk);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const getGithubInfo = async (config) => {
|
|
116
|
+
const integrations = integration.ScmIntegrations.fromConfig(config);
|
|
117
|
+
const credentialsProvider = integration.DefaultGithubCredentialsProvider.fromIntegrations(integrations);
|
|
118
|
+
const host = config.getString("copilot.host");
|
|
119
|
+
const enterprise = config.getString("copilot.enterprise");
|
|
120
|
+
if (!host) {
|
|
121
|
+
throw new Error("The host configuration is missing from the config.");
|
|
122
|
+
}
|
|
123
|
+
if (!enterprise) {
|
|
124
|
+
throw new Error("The enterprise configuration is missing from the config.");
|
|
125
|
+
}
|
|
126
|
+
const githubConfig = integrations.github.byHost(host)?.config;
|
|
127
|
+
if (!githubConfig) {
|
|
128
|
+
throw new Error(
|
|
129
|
+
`GitHub configuration for host "${host}" is missing or incomplete.`
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
const apiBaseUrl = githubConfig.apiBaseUrl ?? "https://api.github.com";
|
|
133
|
+
const credentials = await credentialsProvider.getCredentials({
|
|
134
|
+
url: apiBaseUrl
|
|
135
|
+
});
|
|
136
|
+
if (!credentials.headers) {
|
|
137
|
+
throw new Error("Failed to retrieve credentials headers.");
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
apiBaseUrl,
|
|
141
|
+
credentials,
|
|
142
|
+
enterprise
|
|
143
|
+
};
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
class GithubClient {
|
|
147
|
+
constructor(props) {
|
|
148
|
+
this.props = props;
|
|
149
|
+
}
|
|
150
|
+
static async fromConfig(config) {
|
|
151
|
+
const info = await getGithubInfo(config);
|
|
152
|
+
return new GithubClient(info);
|
|
153
|
+
}
|
|
154
|
+
async getCopilotUsageDataForEnterprise() {
|
|
155
|
+
const path = `/enterprises/${this.props.enterprise}/copilot/usage`;
|
|
156
|
+
return this.get(path);
|
|
157
|
+
}
|
|
158
|
+
async get(path) {
|
|
159
|
+
const response = await fetch__default.default(`${this.props.apiBaseUrl}${path}`, {
|
|
160
|
+
headers: {
|
|
161
|
+
...this.props.credentials.headers,
|
|
162
|
+
Accept: "application/vnd.github+json",
|
|
163
|
+
"X-GitHub-Api-Version": "2022-11-28"
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
if (!response.ok) {
|
|
167
|
+
throw await errors.ResponseError.fromResponse(response);
|
|
168
|
+
}
|
|
169
|
+
return response.json();
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const defaultSchedule = {
|
|
174
|
+
frequency: { cron: "0 2 * * *" },
|
|
175
|
+
timeout: { minutes: 15 },
|
|
176
|
+
initialDelay: { minutes: 1 },
|
|
177
|
+
scope: "local"
|
|
178
|
+
};
|
|
179
|
+
async function createRouterFromConfig(routerOptions) {
|
|
180
|
+
const { config } = routerOptions;
|
|
181
|
+
const pluginOptions = {
|
|
182
|
+
schedule: defaultSchedule
|
|
183
|
+
};
|
|
184
|
+
if (config && config.has("copilot.schedule")) {
|
|
185
|
+
pluginOptions.schedule = backendPluginApi.readSchedulerServiceTaskScheduleDefinitionFromConfig(
|
|
186
|
+
config.getConfig("copilot.schedule")
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
return createRouter(routerOptions, pluginOptions);
|
|
190
|
+
}
|
|
191
|
+
async function createRouter(routerOptions, pluginOptions) {
|
|
192
|
+
const { logger, database, scheduler, config } = routerOptions;
|
|
193
|
+
const { schedule } = pluginOptions;
|
|
194
|
+
const db = await DatabaseHandler.create({ database });
|
|
195
|
+
const api = await GithubClient.fromConfig(config);
|
|
196
|
+
await scheduler.scheduleTask({
|
|
197
|
+
id: "copilot-metrics",
|
|
198
|
+
...schedule ?? defaultSchedule,
|
|
199
|
+
fn: async () => await Scheduler.create({ db, logger, api, config }).run()
|
|
200
|
+
});
|
|
201
|
+
const router = Router__default.default();
|
|
202
|
+
router.use(express__default.default.json());
|
|
203
|
+
router.get("/health", (_, response) => {
|
|
204
|
+
logger.info("PONG!");
|
|
205
|
+
response.json({ status: "ok" });
|
|
206
|
+
});
|
|
207
|
+
router.get("/metrics", async (request, response) => {
|
|
208
|
+
const { startDate, endDate } = request.query;
|
|
209
|
+
if (typeof startDate !== "string" || typeof endDate !== "string") {
|
|
210
|
+
return response.status(400).json("Invalid query parameters");
|
|
211
|
+
}
|
|
212
|
+
const parsedStartDate = luxon.DateTime.fromISO(startDate);
|
|
213
|
+
const parsedEndDate = luxon.DateTime.fromISO(endDate);
|
|
214
|
+
if (!parsedStartDate.isValid || !parsedEndDate.isValid) {
|
|
215
|
+
return response.status(400).json("Invalid date format");
|
|
216
|
+
}
|
|
217
|
+
const result = await db.getByPeriod(startDate, endDate);
|
|
218
|
+
const metrics = result.map((metric) => ({
|
|
219
|
+
...metric,
|
|
220
|
+
breakdown: JSON.parse(metric.breakdown)
|
|
221
|
+
}));
|
|
222
|
+
return response.json(metrics);
|
|
223
|
+
});
|
|
224
|
+
router.get("/metrics/period-range", async (_, response) => {
|
|
225
|
+
const result = await db.getPeriodRange();
|
|
226
|
+
if (!result) {
|
|
227
|
+
return response.status(400).json("No available data");
|
|
228
|
+
}
|
|
229
|
+
return response.json(result);
|
|
230
|
+
});
|
|
231
|
+
router.use(rootHttpRouter.MiddlewareFactory.create({ config, logger }).error);
|
|
232
|
+
return router;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const copilotPlugin = backendPluginApi.createBackendPlugin({
|
|
236
|
+
pluginId: "copilot",
|
|
237
|
+
register(env) {
|
|
238
|
+
env.registerInit({
|
|
239
|
+
deps: {
|
|
240
|
+
httpRouter: backendPluginApi.coreServices.httpRouter,
|
|
241
|
+
logger: backendPluginApi.coreServices.logger,
|
|
242
|
+
database: backendPluginApi.coreServices.database,
|
|
243
|
+
scheduler: backendPluginApi.coreServices.scheduler,
|
|
244
|
+
config: backendPluginApi.coreServices.rootConfig
|
|
245
|
+
},
|
|
246
|
+
async init({ httpRouter, logger, database, scheduler, config }) {
|
|
247
|
+
httpRouter.use(
|
|
248
|
+
await createRouterFromConfig({
|
|
249
|
+
logger,
|
|
250
|
+
database,
|
|
251
|
+
scheduler,
|
|
252
|
+
config
|
|
253
|
+
})
|
|
254
|
+
);
|
|
255
|
+
httpRouter.addAuthPolicy({
|
|
256
|
+
path: "/health",
|
|
257
|
+
allow: "unauthenticated"
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
exports.createRouterFromConfig = createRouterFromConfig;
|
|
265
|
+
exports.default = copilotPlugin;
|
|
266
|
+
//# sourceMappingURL=index.cjs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs.js","sources":["../src/db/DatabaseHandler.ts","../src/task/Scheduler.ts","../src/utils/GithubUtils.ts","../src/client/GithubClient.ts","../src/service/router.ts","../src/plugin.ts"],"sourcesContent":["/*\n * Copyright 2021 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n resolvePackagePath,\n DatabaseService,\n} from '@backstage/backend-plugin-api';\nimport {\n Metric,\n PeriodRange,\n} from '@backstage-community/plugin-copilot-common';\nimport { Knex } from 'knex';\n\nconst migrationsDir = resolvePackagePath(\n '@backstage-community/plugin-copilot-backend',\n 'migrations',\n);\n\ntype Options = {\n database: DatabaseService;\n};\n\nexport type MetricDbRow = Omit<Metric, 'breakdown'> & {\n breakdown: string;\n};\n\nexport class DatabaseHandler {\n static async create(options: Options): Promise<DatabaseHandler> {\n const { database } = options;\n const client = await database.getClient();\n\n if (!database.migrations?.skip) {\n await client.migrate.latest({\n directory: migrationsDir,\n });\n }\n\n return new DatabaseHandler(client);\n }\n\n private constructor(private readonly db: Knex) {}\n\n async getByPeriod(\n startDate: string,\n endDate: string,\n ): Promise<MetricDbRow[]> {\n const records = await this.db<MetricDbRow>('metrics').whereBetween('day', [\n startDate,\n endDate,\n ]);\n return records ?? [];\n }\n\n async getPeriodRange(): Promise<PeriodRange | undefined> {\n const minDate = await this.db<MetricDbRow>('metrics')\n .orderBy('day', 'asc')\n .first('day');\n const maxDate = await this.db<MetricDbRow>('metrics')\n .orderBy('day', 'desc')\n .first('day');\n\n if (!minDate?.day || !maxDate?.day) return undefined;\n\n return { minDate: minDate.day, maxDate: maxDate.day };\n }\n\n async batchInsert(metrics: MetricDbRow[]): Promise<void> {\n await this.db<MetricDbRow[]>('metrics')\n .insert(metrics)\n .onConflict('day')\n .ignore();\n }\n\n async getMostRecentDayFromMetrics(): Promise<string | undefined> {\n try {\n const mostRecent = await this.db<MetricDbRow>('metrics')\n .orderBy('day', 'desc')\n .first('day');\n\n return mostRecent ? mostRecent.day : undefined;\n } catch (e) {\n return undefined;\n }\n }\n}\n","/*\n * Copyright 2024 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { LoggerService } from '@backstage/backend-plugin-api';\nimport { GithubClient } from '../client/GithubClient';\nimport { DatabaseHandler, MetricDbRow } from '../db/DatabaseHandler';\nimport { Config } from '@backstage/config';\nimport { DateTime } from 'luxon';\n\ntype Options = {\n api: GithubClient;\n config: Config;\n logger: LoggerService;\n db: DatabaseHandler;\n};\n\nexport default class Scheduler {\n constructor(private readonly options: Options) {}\n\n static create(options: Options) {\n return new Scheduler(options);\n }\n\n async run() {\n try {\n this.options.logger.info('Starting Github Copilot Processor');\n\n const copilotMetrics =\n await this.options.api.getCopilotUsageDataForEnterprise();\n this.options.logger.info(`Fetched ${copilotMetrics.length} metrics`);\n\n const lastDay = await this.options.db.getMostRecentDayFromMetrics();\n this.options.logger.info(`Found last day: ${lastDay}`);\n\n const diff = copilotMetrics\n .sort(\n (a, b) =>\n DateTime.fromISO(a.day).toMillis() -\n DateTime.fromISO(b.day).toMillis(),\n )\n .filter(metric => {\n const metricDate = DateTime.fromISO(metric.day);\n const lastDayDate = lastDay ? DateTime.fromISO(lastDay) : null;\n return !lastDayDate || metricDate > lastDayDate;\n })\n .map(({ breakdown, ...rest }) => ({\n ...rest,\n breakdown: JSON.stringify(breakdown),\n }));\n\n this.options.logger.info(`Found ${diff.length} new metrics to insert`);\n\n if (diff.length > 0) {\n await batchInsertInChunks<MetricDbRow>(\n diff,\n 30,\n async (chunk: MetricDbRow[]) => {\n await this.options.db.batchInsert(chunk);\n },\n );\n this.options.logger.info('Inserted new metrics into the database');\n } else {\n this.options.logger.info('No new metrics found to insert');\n }\n } catch (error) {\n this.options.logger.error(\n `An error occurred while processing Github Copilot metrics: ${error}`,\n );\n }\n }\n}\n\nasync function batchInsertInChunks<T>(\n data: T[],\n chunkSize: number,\n batchInsertFunc: (chunk: T[]) => Promise<void>,\n): Promise<void> {\n for (let i = 0; i < data.length; i += chunkSize) {\n const chunk = data.slice(i, i + chunkSize);\n await batchInsertFunc(chunk);\n }\n}\n","/*\n * Copyright 2024 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { Config } from '@backstage/config';\nimport {\n DefaultGithubCredentialsProvider,\n GithubCredentials,\n ScmIntegrations,\n} from '@backstage/integration';\n\nexport type GithubInfo = {\n credentials: GithubCredentials;\n apiBaseUrl: string;\n enterprise: string;\n};\n\nexport const getGithubInfo = async (config: Config): Promise<GithubInfo> => {\n const integrations = ScmIntegrations.fromConfig(config);\n const credentialsProvider =\n DefaultGithubCredentialsProvider.fromIntegrations(integrations);\n\n const host = config.getString('copilot.host');\n const enterprise = config.getString('copilot.enterprise');\n\n if (!host) {\n throw new Error('The host configuration is missing from the config.');\n }\n\n if (!enterprise) {\n throw new Error('The enterprise configuration is missing from the config.');\n }\n\n const githubConfig = integrations.github.byHost(host)?.config;\n\n if (!githubConfig) {\n throw new Error(\n `GitHub configuration for host \"${host}\" is missing or incomplete.`,\n );\n }\n\n const apiBaseUrl = githubConfig.apiBaseUrl ?? 'https://api.github.com';\n\n const credentials = await credentialsProvider.getCredentials({\n url: apiBaseUrl,\n });\n\n if (!credentials.headers) {\n throw new Error('Failed to retrieve credentials headers.');\n }\n\n return {\n apiBaseUrl,\n credentials,\n enterprise,\n };\n};\n","/*\n * Copyright 2024 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { ResponseError } from '@backstage/errors';\nimport { Config } from '@backstage/config';\nimport { Metric } from '@backstage-community/plugin-copilot-common';\nimport fetch from 'node-fetch';\nimport { getGithubInfo, GithubInfo } from '../utils/GithubUtils';\n\ninterface GithubApi {\n getCopilotUsageDataForEnterprise: () => Promise<Metric[]>;\n}\n\nexport class GithubClient implements GithubApi {\n constructor(private readonly props: GithubInfo) {}\n\n static async fromConfig(config: Config) {\n const info = await getGithubInfo(config);\n return new GithubClient(info);\n }\n\n async getCopilotUsageDataForEnterprise(): Promise<Metric[]> {\n const path = `/enterprises/${this.props.enterprise}/copilot/usage`;\n return this.get(path);\n }\n\n private async get<T>(path: string): Promise<T> {\n const response = await fetch(`${this.props.apiBaseUrl}${path}`, {\n headers: {\n ...this.props.credentials.headers,\n Accept: 'application/vnd.github+json',\n 'X-GitHub-Api-Version': '2022-11-28',\n },\n });\n\n if (!response.ok) {\n throw await ResponseError.fromResponse(response);\n }\n\n return response.json() as Promise<T>;\n }\n}\n","/*\n * Copyright 2024 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport express from 'express';\nimport Router from 'express-promise-router';\nimport { MiddlewareFactory } from '@backstage/backend-defaults/rootHttpRouter';\nimport {\n DatabaseService,\n LoggerService,\n readSchedulerServiceTaskScheduleDefinitionFromConfig,\n SchedulerService,\n SchedulerServiceTaskScheduleDefinition,\n} from '@backstage/backend-plugin-api';\nimport { Config } from '@backstage/config';\nimport { Metric } from '@backstage-community/plugin-copilot-common';\nimport { DatabaseHandler } from '../db/DatabaseHandler';\nimport Scheduler from '../task/Scheduler';\nimport { GithubClient } from '../client/GithubClient';\nimport { DateTime } from 'luxon';\n\n/**\n * Options for configuring the Copilot plugin.\n *\n * @public\n */\nexport interface PluginOptions {\n /**\n * Schedule configuration for the plugin.\n */\n schedule?: SchedulerServiceTaskScheduleDefinition;\n}\n\n/**\n * Options for configuring the router used by the Copilot plugin.\n *\n * @public\n */\nexport interface RouterOptions {\n /**\n * Logger service for the router.\n */\n logger: LoggerService;\n\n /**\n * Database service for the router.\n */\n database: DatabaseService;\n\n /**\n * Scheduler service for the router.\n */\n scheduler: SchedulerService;\n\n /**\n * Configuration for the router.\n */\n config: Config;\n}\n\nconst defaultSchedule: SchedulerServiceTaskScheduleDefinition = {\n frequency: { cron: '0 2 * * *' },\n timeout: { minutes: 15 },\n initialDelay: { minutes: 1 },\n scope: 'local',\n};\n\n/**\n * Creates an Express router configured based on the provided router options and plugin options.\n *\n * This function initializes the router with the appropriate middleware and routes based on the\n * configuration and options provided. It also schedules tasks if scheduling options are provided.\n *\n * @param routerOptions - Options for configuring the router, including services and configuration.\n * @returns A promise that resolves to an Express router instance.\n *\n * @public\n */\nexport async function createRouterFromConfig(routerOptions: RouterOptions) {\n const { config } = routerOptions;\n const pluginOptions: PluginOptions = {\n schedule: defaultSchedule,\n };\n if (config && config.has('copilot.schedule')) {\n pluginOptions.schedule =\n readSchedulerServiceTaskScheduleDefinitionFromConfig(\n config.getConfig('copilot.schedule'),\n );\n }\n return createRouter(routerOptions, pluginOptions);\n}\n\n/** @private */\nasync function createRouter(\n routerOptions: RouterOptions,\n pluginOptions: PluginOptions,\n): Promise<express.Router> {\n const { logger, database, scheduler, config } = routerOptions;\n const { schedule } = pluginOptions;\n\n const db = await DatabaseHandler.create({ database });\n const api = await GithubClient.fromConfig(config);\n\n await scheduler.scheduleTask({\n id: 'copilot-metrics',\n ...(schedule ?? defaultSchedule),\n fn: async () => await Scheduler.create({ db, logger, api, config }).run(),\n });\n\n const router = Router();\n router.use(express.json());\n\n router.get('/health', (_, response) => {\n logger.info('PONG!');\n response.json({ status: 'ok' });\n });\n\n router.get('/metrics', async (request, response) => {\n const { startDate, endDate } = request.query;\n\n if (typeof startDate !== 'string' || typeof endDate !== 'string') {\n return response.status(400).json('Invalid query parameters');\n }\n\n const parsedStartDate = DateTime.fromISO(startDate);\n const parsedEndDate = DateTime.fromISO(endDate);\n\n if (!parsedStartDate.isValid || !parsedEndDate.isValid) {\n return response.status(400).json('Invalid date format');\n }\n\n const result = await db.getByPeriod(startDate, endDate);\n\n const metrics: Metric[] = result.map(metric => ({\n ...metric,\n breakdown: JSON.parse(metric.breakdown),\n }));\n\n return response.json(metrics);\n });\n\n router.get('/metrics/period-range', async (_, response) => {\n const result = await db.getPeriodRange();\n\n if (!result) {\n return response.status(400).json('No available data');\n }\n\n return response.json(result);\n });\n\n router.use(MiddlewareFactory.create({ config, logger }).error);\n return router;\n}\n","/*\n * Copyright 2024 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport {\n coreServices,\n createBackendPlugin,\n} from '@backstage/backend-plugin-api';\nimport { createRouterFromConfig } from './service/router';\n\n/**\n * Backend plugin for Copilot.\n *\n * @public\n */\nexport const copilotPlugin = createBackendPlugin({\n pluginId: 'copilot',\n register(env) {\n env.registerInit({\n deps: {\n httpRouter: coreServices.httpRouter,\n logger: coreServices.logger,\n database: coreServices.database,\n scheduler: coreServices.scheduler,\n config: coreServices.rootConfig,\n },\n async init({ httpRouter, logger, database, scheduler, config }) {\n httpRouter.use(\n await createRouterFromConfig({\n logger,\n database,\n scheduler,\n config,\n }),\n );\n httpRouter.addAuthPolicy({\n path: '/health',\n allow: 'unauthenticated',\n });\n },\n });\n },\n});\n"],"names":["resolvePackagePath","DateTime","ScmIntegrations","DefaultGithubCredentialsProvider","fetch","ResponseError","readSchedulerServiceTaskScheduleDefinitionFromConfig","Router","express","MiddlewareFactory","createBackendPlugin","coreServices"],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import * as _backstage_backend_plugin_api from '@backstage/backend-plugin-api';
|
|
3
|
+
import { SchedulerServiceTaskScheduleDefinition, LoggerService, DatabaseService, SchedulerService } from '@backstage/backend-plugin-api';
|
|
4
|
+
import { Config } from '@backstage/config';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Options for configuring the Copilot plugin.
|
|
8
|
+
*
|
|
9
|
+
* @public
|
|
10
|
+
*/
|
|
11
|
+
interface PluginOptions {
|
|
12
|
+
/**
|
|
13
|
+
* Schedule configuration for the plugin.
|
|
14
|
+
*/
|
|
15
|
+
schedule?: SchedulerServiceTaskScheduleDefinition;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Options for configuring the router used by the Copilot plugin.
|
|
19
|
+
*
|
|
20
|
+
* @public
|
|
21
|
+
*/
|
|
22
|
+
interface RouterOptions {
|
|
23
|
+
/**
|
|
24
|
+
* Logger service for the router.
|
|
25
|
+
*/
|
|
26
|
+
logger: LoggerService;
|
|
27
|
+
/**
|
|
28
|
+
* Database service for the router.
|
|
29
|
+
*/
|
|
30
|
+
database: DatabaseService;
|
|
31
|
+
/**
|
|
32
|
+
* Scheduler service for the router.
|
|
33
|
+
*/
|
|
34
|
+
scheduler: SchedulerService;
|
|
35
|
+
/**
|
|
36
|
+
* Configuration for the router.
|
|
37
|
+
*/
|
|
38
|
+
config: Config;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Creates an Express router configured based on the provided router options and plugin options.
|
|
42
|
+
*
|
|
43
|
+
* This function initializes the router with the appropriate middleware and routes based on the
|
|
44
|
+
* configuration and options provided. It also schedules tasks if scheduling options are provided.
|
|
45
|
+
*
|
|
46
|
+
* @param routerOptions - Options for configuring the router, including services and configuration.
|
|
47
|
+
* @returns A promise that resolves to an Express router instance.
|
|
48
|
+
*
|
|
49
|
+
* @public
|
|
50
|
+
*/
|
|
51
|
+
declare function createRouterFromConfig(routerOptions: RouterOptions): Promise<express.Router>;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Backend plugin for Copilot.
|
|
55
|
+
*
|
|
56
|
+
* @public
|
|
57
|
+
*/
|
|
58
|
+
declare const copilotPlugin: _backstage_backend_plugin_api.BackendFeatureCompat;
|
|
59
|
+
|
|
60
|
+
export { type PluginOptions, type RouterOptions, createRouterFromConfig, copilotPlugin as default };
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2021 The Backstage Authors
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// @ts-check
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param {import('knex').Knex} knex
|
|
21
|
+
*/
|
|
22
|
+
exports.up = async function up(knex) {
|
|
23
|
+
await knex.schema.createTable('metrics', table => {
|
|
24
|
+
table.comment('Table for storing metrics');
|
|
25
|
+
table
|
|
26
|
+
.date('day')
|
|
27
|
+
.notNullable()
|
|
28
|
+
.primary()
|
|
29
|
+
.comment('Date of the metrics data');
|
|
30
|
+
table
|
|
31
|
+
.integer('total_suggestions_count')
|
|
32
|
+
.notNullable()
|
|
33
|
+
.comment('Total suggestions count for the day');
|
|
34
|
+
table
|
|
35
|
+
.integer('total_acceptances_count')
|
|
36
|
+
.notNullable()
|
|
37
|
+
.comment('Total acceptances count for the day');
|
|
38
|
+
table
|
|
39
|
+
.integer('total_lines_suggested')
|
|
40
|
+
.notNullable()
|
|
41
|
+
.comment('Total lines suggested for the day');
|
|
42
|
+
table
|
|
43
|
+
.integer('total_lines_accepted')
|
|
44
|
+
.notNullable()
|
|
45
|
+
.comment('Total lines accepted for the day');
|
|
46
|
+
table
|
|
47
|
+
.integer('total_active_users')
|
|
48
|
+
.notNullable()
|
|
49
|
+
.comment('Total active users for the day');
|
|
50
|
+
table
|
|
51
|
+
.integer('total_chat_acceptances')
|
|
52
|
+
.notNullable()
|
|
53
|
+
.comment('Total chat acceptances for the day');
|
|
54
|
+
table
|
|
55
|
+
.integer('total_chat_turns')
|
|
56
|
+
.notNullable()
|
|
57
|
+
.comment('Total chat turns for the day');
|
|
58
|
+
table
|
|
59
|
+
.integer('total_active_chat_users')
|
|
60
|
+
.notNullable()
|
|
61
|
+
.comment('Total active chat users for the day');
|
|
62
|
+
table.text('breakdown').notNullable().comment('Breakdown information');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
await knex.schema.alterTable('metrics', table => {
|
|
66
|
+
table.index('day', 'idx_metrics_day');
|
|
67
|
+
});
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* @param {import('knex').Knex} knex
|
|
72
|
+
*/
|
|
73
|
+
exports.down = async function down(knex) {
|
|
74
|
+
await knex.schema.dropTable('metrics');
|
|
75
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@backstage-community/plugin-copilot-backend",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"homepage": "https://backstage.io",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"main": "dist/index.cjs.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"config.d.ts",
|
|
11
|
+
"migrations/**/*.{js,d.ts}"
|
|
12
|
+
],
|
|
13
|
+
"backstage": {
|
|
14
|
+
"role": "backend-plugin",
|
|
15
|
+
"pluginId": "copilot",
|
|
16
|
+
"pluginPackages": [
|
|
17
|
+
"@backstage-community/plugin-copilot",
|
|
18
|
+
"@backstage-community/plugin-copilot-backend",
|
|
19
|
+
"@backstage-community/plugin-copilot-common"
|
|
20
|
+
]
|
|
21
|
+
},
|
|
22
|
+
"publishConfig": {
|
|
23
|
+
"access": "public",
|
|
24
|
+
"main": "dist/index.cjs.js",
|
|
25
|
+
"types": "dist/index.d.ts"
|
|
26
|
+
},
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "https://github.com/backstage/community-plugins",
|
|
30
|
+
"directory": "workspaces/copilot/plugins/copilot-backend"
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"start": "backstage-cli package start",
|
|
34
|
+
"build": "backstage-cli package build",
|
|
35
|
+
"lint": "backstage-cli package lint",
|
|
36
|
+
"test": "backstage-cli package test",
|
|
37
|
+
"clean": "backstage-cli package clean",
|
|
38
|
+
"prepack": "backstage-cli package prepack",
|
|
39
|
+
"postpack": "backstage-cli package postpack"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@backstage-community/plugin-copilot-common": "^0.1.0",
|
|
43
|
+
"@backstage/backend-app-api": "^0.8.0",
|
|
44
|
+
"@backstage/backend-common": "^0.23.3",
|
|
45
|
+
"@backstage/backend-defaults": "^0.4.1",
|
|
46
|
+
"@backstage/backend-plugin-api": "^0.7.0",
|
|
47
|
+
"@backstage/backend-tasks": "^0.5.27",
|
|
48
|
+
"@backstage/config": "^1.2.0",
|
|
49
|
+
"@backstage/errors": "^1.2.4",
|
|
50
|
+
"@backstage/integration": "^1.13.0",
|
|
51
|
+
"@types/express": "*",
|
|
52
|
+
"express": "^4.17.1",
|
|
53
|
+
"express-promise-router": "^4.1.0",
|
|
54
|
+
"knex": "^3.0.0",
|
|
55
|
+
"luxon": "^3.5.0",
|
|
56
|
+
"node-fetch": "^2.6.7",
|
|
57
|
+
"winston": "^3.2.1",
|
|
58
|
+
"yn": "^4.0.0"
|
|
59
|
+
},
|
|
60
|
+
"devDependencies": {
|
|
61
|
+
"@backstage/backend-test-utils": "^0.4.4",
|
|
62
|
+
"@backstage/cli": "^0.26.11",
|
|
63
|
+
"@backstage/plugin-auth-backend": "^0.22.9",
|
|
64
|
+
"@backstage/plugin-auth-backend-module-guest-provider": "^0.1.8",
|
|
65
|
+
"@backstage/test-utils": "^1.5.9",
|
|
66
|
+
"@types/node-fetch": "^2.6.11",
|
|
67
|
+
"@types/supertest": "^2.0.8",
|
|
68
|
+
"msw": "^1.0.0",
|
|
69
|
+
"supertest": "^6.2.4"
|
|
70
|
+
},
|
|
71
|
+
"configSchema": "config.d.ts"
|
|
72
|
+
}
|