diff --git a/workspaces/github/.changeset/github-issues-group-host-resolution.md b/workspaces/github/.changeset/github-issues-group-host-resolution.md new file mode 100644 index 00000000000..56926c3cca6 --- /dev/null +++ b/workspaces/github/.changeset/github-issues-group-host-resolution.md @@ -0,0 +1,7 @@ +--- +'@backstage-community/plugin-github-issues': patch +--- + +Fixed the GitHub Issues card crashing with `TypeError: Failed to construct 'URL': Invalid URL` for `Group` and `User` entities whose `backstage.io/source-location` / `backstage.io/managed-by-location` is not a parseable URL (for example entities registered from a `file:` location, or with no location at all). + +For owner entities (`Group`/`User`) the card now resolves the GitHub host from the configured `integrations.github` instead of the owner entity's location annotation — the same approach used by the `github-pull-requests-board` plugin — so a team simply sees the open issues of every repository it owns. `getHostnameFromEntity` is now non-throwing, and repositories whose host cannot be determined are no longer silently dropped. diff --git a/workspaces/github/plugins/github-issues/README.md b/workspaces/github/plugins/github-issues/README.md index d19eca162e0..6856fb355f8 100644 --- a/workspaces/github/plugins/github-issues/README.md +++ b/workspaces/github/plugins/github-issues/README.md @@ -6,8 +6,13 @@ Welcome to the GitHub Issues plugin! > Backstage UI (BUI) is now required for the GitHub Issues plugin to function, BUI has been included as part of Backstage since `1.41.0` which means you're very likely to already have it installed. The [BUI documentation](https://ui.backstage.io/) has details on installation if needed and the Backstage [User Interface documentation](https://backstage.io/docs/conf/user-interface/) has details on creating a custom BUI theme. Based on the [well-known GitHub slug annotation](https://backstage.io/docs/features/software-catalog/well-known-annotations#githubcomproject-slug) associated with the Entity, it renders the list of Open issues in GitHub. -The plugin will attempt to determine the source code location using the [well-known Source location slug annotation](https://backstage.io/docs/features/software-catalog/well-known-annotations/#backstageiosource-location) or [Managed by location slug annotation](https://backstage.io/docs/features/software-catalog/well-known-annotations/#backstageiomanaged-by-location) associated with the Entity. -If no configured Github provider will match, the first one will be used. + +The GitHub host (and therefore which `integrations.github` credentials are used) is resolved depending on the Entity kind: + +- **Group / User**: the host is taken from the configured GitHub integration, **not** from the owner entity's location annotation. This means owners ingested from a non-GitHub source — e.g. Microsoft Entra / Microsoft Graph (`msgraph:`), LDAP, or a static `org.yaml` — work correctly and no longer crash the card. The repositories are taken from the [`github.com/project-slug`](https://backstage.io/docs/features/software-catalog/well-known-annotations#githubcomproject-slug) annotation of the Components the Entity owns, so a `github.com/team-slug` annotation on the Group/User is not required. +- **All other kinds** (e.g. Component): the host is determined from the entity's [Source location](https://backstage.io/docs/features/software-catalog/well-known-annotations/#backstageiosource-location) or [Managed by location](https://backstage.io/docs/features/software-catalog/well-known-annotations/#backstageiomanaged-by-location) annotation. + +If no configured GitHub provider matches the resolved host, the first configured integration is used. The plugin is designed to work with any Entity kind, and it behaves a bit differently depending on the target kind: diff --git a/workspaces/github/plugins/github-issues/src/api/githubIssuesApi.ts b/workspaces/github/plugins/github-issues/src/api/githubIssuesApi.ts index 2f1e3ceec94..4dcd7203f75 100644 --- a/workspaces/github/plugins/github-issues/src/api/githubIssuesApi.ts +++ b/workspaces/github/plugins/github-issues/src/api/githubIssuesApi.ts @@ -24,7 +24,7 @@ import { ScmAuthApi } from '@backstage/integration-react'; /** @internal */ export type Repository = { name: string; - locationHostname: string; + locationHostname?: string; }; /** @internal */ export type Assignee = { @@ -115,7 +115,7 @@ export const githubIssuesApi = ( configApi: ConfigApi, errorApi: ErrorApi, ) => { - const getOctokit = async (hostname: string) => { + const getOctokit = async (hostname?: string) => { const configs = readGithubIntegrationConfigs( configApi.getOptionalConfigArray('integrations.github') ?? [], ); @@ -140,7 +140,7 @@ export const githubIssuesApi = ( const fetchIssuesByRepoFromGithub = async ( repos: Array, itemsPerRepo: number, - hostname: string, + hostname?: string, { filterBy, orderBy = { @@ -152,8 +152,11 @@ export const githubIssuesApi = ( const { octokit, host } = await getOctokit(hostname); const safeNames: Array = []; const repositories = repos - // only tries to fetch issues from repositories that are hosted on the same GitHub instance as the octokit - .filter(repo => repo.locationHostname === host) + // Only fetch issues from repositories hosted on the same GitHub instance + // as the octokit. Repositories whose host could not be determined (no + // usable location annotation) are assumed to live on the resolved host, + // so they are kept rather than silently dropped. + .filter(repo => !repo.locationHostname || repo.locationHostname === host) .map(repo => { const [owner, name] = repo.name.split('/'); diff --git a/workspaces/github/plugins/github-issues/src/components/GithubIssues/GithubIssues.test.tsx b/workspaces/github/plugins/github-issues/src/components/GithubIssues/GithubIssues.test.tsx index e451e58c344..a2b1fea9631 100644 --- a/workspaces/github/plugins/github-issues/src/components/GithubIssues/GithubIssues.test.tsx +++ b/workspaces/github/plugins/github-issues/src/components/GithubIssues/GithubIssues.test.tsx @@ -280,6 +280,64 @@ describe('GithubIssues', () => { ); }); + it('should render Group issues when the group has no annotations (no team-slug or location)', async () => { + // Regression test: a Group with no annotations at all. Previously the card + // crashed with "Failed to construct 'URL': Invalid URL" because it parsed + // the group's source/managed-by location to determine the host. The host + // is now resolved from the configured integration, and the repositories + // come from the components owned by the group (their github.com/project-slug), + // so neither github.com/team-slug nor a location annotation is required. + const testIssue = getTestIssue({ + repository: { nameWithOwner: 'backstage/owned-repo' }, + }); + + const ownedComponent = makeEntityWithKind('Component', { + name: 'owned-repo', + spec: { type: 'service', lifecycle: 'production', owner: 'my-team' }, + }); + + const catalogApiWithOwned = { + getEntities: async () => ({ items: [ownedComponent] }), + } as CatalogApi; + + const groupWithoutAnnotations = { + metadata: { + name: 'my-team', + }, + spec: { type: 'team', children: [] }, + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + } as unknown as Entity; + + const mockApi: GithubIssuesApi = { + fetchIssuesByRepoFromGithub: async () => ({ + 'owned-repo': { + issues: { + totalCount: 1, + edges: [testIssue], + }, + }, + }), + } as GithubIssuesApi; + + const apis = [ + [githubIssuesApiRef, mockApi], + [catalogApiRef, catalogApiWithOwned], + ] as const; + + const { getByTestId } = await renderInTestApp( + + + + + , + ); + + expect(getByTestId(`issue-${testIssue.node.url}`)).toHaveTextContent( + testIssue.node.title, + ); + }); + it('should render correctly when there are no issues in GitHub for kind: User', async () => { const ownedComponent = makeEntityWithKind('Component', { name: 'owned-repo', diff --git a/workspaces/github/plugins/github-issues/src/hooks/useEntityGithubRepositories.ts b/workspaces/github/plugins/github-issues/src/hooks/useEntityGithubRepositories.ts index 26e24a47e15..4492a249b05 100644 --- a/workspaces/github/plugins/github-issues/src/hooks/useEntityGithubRepositories.ts +++ b/workspaces/github/plugins/github-issues/src/hooks/useEntityGithubRepositories.ts @@ -30,9 +30,22 @@ export const getProjectNameFromEntity = (entity: Entity): string => { return entity?.metadata.annotations?.[GITHUB_PROJECT_SLUG_ANNOTATION] ?? ''; }; -export const getHostnameFromEntity = (entity: Entity): string => { - const { target } = getEntitySourceLocation(entity); - return new URL(target).hostname; +export const getHostnameFromEntity = (entity: Entity): string | undefined => { + try { + const { target } = getEntitySourceLocation(entity); + // The location type (`url`, `github`, `gitlab`, ...) is not a reliable + // signal here: a non-`url` type can still carry a parseable URL target. + // Attempt to parse the host regardless of type and let the `catch` handle + // targets that are not URLs. + return new URL(target).hostname; + } catch { + // The entity has no usable URL source/managed-by location (e.g. it was + // registered from a non-URL location such as `file:`, or has no location + // annotation at all). The GitHub host is resolved from the configured + // integration instead, so fall back to `undefined` rather than throwing + // and crashing the card with "Failed to construct 'URL': Invalid URL". + return undefined; + } }; export function useEntityGithubRepositories() { diff --git a/workspaces/github/plugins/github-issues/src/hooks/useGetIssuesByRepoFromGithub.ts b/workspaces/github/plugins/github-issues/src/hooks/useGetIssuesByRepoFromGithub.ts index 37134d7f059..f39b668edba 100644 --- a/workspaces/github/plugins/github-issues/src/hooks/useGetIssuesByRepoFromGithub.ts +++ b/workspaces/github/plugins/github-issues/src/hooks/useGetIssuesByRepoFromGithub.ts @@ -31,7 +31,14 @@ export const useGetIssuesByRepoFromGithub = ( ) => { const githubIssuesApi = useApi(githubIssuesApiRef); const { entity } = useEntity(); - const hostname = getHostnameFromEntity(entity); + // For Group/User entities the issues come from the *owned* repositories, not + // from the owner entity itself. Its own source/managed-by location therefore + // must not constrain the GitHub host — and is frequently a non-URL location + // that used to crash the card. Let the API resolve the host from the + // configured `integrations.github`, the same way the + // github-pull-requests-board plugin does. + const isOwnerEntity = entity.kind === 'Group' || entity.kind === 'User'; + const hostname = isOwnerEntity ? undefined : getHostnameFromEntity(entity); const { value: issues,