From 08c2bd11298d63b6b063b43470bc59c33379876a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20=C5=A0vikruha?= Date: Thu, 25 Jun 2026 16:33:36 +0200 Subject: [PATCH 1/3] fix(github-issues): Load issues from Github based on project slug from owned entities (same logic as Pull Request) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Patrik Švikruha --- .../github-issues-group-host-resolution.md | 7 +++ .../github-issues/src/api/githubIssuesApi.ts | 11 ++-- .../GithubIssues/GithubIssues.test.tsx | 58 +++++++++++++++++++ .../src/hooks/useEntityGithubRepositories.ts | 15 ++++- .../src/hooks/useGetIssuesByRepoFromGithub.ts | 9 ++- 5 files changed, 92 insertions(+), 8 deletions(-) create mode 100644 workspaces/github/.changeset/github-issues-group-host-resolution.md 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/src/api/githubIssuesApi.ts b/workspaces/github/plugins/github-issues/src/api/githubIssuesApi.ts index 2f1e3ceec94..9fdf88ced1e 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 = { @@ -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..c62a0520c46 100644 --- a/workspaces/github/plugins/github-issues/src/hooks/useEntityGithubRepositories.ts +++ b/workspaces/github/plugins/github-issues/src/hooks/useEntityGithubRepositories.ts @@ -30,9 +30,18 @@ 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 { type, target } = getEntitySourceLocation(entity); + return type === 'url' ? new URL(target).hostname : undefined; + } 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, From 7a4f2574a2322b754a1aac8e2be1adf51b8e764a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20=C5=A0vikruha?= Date: Thu, 25 Jun 2026 16:37:11 +0200 Subject: [PATCH 2/3] docs(github-issues): Update README to clarify GitHub host resolution based on Entity kind MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Patrik Švikruha --- workspaces/github/plugins/github-issues/README.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) 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: From 05889f914607d67b70d797fa9b66ce4e3a8ccabf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20=C5=A0vikruha?= Date: Fri, 26 Jun 2026 14:04:46 +0200 Subject: [PATCH 3/3] fix(github-issues): make hostname parameter optional in getOctokit and improve URL parsing in getHostnameFromEntity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Patrik Švikruha --- .../plugins/github-issues/src/api/githubIssuesApi.ts | 2 +- .../src/hooks/useEntityGithubRepositories.ts | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/workspaces/github/plugins/github-issues/src/api/githubIssuesApi.ts b/workspaces/github/plugins/github-issues/src/api/githubIssuesApi.ts index 9fdf88ced1e..4dcd7203f75 100644 --- a/workspaces/github/plugins/github-issues/src/api/githubIssuesApi.ts +++ b/workspaces/github/plugins/github-issues/src/api/githubIssuesApi.ts @@ -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') ?? [], ); diff --git a/workspaces/github/plugins/github-issues/src/hooks/useEntityGithubRepositories.ts b/workspaces/github/plugins/github-issues/src/hooks/useEntityGithubRepositories.ts index c62a0520c46..4492a249b05 100644 --- a/workspaces/github/plugins/github-issues/src/hooks/useEntityGithubRepositories.ts +++ b/workspaces/github/plugins/github-issues/src/hooks/useEntityGithubRepositories.ts @@ -32,10 +32,14 @@ export const getProjectNameFromEntity = (entity: Entity): string => { export const getHostnameFromEntity = (entity: Entity): string | undefined => { try { - const { type, target } = getEntitySourceLocation(entity); - return type === 'url' ? new URL(target).hostname : undefined; + 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 + // 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