@backstage-community/plugin-cicd-statistics-module-github 0.12.0 → 0.13.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 CHANGED
@@ -1,5 +1,11 @@
1
1
  # @backstage-community/plugin-cicd-statistics-module-github
2
2
 
3
+ ## 0.13.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 99baea4: Updated Github Authentication methods to prompt for required scopes and refactored auth to align with Backstage GitHub plugin conventions
8
+
3
9
  ## 0.12.0
4
10
 
5
11
  ### Minor Changes
package/README.md CHANGED
@@ -11,7 +11,8 @@ This is an extension module to the `cicd-statistics` plugin, providing a `CicdSt
11
11
 
12
12
  ```tsx
13
13
  // packages/app/src/apis.ts
14
- import { githubAuthApiRef } from '@backstage/core-plugin-api';
14
+ import { configApiRef } from '@backstage/core-plugin-api';
15
+ import { scmAuthApiRef } from '@backstage/integration-react';
15
16
 
16
17
  import { cicdStatisticsApiRef } from '@backstage-community/plugin-cicd-statistics';
17
18
  import { CicdStatisticsApiGithub } from '@backstage-community/plugin-cicd-statistics-module-github';
@@ -20,11 +21,11 @@ export const apis: AnyApiFactory[] = [
20
21
  createApiFactory({
21
22
  api: cicdStatisticsApiRef,
22
23
  deps: {
23
- githubAuthApi: githubAuthApiRef,
24
+ scmAuthApi: scmAuthApiRef,
24
25
  configApi: configApiRef,
25
26
  },
26
- factory: ({ githubAuthApi, configApi }) => {
27
- return new CicdStatisticsApiGithub(githubAuthApi, configApi);
27
+ factory: ({ scmAuthApi, configApi }) => {
28
+ return new CicdStatisticsApiGithub({ scmAuthApi, configApi });
28
29
  },
29
30
  }),
30
31
  ];
@@ -0,0 +1,16 @@
1
+ import { ApiBlueprint, configApiRef } from '@backstage/frontend-plugin-api';
2
+ import { scmAuthApiRef } from '@backstage/integration-react';
3
+ import { cicdStatisticsApiRef } from '@backstage-community/plugin-cicd-statistics';
4
+ import { CicdStatisticsApiGithub } from '../api/github.esm.js';
5
+
6
+ const cicdStatisticsGithubApi = ApiBlueprint.make({
7
+ name: "cicd-statistics/cicd-statistics-github-api",
8
+ params: (defineParams) => defineParams({
9
+ api: cicdStatisticsApiRef,
10
+ deps: { configApi: configApiRef, scmAuthApi: scmAuthApiRef },
11
+ factory: ({ configApi, scmAuthApi }) => new CicdStatisticsApiGithub({ configApi, scmAuthApi })
12
+ })
13
+ });
14
+
15
+ export { cicdStatisticsGithubApi };
16
+ //# sourceMappingURL=apis.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apis.esm.js","sources":["../../src/alpha/apis.ts"],"sourcesContent":["/*\n * Copyright 2025 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 { configApiRef, ApiBlueprint } from '@backstage/frontend-plugin-api';\nimport { scmAuthApiRef } from '@backstage/integration-react';\nimport { cicdStatisticsApiRef } from '@backstage-community/plugin-cicd-statistics';\nimport { CicdStatisticsApiGithub } from '../api';\n\n/**\n * @alpha\n */\nexport const cicdStatisticsGithubApi = ApiBlueprint.make({\n name: 'cicd-statistics/cicd-statistics-github-api',\n params: defineParams =>\n defineParams({\n api: cicdStatisticsApiRef,\n deps: { configApi: configApiRef, scmAuthApi: scmAuthApiRef },\n factory: ({ configApi, scmAuthApi }) =>\n new CicdStatisticsApiGithub({ configApi, scmAuthApi }),\n }),\n});\n"],"names":[],"mappings":";;;;;AAuBa,MAAA,uBAAA,GAA0B,aAAa,IAAK,CAAA;AAAA,EACvD,IAAM,EAAA,4CAAA;AAAA,EACN,MAAA,EAAQ,kBACN,YAAa,CAAA;AAAA,IACX,GAAK,EAAA,oBAAA;AAAA,IACL,IAAM,EAAA,EAAE,SAAW,EAAA,YAAA,EAAc,YAAY,aAAc,EAAA;AAAA,IAC3D,OAAA,EAAS,CAAC,EAAE,SAAW,EAAA,UAAA,EACrB,KAAA,IAAI,uBAAwB,CAAA,EAAE,SAAW,EAAA,UAAA,EAAY;AAAA,GACxD;AACL,CAAC;;;;"}
package/dist/alpha.d.ts CHANGED
@@ -3,20 +3,6 @@ import * as _backstage_frontend_plugin_api from '@backstage/frontend-plugin-api'
3
3
  /**
4
4
  * @alpha
5
5
  */
6
- declare const cicdStatisticsGithubExtension: _backstage_frontend_plugin_api.OverridableExtensionDefinition<{
7
- kind: "api";
8
- name: "cicd-statistics-github-api";
9
- config: {};
10
- configInput: {};
11
- output: _backstage_frontend_plugin_api.ExtensionDataRef<_backstage_frontend_plugin_api.AnyApiFactory, "core.api.factory", {}>;
12
- inputs: {};
13
- params: <TApi, TImpl extends TApi, TDeps extends {
14
- [x: string]: unknown;
15
- }>(params: _backstage_frontend_plugin_api.ApiFactory<TApi, TImpl, TDeps>) => _backstage_frontend_plugin_api.ExtensionBlueprintParams<_backstage_frontend_plugin_api.AnyApiFactory>;
16
- }>;
17
- /**
18
- * @alpha
19
- */
20
- declare const cicdStatisticsExtensionOverrides: _backstage_frontend_plugin_api.FrontendModule;
6
+ declare const _default: _backstage_frontend_plugin_api.FrontendModule;
21
7
 
22
- export { cicdStatisticsGithubExtension, cicdStatisticsExtensionOverrides as default };
8
+ export { _default as default };
package/dist/alpha.esm.js CHANGED
@@ -1,24 +1,10 @@
1
- import { cicdStatisticsApiRef } from '@backstage-community/plugin-cicd-statistics';
2
- import { ApiBlueprint, githubAuthApiRef, configApiRef, createFrontendModule } from '@backstage/frontend-plugin-api';
3
- import { CicdStatisticsApiGithub } from './api/github.esm.js';
1
+ import { createFrontendModule } from '@backstage/frontend-plugin-api';
2
+ import { cicdStatisticsGithubApi } from './alpha/apis.esm.js';
4
3
 
5
- const cicdStatisticsGithubExtension = ApiBlueprint.make({
6
- name: "cicd-statistics-github-api",
7
- params: (defineParams) => defineParams({
8
- api: cicdStatisticsApiRef,
9
- deps: {
10
- githubAuthApi: githubAuthApiRef,
11
- configApi: configApiRef
12
- },
13
- factory: ({ githubAuthApi, configApi }) => {
14
- return new CicdStatisticsApiGithub(githubAuthApi, configApi);
15
- }
16
- })
17
- });
18
- const cicdStatisticsExtensionOverrides = createFrontendModule({
4
+ var alpha = createFrontendModule({
19
5
  pluginId: "cicd-statistics",
20
- extensions: [cicdStatisticsGithubExtension]
6
+ extensions: [cicdStatisticsGithubApi]
21
7
  });
22
8
 
23
- export { cicdStatisticsGithubExtension, cicdStatisticsExtensionOverrides as default };
9
+ export { alpha as default };
24
10
  //# sourceMappingURL=alpha.esm.js.map
@@ -1 +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 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: defineParams =>\n defineParams({\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 * @alpha\n */\nconst cicdStatisticsExtensionOverrides = createFrontendModule({\n pluginId: 'cicd-statistics',\n extensions: [cicdStatisticsGithubExtension],\n});\n\nexport default cicdStatisticsExtensionOverrides;\n"],"names":[],"mappings":";;;;AA2Ba,MAAA,6BAAA,GAAgC,aAAa,IAAK,CAAA;AAAA,EAC7D,IAAM,EAAA,4BAAA;AAAA,EACN,MAAA,EAAQ,kBACN,YAAa,CAAA;AAAA,IACX,GAAK,EAAA,oBAAA;AAAA,IACL,IAAM,EAAA;AAAA,MACJ,aAAe,EAAA,gBAAA;AAAA,MACf,SAAW,EAAA;AAAA,KACb;AAAA,IACA,OAAS,EAAA,CAAC,EAAE,aAAA,EAAe,WAAgB,KAAA;AACzC,MAAO,OAAA,IAAI,uBAAwB,CAAA,aAAA,EAAe,SAAS,CAAA;AAAA;AAC7D,GACD;AACL,CAAC;AAKD,MAAM,mCAAmC,oBAAqB,CAAA;AAAA,EAC5D,QAAU,EAAA,iBAAA;AAAA,EACV,UAAA,EAAY,CAAC,6BAA6B;AAC5C,CAAC;;;;"}
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 { createFrontendModule } from '@backstage/frontend-plugin-api';\nimport { cicdStatisticsGithubApi } from './alpha/index';\n\n/**\n * @alpha\n */\nexport default createFrontendModule({\n pluginId: 'cicd-statistics',\n extensions: [cicdStatisticsGithubApi],\n});\n"],"names":[],"mappings":";;;AAqBA,YAAe,oBAAqB,CAAA;AAAA,EAClC,QAAU,EAAA,iBAAA;AAAA,EACV,UAAA,EAAY,CAAC,uBAAuB;AACtC,CAAC,CAAA;;;;"}
@@ -7,26 +7,38 @@ import { Octokit } from '@octokit/rest';
7
7
  const GITHUB_ACTIONS_ANNOTATION = "github.com/project-slug";
8
8
  const getProjectNameFromEntity = (entity) => entity?.metadata.annotations?.[GITHUB_ACTIONS_ANNOTATION] ?? "";
9
9
  class CicdStatisticsApiGithub {
10
- #githubAuthApi;
11
- #cicdDefaults;
10
+ scmAuthApi;
12
11
  configApi;
13
- constructor(githubAuthApi, configApi, cicdDefaults = {}) {
14
- this.#githubAuthApi = githubAuthApi;
15
- this.#cicdDefaults = cicdDefaults;
16
- this.configApi = configApi;
12
+ cicdDefaults;
13
+ constructor(options) {
14
+ this.scmAuthApi = options.scmAuthApi;
15
+ this.configApi = options.configApi;
16
+ this.cicdDefaults = options.cicdDefaults ?? {};
17
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);
18
+ async getOctokit(hostname = "github.com") {
19
+ const { token } = await this.scmAuthApi.getCredentials({
20
+ url: `https://${hostname}/`,
21
+ additionalScope: {
22
+ customScopes: {
23
+ github: ["repo"]
24
+ }
25
+ }
26
+ });
23
27
  const configs = readGithubIntegrationConfigs(
24
28
  this.configApi.getOptionalConfigArray("integrations.github") ?? []
25
29
  );
26
- const githubIntegrationConfig = configs.find((v) => v.host === url.hostname);
30
+ const githubIntegrationConfig = configs.find((v) => v.host === hostname);
27
31
  const baseUrl = githubIntegrationConfig?.apiBaseUrl;
32
+ return new Octokit({ auth: token, baseUrl });
33
+ }
34
+ async createGithubClient(entity) {
35
+ const entityInfo = getEntitySourceLocation(entity);
36
+ const [owner, repo] = getProjectNameFromEntity(entity).split("/");
37
+ const url = new URL(entityInfo.target);
38
+ const hostname = url.hostname;
39
+ const octokit = await this.getOctokit(hostname);
28
40
  return {
29
- api: new Octokit({ auth: oauthToken, baseUrl }),
41
+ octokit,
30
42
  owner,
31
43
  repo
32
44
  };
@@ -64,13 +76,11 @@ class CicdStatisticsApiGithub {
64
76
  filterStatus = ["all"],
65
77
  filterType = "all"
66
78
  } = options;
67
- const { api, owner, repo } = await this.createGithubApi(entity, [
68
- "read_api"
69
- ]);
79
+ const { octokit, owner, repo } = await this.createGithubClient(entity);
70
80
  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,
81
+ const branch = filterType === "master" ? await CicdStatisticsApiGithub.getDefaultBranch(octokit, owner, repo) : void 0;
82
+ const workflowsRuns = await octokit.paginate(
83
+ octokit.actions.listWorkflowRunsForRepo,
74
84
  {
75
85
  owner,
76
86
  repo,
@@ -86,10 +96,15 @@ class CicdStatisticsApiGithub {
86
96
  const builds = workflowsRuns.map(async (build) => ({
87
97
  ...build,
88
98
  duration: await limiter(
89
- () => CicdStatisticsApiGithub.getDurationOfBuild(api, owner, repo, build)
99
+ () => CicdStatisticsApiGithub.getDurationOfBuild(octokit, owner, repo, build)
90
100
  ),
91
101
  stages: await limiter(
92
- () => CicdStatisticsApiGithub.updateBuildWithStages(api, owner, repo, build)
102
+ () => CicdStatisticsApiGithub.updateBuildWithStages(
103
+ octokit,
104
+ owner,
105
+ repo,
106
+ build
107
+ )
93
108
  )
94
109
  }));
95
110
  const promisedBuilds = (await Promise.all(builds)).filter(
@@ -110,7 +125,7 @@ class CicdStatisticsApiGithub {
110
125
  "enqueued",
111
126
  "scheduled"
112
127
  ],
113
- defaults: this.#cicdDefaults
128
+ defaults: this.cicdDefaults
114
129
  };
115
130
  }
116
131
  }
@@ -1 +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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"}
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 } 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 { ScmAuthApi } from '@backstage/integration-react';\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 an initialized github client with octokit\n *\n * @public\n */\nexport type GithubClient = {\n /* the octokit instance for making GitHub API calls */\n octokit: InstanceType<typeof Octokit>;\n /* the owner of 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 private readonly scmAuthApi: ScmAuthApi;\n private readonly configApi: ConfigApi;\n private readonly cicdDefaults: Partial<CicdDefaults>;\n\n constructor(options: {\n scmAuthApi: ScmAuthApi;\n configApi: ConfigApi;\n cicdDefaults?: Partial<CicdDefaults>;\n }) {\n this.scmAuthApi = options.scmAuthApi;\n this.configApi = options.configApi;\n this.cicdDefaults = options.cicdDefaults ?? {};\n }\n\n private async getOctokit(hostname: string = 'github.com'): Promise<Octokit> {\n const { token } = await this.scmAuthApi.getCredentials({\n url: `https://${hostname}/`,\n additionalScope: {\n customScopes: {\n github: ['repo'],\n },\n },\n });\n const configs = readGithubIntegrationConfigs(\n this.configApi.getOptionalConfigArray('integrations.github') ?? [],\n );\n const githubIntegrationConfig = configs.find(v => v.host === hostname);\n const baseUrl = githubIntegrationConfig?.apiBaseUrl;\n return new Octokit({ auth: token, baseUrl });\n }\n\n public async createGithubClient(entity: Entity): Promise<GithubClient> {\n const entityInfo = getEntitySourceLocation(entity);\n const [owner, repo] = getProjectNameFromEntity(entity).split('/');\n const url = new URL(entityInfo.target);\n const hostname = url.hostname;\n\n const octokit = await this.getOctokit(hostname);\n return {\n octokit,\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 { octokit, owner, repo } = await this.createGithubClient(entity);\n updateProgress(0, 0, 0);\n\n const branch =\n filterType === 'master'\n ? await CicdStatisticsApiGithub.getDefaultBranch(octokit, owner, repo)\n : undefined;\n\n const workflowsRuns = await octokit.paginate(\n octokit.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(octokit, owner, repo, build),\n ),\n stages: await limiter(() =>\n CicdStatisticsApiGithub.updateBuildWithStages(\n octokit,\n owner,\n repo,\n build,\n ),\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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"}
package/dist/index.d.ts CHANGED
@@ -1,15 +1,16 @@
1
1
  import { CicdStatisticsApi, CicdDefaults, FetchBuildsOptions, CicdState, CicdConfiguration } from '@backstage-community/plugin-cicd-statistics';
2
- import { ConfigApi, OAuthApi } from '@backstage/core-plugin-api';
2
+ import { ConfigApi } from '@backstage/core-plugin-api';
3
3
  import { Entity } from '@backstage/catalog-model';
4
+ import { ScmAuthApi } from '@backstage/integration-react';
4
5
  import { Octokit } from '@octokit/rest';
5
6
 
6
7
  /**
7
- * This type represents a initialized github client with octokit
8
+ * This type represents an initialized github client with octokit
8
9
  *
9
10
  * @public
10
11
  */
11
12
  type GithubClient = {
12
- api: InstanceType<typeof Octokit>;
13
+ octokit: InstanceType<typeof Octokit>;
13
14
  owner: string;
14
15
  repo: string;
15
16
  };
@@ -19,10 +20,16 @@ type GithubClient = {
19
20
  * @public
20
21
  */
21
22
  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>;
23
+ private readonly scmAuthApi;
24
+ private readonly configApi;
25
+ private readonly cicdDefaults;
26
+ constructor(options: {
27
+ scmAuthApi: ScmAuthApi;
28
+ configApi: ConfigApi;
29
+ cicdDefaults?: Partial<CicdDefaults>;
30
+ });
31
+ private getOctokit;
32
+ createGithubClient(entity: Entity): Promise<GithubClient>;
26
33
  private static updateBuildWithStages;
27
34
  private static getDurationOfBuild;
28
35
  private static getDefaultBranch;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@backstage-community/plugin-cicd-statistics-module-github",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "description": "CI/CD Statistics plugin module; Github CICD",
5
5
  "backstage": {
6
6
  "role": "frontend-plugin-module",
@@ -70,6 +70,7 @@
70
70
  "@backstage/core-plugin-api": "^1.12.2",
71
71
  "@backstage/frontend-plugin-api": "^0.13.4",
72
72
  "@backstage/integration": "^1.19.2",
73
+ "@backstage/integration-react": "^1.2.14",
73
74
  "@octokit/rest": "^21.1.1",
74
75
  "p-limit": "^3.1.0"
75
76
  },