diff --git a/.changeset/silver-cars-type.md b/.changeset/silver-cars-type.md new file mode 100644 index 0000000000..126005db18 --- /dev/null +++ b/.changeset/silver-cars-type.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-github-actions': minor +--- + +add support for GHES hosted repositories diff --git a/plugins/github-actions/README.md b/plugins/github-actions/README.md index cb6867193c..b94862db2b 100644 --- a/plugins/github-actions/README.md +++ b/plugins/github-actions/README.md @@ -64,6 +64,22 @@ const serviceEntityPage = ( 3. Run the app with `yarn start` and the backend with `yarn start-backend`. Then navigate to `/github-actions/` under any entity. +### Self-hosted / Enterprise GitHub + +The plugin will try to use `backstage.io/source-location` or `backstage.io/managed-by-location` +annotations to figure out the location of the source code. + +1. Add the `host` and `apiBaseUrl` to your `app-config.yaml` + +```yaml +# app-config.yaml + +integrations: + github: + - host: 'your-github-host.com' + apiBaseUrl: 'https://api.your-github-host.com' +``` + ## Features - List workflow runs for a project diff --git a/plugins/github-actions/api-report.md b/plugins/github-actions/api-report.md index b1628996e0..0c9d06d90e 100644 --- a/plugins/github-actions/api-report.md +++ b/plugins/github-actions/api-report.md @@ -10,9 +10,9 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; import { ConfigApi } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; import { InfoCardVariants } from '@backstage/core-components'; -import { OAuthApi } from '@backstage/core-plugin-api'; import { RestEndpointMethodTypes } from '@octokit/rest'; import { RouteRef } from '@backstage/core-plugin-api'; +import { ScmAuthApi } from '@backstage/integration-react'; // @public (undocumented) export enum BuildStatus { @@ -111,7 +111,7 @@ export const githubActionsApiRef: ApiRef; // @public export class GithubActionsClient implements GithubActionsApi { - constructor(options: { configApi: ConfigApi; githubAuthApi: OAuthApi }); + constructor(options: { configApi: ConfigApi; scmAuthApi: ScmAuthApi }); // (undocumented) downloadJobLogsForWorkflowRun(options: { hostname?: string; diff --git a/plugins/github-actions/package.json b/plugins/github-actions/package.json index a2c25741be..b69f89e475 100644 --- a/plugins/github-actions/package.json +++ b/plugins/github-actions/package.json @@ -38,12 +38,14 @@ "@backstage/core-components": "workspace:^", "@backstage/core-plugin-api": "workspace:^", "@backstage/integration": "workspace:^", + "@backstage/integration-react": "workspace:^", "@backstage/plugin-catalog-react": "workspace:^", "@backstage/theme": "workspace:^", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", "@octokit/rest": "^19.0.3", + "git-url-parse": "^13.0.0", "luxon": "^3.0.0", "react-use": "^17.2.4" }, diff --git a/plugins/github-actions/src/api/GithubActionsClient.ts b/plugins/github-actions/src/api/GithubActionsClient.ts index 724081320f..d1ff689f2e 100644 --- a/plugins/github-actions/src/api/GithubActionsClient.ts +++ b/plugins/github-actions/src/api/GithubActionsClient.ts @@ -15,9 +15,10 @@ */ import { readGithubIntegrationConfigs } from '@backstage/integration'; +import { ScmAuthApi } from '@backstage/integration-react'; import { GithubActionsApi } from './GithubActionsApi'; import { Octokit, RestEndpointMethodTypes } from '@octokit/rest'; -import { ConfigApi, OAuthApi } from '@backstage/core-plugin-api'; +import { ConfigApi } from '@backstage/core-plugin-api'; /** * A client for fetching information about GitHub actions. @@ -26,16 +27,22 @@ import { ConfigApi, OAuthApi } from '@backstage/core-plugin-api'; */ export class GithubActionsClient implements GithubActionsApi { private readonly configApi: ConfigApi; - private readonly githubAuthApi: OAuthApi; + private readonly scmAuthApi: ScmAuthApi; - constructor(options: { configApi: ConfigApi; githubAuthApi: OAuthApi }) { + constructor(options: { configApi: ConfigApi; scmAuthApi: ScmAuthApi }) { this.configApi = options.configApi; - this.githubAuthApi = options.githubAuthApi; + this.scmAuthApi = options.scmAuthApi; } - private async getOctokit(hostname?: string): Promise { - // TODO: Get access token for the specified hostname - const token = await this.githubAuthApi.getAccessToken(['repo']); + private async getOctokit(hostname: string = 'github.com'): Promise { + const { token } = await this.scmAuthApi.getCredentials({ + url: `https://${hostname}/`, + additionalScope: { + customScopes: { + github: ['repo'], + }, + }, + }); const configs = readGithubIntegrationConfigs( this.configApi.getOptionalConfigArray('integrations.github') ?? [], ); diff --git a/plugins/github-actions/src/components/Cards/Cards.tsx b/plugins/github-actions/src/components/Cards/Cards.tsx index e23f7f7d59..205288cba7 100644 --- a/plugins/github-actions/src/components/Cards/Cards.tsx +++ b/plugins/github-actions/src/components/Cards/Cards.tsx @@ -14,7 +14,6 @@ * limitations under the License. */ -import { readGithubIntegrationConfigs } from '@backstage/integration'; import { useEntity } from '@backstage/plugin-catalog-react'; import { LinearProgress, @@ -28,13 +27,14 @@ import { GITHUB_ACTIONS_ANNOTATION } from '../getProjectNameFromEntity'; import { useWorkflowRuns, WorkflowRun } from '../useWorkflowRuns'; import { WorkflowRunsTable } from '../WorkflowRunsTable'; import { WorkflowRunStatus } from '../WorkflowRunStatus'; -import { configApiRef, errorApiRef, useApi } from '@backstage/core-plugin-api'; +import { errorApiRef, useApi } from '@backstage/core-plugin-api'; import { InfoCard, InfoCardVariants, Link, StructuredMetadataTable, } from '@backstage/core-components'; +import { getHostnameFromEntity } from '../getHostnameFromEntity'; const useStyles = makeStyles({ externalLinkIcon: { @@ -85,12 +85,8 @@ export const LatestWorkflowRunCard = (props: { }) => { const { branch = 'master', variant } = props; const { entity } = useEntity(); - const config = useApi(configApiRef); const errorApi = useApi(errorApiRef); - // TODO: Get github hostname from metadata annotation - const hostname = readGithubIntegrationConfigs( - config.getOptionalConfigArray('integrations.github') ?? [], - )[0].host; + const hostname = getHostnameFromEntity(entity); const [owner, repo] = ( entity?.metadata.annotations?.[GITHUB_ACTIONS_ANNOTATION] ?? '/' ).split('/'); diff --git a/plugins/github-actions/src/components/Cards/RecentWorkflowRunsCard.tsx b/plugins/github-actions/src/components/Cards/RecentWorkflowRunsCard.tsx index 59d64af9c9..d48e4a6604 100644 --- a/plugins/github-actions/src/components/Cards/RecentWorkflowRunsCard.tsx +++ b/plugins/github-actions/src/components/Cards/RecentWorkflowRunsCard.tsx @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { readGithubIntegrationConfigs } from '@backstage/integration'; import { useEntity } from '@backstage/plugin-catalog-react'; import React, { useEffect } from 'react'; import { Link as RouterLink } from 'react-router-dom'; @@ -22,12 +21,7 @@ import { useWorkflowRuns, WorkflowRun } from '../useWorkflowRuns'; import { WorkflowRunStatus } from '../WorkflowRunStatus'; import { Typography } from '@material-ui/core'; -import { - configApiRef, - errorApiRef, - useApi, - useRouteRef, -} from '@backstage/core-plugin-api'; +import { errorApiRef, useApi, useRouteRef } from '@backstage/core-plugin-api'; import { ErrorPanel, InfoCard, @@ -36,6 +30,7 @@ import { Table, } from '@backstage/core-components'; import { buildRouteRef } from '../../routes'; +import { getHostnameFromEntity } from '../getHostnameFromEntity'; const firstLine = (message: string): string => message.split('\n')[0]; @@ -49,13 +44,9 @@ export const RecentWorkflowRunsCard = (props: { const { branch, dense = false, limit = 5, variant } = props; const { entity } = useEntity(); - const config = useApi(configApiRef); const errorApi = useApi(errorApiRef); - // TODO: Get github hostname from metadata annotation - const hostname = readGithubIntegrationConfigs( - config.getOptionalConfigArray('integrations.github') ?? [], - )[0].host; + const hostname = getHostnameFromEntity(entity); const [owner, repo] = ( entity?.metadata.annotations?.[GITHUB_ACTIONS_ANNOTATION] ?? '/' diff --git a/plugins/github-actions/src/components/WorkflowRunDetails/WorkflowRunDetails.tsx b/plugins/github-actions/src/components/WorkflowRunDetails/WorkflowRunDetails.tsx index a2c87772d5..76ce8451e2 100644 --- a/plugins/github-actions/src/components/WorkflowRunDetails/WorkflowRunDetails.tsx +++ b/plugins/github-actions/src/components/WorkflowRunDetails/WorkflowRunDetails.tsx @@ -15,7 +15,6 @@ */ import { Entity } from '@backstage/catalog-model'; -import { readGithubIntegrationConfigs } from '@backstage/integration'; import { Accordion, AccordionDetails, @@ -44,8 +43,8 @@ import { WorkflowRunStatus } from '../WorkflowRunStatus'; import { useWorkflowRunJobs } from './useWorkflowRunJobs'; import { useWorkflowRunsDetails } from './useWorkflowRunsDetails'; import { WorkflowRunLogs } from '../WorkflowRunLogs'; -import { configApiRef, useApi } from '@backstage/core-plugin-api'; import { Breadcrumbs, Link } from '@backstage/core-components'; +import { getHostnameFromEntity } from '../getHostnameFromEntity'; const useStyles = makeStyles(theme => ({ root: { @@ -163,13 +162,9 @@ const JobsList = ({ jobs, entity }: { jobs?: Jobs; entity: Entity }) => { }; export const WorkflowRunDetails = ({ entity }: { entity: Entity }) => { - const config = useApi(configApiRef); const projectName = getProjectNameFromEntity(entity); - // TODO: Get github hostname from metadata annotation - const hostname = readGithubIntegrationConfigs( - config.getOptionalConfigArray('integrations.github') ?? [], - )[0].host; + const hostname = getHostnameFromEntity(entity); const [owner, repo] = (projectName && projectName.split('/')) || []; const details = useWorkflowRunsDetails({ hostname, owner, repo }); const jobs = useWorkflowRunJobs({ hostname, owner, repo }); diff --git a/plugins/github-actions/src/components/WorkflowRunLogs/WorkflowRunLogs.tsx b/plugins/github-actions/src/components/WorkflowRunLogs/WorkflowRunLogs.tsx index 2c9d41398a..2bf8d3356b 100644 --- a/plugins/github-actions/src/components/WorkflowRunLogs/WorkflowRunLogs.tsx +++ b/plugins/github-actions/src/components/WorkflowRunLogs/WorkflowRunLogs.tsx @@ -16,8 +16,6 @@ import { Entity } from '@backstage/catalog-model'; import { LogViewer } from '@backstage/core-components'; -import { configApiRef, useApi } from '@backstage/core-plugin-api'; -import { readGithubIntegrationConfigs } from '@backstage/integration'; import { Accordion, AccordionSummary, @@ -35,6 +33,7 @@ import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; import React from 'react'; import { getProjectNameFromEntity } from '../getProjectNameFromEntity'; import { useDownloadWorkflowRunLogs } from './useDownloadWorkflowRunLogs'; +import { getHostnameFromEntity } from '../getHostnameFromEntity'; const useStyles = makeStyles(theme => ({ button: { @@ -75,14 +74,10 @@ export const WorkflowRunLogs = ({ runId: number; inProgress: boolean; }) => { - const config = useApi(configApiRef); const classes = useStyles(); const projectName = getProjectNameFromEntity(entity); - // TODO: Get github hostname from metadata annotation - const hostname = readGithubIntegrationConfigs( - config.getOptionalConfigArray('integrations.github') ?? [], - )[0].host; + const hostname = getHostnameFromEntity(entity); const [owner, repo] = (projectName && projectName.split('/')) || []; const jobLogs = useDownloadWorkflowRunLogs({ hostname, diff --git a/plugins/github-actions/src/components/WorkflowRunsTable/WorkflowRunsTable.tsx b/plugins/github-actions/src/components/WorkflowRunsTable/WorkflowRunsTable.tsx index f05a41e71b..8471a5d7d6 100644 --- a/plugins/github-actions/src/components/WorkflowRunsTable/WorkflowRunsTable.tsx +++ b/plugins/github-actions/src/components/WorkflowRunsTable/WorkflowRunsTable.tsx @@ -30,7 +30,6 @@ import SyncIcon from '@material-ui/icons/Sync'; import { buildRouteRef } from '../../routes'; import { getProjectNameFromEntity } from '../getProjectNameFromEntity'; import { Entity } from '@backstage/catalog-model'; -import { readGithubIntegrationConfigs } from '@backstage/integration'; import { EmptyState, @@ -38,7 +37,8 @@ import { TableColumn, Link, } from '@backstage/core-components'; -import { configApiRef, useApi, useRouteRef } from '@backstage/core-plugin-api'; +import { useRouteRef } from '@backstage/core-plugin-api'; +import { getHostnameFromEntity } from '../getHostnameFromEntity'; const generatedColumns: TableColumn[] = [ { @@ -164,12 +164,8 @@ export const WorkflowRunsTable = ({ entity: Entity; branch?: string; }) => { - const config = useApi(configApiRef); const projectName = getProjectNameFromEntity(entity); - // TODO: Get github hostname from metadata annotation - const hostname = readGithubIntegrationConfigs( - config.getOptionalConfigArray('integrations.github') ?? [], - )[0].host; + const hostname = getHostnameFromEntity(entity); const [owner, repo] = (projectName ?? '/').split('/'); const [{ runs, ...tableProps }, { retry, setPage, setPageSize }] = useWorkflowRuns({ diff --git a/plugins/github-actions/src/components/getHostnameFromEntity.ts b/plugins/github-actions/src/components/getHostnameFromEntity.ts new file mode 100644 index 0000000000..27e3213d32 --- /dev/null +++ b/plugins/github-actions/src/components/getHostnameFromEntity.ts @@ -0,0 +1,32 @@ +/* + * Copyright 2020 The Backstage Authors + * + * 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 { + ANNOTATION_LOCATION, + ANNOTATION_SOURCE_LOCATION, + Entity, +} from '@backstage/catalog-model'; +import gitUrlParse from 'git-url-parse'; + +export const getHostnameFromEntity = (entity: Entity) => { + const location = + entity?.metadata.annotations?.[ANNOTATION_SOURCE_LOCATION] ?? + entity?.metadata.annotations?.[ANNOTATION_LOCATION]; + + return location && location.startsWith('url:') + ? gitUrlParse(location.slice(4)).resource + : ''; +}; diff --git a/plugins/github-actions/src/plugin.ts b/plugins/github-actions/src/plugin.ts index 26cf246fb9..906ab7c91c 100644 --- a/plugins/github-actions/src/plugin.ts +++ b/plugins/github-actions/src/plugin.ts @@ -20,10 +20,10 @@ import { configApiRef, createPlugin, createApiFactory, - githubAuthApiRef, createRoutableExtension, createComponentExtension, } from '@backstage/core-plugin-api'; +import { scmAuthApiRef } from '@backstage/integration-react'; /** @public */ export const githubActionsPlugin = createPlugin({ @@ -31,9 +31,9 @@ export const githubActionsPlugin = createPlugin({ apis: [ createApiFactory({ api: githubActionsApiRef, - deps: { configApi: configApiRef, githubAuthApi: githubAuthApiRef }, - factory: ({ configApi, githubAuthApi }) => - new GithubActionsClient({ configApi, githubAuthApi }), + deps: { configApi: configApiRef, scmAuthApi: scmAuthApiRef }, + factory: ({ configApi, scmAuthApi }) => + new GithubActionsClient({ configApi, scmAuthApi }), }), ], routes: { diff --git a/yarn.lock b/yarn.lock index 977fcbf506..c55735b5c7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6871,6 +6871,7 @@ __metadata: "@backstage/core-plugin-api": "workspace:^" "@backstage/dev-utils": "workspace:^" "@backstage/integration": "workspace:^" + "@backstage/integration-react": "workspace:^" "@backstage/plugin-catalog-react": "workspace:^" "@backstage/test-utils": "workspace:^" "@backstage/theme": "workspace:^" @@ -6885,6 +6886,7 @@ __metadata: "@types/node": ^16.11.26 "@types/react": ^16.13.1 || ^17.0.0 cross-fetch: ^3.1.5 + git-url-parse: ^13.0.0 luxon: ^3.0.0 msw: ^1.0.0 react-use: ^17.2.4