Support GHE

This commit is contained in:
Erik Larsson
2021-01-12 01:20:17 +01:00
parent 54dd7b3003
commit 46bba09ea0
14 changed files with 165 additions and 55 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-github-actions': minor
---
Support GHE
+1
View File
@@ -34,6 +34,7 @@
"dependencies": {
"@backstage/catalog-model": "^0.6.1",
"@backstage/core": "^0.4.4",
"@backstage/integration": "^0.2.0",
"@backstage/plugin-catalog": "^0.2.11",
"@backstage/theme": "^0.2.2",
"@material-ui/core": "^4.11.0",
@@ -24,14 +24,14 @@ export const githubActionsApiRef = createApiRef<GithubActionsApi>({
export type GithubActionsApi = {
listWorkflowRuns: ({
token,
hostname,
owner,
repo,
pageSize,
page,
branch,
}: {
token: string;
hostname?: string;
owner: string;
repo: string;
pageSize?: number;
@@ -41,12 +41,12 @@ export type GithubActionsApi = {
RestEndpointMethodTypes['actions']['listWorkflowRuns']['response']['data']
>;
getWorkflow: ({
token,
hostname,
owner,
repo,
id,
}: {
token: string;
hostname?: string;
owner: string;
repo: string;
id: number;
@@ -54,12 +54,12 @@ export type GithubActionsApi = {
RestEndpointMethodTypes['actions']['getWorkflow']['response']['data']
>;
getWorkflowRun: ({
token,
hostname,
owner,
repo,
id,
}: {
token: string;
hostname?: string;
owner: string;
repo: string;
id: number;
@@ -67,23 +67,23 @@ export type GithubActionsApi = {
RestEndpointMethodTypes['actions']['getWorkflowRun']['response']['data']
>;
reRunWorkflow: ({
token,
hostname,
owner,
repo,
runId,
}: {
token: string;
hostname?: string;
owner: string;
repo: string;
runId: number;
}) => Promise<any>;
downloadJobLogsForWorkflowRun: ({
token,
hostname,
owner,
repo,
runId,
}: {
token: string;
hostname?: string;
owner: string;
repo: string;
runId: number;
@@ -14,36 +14,60 @@
* limitations under the License.
*/
import { ConfigApi, OAuthApi } from '@backstage/core';
import { readGitHubIntegrationConfigs } from '@backstage/integration';
import { GithubActionsApi } from './GithubActionsApi';
import { Octokit, RestEndpointMethodTypes } from '@octokit/rest';
export class GithubActionsClient implements GithubActionsApi {
private readonly configApi: ConfigApi;
private readonly githubAuthApi: OAuthApi;
constructor(options: { configApi: ConfigApi; githubAuthApi: OAuthApi }) {
this.configApi = options.configApi;
this.githubAuthApi = options.githubAuthApi;
}
private async getOctokit(hostname?: string): Promise<Octokit> {
// TODO: Get access token for the specified hostname
const token = await this.githubAuthApi.getAccessToken(['repo']);
const configs = readGitHubIntegrationConfigs(
this.configApi.getOptionalConfigArray('integrations.github') ?? [],
);
const githubIntegrationConfig = configs.find(
v => v.host === hostname ?? 'github.com',
);
const baseUrl = githubIntegrationConfig?.apiBaseUrl;
return new Octokit({ auth: token, baseUrl });
}
async reRunWorkflow({
token,
hostname,
owner,
repo,
runId,
}: {
token: string;
hostname?: string;
owner: string;
repo: string;
runId: number;
}): Promise<any> {
return new Octokit({ auth: token }).actions.reRunWorkflow({
const octokit = await this.getOctokit(hostname);
return octokit.actions.reRunWorkflow({
owner,
repo,
run_id: runId,
});
}
async listWorkflowRuns({
token,
hostname,
owner,
repo,
pageSize = 100,
page = 0,
branch,
}: {
token: string;
hostname?: string;
owner: string;
repo: string;
pageSize?: number;
@@ -52,9 +76,8 @@ export class GithubActionsClient implements GithubActionsApi {
}): Promise<
RestEndpointMethodTypes['actions']['listWorkflowRuns']['response']['data']
> {
const workflowRuns = await new Octokit({
auth: token,
}).actions.listWorkflowRunsForRepo({
const octokit = await this.getOctokit(hostname);
const workflowRuns = await octokit.actions.listWorkflowRunsForRepo({
owner,
repo,
per_page: pageSize,
@@ -64,19 +87,20 @@ export class GithubActionsClient implements GithubActionsApi {
return workflowRuns.data;
}
async getWorkflow({
token,
hostname,
owner,
repo,
id,
}: {
token: string;
hostname?: string;
owner: string;
repo: string;
id: number;
}): Promise<
RestEndpointMethodTypes['actions']['getWorkflow']['response']['data']
> {
const workflow = await new Octokit({ auth: token }).actions.getWorkflow({
const octokit = await this.getOctokit(hostname);
const workflow = await octokit.actions.getWorkflow({
owner,
repo,
workflow_id: id,
@@ -84,19 +108,20 @@ export class GithubActionsClient implements GithubActionsApi {
return workflow.data;
}
async getWorkflowRun({
token,
hostname,
owner,
repo,
id,
}: {
token: string;
hostname?: string;
owner: string;
repo: string;
id: number;
}): Promise<
RestEndpointMethodTypes['actions']['getWorkflowRun']['response']['data']
> {
const run = await new Octokit({ auth: token }).actions.getWorkflowRun({
const octokit = await this.getOctokit(hostname);
const run = await octokit.actions.getWorkflowRun({
owner,
repo,
run_id: id,
@@ -104,21 +129,20 @@ export class GithubActionsClient implements GithubActionsApi {
return run.data;
}
async downloadJobLogsForWorkflowRun({
token,
hostname,
owner,
repo,
runId,
}: {
token: string;
hostname?: string;
owner: string;
repo: string;
runId: number;
}): Promise<
RestEndpointMethodTypes['actions']['downloadJobLogsForWorkflowRun']['response']['data']
> {
const workflow = await new Octokit({
auth: token,
}).actions.downloadJobLogsForWorkflowRun({
const octokit = await this.getOctokit(hostname);
const workflow = await octokit.actions.downloadJobLogsForWorkflowRun({
owner,
repo,
job_id: runId,
@@ -17,6 +17,7 @@ import React, { useEffect } from 'react';
import { useWorkflowRuns } from '../useWorkflowRuns';
import { WorkflowRun, WorkflowRunsTable } from '../WorkflowRunsTable';
import { Entity } from '@backstage/catalog-model';
import { readGitHubIntegrationConfigs } from '@backstage/integration';
import { WorkflowRunStatus } from '../WorkflowRunStatus';
import {
Link,
@@ -28,6 +29,7 @@ import {
import {
InfoCard,
StructuredMetadataTable,
configApiRef,
errorApiRef,
useApi,
} from '@backstage/core';
@@ -84,11 +86,17 @@ export const LatestWorkflowRunCard = ({
// Display the card full height suitable for
variant,
}: Props) => {
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 [owner, repo] = (
entity?.metadata.annotations?.[GITHUB_ACTIONS_ANNOTATION] ?? '/'
).split('/');
const [{ runs, loading, error }] = useWorkflowRuns({
hostname,
owner,
repo,
branch,
@@ -14,7 +14,14 @@
* limitations under the License.
*/
import { ApiProvider, ApiRegistry, errorApiRef } from '@backstage/core';
import {
ApiProvider,
ApiRegistry,
errorApiRef,
configApiRef,
ConfigApi,
ConfigReader,
} from '@backstage/core';
import { lightTheme } from '@backstage/theme';
import { ThemeProvider } from '@material-ui/core';
import { render } from '@testing-library/react';
@@ -33,6 +40,8 @@ const mockErrorApi: jest.Mocked<typeof errorApiRef.T> = {
error$: jest.fn(),
};
const configApi: ConfigApi = new ConfigReader({});
describe('<RecentWorkflowRunsCard />', () => {
const entity = {
apiVersion: 'v1',
@@ -69,7 +78,12 @@ describe('<RecentWorkflowRunsCard />', () => {
render(
<ThemeProvider theme={lightTheme}>
<MemoryRouter>
<ApiProvider apis={ApiRegistry.with(errorApiRef, mockErrorApi)}>
<ApiProvider
apis={ApiRegistry.with(errorApiRef, mockErrorApi).with(
configApiRef,
configApi,
)}
>
<RecentWorkflowRunsCard {...props} />
</ApiProvider>
</MemoryRouter>
@@ -15,12 +15,14 @@
*/
import { Entity } from '@backstage/catalog-model';
import {
configApiRef,
EmptyState,
errorApiRef,
InfoCard,
Table,
useApi,
} from '@backstage/core';
import { readGitHubIntegrationConfigs } from '@backstage/integration';
import { Button, Link } from '@material-ui/core';
import React, { useEffect } from 'react';
import { generatePath, Link as RouterLink } from 'react-router-dom';
@@ -45,11 +47,17 @@ export const RecentWorkflowRunsCard = ({
limit = 5,
variant,
}: Props) => {
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 [owner, repo] = (
entity?.metadata.annotations?.[GITHUB_ACTIONS_ANNOTATION] ?? '/'
).split('/');
const [{ runs = [], loading, error }] = useWorkflowRuns({
hostname,
owner,
repo,
branch,
@@ -14,7 +14,8 @@
* limitations under the License.
*/
import { Entity } from '@backstage/catalog-model';
import { Link } from '@backstage/core';
import { configApiRef, Link, useApi } from '@backstage/core';
import { readGitHubIntegrationConfigs } from '@backstage/integration';
import {
Accordion,
AccordionDetails,
@@ -159,10 +160,15 @@ const JobListItem = ({
};
export const WorkflowRunDetails = ({ entity }: { entity: Entity }) => {
const config = useApi(configApiRef);
const projectName = useProjectName(entity);
// TODO: Get github hostname from metadata annotation
const hostname = readGitHubIntegrationConfigs(
config.getOptionalConfigArray('integrations.github') ?? [],
)[0].host;
const [owner, repo] = projectName.value ? projectName.value.split('/') : [];
const details = useWorkflowRunsDetails(repo, owner);
const details = useWorkflowRunsDetails({ hostname, owner, repo });
const jobs = useWorkflowRunJobs(details.value?.jobs_url);
const error = projectName.error || (projectName.value && details.error);
@@ -13,20 +13,26 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useApi, githubAuthApiRef } from '@backstage/core';
import { useApi } from '@backstage/core';
import { useParams } from 'react-router-dom';
import { useAsync } from 'react-use';
import { githubActionsApiRef } from '../../api';
export const useWorkflowRunsDetails = (repo: string, owner: string) => {
export const useWorkflowRunsDetails = ({
hostname,
owner,
repo,
}: {
hostname?: string;
owner: string;
repo: string;
}) => {
const api = useApi(githubActionsApiRef);
const auth = useApi(githubAuthApiRef);
const { id } = useParams();
const details = useAsync(async () => {
const token = await auth.getAccessToken(['repo']);
return repo && owner
? api.getWorkflowRun({
token,
hostname,
owner,
repo,
id: parseInt(id, 10),
@@ -34,6 +34,8 @@ import { useProjectName } from '../useProjectName';
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
import DescriptionIcon from '@material-ui/icons/Description';
import { Entity } from '@backstage/catalog-model';
import { configApiRef, useApi } from '@backstage/core';
import { readGitHubIntegrationConfigs } from '@backstage/integration';
const LazyLog = React.lazy(() => import('react-lazylog/build/LazyLog'));
const LinePart = React.lazy(() => import('react-lazylog/build/LinePart'));
@@ -107,11 +109,21 @@ export const WorkflowRunLogs = ({
runId: string;
inProgress: boolean;
}) => {
const config = useApi(configApiRef);
const classes = useStyles();
const projectName = useProjectName(entity);
// TODO: Get github hostname from metadata annotation
const hostname = readGitHubIntegrationConfigs(
config.getOptionalConfigArray('integrations.github') ?? [],
)[0].host;
const [owner, repo] = projectName.value ? projectName.value.split('/') : [];
const jobLogs = useDownloadWorkflowRunLogs(repo, owner, runId);
const jobLogs = useDownloadWorkflowRunLogs({
hostname,
owner,
repo,
id: runId,
});
const [open, setOpen] = React.useState(false);
const handleOpen = () => {
@@ -14,22 +14,26 @@
* limitations under the License.
*/
import { useApi, githubAuthApiRef } from '@backstage/core';
import { useApi } from '@backstage/core';
import { useAsync } from 'react-use';
import { githubActionsApiRef } from '../../api';
export const useDownloadWorkflowRunLogs = (
repo: string,
owner: string,
id: string,
) => {
export const useDownloadWorkflowRunLogs = ({
hostname,
owner,
repo,
id,
}: {
hostname?: string;
owner: string;
repo: string;
id: string;
}) => {
const api = useApi(githubActionsApiRef);
const auth = useApi(githubAuthApiRef);
const details = useAsync(async () => {
const token = await auth.getAccessToken(['repo']);
return repo && owner
? api.downloadJobLogsForWorkflowRun({
token,
hostname,
owner,
repo,
runId: parseInt(id, 10),
@@ -25,13 +25,20 @@ import {
import RetryIcon from '@material-ui/icons/Replay';
import GitHubIcon from '@material-ui/icons/GitHub';
import { Link as RouterLink, generatePath } from 'react-router-dom';
import { EmptyState, Table, TableColumn } from '@backstage/core';
import {
EmptyState,
Table,
TableColumn,
configApiRef,
useApi,
} from '@backstage/core';
import { useWorkflowRuns } from '../useWorkflowRuns';
import { WorkflowRunStatus } from '../WorkflowRunStatus';
import SyncIcon from '@material-ui/icons/Sync';
import { buildRouteRef } from '../../plugin';
import { useProjectName } from '../useProjectName';
import { Entity } from '@backstage/catalog-model';
import { readGitHubIntegrationConfigs } from '@backstage/integration';
export type WorkflowRun = {
id: string;
@@ -162,12 +169,18 @@ export const WorkflowRunsTable = ({
entity: Entity;
branch?: string;
}) => {
const config = useApi(configApiRef);
const { value: projectName, loading } = useProjectName(entity);
// TODO: Get github hostname from metadata annotation
const hostname = readGitHubIntegrationConfigs(
config.getOptionalConfigArray('integrations.github') ?? [],
)[0].host;
const [owner, repo] = (projectName ?? '/').split('/');
const [
{ runs, ...tableProps },
{ retry, setPage, setPageSize },
] = useWorkflowRuns({
hostname,
owner,
repo,
branch,
@@ -17,21 +17,22 @@ import { useState } from 'react';
import { useAsyncRetry } from 'react-use';
import { WorkflowRun } from './WorkflowRunsTable/WorkflowRunsTable';
import { githubActionsApiRef } from '../api/GithubActionsApi';
import { useApi, githubAuthApiRef, errorApiRef } from '@backstage/core';
import { useApi, errorApiRef } from '@backstage/core';
export function useWorkflowRuns({
hostname,
owner,
repo,
branch,
initialPageSize = 5,
}: {
hostname?: string;
owner: string;
repo: string;
branch?: string;
initialPageSize?: number;
}) {
const api = useApi(githubActionsApiRef);
const auth = useApi(githubAuthApiRef);
const errorApi = useApi(errorApiRef);
@@ -42,12 +43,11 @@ export function useWorkflowRuns({
const { loading, value: runs, retry, error } = useAsyncRetry<
WorkflowRun[]
>(async () => {
const token = await auth.getAccessToken(['repo']);
return (
api
// GitHub API pagination count starts from 1
.listWorkflowRuns({
token,
hostname,
owner,
repo,
pageSize,
@@ -63,7 +63,7 @@ export function useWorkflowRuns({
onReRunClick: async () => {
try {
await api.reRunWorkflow({
token,
hostname,
owner,
repo,
runId: run.id,
+10 -1
View File
@@ -15,9 +15,11 @@
*/
import {
configApiRef,
createPlugin,
createRouteRef,
createApiFactory,
githubAuthApiRef,
} from '@backstage/core';
import { githubActionsApiRef, GithubActionsClient } from './api';
@@ -34,5 +36,12 @@ export const buildRouteRef = createRouteRef({
export const plugin = createPlugin({
id: 'github-actions',
apis: [createApiFactory(githubActionsApiRef, new GithubActionsClient())],
apis: [
createApiFactory({
api: githubActionsApiRef,
deps: { configApi: configApiRef, githubAuthApi: githubAuthApiRef },
factory: ({ configApi, githubAuthApi }) =>
new GithubActionsClient({ configApi, githubAuthApi }),
}),
],
});