diff --git a/plugins/github-actions/package.json b/plugins/github-actions/package.json index a4528c42a5..8ac474169f 100644 --- a/plugins/github-actions/package.json +++ b/plugins/github-actions/package.json @@ -21,6 +21,7 @@ "clean": "backstage-cli clean" }, "dependencies": { + "@backstage/catalog-model": "^0.1.1-alpha.13", "@backstage/core": "^0.1.1-alpha.13", "@backstage/core-api": "^0.1.1-alpha.13", "@backstage/theme": "^0.1.1-alpha.13", diff --git a/plugins/github-actions/src/api/GithubActionsApi.ts b/plugins/github-actions/src/api/GithubActionsApi.ts index 7c52bbcb70..c47cff7eda 100644 --- a/plugins/github-actions/src/api/GithubActionsApi.ts +++ b/plugins/github-actions/src/api/GithubActionsApi.ts @@ -16,6 +16,8 @@ import { createApiRef } from '@backstage/core'; import { Build, BuildDetails } from './types'; +import { Entity } from '@backstage/catalog-model'; + export const githubActionsApiRef = createApiRef({ id: 'plugin.githubactions.service', @@ -23,6 +25,6 @@ export const githubActionsApiRef = createApiRef({ }); export type GithubActionsApi = { - listBuilds: (_entityUri: string) => Promise; - getBuild: (_buildUri: string) => Promise; + listBuilds: (entity: Entity, token: Promise) => Promise; + getBuild: (buildUri: string, token: Promise) => Promise; }; diff --git a/plugins/github-actions/src/api/GithubActionsClient.ts b/plugins/github-actions/src/api/GithubActionsClient.ts new file mode 100644 index 0000000000..0c9a98e57c --- /dev/null +++ b/plugins/github-actions/src/api/GithubActionsClient.ts @@ -0,0 +1,154 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { GithubActionsApi } from './GithubActionsApi'; +import { Build, BuildDetails, BuildStatus, WorkflowRun } from './types'; +import { Entity } from '@backstage/catalog-model'; + +export class GithubActionsClient implements GithubActionsApi { + async listBuilds(entity: Entity, token: Promise): Promise { + // ### Feedback request ### + // I asumed the following: (maybe not the best. Ideally this should come from the link to the component.yaml file) + // entity.metadata.namespace => org name + // entity.metadata.name => repo name + // entityUri -> entity:spotify:backstage + + let url: string; + if (entity.metadata.name !== '') { + url = `https://api.github.com/repos/${entity.metadata.namespace}/${entity.metadata.name}/runs`; + } else { + url = 'https://api.github.com/repos/spotify/backstage/actions/runs'; + } + + const response = await fetch(url, { + headers: new Headers({ + Authorization: `Bearer ${await token}`, + }), + }); + + if (response.status > 200) { + return [ + { + commitId: 'Error', + message: 'ResponseCode > 200', + branch: 'Error', + status: BuildStatus.Failure, + uri: 'Error', + }, + ]; + } + + const data = await response.json(); + + const newData: WorkflowRun[] = data.workflow_runs; + + const endData: Build[] = []; + + newData.forEach((element, index) => { + const transData: Build = { + commitId: '', + message: '', + branch: '', + status: BuildStatus.Null, + uri: '', + }; + transData.commitId = String(element.head_commit.id); + transData.branch = element.head_branch; + + // ### Feedback request ### + // TODO: I am not sure about this part. Looks ugly. Maybe there is a better way of doing this. + if (element.conclusion === 'success') { + transData.status = BuildStatus.Success; + } else if (element.conclusion === 'failure') { + transData.status = BuildStatus.Failure; + } else if (element.conclusion === 'pending') { + transData.status = BuildStatus.Pending; + } else if (element.conclusion === 'running') { + transData.status = BuildStatus.Running; + } else { + if (element.status === 'in_progress') { + transData.status = BuildStatus.Running; + } else { + transData.status = BuildStatus.Null; + } + } + transData.message = element.head_commit.message; + transData.uri = element.url; + endData[index] = transData; + }); + + return endData; + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async getBuild( + buildUri: string, + token: Promise, + ): Promise { + const response = await fetch(buildUri, { + headers: new Headers({ + Authorization: `Bearer ${await token}`, + }), + }); + const buildBlank: Build = { + commitId: '', + message: '', + branch: '', + status: BuildStatus.Null, + uri: '', + }; + + const dataBlank: BuildDetails = { + build: buildBlank, + author: '', + logUrl: '', + overviewUrl: '', + }; + + if (response.status > 200) { + return dataBlank; + } + + const data = await response.json(); + + const newData: WorkflowRun = data; + + dataBlank.author = newData.head_commit.author.name; + dataBlank.build.branch = newData.head_branch; + dataBlank.build.commitId = newData.head_commit.id; + dataBlank.build.message = newData.head_commit.message; + + // ### Feedback request ### + // TODO: I am not sure about this part. Look ugly. Maybe there is a better way of doing this. + if (newData.status === 'completed') { + dataBlank.build.status = BuildStatus.Success; + } else if (newData.status === 'in_progress') { + dataBlank.build.status = BuildStatus.Running; + } else if (newData.status === 'pending') { + dataBlank.build.status = BuildStatus.Pending; + } else if (newData.status === 'failure') { + dataBlank.build.status = BuildStatus.Failure; + } else { + dataBlank.build.status = BuildStatus.Null; + } + + dataBlank.build.uri = newData.url; + dataBlank.logUrl = newData.logs_url; + dataBlank.overviewUrl = newData.html_url; + + return dataBlank; + } +} diff --git a/plugins/github-actions/src/api/index.test.ts b/plugins/github-actions/src/api/index.test.ts index 70319b8120..feff353115 100644 --- a/plugins/github-actions/src/api/index.test.ts +++ b/plugins/github-actions/src/api/index.test.ts @@ -14,13 +14,13 @@ * limitations under the License. */ -import { MockGithubActionsClient } from './MockGithubActionsClient'; +import { GithubActionsClient } from './GithubActionsClient'; import { BuildStatus } from './types'; describe('Github Actions API', () => { - let client: MockGithubActionsClient; + let client: GithubActionsClient; beforeEach(() => { - client = new MockGithubActionsClient(); + client = new GithubActionsClient(); }); describe('Mock client', () => { it('gets a list of builds by a project id', async () => { diff --git a/plugins/github-actions/src/api/index.ts b/plugins/github-actions/src/api/index.ts index 66c2c053fe..9383250bfb 100644 --- a/plugins/github-actions/src/api/index.ts +++ b/plugins/github-actions/src/api/index.ts @@ -15,5 +15,5 @@ */ export * from './GithubActionsApi'; -export * from './MockGithubActionsClient'; +export * from './GithubActionsClient'; export * from './types'; diff --git a/plugins/github-actions/src/api/types.ts b/plugins/github-actions/src/api/types.ts index 134a663d02..8a7b2ca548 100644 --- a/plugins/github-actions/src/api/types.ts +++ b/plugins/github-actions/src/api/types.ts @@ -36,3 +36,189 @@ export type BuildDetails = { logUrl: string; overviewUrl: string; }; + +export interface Author { + name: string; + email: string; +} + +export interface Committer { + name: string; + email: string; +} + +export interface HeadCommit { + id: string; + tree_id: string; + message: string; + timestamp: Date; + author: Author; + committer: Committer; +} + +export interface Owner { + login: string; + id: number; + node_id: string; + avatar_url: string; + gravatar_id: string; + url: string; + html_url: string; + followers_url: string; + following_url: string; + gists_url: string; + starred_url: string; + subscriptions_url: string; + organizations_url: string; + repos_url: string; + events_url: string; + received_events_url: string; + type: string; + site_admin: boolean; +} + +export interface Repository { + id: number; + node_id: string; + name: string; + full_name: string; + private: boolean; + owner: Owner; + html_url: string; + description?: any; + fork: boolean; + url: string; + forks_url: string; + keys_url: string; + collaborators_url: string; + teams_url: string; + hooks_url: string; + issue_events_url: string; + events_url: string; + assignees_url: string; + branches_url: string; + tags_url: string; + blobs_url: string; + git_tags_url: string; + git_refs_url: string; + trees_url: string; + statuses_url: string; + languages_url: string; + stargazers_url: string; + contributors_url: string; + subscribers_url: string; + subscription_url: string; + commits_url: string; + git_commits_url: string; + comments_url: string; + issue_comment_url: string; + contents_url: string; + compare_url: string; + merges_url: string; + archive_url: string; + downloads_url: string; + issues_url: string; + pulls_url: string; + milestones_url: string; + notifications_url: string; + labels_url: string; + releases_url: string; + deployments_url: string; +} + +export interface Owner2 { + login: string; + id: number; + node_id: string; + avatar_url: string; + gravatar_id: string; + url: string; + html_url: string; + followers_url: string; + following_url: string; + gists_url: string; + starred_url: string; + subscriptions_url: string; + organizations_url: string; + repos_url: string; + events_url: string; + received_events_url: string; + type: string; + site_admin: boolean; +} + +export interface HeadRepository { + id: number; + node_id: string; + name: string; + full_name: string; + private: boolean; + owner: Owner2; + html_url: string; + description?: any; + fork: boolean; + url: string; + forks_url: string; + keys_url: string; + collaborators_url: string; + teams_url: string; + hooks_url: string; + issue_events_url: string; + events_url: string; + assignees_url: string; + branches_url: string; + tags_url: string; + blobs_url: string; + git_tags_url: string; + git_refs_url: string; + trees_url: string; + statuses_url: string; + languages_url: string; + stargazers_url: string; + contributors_url: string; + subscribers_url: string; + subscription_url: string; + commits_url: string; + git_commits_url: string; + comments_url: string; + issue_comment_url: string; + contents_url: string; + compare_url: string; + merges_url: string; + archive_url: string; + downloads_url: string; + issues_url: string; + pulls_url: string; + milestones_url: string; + notifications_url: string; + labels_url: string; + releases_url: string; + deployments_url: string; +} + +export interface WorkflowRun { + id: number; + node_id: string; + head_branch: string; + head_sha: string; + run_number: number; + event: string; + status: string; + conclusion: string; + workflow_id: number; + url: string; + html_url: string; + pull_requests: any[]; + created_at: Date; + updated_at: Date; + jobs_url: string; + logs_url: string; + check_suite_url: string; + artifacts_url: string; + cancel_url: string; + rerun_url: string; + workflow_url: string; + head_commit: HeadCommit; + repository: Repository; + head_repository: HeadRepository; +} diff --git a/plugins/github-actions/src/components/BuildDetailsPage/BuildDetailsPage.tsx b/plugins/github-actions/src/components/BuildDetailsPage/BuildDetailsPage.tsx index 2244a4c432..84e9332837 100644 --- a/plugins/github-actions/src/components/BuildDetailsPage/BuildDetailsPage.tsx +++ b/plugins/github-actions/src/components/BuildDetailsPage/BuildDetailsPage.tsx @@ -33,7 +33,7 @@ import React from 'react'; import { useParams } from 'react-router-dom'; import { useAsync } from 'react-use'; import { BuildStatusIndicator } from '../BuildStatusIndicator'; -import { useApi } from '@backstage/core-api'; +import { useApi, githubAuthApiRef } from '@backstage/core-api'; import { githubActionsApiRef } from '../../api'; const useStyles = makeStyles(theme => ({ @@ -51,9 +51,12 @@ const useStyles = makeStyles(theme => ({ export const BuildDetailsPage = () => { const api = useApi(githubActionsApiRef); + const githubApi = useApi(githubAuthApiRef); + const token = githubApi.getAccessToken('repo'); + const classes = useStyles(); const { buildUri } = useParams(); - const status = useAsync(() => api.getBuild(buildUri), [buildUri]); + const status = useAsync(() => api.getBuild(buildUri, token), [buildUri]); if (status.loading) { return ; diff --git a/plugins/github-actions/src/components/BuildListPage/BuildListPage.tsx b/plugins/github-actions/src/components/BuildListPage/BuildListPage.tsx index d68f756129..85568c8c07 100644 --- a/plugins/github-actions/src/components/BuildListPage/BuildListPage.tsx +++ b/plugins/github-actions/src/components/BuildListPage/BuildListPage.tsx @@ -29,11 +29,13 @@ import { Tooltip, Typography, } from '@material-ui/core'; -import React from 'react'; +import React, { FC } from 'react'; import { useAsync } from 'react-use'; import { BuildStatusIndicator } from '../BuildStatusIndicator'; import { githubActionsApiRef } from '../../api'; -import { useApi } from '@backstage/core-api'; +import { useApi, githubAuthApiRef } from '@backstage/core-api'; +import { Entity } from '@backstage/catalog-model'; + const LongText = ({ text, max }: { text: string; max: number }) => { if (text.length < max) { @@ -55,10 +57,13 @@ const useStyles = makeStyles(theme => ({ }, })); -const PageContents = () => { +const PageContents: FC<{ entity: Entity }> = ({ entity }) => { const api = useApi(githubActionsApiRef); + const githubApi = useApi(githubAuthApiRef); + const token = githubApi.getAccessToken('repo'); + const { loading, error, value } = useAsync(() => - api.listBuilds('entity:spotify:backstage'), + api.listBuilds(entity, token), ); if (loading) { @@ -115,14 +120,14 @@ const PageContents = () => { ); }; -export const BuildListPage = () => { +export const BuildListPage: FC<{ entity: Entity }> = ({ entity }) => { const classes = useStyles(); return (
CI/CD Builds - +
); };