From bc1c04507c8757e4be5adb605131e1d2510df073 Mon Sep 17 00:00:00 2001 From: Fortune-Ndlovu Date: Mon, 11 May 2026 11:06:26 +0100 Subject: [PATCH] fix(github-issues): handle file: type source-locations in getHostnameFromEntity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `getHostnameFromEntity` unconditionally passes the entity's source-location target to `new URL()`, which throws `TypeError: Invalid URL` when the source-location is `file:` type (e.g. `file:./catalog-entities/foo.yaml`). This affects any Backstage deployment where entities are registered via `file:` type catalog locations — the standard approach for local, Helm, and operator-managed instances. The fix checks the `type` field returned by `getEntitySourceLocation()`: - `url:` type → parse hostname from the URL (existing behavior) - `file:` type → fall back to `github.com` Signed-off-by: Fortune-Ndlovu --- .../fix-github-issues-file-source-location.md | 5 + .../hooks/useEntityGithubRepositories.test.ts | 110 ++++++++++++++++++ .../src/hooks/useEntityGithubRepositories.ts | 7 +- 3 files changed, 120 insertions(+), 2 deletions(-) create mode 100644 .changeset/fix-github-issues-file-source-location.md create mode 100644 workspaces/github/plugins/github-issues/src/hooks/useEntityGithubRepositories.test.ts diff --git a/.changeset/fix-github-issues-file-source-location.md b/.changeset/fix-github-issues-file-source-location.md new file mode 100644 index 00000000000..c6be384b86c --- /dev/null +++ b/.changeset/fix-github-issues-file-source-location.md @@ -0,0 +1,5 @@ +--- +'@backstage-community/plugin-github-issues': patch +--- + +Fixed `getHostnameFromEntity` to handle `file:` type source-locations gracefully instead of throwing `TypeError: Invalid URL` diff --git a/workspaces/github/plugins/github-issues/src/hooks/useEntityGithubRepositories.test.ts b/workspaces/github/plugins/github-issues/src/hooks/useEntityGithubRepositories.test.ts new file mode 100644 index 00000000000..94d189756c9 --- /dev/null +++ b/workspaces/github/plugins/github-issues/src/hooks/useEntityGithubRepositories.test.ts @@ -0,0 +1,110 @@ +/* + * Copyright 2022 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 { Entity } from '@backstage/catalog-model'; +import { + getHostnameFromEntity, + getProjectNameFromEntity, +} from './useEntityGithubRepositories'; + +describe('getProjectNameFromEntity', () => { + it('returns the project slug annotation', () => { + const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'my-service', + annotations: { + 'github.com/project-slug': 'my-org/my-service', + }, + }, + }; + expect(getProjectNameFromEntity(entity)).toBe('my-org/my-service'); + }); + + it('returns empty string when annotation is missing', () => { + const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'my-service', + }, + }; + expect(getProjectNameFromEntity(entity)).toBe(''); + }); +}); + +describe('getHostnameFromEntity', () => { + it('returns hostname from url type source-location', () => { + const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'my-service', + annotations: { + 'backstage.io/source-location': + 'url:https://github.com/my-org/my-service/tree/main/', + }, + }, + }; + expect(getHostnameFromEntity(entity)).toBe('github.com'); + }); + + it('returns hostname for GitHub Enterprise source-location', () => { + const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'my-service', + annotations: { + 'backstage.io/source-location': + 'url:https://github.mycompany.com/my-org/my-service/', + }, + }, + }; + expect(getHostnameFromEntity(entity)).toBe('github.mycompany.com'); + }); + + it('returns github.com for file type source-location', () => { + const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'my-service', + annotations: { + 'backstage.io/source-location': + 'file:./catalog-entities/my-service.yaml', + }, + }, + }; + expect(getHostnameFromEntity(entity)).toBe('github.com'); + }); + + it('returns github.com for managed-by-location fallback with file type', () => { + const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'my-service', + annotations: { + 'backstage.io/managed-by-location': + 'file:/opt/app-root/src/catalog-info.yaml', + }, + }, + }; + expect(getHostnameFromEntity(entity)).toBe('github.com'); + }); +}); diff --git a/workspaces/github/plugins/github-issues/src/hooks/useEntityGithubRepositories.ts b/workspaces/github/plugins/github-issues/src/hooks/useEntityGithubRepositories.ts index 26e24a47e15..dec4e327699 100644 --- a/workspaces/github/plugins/github-issues/src/hooks/useEntityGithubRepositories.ts +++ b/workspaces/github/plugins/github-issues/src/hooks/useEntityGithubRepositories.ts @@ -31,8 +31,11 @@ export const getProjectNameFromEntity = (entity: Entity): string => { }; export const getHostnameFromEntity = (entity: Entity): string => { - const { target } = getEntitySourceLocation(entity); - return new URL(target).hostname; + const { type, target } = getEntitySourceLocation(entity); + if (type === 'url') { + return new URL(target).hostname; + } + return 'github.com'; }; export function useEntityGithubRepositories() {