feat(github-actions): add support for GHES hosted repos

Signed-off-by: Adam Letizia <LetiziaAdam@JohnDeere.com>
This commit is contained in:
Adam Letizia
2023-05-11 15:54:41 -05:00
parent 21403149d2
commit 96e1004e2a
13 changed files with 90 additions and 53 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-github-actions': minor
---
add support for GHES hosted repositories
+16
View File
@@ -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
+2 -2
View File
@@ -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<GithubActionsApi>;
// @public
export class GithubActionsClient implements GithubActionsApi {
constructor(options: { configApi: ConfigApi; githubAuthApi: OAuthApi });
constructor(options: { configApi: ConfigApi; scmAuthApi: ScmAuthApi });
// (undocumented)
downloadJobLogsForWorkflowRun(options: {
hostname?: string;
+2
View File
@@ -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"
},
@@ -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<Octokit> {
// TODO: Get access token for the specified hostname
const token = await this.githubAuthApi.getAccessToken(['repo']);
private async getOctokit(hostname: string = 'github.com'): Promise<Octokit> {
const { token } = await this.scmAuthApi.getCredentials({
url: `https://${hostname}/`,
additionalScope: {
customScopes: {
github: ['repo'],
},
},
});
const configs = readGithubIntegrationConfigs(
this.configApi.getOptionalConfigArray('integrations.github') ?? [],
);
@@ -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<Theme>({
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('/');
@@ -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] ?? '/'
@@ -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>(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 });
@@ -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>(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,
@@ -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({
@@ -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
: '';
};
+4 -4
View File
@@ -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: {
+2
View File
@@ -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