diff --git a/packages/app/src/apis.ts b/packages/app/src/apis.ts index abc490b658..e80f031bf3 100644 --- a/packages/app/src/apis.ts +++ b/packages/app/src/apis.ts @@ -58,6 +58,10 @@ import { import { scaffolderApiRef, ScaffolderApi } from '@backstage/plugin-scaffolder'; import { rollbarApiRef, RollbarClient } from '@backstage/plugin-rollbar'; +import { + GithubActionsClient, + githubActionsApiRef, +} from '@backstage/plugin-github-actions'; export const apis = (config: ConfigApi) => { // eslint-disable-next-line no-console @@ -75,6 +79,7 @@ export const apis = (config: ConfigApi) => { builder.add(storageApiRef, WebStorage.create({ errorApi })); builder.add(circleCIApiRef, new CircleCIApi()); + builder.add(githubActionsApiRef, new GithubActionsClient()); builder.add(featureFlagsApiRef, new FeatureFlags()); builder.add(lighthouseApiRef, new LighthouseRestApi('http://localhost:3003')); diff --git a/plugins/github-actions/package.json b/plugins/github-actions/package.json index 456bd826c0..8ac474169f 100644 --- a/plugins/github-actions/package.json +++ b/plugins/github-actions/package.json @@ -21,7 +21,9 @@ "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", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", diff --git a/plugins/github-actions/src/apis/builds/types.ts b/plugins/github-actions/src/api/GithubActionsApi.ts similarity index 53% rename from plugins/github-actions/src/apis/builds/types.ts rename to plugins/github-actions/src/api/GithubActionsApi.ts index 134a663d02..dd75f34bf7 100644 --- a/plugins/github-actions/src/apis/builds/types.ts +++ b/plugins/github-actions/src/api/GithubActionsApi.ts @@ -14,25 +14,23 @@ * limitations under the License. */ -export enum BuildStatus { - Null, - Success, - Failure, - Pending, - Running, -} +import { createApiRef } from '@backstage/core'; +import { Build, BuildDetails } from './types'; -export type Build = { - commitId: string; - message: string; - branch: string; - status: BuildStatus; - uri: string; -}; +export const githubActionsApiRef = createApiRef({ + id: 'plugin.githubactions.service', + description: 'Used by the Github Actions plugin to make requests', +}); -export type BuildDetails = { - build: Build; - author: string; - logUrl: string; - overviewUrl: string; +export type GithubActionsApi = { + listBuilds: ({ + owner, + repo, + token, + }: { + owner: string; + repo: string; + token: string; + }) => 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..58d7280c1f --- /dev/null +++ b/plugins/github-actions/src/api/GithubActionsClient.ts @@ -0,0 +1,131 @@ +/* + * 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'; + +const statusToBuildStatus: { [status: string]: BuildStatus } = { + success: BuildStatus.Success, + failure: BuildStatus.Failure, + pending: BuildStatus.Pending, + running: BuildStatus.Running, + in_progress: BuildStatus.Running, + completed: BuildStatus.Success, +}; + +const conclusionToStatus = (conslusion: string): BuildStatus => + statusToBuildStatus[conslusion] ?? BuildStatus.Null; + +export class GithubActionsClient implements GithubActionsApi { + async listBuilds({ + owner, + repo, + token, + }: { + owner: string; + repo: string; + token: string; + }): Promise { + const url = `https://api.github.com/repos/${owner}/${repo}/actions/runs`; + + const response = await fetch(url, { + headers: new Headers({ + Authorization: `Bearer ${token}`, + }), + }); + + if (!response.ok) { + return [ + { + commitId: 'Error', + message: 'Response status is not OK', + 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; + transData.status = conclusionToStatus(element.conclusion); + 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.ok) { + 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; + dataBlank.build.status = conclusionToStatus(newData.status); + dataBlank.build.uri = newData.url; + dataBlank.logUrl = newData.logs_url; + dataBlank.overviewUrl = newData.html_url; + + return dataBlank; + } +} diff --git a/plugins/github-actions/src/apis/builds/index.ts b/plugins/github-actions/src/api/index.ts similarity index 89% rename from plugins/github-actions/src/apis/builds/index.ts rename to plugins/github-actions/src/api/index.ts index 9ce2150893..9383250bfb 100644 --- a/plugins/github-actions/src/apis/builds/index.ts +++ b/plugins/github-actions/src/api/index.ts @@ -14,5 +14,6 @@ * limitations under the License. */ -export { BuildsClient } from './BuildsClient'; +export * from './GithubActionsApi'; +export * from './GithubActionsClient'; export * from './types'; diff --git a/plugins/github-actions/src/api/types.ts b/plugins/github-actions/src/api/types.ts new file mode 100644 index 0000000000..8a7b2ca548 --- /dev/null +++ b/plugins/github-actions/src/api/types.ts @@ -0,0 +1,224 @@ +/* + * 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. + */ + +export enum BuildStatus { + Null, + Success, + Failure, + Pending, + Running, +} + +export type Build = { + commitId: string; + message: string; + branch: string; + status: BuildStatus; + uri: string; +}; + +export type BuildDetails = { + build: Build; + author: string; + 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/apis/builds/BuildsClient.ts b/plugins/github-actions/src/apis/builds/BuildsClient.ts deleted file mode 100644 index 6fbc6a6c53..0000000000 --- a/plugins/github-actions/src/apis/builds/BuildsClient.ts +++ /dev/null @@ -1,44 +0,0 @@ -/* - * 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 { Build, BuildDetails, BuildStatus } from './types'; - -export class BuildsClient { - static create(): BuildsClient { - return new BuildsClient(); - } - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - async listBuilds(_entityUri: string): Promise { - return []; - } - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - async getBuild(_buildUri: string): Promise { - return { - build: { - commitId: 'TODO', - branch: 'TODO', - uri: 'TODO', - status: BuildStatus.Running, - message: 'TODO', - }, - author: 'TODO', - logUrl: 'TODO', - overviewUrl: 'TODO', - }; - } -} diff --git a/plugins/github-actions/src/components/BuildDetailsPage/BuildDetailsPage.tsx b/plugins/github-actions/src/components/BuildDetailsPage/BuildDetailsPage.tsx index bb07e0d6aa..707c670f40 100644 --- a/plugins/github-actions/src/components/BuildDetailsPage/BuildDetailsPage.tsx +++ b/plugins/github-actions/src/components/BuildDetailsPage/BuildDetailsPage.tsx @@ -14,7 +14,6 @@ * limitations under the License. */ -import { Link } from '@backstage/core'; import { Button, ButtonGroup, @@ -30,10 +29,11 @@ import { Typography, } from '@material-ui/core'; import React from 'react'; -import { useParams } from 'react-router-dom'; +import { useLocation } from 'react-router-dom'; import { useAsync } from 'react-use'; -import { BuildsClient } from '../../apis/builds'; import { BuildStatusIndicator } from '../BuildStatusIndicator'; +import { Link, useApi, githubAuthApiRef } from '@backstage/core'; +import { githubActionsApiRef } from '../../api'; const useStyles = makeStyles(theme => ({ root: { @@ -48,12 +48,18 @@ const useStyles = makeStyles(theme => ({ }, })); -const client = BuildsClient.create(); - 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(() => client.getBuild(buildUri), [buildUri]); + const location = useLocation(); + const status = useAsync( + () => + api.getBuild(decodeURIComponent(location.search.split('uri=')[1]), token), + [location.search], + ); if (status.loading) { return ; @@ -70,7 +76,7 @@ export const BuildDetailsPage = () => { return (
- + < @@ -124,12 +130,12 @@ export const BuildDetailsPage = () => { > {details?.overviewUrl && ( )} {details?.logUrl && ( )} diff --git a/plugins/github-actions/src/components/BuildInfoCard/BuildInfoCard.tsx b/plugins/github-actions/src/components/BuildInfoCard/BuildInfoCard.tsx index a04189debc..cbbc1da339 100644 --- a/plugins/github-actions/src/components/BuildInfoCard/BuildInfoCard.tsx +++ b/plugins/github-actions/src/components/BuildInfoCard/BuildInfoCard.tsx @@ -14,7 +14,6 @@ * limitations under the License. */ -import { Link } from '@backstage/core'; import { LinearProgress, makeStyles, @@ -27,10 +26,9 @@ import { } from '@material-ui/core'; import React from 'react'; import { useAsync } from 'react-use'; -import { BuildsClient } from '../../apis/builds'; import { BuildStatusIndicator } from '../BuildStatusIndicator'; - -const client = BuildsClient.create(); +import { githubActionsApiRef } from '../../api'; +import { Link, useApi, githubAuthApiRef } from '@backstage/core'; const useStyles = makeStyles(theme => ({ root: { @@ -41,62 +39,69 @@ const useStyles = makeStyles(theme => ({ }, })); -export const BuildInfoCard = () => { - const classes = useStyles(); - const status = useAsync(() => client.listBuilds('entity:spotify:backstage')); +const BuildInfoCardContent = () => { + const api = useApi(githubActionsApiRef); + const githubApi = useApi(githubAuthApiRef); - let content: JSX.Element; + const status = useAsync(async () => { + const token = await githubApi.getAccessToken('repo'); + return api.listBuilds({ owner: 'spotify', repo: 'backstage', token }); + }); if (status.loading) { - content = ; + return ; } else if (status.error) { - content = ( + return ( Failed to load builds, {status.error.message} ); - } else { - const [build] = - status.value?.filter(({ branch }) => branch === 'master') ?? []; - - content = ( - - - - - Message - - - - {build?.message} - - - - - - Commit ID - - {build?.commitId} - - - - Status - - - - - - -
- ); } + const [build] = + status.value?.filter(({ branch }) => branch === 'master') ?? []; + + return ( + + + + + Message + + + + {build?.message} + + + + + + Commit ID + + {build?.commitId} + + + + Status + + + + + + +
+ ); +}; + +export const BuildInfoCard = () => { + const classes = useStyles(); + return (
Master Build - {content} +
); }; diff --git a/plugins/github-actions/src/components/BuildListPage/BuildListPage.tsx b/plugins/github-actions/src/components/BuildListPage/BuildListPage.tsx index fc784108d4..236bc182ef 100644 --- a/plugins/github-actions/src/components/BuildListPage/BuildListPage.tsx +++ b/plugins/github-actions/src/components/BuildListPage/BuildListPage.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Link } from '@backstage/core'; +import { Link, useApi, githubAuthApiRef } from '@backstage/core'; import { LinearProgress, makeStyles, @@ -31,10 +31,8 @@ import { } from '@material-ui/core'; import React from 'react'; import { useAsync } from 'react-use'; -import { BuildsClient } from '../../apis/builds'; import { BuildStatusIndicator } from '../BuildStatusIndicator'; - -const client = BuildsClient.create(); +import { githubActionsApiRef, Build } from '../../api'; const LongText = ({ text, max }: { text: string; max: number }) => { if (text.length < max) { @@ -56,10 +54,15 @@ const useStyles = makeStyles(theme => ({ }, })); -const PageContents = () => { - const { loading, error, value } = useAsync(() => - client.listBuilds('entity:spotify:backstage'), - ); +const PageContents = ({ owner, repo }: { owner: string; repo: string }) => { + const api = useApi(githubActionsApiRef); + const githubApi = useApi(githubAuthApiRef); + + const { loading, error, value } = useAsync(async () => { + const token = await githubApi.getAccessToken('repo'); + + return api.listBuilds({ owner, repo, token }); + }, [githubApi, owner, repo]); if (loading) { return ; @@ -85,7 +88,7 @@ const PageContents = () => { - {value!.map(build => ( + {value?.map((build: Build) => ( @@ -96,7 +99,7 @@ const PageContents = () => {
- + @@ -117,12 +120,13 @@ const PageContents = () => { export const BuildListPage = () => { const classes = useStyles(); + return (
CI/CD Builds - +
); }; diff --git a/plugins/github-actions/src/components/BuildStatusIndicator/BuildStatusIndicator.tsx b/plugins/github-actions/src/components/BuildStatusIndicator/BuildStatusIndicator.tsx index 332a03d67a..1a198f4df2 100644 --- a/plugins/github-actions/src/components/BuildStatusIndicator/BuildStatusIndicator.tsx +++ b/plugins/github-actions/src/components/BuildStatusIndicator/BuildStatusIndicator.tsx @@ -21,7 +21,7 @@ import SuccessIcon from '@material-ui/icons/CheckCircle'; import FailureIcon from '@material-ui/icons/Error'; import UnknownIcon from '@material-ui/icons/Help'; import React from 'react'; -import { BuildStatus } from '../../apis/builds'; +import { BuildStatus } from '../../api/types'; type Props = { status?: BuildStatus; diff --git a/plugins/github-actions/src/index.ts b/plugins/github-actions/src/index.ts index 3a0a0fe2d3..d67bc6a864 100644 --- a/plugins/github-actions/src/index.ts +++ b/plugins/github-actions/src/index.ts @@ -15,3 +15,4 @@ */ export { plugin } from './plugin'; +export * from './api'; diff --git a/plugins/github-actions/src/plugin.ts b/plugins/github-actions/src/plugin.ts index 638c8d2c8c..8897fe53db 100644 --- a/plugins/github-actions/src/plugin.ts +++ b/plugins/github-actions/src/plugin.ts @@ -24,7 +24,7 @@ export const rootRouteRef = createRouteRef({ title: 'GitHub Actions', }); export const buildRouteRef = createRouteRef({ - path: '/github-actions/builds/:buildUri', + path: '/github-actions/builds', title: 'GitHub Actions Build', });