diff --git a/.changeset/bright-radios-eat.md b/.changeset/bright-radios-eat.md new file mode 100644 index 0000000000..73d2b32e23 --- /dev/null +++ b/.changeset/bright-radios-eat.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-github-actions': minor +--- + +Support GHE diff --git a/plugins/github-actions/package.json b/plugins/github-actions/package.json index 9b2d199b1f..1cef46df02 100644 --- a/plugins/github-actions/package.json +++ b/plugins/github-actions/package.json @@ -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", diff --git a/plugins/github-actions/src/api/GithubActionsApi.ts b/plugins/github-actions/src/api/GithubActionsApi.ts index 75086998c9..637bb23216 100644 --- a/plugins/github-actions/src/api/GithubActionsApi.ts +++ b/plugins/github-actions/src/api/GithubActionsApi.ts @@ -24,14 +24,14 @@ export const githubActionsApiRef = createApiRef({ 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; downloadJobLogsForWorkflowRun: ({ - token, + hostname, owner, repo, runId, }: { - token: string; + hostname?: string; owner: string; repo: string; runId: number; diff --git a/plugins/github-actions/src/api/GithubActionsClient.ts b/plugins/github-actions/src/api/GithubActionsClient.ts index 5be2c9a743..a667ddc4ad 100644 --- a/plugins/github-actions/src/api/GithubActionsClient.ts +++ b/plugins/github-actions/src/api/GithubActionsClient.ts @@ -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 { + // 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 { - 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, diff --git a/plugins/github-actions/src/components/Cards/Cards.tsx b/plugins/github-actions/src/components/Cards/Cards.tsx index 1f1e2997df..f1c27cb666 100644 --- a/plugins/github-actions/src/components/Cards/Cards.tsx +++ b/plugins/github-actions/src/components/Cards/Cards.tsx @@ -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, diff --git a/plugins/github-actions/src/components/Cards/RecentWorkflowRunsCard.test.tsx b/plugins/github-actions/src/components/Cards/RecentWorkflowRunsCard.test.tsx index 0fd77c0443..0deb4ae39d 100644 --- a/plugins/github-actions/src/components/Cards/RecentWorkflowRunsCard.test.tsx +++ b/plugins/github-actions/src/components/Cards/RecentWorkflowRunsCard.test.tsx @@ -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 = { error$: jest.fn(), }; +const configApi: ConfigApi = new ConfigReader({}); + describe('', () => { const entity = { apiVersion: 'v1', @@ -69,7 +78,12 @@ describe('', () => { render( - + diff --git a/plugins/github-actions/src/components/Cards/RecentWorkflowRunsCard.tsx b/plugins/github-actions/src/components/Cards/RecentWorkflowRunsCard.tsx index 9bb52b9ab1..aa30423c3e 100644 --- a/plugins/github-actions/src/components/Cards/RecentWorkflowRunsCard.tsx +++ b/plugins/github-actions/src/components/Cards/RecentWorkflowRunsCard.tsx @@ -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, diff --git a/plugins/github-actions/src/components/WorkflowRunDetails/WorkflowRunDetails.tsx b/plugins/github-actions/src/components/WorkflowRunDetails/WorkflowRunDetails.tsx index ac238b8d0f..8503210c5e 100644 --- a/plugins/github-actions/src/components/WorkflowRunDetails/WorkflowRunDetails.tsx +++ b/plugins/github-actions/src/components/WorkflowRunDetails/WorkflowRunDetails.tsx @@ -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); diff --git a/plugins/github-actions/src/components/WorkflowRunDetails/useWorkflowRunsDetails.ts b/plugins/github-actions/src/components/WorkflowRunDetails/useWorkflowRunsDetails.ts index b38284d52f..d0141c27fa 100644 --- a/plugins/github-actions/src/components/WorkflowRunDetails/useWorkflowRunsDetails.ts +++ b/plugins/github-actions/src/components/WorkflowRunDetails/useWorkflowRunsDetails.ts @@ -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), diff --git a/plugins/github-actions/src/components/WorkflowRunLogs/WorkflowRunLogs.tsx b/plugins/github-actions/src/components/WorkflowRunLogs/WorkflowRunLogs.tsx index d03c2d6be8..fe1c774dad 100644 --- a/plugins/github-actions/src/components/WorkflowRunLogs/WorkflowRunLogs.tsx +++ b/plugins/github-actions/src/components/WorkflowRunLogs/WorkflowRunLogs.tsx @@ -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 = () => { diff --git a/plugins/github-actions/src/components/WorkflowRunLogs/useDownloadWorkflowRunLogs.ts b/plugins/github-actions/src/components/WorkflowRunLogs/useDownloadWorkflowRunLogs.ts index 734f617b1f..0acf67a344 100644 --- a/plugins/github-actions/src/components/WorkflowRunLogs/useDownloadWorkflowRunLogs.ts +++ b/plugins/github-actions/src/components/WorkflowRunLogs/useDownloadWorkflowRunLogs.ts @@ -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), diff --git a/plugins/github-actions/src/components/WorkflowRunsTable/WorkflowRunsTable.tsx b/plugins/github-actions/src/components/WorkflowRunsTable/WorkflowRunsTable.tsx index f49dfeba64..6a3b88b052 100644 --- a/plugins/github-actions/src/components/WorkflowRunsTable/WorkflowRunsTable.tsx +++ b/plugins/github-actions/src/components/WorkflowRunsTable/WorkflowRunsTable.tsx @@ -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, diff --git a/plugins/github-actions/src/components/useWorkflowRuns.ts b/plugins/github-actions/src/components/useWorkflowRuns.ts index 4261012c26..4a4ded2d94 100644 --- a/plugins/github-actions/src/components/useWorkflowRuns.ts +++ b/plugins/github-actions/src/components/useWorkflowRuns.ts @@ -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, diff --git a/plugins/github-actions/src/plugin.ts b/plugins/github-actions/src/plugin.ts index 9e3c965d46..39ffc468b1 100644 --- a/plugins/github-actions/src/plugin.ts +++ b/plugins/github-actions/src/plugin.ts @@ -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 }), + }), + ], });