From 6999f6df21c255428f1bb5e1c598f4b3a77812ab Mon Sep 17 00:00:00 2001 From: Gio Divino Date: Sat, 8 Nov 2025 04:17:21 +0800 Subject: [PATCH 1/2] feat: add git tag to AzureUrl class Signed-off-by: Gio Divino --- .changeset/lemon-lines-give.md | 5 +++++ packages/integration/src/azure/AzureUrl.ts | 18 +++++++++++------- 2 files changed, 16 insertions(+), 7 deletions(-) create mode 100644 .changeset/lemon-lines-give.md diff --git a/.changeset/lemon-lines-give.md b/.changeset/lemon-lines-give.md new file mode 100644 index 0000000000..215f9f1a0e --- /dev/null +++ b/.changeset/lemon-lines-give.md @@ -0,0 +1,5 @@ +--- +'@backstage/integration': minor +--- + +The AzureUrl class in the @backstage/integration package is now able to process BOTH git branches and git tags. Initially this class only processed git branches and threw an error when non-branch Azure URLs were passed in. diff --git a/packages/integration/src/azure/AzureUrl.ts b/packages/integration/src/azure/AzureUrl.ts index 1161ab7a1b..627bf98567 100644 --- a/packages/integration/src/azure/AzureUrl.ts +++ b/packages/integration/src/azure/AzureUrl.ts @@ -14,8 +14,6 @@ * limitations under the License. */ -const VERSION_PREFIX_GIT_BRANCH = 'GB'; - export class AzureUrl { /** * Parses an azure URL as copied from the browser address bar. @@ -50,16 +48,19 @@ export class AzureUrl { const path = url.searchParams.get('path') ?? undefined; let ref; + let prefix; const version = url.searchParams.get('version'); if (version) { - const prefix = version.slice(0, 2); - if (prefix !== 'GB') { - throw new Error('Azure URL version must point to a git branch'); + prefix = version.slice(0, 2); + if (prefix !== 'GB' && prefix !== 'GT') { + throw new Error( + 'Azure URL version must point to a git branch or git tag', + ); } ref = version.slice(2); } - return new AzureUrl(url.origin, owner, project, repo, path, ref); + return new AzureUrl(url.origin, owner, project, repo, path, ref, prefix); } #origin: string; @@ -68,6 +69,7 @@ export class AzureUrl { #repo: string; #path?: string; #ref?: string; + #prefix?: string; private constructor( origin: string, @@ -76,6 +78,7 @@ export class AzureUrl { repo: string, path?: string, ref?: string, + prefix?: string, ) { this.#origin = origin; this.#owner = owner; @@ -83,6 +86,7 @@ export class AzureUrl { this.#repo = repo; this.#path = path; this.#ref = ref; + this.#prefix = prefix; } #baseUrl = (...parts: string[]): URL => { @@ -108,7 +112,7 @@ export class AzureUrl { url.searchParams.set('path', this.#path); } if (this.#ref) { - url.searchParams.set('version', VERSION_PREFIX_GIT_BRANCH + this.#ref); + url.searchParams.set('version', this.#prefix + this.#ref); } return url.toString(); From faab29a0148173d064b6bc47d86a1bfacdf711a5 Mon Sep 17 00:00:00 2001 From: Gio Divino Date: Tue, 25 Nov 2025 08:59:51 +0800 Subject: [PATCH 2/2] feat: add getPrefix function and more tests Signed-off-by: Gio Divino --- .../integration/src/azure/AzureUrl.test.ts | 82 +++++++++++++++++-- packages/integration/src/azure/AzureUrl.ts | 7 ++ 2 files changed, 84 insertions(+), 5 deletions(-) diff --git a/packages/integration/src/azure/AzureUrl.test.ts b/packages/integration/src/azure/AzureUrl.test.ts index a56f800133..7382476da4 100644 --- a/packages/integration/src/azure/AzureUrl.test.ts +++ b/packages/integration/src/azure/AzureUrl.test.ts @@ -27,6 +27,7 @@ describe('AzureUrl', () => { expect(url.getRepo()).toBe('my-project'); expect(url.getRef()).toBeUndefined(); expect(url.getPath()).toBeUndefined(); + expect(url.getPrefix()).toBeUndefined(); expect(url.toRepoUrl()).toBe( 'https://dev.azure.com/my-org/_git/my-project', @@ -52,6 +53,7 @@ describe('AzureUrl', () => { expect(url.getRepo()).toBe('my-project'); expect(url.getRef()).toBeUndefined(); expect(url.getPath()).toBe('/test.yaml'); + expect(url.getPrefix()).toBeUndefined(); expect(url.toRepoUrl()).toBe( 'https://dev.azure.com/my-org/_git/my-project?path=%2Ftest.yaml', @@ -67,7 +69,7 @@ describe('AzureUrl', () => { ); }); - it('should work with the short URL form with a path and ref', () => { + it('should work with the short URL form with a path and branch ref', () => { const url = AzureUrl.fromRepoUrl( 'https://dev.azure.com/my-org/_git/my-project?path=%2Ftest.yaml&version=GBtest-branch', ); @@ -77,6 +79,7 @@ describe('AzureUrl', () => { expect(url.getRepo()).toBe('my-project'); expect(url.getRef()).toBe('test-branch'); expect(url.getPath()).toBe('/test.yaml'); + expect(url.getPrefix()).toBe('GB'); expect(url.toRepoUrl()).toBe( 'https://dev.azure.com/my-org/_git/my-project?path=%2Ftest.yaml&version=GBtest-branch', @@ -92,6 +95,32 @@ describe('AzureUrl', () => { ); }); + it('should work with the short URL form with a path and tag ref', () => { + const url = AzureUrl.fromRepoUrl( + 'https://dev.azure.com/my-org/_git/my-project?path=%2Ftest.yaml&version=GTtest-tag', + ); + + expect(url.getOwner()).toBe('my-org'); + expect(url.getProject()).toBe('my-project'); + expect(url.getRepo()).toBe('my-project'); + expect(url.getRef()).toBe('test-tag'); + expect(url.getPath()).toBe('/test.yaml'); + expect(url.getPrefix()).toBe('GT'); + + expect(url.toRepoUrl()).toBe( + 'https://dev.azure.com/my-org/_git/my-project?path=%2Ftest.yaml&version=GTtest-tag', + ); + expect(url.toFileUrl()).toBe( + 'https://dev.azure.com/my-org/my-project/_apis/git/repositories/my-project/items?api-version=6.0&path=%2Ftest.yaml&version=test-tag', + ); + expect(url.toArchiveUrl()).toBe( + 'https://dev.azure.com/my-org/my-project/_apis/git/repositories/my-project/items?recursionLevel=full&download=true&api-version=6.0&scopePath=%2Ftest.yaml&version=test-tag', + ); + expect(url.toCommitsUrl()).toBe( + 'https://dev.azure.com/my-org/my-project/_apis/git/repositories/my-project/commits?api-version=6.0&searchCriteria.itemVersion.version=test-tag', + ); + }); + it('should work with the long URL', () => { const url = AzureUrl.fromRepoUrl( 'http://my-host/my-org/my-project/_git/my-repo', @@ -102,6 +131,7 @@ describe('AzureUrl', () => { expect(url.getRepo()).toBe('my-repo'); expect(url.getRef()).toBeUndefined(); expect(url.getPath()).toBeUndefined(); + expect(url.getPrefix()).toBeUndefined(); expect(url.toRepoUrl()).toBe( 'http://my-host/my-org/my-project/_git/my-repo', @@ -117,7 +147,7 @@ describe('AzureUrl', () => { ); }); - it('should work with the long URL form with a path and ref', () => { + it('should work with the long URL form with a path and branch ref', () => { const url = AzureUrl.fromRepoUrl( 'http://my-host/my-org/my-project/_git/my-repo?path=%2Ffolder&version=GBtest-branch', ); @@ -127,6 +157,7 @@ describe('AzureUrl', () => { expect(url.getRepo()).toBe('my-repo'); expect(url.getRef()).toBe('test-branch'); expect(url.getPath()).toBe('/folder'); + expect(url.getPrefix()).toBe('GB'); expect(url.toRepoUrl()).toBe( 'http://my-host/my-org/my-project/_git/my-repo?path=%2Ffolder&version=GBtest-branch', @@ -142,6 +173,32 @@ describe('AzureUrl', () => { ); }); + it('should work with the long URL form with a path and tag ref', () => { + const url = AzureUrl.fromRepoUrl( + 'http://my-host/my-org/my-project/_git/my-repo?path=%2Ffolder&version=GTtest-tag', + ); + + expect(url.getOwner()).toBe('my-org'); + expect(url.getProject()).toBe('my-project'); + expect(url.getRepo()).toBe('my-repo'); + expect(url.getRef()).toBe('test-tag'); + expect(url.getPath()).toBe('/folder'); + expect(url.getPrefix()).toBe('GT'); + + expect(url.toRepoUrl()).toBe( + 'http://my-host/my-org/my-project/_git/my-repo?path=%2Ffolder&version=GTtest-tag', + ); + expect(url.toFileUrl()).toBe( + 'http://my-host/my-org/my-project/_apis/git/repositories/my-repo/items?api-version=6.0&path=%2Ffolder&version=test-tag', + ); + expect(url.toArchiveUrl()).toBe( + 'http://my-host/my-org/my-project/_apis/git/repositories/my-repo/items?recursionLevel=full&download=true&api-version=6.0&scopePath=%2Ffolder&version=test-tag', + ); + expect(url.toCommitsUrl()).toBe( + 'http://my-host/my-org/my-project/_apis/git/repositories/my-repo/commits?api-version=6.0&searchCriteria.itemVersion.version=test-tag', + ); + }); + it('should work with the old tfs long URL', () => { const url = AzureUrl.fromRepoUrl( 'http://my-host/tfs/projects/my-project/_git/my-repo', @@ -152,9 +209,10 @@ describe('AzureUrl', () => { expect(url.getRepo()).toBe('my-repo'); expect(url.getRef()).toBeUndefined(); expect(url.getPath()).toBeUndefined(); + expect(url.getPrefix()).toBeUndefined(); }); - it('should work with the old tfs long URL form with a path and ref', () => { + it('should work with the old tfs long URL form with a path and branch ref', () => { const url = AzureUrl.fromRepoUrl( 'http://my-host/tfs/projects/my-project/_git/my-repo?path=%2Ffolder&version=GBtest-branch', ); @@ -164,14 +222,28 @@ describe('AzureUrl', () => { expect(url.getRepo()).toBe('my-repo'); expect(url.getRef()).toBe('test-branch'); expect(url.getPath()).toBe('/folder'); + expect(url.getPrefix()).toBe('GB'); }); - it('should reject non-branch refs', () => { + it('should work with the old tfs long URL form with a path and tag ref', () => { + const url = AzureUrl.fromRepoUrl( + 'http://my-host/tfs/projects/my-project/_git/my-repo?path=%2Ffolder&version=GTtest-tag', + ); + + expect(url.getOwner()).toBe('tfs/projects'); + expect(url.getProject()).toBe('my-project'); + expect(url.getRepo()).toBe('my-repo'); + expect(url.getRef()).toBe('test-tag'); + expect(url.getPath()).toBe('/folder'); + expect(url.getPrefix()).toBe('GT'); + }); + + it('should reject non-branch and non-tag refs', () => { expect(() => AzureUrl.fromRepoUrl( 'https://dev.azure.com/my-org/_git/my-project?version=GC6eead79870d998a3befd4bc7c72cc89e446f2970', ), - ).toThrow('Azure URL version must point to a git branch'); + ).toThrow('Azure URL version must point to a git branch or git tag'); }); it('should reject non-repo URLs', () => { diff --git a/packages/integration/src/azure/AzureUrl.ts b/packages/integration/src/azure/AzureUrl.ts index 627bf98567..a271403554 100644 --- a/packages/integration/src/azure/AzureUrl.ts +++ b/packages/integration/src/azure/AzureUrl.ts @@ -236,4 +236,11 @@ export class AzureUrl { getRef(): string | undefined { return this.#ref; } + + /** + * Returns the git prefix in the repo if the URL contains one. + */ + getPrefix(): string | undefined { + return this.#prefix; + } }