@backstage-community/plugin-cicd-statistics-module-github 0.2.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 ADDED
@@ -0,0 +1,7 @@
1
+ # @backstage-community/plugin-cicd-statistics-module-github
2
+
3
+ ## 0.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 84e88ce: New cicd-satistics for querying github actions metrics
package/README.md ADDED
@@ -0,0 +1,42 @@
1
+ # cicd-statistics-module-github
2
+
3
+ This is an extension module to the `cicd-statistics` plugin, providing a `CicdStatisticsApiGithub` that you can use to extract the CI/CD statistics from your Github repository.
4
+
5
+ ## Getting started
6
+
7
+ 1. Install the `cicd-statistics` and `cicd-statistics-module-github` plugins in the `app` package.
8
+
9
+ 2. Configure your ApiFactory:
10
+ - You can optionally pass in a third argument to `CicdStatisticsApiGithub` of type [CicdDefaults](https://github.com/backstage/backstage/blob/2881c53cb383bf127c150f837f37fe535d8cf97b/plugins/cicd-statistics/src/apis/types.ts#L179) to alter the default CICD UI configuration
11
+
12
+ ```tsx
13
+ // packages/app/src/apis.ts
14
+ import { githubAuthApiRef } from '@backstage/core-plugin-api';
15
+
16
+ import { cicdStatisticsApiRef } from '@backstage-community/plugin-cicd-statistics';
17
+ import { CicdStatisticsApiGithub } from '@backstage-community/plugin-cicd-statistics-module-github';
18
+
19
+ export const apis: AnyApiFactory[] = [
20
+ createApiFactory({
21
+ api: cicdStatisticsApiRef,
22
+ deps: {
23
+ githubAuthApi: githubAuthApiRef,
24
+ configApi: configApiRef,
25
+ },
26
+ factory: ({ githubAuthApi, configApi }) => {
27
+ return new CicdStatisticsApiGithub(githubAuthApi, configApi);
28
+ },
29
+ }),
30
+ ];
31
+ ```
32
+
33
+ 3. Add the component to your EntityPage:
34
+
35
+ ```tsx
36
+ // packages/app/src/components/catalog/EntityPage.tsx
37
+ import { EntityCicdStatisticsContent } from '@backstage-community/plugin-cicd-statistics';
38
+
39
+ <EntityLayout.Route path="/ci-cd-statistics" title="CI/CD Statistics">
40
+ <EntityCicdStatisticsContent />
41
+ </EntityLayout.Route>;
42
+ ```
@@ -0,0 +1,23 @@
1
+ import * as _backstage_frontend_plugin_api from '@backstage/frontend-plugin-api';
2
+ import * as _backstage_core_plugin_api__ from '@backstage/core-plugin-api/*';
3
+
4
+ /**
5
+ * @alpha
6
+ */
7
+ declare const cicdStatisticsGithubExtension: _backstage_frontend_plugin_api.ExtensionDefinition<{
8
+ kind: "api";
9
+ name: "cicd-statistics-github-api";
10
+ config: {};
11
+ configInput: {};
12
+ output: _backstage_frontend_plugin_api.ConfigurableExtensionDataRef<_backstage_core_plugin_api__.AnyApiFactory, "core.api.factory", {}>;
13
+ inputs: {};
14
+ params: {
15
+ factory: _backstage_core_plugin_api__.AnyApiFactory;
16
+ };
17
+ }>;
18
+ /**
19
+ * @alpha
20
+ */
21
+ declare const cicdStatisticsExtensionOverrides: _backstage_frontend_plugin_api.FrontendModule;
22
+
23
+ export { cicdStatisticsGithubExtension, cicdStatisticsExtensionOverrides as default };
@@ -0,0 +1,26 @@
1
+ import { cicdStatisticsApiRef } from '@backstage-community/plugin-cicd-statistics';
2
+ import { ApiBlueprint, createApiFactory, githubAuthApiRef, configApiRef, createFrontendModule } from '@backstage/frontend-plugin-api';
3
+ import { CicdStatisticsApiGithub } from './api/github.esm.js';
4
+
5
+ const cicdStatisticsGithubExtension = ApiBlueprint.make({
6
+ name: "cicd-statistics-github-api",
7
+ params: {
8
+ factory: createApiFactory({
9
+ api: cicdStatisticsApiRef,
10
+ deps: {
11
+ githubAuthApi: githubAuthApiRef,
12
+ configApi: configApiRef
13
+ },
14
+ factory: ({ githubAuthApi, configApi }) => {
15
+ return new CicdStatisticsApiGithub(githubAuthApi, configApi);
16
+ }
17
+ })
18
+ }
19
+ });
20
+ const cicdStatisticsExtensionOverrides = createFrontendModule({
21
+ pluginId: "cicd-statistics",
22
+ extensions: [cicdStatisticsGithubExtension]
23
+ });
24
+
25
+ export { cicdStatisticsGithubExtension, cicdStatisticsExtensionOverrides as default };
26
+ //# sourceMappingURL=alpha.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"alpha.esm.js","sources":["../src/alpha.ts"],"sourcesContent":["/*\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 { cicdStatisticsApiRef } from '@backstage-community/plugin-cicd-statistics';\nimport {\n ApiBlueprint,\n configApiRef,\n createApiFactory,\n createFrontendModule,\n githubAuthApiRef,\n} from '@backstage/frontend-plugin-api';\nimport { CicdStatisticsApiGithub } from './api';\n\n/**\n * @alpha\n */\nexport const cicdStatisticsGithubExtension = ApiBlueprint.make({\n name: 'cicd-statistics-github-api',\n params: {\n factory: createApiFactory({\n api: cicdStatisticsApiRef,\n deps: {\n githubAuthApi: githubAuthApiRef,\n configApi: configApiRef,\n },\n factory: ({ githubAuthApi, configApi }) => {\n return new CicdStatisticsApiGithub(githubAuthApi, configApi);\n },\n }),\n },\n});\n\n/**\n * @alpha\n */\nconst cicdStatisticsExtensionOverrides = createFrontendModule({\n pluginId: 'cicd-statistics',\n extensions: [cicdStatisticsGithubExtension],\n});\n\nexport default cicdStatisticsExtensionOverrides;\n"],"names":[],"mappings":";;;;AA4Ba,MAAA,6BAAA,GAAgC,aAAa,IAAK,CAAA;AAAA,EAC7D,IAAM,EAAA,4BAAA;AAAA,EACN,MAAQ,EAAA;AAAA,IACN,SAAS,gBAAiB,CAAA;AAAA,MACxB,GAAK,EAAA,oBAAA;AAAA,MACL,IAAM,EAAA;AAAA,QACJ,aAAe,EAAA,gBAAA;AAAA,QACf,SAAW,EAAA;AAAA,OACb;AAAA,MACA,OAAS,EAAA,CAAC,EAAE,aAAA,EAAe,WAAgB,KAAA;AACzC,QAAO,OAAA,IAAI,uBAAwB,CAAA,aAAA,EAAe,SAAS,CAAA;AAAA;AAC7D,KACD;AAAA;AAEL,CAAC;AAKD,MAAM,mCAAmC,oBAAqB,CAAA;AAAA,EAC5D,QAAU,EAAA,iBAAA;AAAA,EACV,UAAA,EAAY,CAAC,6BAA6B;AAC5C,CAAC;;;;"}
@@ -0,0 +1,119 @@
1
+ import limiterFactory from 'p-limit';
2
+ import { getEntitySourceLocation } from '@backstage/catalog-model';
3
+ import { jobToStages, workflowToBuild } from './utils.esm.js';
4
+ import { readGithubIntegrationConfigs } from '@backstage/integration';
5
+ import { Octokit } from '@octokit/rest';
6
+
7
+ const GITHUB_ACTIONS_ANNOTATION = "github.com/project-slug";
8
+ const getProjectNameFromEntity = (entity) => entity?.metadata.annotations?.[GITHUB_ACTIONS_ANNOTATION] ?? "";
9
+ class CicdStatisticsApiGithub {
10
+ #githubAuthApi;
11
+ #cicdDefaults;
12
+ configApi;
13
+ constructor(githubAuthApi, configApi, cicdDefaults = {}) {
14
+ this.#githubAuthApi = githubAuthApi;
15
+ this.#cicdDefaults = cicdDefaults;
16
+ this.configApi = configApi;
17
+ }
18
+ async createGithubApi(entity, scopes) {
19
+ const entityInfo = getEntitySourceLocation(entity);
20
+ const [owner, repo] = getProjectNameFromEntity(entity).split("/");
21
+ const url = new URL(entityInfo.target);
22
+ const oauthToken = await this.#githubAuthApi.getAccessToken(scopes);
23
+ const configs = readGithubIntegrationConfigs(
24
+ this.configApi.getOptionalConfigArray("integrations.github") ?? []
25
+ );
26
+ const githubIntegrationConfig = configs.find((v) => v.host === url.hostname);
27
+ const baseUrl = githubIntegrationConfig?.apiBaseUrl;
28
+ return {
29
+ api: new Octokit({ auth: oauthToken, baseUrl }),
30
+ owner,
31
+ repo
32
+ };
33
+ }
34
+ static async updateBuildWithStages(octokit, owner, repo, build) {
35
+ const jobs = await octokit.actions.listJobsForWorkflowRun({
36
+ repo,
37
+ owner,
38
+ run_id: parseInt(build.id, 10)
39
+ });
40
+ const stages = jobs.data.jobs.map(jobToStages);
41
+ return stages;
42
+ }
43
+ static async getDurationOfBuild(octokit, owner, repo, build) {
44
+ const workflow = await octokit.actions.getWorkflowRunUsage({
45
+ owner,
46
+ repo,
47
+ run_id: parseInt(build.id, 10)
48
+ });
49
+ return workflow.data?.run_duration_ms ?? 0;
50
+ }
51
+ static async getDefaultBranch(octokit, owner, repo) {
52
+ const repository = await octokit.repos.get({
53
+ owner,
54
+ repo
55
+ });
56
+ return repository.data.default_branch;
57
+ }
58
+ async fetchBuilds(options) {
59
+ const {
60
+ entity,
61
+ updateProgress,
62
+ timeFrom,
63
+ timeTo,
64
+ filterStatus = ["all"],
65
+ filterType = "all"
66
+ } = options;
67
+ const { api, owner, repo } = await this.createGithubApi(entity, [
68
+ "read_api"
69
+ ]);
70
+ updateProgress(0, 0, 0);
71
+ const branch = filterType === "master" ? await CicdStatisticsApiGithub.getDefaultBranch(api, owner, repo) : void 0;
72
+ const workflowsRuns = await api.paginate(
73
+ api.actions.listWorkflowRunsForRepo,
74
+ {
75
+ owner,
76
+ repo,
77
+ per_page: 1e3,
78
+ // max items per page
79
+ ...branch ? { branch } : {},
80
+ created: `${timeFrom.toISOString()}..${timeTo.toISOString()}`
81
+ // see https://docs.github.com/en/search-github/getting-started-with-searching-on-github/understanding-the-search-syntax#query-for-dates
82
+ },
83
+ (response) => response.data.map(workflowToBuild)
84
+ );
85
+ const limiter = limiterFactory(10);
86
+ const builds = workflowsRuns.map(async (build) => ({
87
+ ...build,
88
+ duration: await limiter(
89
+ () => CicdStatisticsApiGithub.getDurationOfBuild(api, owner, repo, build)
90
+ ),
91
+ stages: await limiter(
92
+ () => CicdStatisticsApiGithub.updateBuildWithStages(api, owner, repo, build)
93
+ )
94
+ }));
95
+ const promisedBuilds = (await Promise.all(builds)).filter(
96
+ (b) => filterStatus.includes(b.status)
97
+ );
98
+ return { builds: promisedBuilds };
99
+ }
100
+ async getConfiguration() {
101
+ return {
102
+ availableStatuses: [
103
+ "succeeded",
104
+ "running",
105
+ "aborted",
106
+ "failed",
107
+ "unknown",
108
+ "stalled",
109
+ "expired",
110
+ "enqueued",
111
+ "scheduled"
112
+ ],
113
+ defaults: this.#cicdDefaults
114
+ };
115
+ }
116
+ }
117
+
118
+ export { CicdStatisticsApiGithub, GITHUB_ACTIONS_ANNOTATION, getProjectNameFromEntity };
119
+ //# sourceMappingURL=github.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"github.esm.js","sources":["../../src/api/github.ts"],"sourcesContent":["/*\n * Copyright 2022 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 CicdStatisticsApi,\n CicdState,\n CicdConfiguration,\n CicdDefaults,\n Build,\n FetchBuildsOptions,\n Stage,\n} from '@backstage-community/plugin-cicd-statistics';\nimport { ConfigApi, OAuthApi } from '@backstage/core-plugin-api';\nimport limiterFactory from 'p-limit';\nimport { Entity, getEntitySourceLocation } from '@backstage/catalog-model';\nimport { jobToStages, workflowToBuild } from './utils';\n\nimport { readGithubIntegrationConfigs } from '@backstage/integration';\nimport { Octokit } from '@octokit/rest';\n\n/** @public */\nexport const GITHUB_ACTIONS_ANNOTATION = 'github.com/project-slug';\n\nexport const getProjectNameFromEntity = (entity: Entity) =>\n entity?.metadata.annotations?.[GITHUB_ACTIONS_ANNOTATION] ?? '';\n\n/**\n * This type represents a initialized github client with octokit\n *\n * @public\n */\nexport type GithubClient = {\n /* the actual API of octokit */\n api: InstanceType<typeof Octokit>;\n /* the owner the repository, retrieved from the entity source location */\n owner: string;\n /* the repository name, retrieved from the entity source location */\n repo: string;\n};\n\n/**\n * Extracts the CI/CD statistics from a Github repository\n *\n * @public\n */\nexport class CicdStatisticsApiGithub implements CicdStatisticsApi {\n readonly #githubAuthApi: OAuthApi;\n readonly #cicdDefaults: Partial<CicdDefaults>;\n readonly configApi: ConfigApi;\n\n constructor(\n githubAuthApi: OAuthApi,\n configApi: ConfigApi,\n cicdDefaults: Partial<CicdDefaults> = {},\n ) {\n this.#githubAuthApi = githubAuthApi;\n this.#cicdDefaults = cicdDefaults;\n this.configApi = configApi;\n }\n\n public async createGithubApi(\n entity: Entity,\n scopes: string[],\n ): Promise<GithubClient> {\n const entityInfo = getEntitySourceLocation(entity);\n const [owner, repo] = getProjectNameFromEntity(entity).split('/');\n const url = new URL(entityInfo.target);\n const oauthToken = await this.#githubAuthApi.getAccessToken(scopes);\n\n const configs = readGithubIntegrationConfigs(\n this.configApi.getOptionalConfigArray('integrations.github') ?? [],\n );\n const githubIntegrationConfig = configs.find(v => v.host === url.hostname);\n const baseUrl = githubIntegrationConfig?.apiBaseUrl;\n return {\n api: new Octokit({ auth: oauthToken, baseUrl }),\n owner,\n repo,\n };\n }\n\n private static async updateBuildWithStages(\n octokit: InstanceType<typeof Octokit>,\n owner: string,\n repo: string,\n build: Build,\n ): Promise<Stage[]> {\n const jobs = await octokit.actions.listJobsForWorkflowRun({\n repo,\n owner,\n run_id: parseInt(build.id, 10),\n });\n const stages = jobs.data.jobs.map(jobToStages);\n return stages;\n }\n\n private static async getDurationOfBuild(\n octokit: InstanceType<typeof Octokit>,\n owner: string,\n repo: string,\n build: Build,\n ): Promise<number> {\n const workflow = await octokit.actions.getWorkflowRunUsage({\n owner,\n repo,\n run_id: parseInt(build.id, 10),\n });\n return workflow.data?.run_duration_ms ?? 0;\n }\n\n private static async getDefaultBranch(\n octokit: InstanceType<typeof Octokit>,\n owner: string,\n repo: string,\n ): Promise<string | undefined> {\n const repository = await octokit.repos.get({\n owner,\n repo,\n });\n return repository.data.default_branch;\n }\n\n public async fetchBuilds(options: FetchBuildsOptions): Promise<CicdState> {\n const {\n entity,\n updateProgress,\n timeFrom,\n timeTo,\n filterStatus = ['all'],\n filterType = 'all',\n } = options;\n const { api, owner, repo } = await this.createGithubApi(entity, [\n 'read_api',\n ]);\n updateProgress(0, 0, 0);\n\n const branch =\n filterType === 'master'\n ? await CicdStatisticsApiGithub.getDefaultBranch(api, owner, repo)\n : undefined;\n\n const workflowsRuns = await api.paginate(\n api.actions.listWorkflowRunsForRepo,\n {\n owner,\n repo,\n per_page: 1000, // max items per page\n ...(branch ? { branch } : {}),\n created: `${timeFrom.toISOString()}..${timeTo.toISOString()}`, // see https://docs.github.com/en/search-github/getting-started-with-searching-on-github/understanding-the-search-syntax#query-for-dates\n },\n response => response.data.map(workflowToBuild),\n );\n\n const limiter = limiterFactory(10);\n const builds = workflowsRuns.map(async build => ({\n ...build,\n duration: await limiter(() =>\n CicdStatisticsApiGithub.getDurationOfBuild(api, owner, repo, build),\n ),\n stages: await limiter(() =>\n CicdStatisticsApiGithub.updateBuildWithStages(api, owner, repo, build),\n ),\n }));\n const promisedBuilds = (await Promise.all(builds)).filter(b =>\n filterStatus.includes(b.status),\n );\n\n return { builds: promisedBuilds };\n }\n\n public async getConfiguration(): Promise<Partial<CicdConfiguration>> {\n return {\n availableStatuses: [\n 'succeeded',\n 'running',\n 'aborted',\n 'failed',\n 'unknown',\n 'stalled',\n 'expired',\n 'enqueued',\n 'scheduled',\n ] as const,\n defaults: this.#cicdDefaults,\n };\n }\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,56 @@
1
+ const statusMap = {
2
+ // completed, action_required, cancelled, failure, neutral, skipped, stale, success, timed_out, in_progress, queued, requested, waiting, pending
3
+ completed: "succeeded",
4
+ action_required: "running",
5
+ cancelled: "aborted",
6
+ failure: "failed",
7
+ neutral: "unknown",
8
+ skipped: "aborted",
9
+ stale: "stalled",
10
+ success: "succeeded",
11
+ timed_out: "expired",
12
+ in_progress: "running",
13
+ queued: "enqueued",
14
+ requested: "scheduled",
15
+ waiting: "running",
16
+ pending: "running"
17
+ };
18
+ const triggerEventMap = {
19
+ push: "scm",
20
+ workflow_dispatch: "manual",
21
+ pull_request: "scm",
22
+ schedule: "internal",
23
+ deployment: "scm",
24
+ dynamic: "internal"
25
+ };
26
+ function workflowToBuild(workflow) {
27
+ return {
28
+ id: workflow.id.toString(),
29
+ status: statusMap[workflow.conclusion ?? ""] ?? "unknown",
30
+ branchType: ["main", "master"].includes(workflow.head_branch ?? "") ? "master" : "branch",
31
+ duration: 0,
32
+ // will get filled in later in a separate API call
33
+ requestedAt: new Date(workflow.created_at),
34
+ triggeredBy: triggerEventMap[workflow.event] ?? "other",
35
+ stages: []
36
+ };
37
+ }
38
+ function jobToStages(job) {
39
+ const status = statusMap[job.status] ? statusMap[job.status] : "unknown";
40
+ const duration = job.started_at && job.completed_at ? new Date(job.completed_at).getTime() - new Date(job.started_at).getTime() : 0;
41
+ return {
42
+ name: job.name,
43
+ status,
44
+ duration,
45
+ stages: job.steps?.map((step) => {
46
+ return {
47
+ name: step.name,
48
+ status: statusMap[step.status] ?? "unknown",
49
+ duration: step.started_at && step.completed_at ? new Date(step.completed_at).getTime() - new Date(step.started_at).getTime() : 0
50
+ };
51
+ })
52
+ };
53
+ }
54
+
55
+ export { jobToStages, workflowToBuild };
56
+ //# sourceMappingURL=utils.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.esm.js","sources":["../../src/api/utils.ts"],"sourcesContent":["/*\n * Copyright 2022 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 Build,\n FilterStatusType,\n TriggerReason,\n Stage,\n} from '@backstage-community/plugin-cicd-statistics';\n\nimport { RestEndpointMethodTypes } from '@octokit/rest';\n\n// \"unknown\" | \"enqueued\" | \"scheduled\" | \"running\" | \"aborted\" | \"succeeded\" | \"failed\" | \"stalled\" | \"expired\"\n//\nconst statusMap: Record<string, FilterStatusType> = {\n // completed, action_required, cancelled, failure, neutral, skipped, stale, success, timed_out, in_progress, queued, requested, waiting, pending\n\n completed: 'succeeded',\n action_required: 'running',\n cancelled: 'aborted',\n failure: 'failed',\n neutral: 'unknown',\n skipped: 'aborted',\n stale: 'stalled',\n success: 'succeeded',\n timed_out: 'expired',\n in_progress: 'running',\n queued: 'enqueued',\n requested: 'scheduled',\n waiting: 'running',\n pending: 'running',\n};\n\n// see https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows\nconst triggerEventMap: Record<string, TriggerReason> = {\n push: 'scm',\n workflow_dispatch: 'manual',\n pull_request: 'scm',\n schedule: 'internal',\n deployment: 'scm',\n dynamic: 'internal',\n};\n\n/**\n * Takes the Workflow object from Github and transforms it to the Build object\n *\n * @param workflow - Workflow object that gets returned from Github\n *\n * @public\n */\nexport function workflowToBuild(\n workflow: RestEndpointMethodTypes['actions']['listWorkflowRuns']['response']['data']['workflow_runs'][0],\n): Build {\n return {\n id: workflow.id.toString(),\n status: statusMap[workflow.conclusion ?? ''] ?? 'unknown',\n branchType: ['main', 'master'].includes(workflow.head_branch ?? '')\n ? 'master'\n : 'branch',\n duration: 0, // will get filled in later in a separate API call\n requestedAt: new Date(workflow.created_at),\n triggeredBy: triggerEventMap[workflow.event] ?? 'other',\n stages: [],\n };\n}\n\n/**\n * Takes the Job object from Github and transforms it to the Stage object\n *\n * @param jobs - Job object that gets returned from Github\n *\n * @public\n *\n * @remarks\n *\n */\nexport function jobToStages(\n job: RestEndpointMethodTypes['actions']['listJobsForWorkflowRun']['response']['data']['jobs'][0],\n): Stage {\n const status = statusMap[job.status] ? statusMap[job.status] : 'unknown';\n const duration =\n job.started_at && job.completed_at\n ? new Date(job.completed_at).getTime() -\n new Date(job.started_at).getTime()\n : 0;\n return {\n name: job.name,\n status,\n duration,\n stages: job.steps?.map(step => {\n return {\n name: step.name,\n status: statusMap[step.status] ?? 'unknown',\n duration:\n step.started_at && step.completed_at\n ? new Date(step.completed_at).getTime() -\n new Date(step.started_at).getTime()\n : 0,\n };\n }),\n };\n}\n"],"names":[],"mappings":"AA2BA,MAAM,SAA8C,GAAA;AAAA;AAAA,EAGlD,SAAW,EAAA,WAAA;AAAA,EACX,eAAiB,EAAA,SAAA;AAAA,EACjB,SAAW,EAAA,SAAA;AAAA,EACX,OAAS,EAAA,QAAA;AAAA,EACT,OAAS,EAAA,SAAA;AAAA,EACT,OAAS,EAAA,SAAA;AAAA,EACT,KAAO,EAAA,SAAA;AAAA,EACP,OAAS,EAAA,WAAA;AAAA,EACT,SAAW,EAAA,SAAA;AAAA,EACX,WAAa,EAAA,SAAA;AAAA,EACb,MAAQ,EAAA,UAAA;AAAA,EACR,SAAW,EAAA,WAAA;AAAA,EACX,OAAS,EAAA,SAAA;AAAA,EACT,OAAS,EAAA;AACX,CAAA;AAGA,MAAM,eAAiD,GAAA;AAAA,EACrD,IAAM,EAAA,KAAA;AAAA,EACN,iBAAmB,EAAA,QAAA;AAAA,EACnB,YAAc,EAAA,KAAA;AAAA,EACd,QAAU,EAAA,UAAA;AAAA,EACV,UAAY,EAAA,KAAA;AAAA,EACZ,OAAS,EAAA;AACX,CAAA;AASO,SAAS,gBACd,QACO,EAAA;AACP,EAAO,OAAA;AAAA,IACL,EAAA,EAAI,QAAS,CAAA,EAAA,CAAG,QAAS,EAAA;AAAA,IACzB,MAAQ,EAAA,SAAA,CAAU,QAAS,CAAA,UAAA,IAAc,EAAE,CAAK,IAAA,SAAA;AAAA,IAChD,UAAA,EAAY,CAAC,MAAA,EAAQ,QAAQ,CAAA,CAAE,SAAS,QAAS,CAAA,WAAA,IAAe,EAAE,CAAA,GAC9D,QACA,GAAA,QAAA;AAAA,IACJ,QAAU,EAAA,CAAA;AAAA;AAAA,IACV,WAAa,EAAA,IAAI,IAAK,CAAA,QAAA,CAAS,UAAU,CAAA;AAAA,IACzC,WAAa,EAAA,eAAA,CAAgB,QAAS,CAAA,KAAK,CAAK,IAAA,OAAA;AAAA,IAChD,QAAQ;AAAC,GACX;AACF;AAYO,SAAS,YACd,GACO,EAAA;AACP,EAAM,MAAA,MAAA,GAAS,UAAU,GAAI,CAAA,MAAM,IAAI,SAAU,CAAA,GAAA,CAAI,MAAM,CAAI,GAAA,SAAA;AAC/D,EAAA,MAAM,WACJ,GAAI,CAAA,UAAA,IAAc,IAAI,YAClB,GAAA,IAAI,KAAK,GAAI,CAAA,YAAY,CAAE,CAAA,OAAA,KAC3B,IAAI,IAAA,CAAK,IAAI,UAAU,CAAA,CAAE,SACzB,GAAA,CAAA;AACN,EAAO,OAAA;AAAA,IACL,MAAM,GAAI,CAAA,IAAA;AAAA,IACV,MAAA;AAAA,IACA,QAAA;AAAA,IACA,MAAQ,EAAA,GAAA,CAAI,KAAO,EAAA,GAAA,CAAI,CAAQ,IAAA,KAAA;AAC7B,MAAO,OAAA;AAAA,QACL,MAAM,IAAK,CAAA,IAAA;AAAA,QACX,MAAQ,EAAA,SAAA,CAAU,IAAK,CAAA,MAAM,CAAK,IAAA,SAAA;AAAA,QAClC,UACE,IAAK,CAAA,UAAA,IAAc,KAAK,YACpB,GAAA,IAAI,KAAK,IAAK,CAAA,YAAY,CAAE,CAAA,OAAA,KAC5B,IAAI,IAAA,CAAK,KAAK,UAAU,CAAA,CAAE,SAC1B,GAAA;AAAA,OACR;AAAA,KACD;AAAA,GACH;AACF;;;;"}
@@ -0,0 +1,33 @@
1
+ import { CicdStatisticsApi, CicdDefaults, FetchBuildsOptions, CicdState, CicdConfiguration } from '@backstage-community/plugin-cicd-statistics';
2
+ import { ConfigApi, OAuthApi } from '@backstage/core-plugin-api';
3
+ import { Entity } from '@backstage/catalog-model';
4
+ import { Octokit } from '@octokit/rest';
5
+
6
+ /**
7
+ * This type represents a initialized github client with octokit
8
+ *
9
+ * @public
10
+ */
11
+ type GithubClient = {
12
+ api: InstanceType<typeof Octokit>;
13
+ owner: string;
14
+ repo: string;
15
+ };
16
+ /**
17
+ * Extracts the CI/CD statistics from a Github repository
18
+ *
19
+ * @public
20
+ */
21
+ declare class CicdStatisticsApiGithub implements CicdStatisticsApi {
22
+ #private;
23
+ readonly configApi: ConfigApi;
24
+ constructor(githubAuthApi: OAuthApi, configApi: ConfigApi, cicdDefaults?: Partial<CicdDefaults>);
25
+ createGithubApi(entity: Entity, scopes: string[]): Promise<GithubClient>;
26
+ private static updateBuildWithStages;
27
+ private static getDurationOfBuild;
28
+ private static getDefaultBranch;
29
+ fetchBuilds(options: FetchBuildsOptions): Promise<CicdState>;
30
+ getConfiguration(): Promise<Partial<CicdConfiguration>>;
31
+ }
32
+
33
+ export { CicdStatisticsApiGithub, type GithubClient };
@@ -0,0 +1,2 @@
1
+ export { CicdStatisticsApiGithub } from './api/github.esm.js';
2
+ //# sourceMappingURL=index.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.esm.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}
package/package.json ADDED
@@ -0,0 +1,94 @@
1
+ {
2
+ "name": "@backstage-community/plugin-cicd-statistics-module-github",
3
+ "version": "0.2.0",
4
+ "description": "CI/CD Statistics plugin module; Github CICD",
5
+ "backstage": {
6
+ "role": "frontend-plugin-module",
7
+ "pluginId": "cicd-statistics",
8
+ "pluginPackage": "@backstage-community/plugin-cicd-statistics",
9
+ "features": {
10
+ "./alpha": "@backstage/FrontendModule"
11
+ }
12
+ },
13
+ "publishConfig": {
14
+ "access": "public"
15
+ },
16
+ "keywords": [
17
+ "backstage",
18
+ "cicd statistics",
19
+ "github"
20
+ ],
21
+ "homepage": "https://backstage.io",
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "https://github.com/backstage/community-plugins",
25
+ "directory": "workspaces/cicd-statistics/plugins/cicd-statistics-module-github"
26
+ },
27
+ "exports": {
28
+ ".": {
29
+ "import": "./dist/index.esm.js",
30
+ "types": "./dist/index.d.ts",
31
+ "default": "./dist/index.esm.js"
32
+ },
33
+ "./alpha": {
34
+ "backstage": "@backstage/FrontendModule",
35
+ "import": "./dist/alpha.esm.js",
36
+ "types": "./dist/alpha.d.ts",
37
+ "default": "./dist/alpha.esm.js"
38
+ },
39
+ "./package.json": "./package.json"
40
+ },
41
+ "typesVersions": {
42
+ "*": {
43
+ "index": [
44
+ "dist/index.d.ts"
45
+ ],
46
+ "alpha": [
47
+ "dist/alpha.d.ts"
48
+ ]
49
+ }
50
+ },
51
+ "license": "Apache-2.0",
52
+ "sideEffects": false,
53
+ "main": "./dist/index.esm.js",
54
+ "types": "./dist/index.d.ts",
55
+ "files": [
56
+ "dist"
57
+ ],
58
+ "scripts": {
59
+ "build": "backstage-cli package build",
60
+ "clean": "backstage-cli package clean",
61
+ "lint": "backstage-cli package lint",
62
+ "prepack": "backstage-cli package prepack",
63
+ "postpack": "backstage-cli package postpack",
64
+ "start": "backstage-cli package start",
65
+ "test": "backstage-cli package test"
66
+ },
67
+ "dependencies": {
68
+ "@backstage-community/plugin-cicd-statistics": "^0.5.0",
69
+ "@backstage/catalog-model": "^1.7.3",
70
+ "@backstage/core-plugin-api": "^1.10.4",
71
+ "@backstage/frontend-plugin-api": "^0.9.5",
72
+ "@backstage/integration": "^1.16.1",
73
+ "@octokit/rest": "^21.1.1",
74
+ "p-limit": "^3.1.0"
75
+ },
76
+ "devDependencies": {
77
+ "@backstage/cli": "^0.30.0",
78
+ "@backstage/dev-utils": "^1.1.7",
79
+ "@backstage/integration": "^1.16.1",
80
+ "@backstage/plugin-catalog": "^1.27.0",
81
+ "@backstage/plugin-catalog-react": "^1.15.2",
82
+ "@types/react": "^16.13.1 || ^17.0.0",
83
+ "@types/react-dom": "^18.2.19",
84
+ "react": "^16.13.1 || ^17.0.0 || ^18.0.0",
85
+ "react-dom": "^16.13.1 || ^17.0.0 || ^18.0.0",
86
+ "react-router-dom": "6.0.0-beta.0 || ^6.3.0"
87
+ },
88
+ "peerDependencies": {
89
+ "react": "^16.13.1 || ^17.0.0 || ^18.0.0",
90
+ "react-dom": "^16.13.1 || ^17.0.0 || ^18.0.0",
91
+ "react-router-dom": "6.0.0-beta.0 || ^6.3.0"
92
+ },
93
+ "module": "./dist/index.esm.js"
94
+ }