From b883f85f1377d23e330365c440abcda1e249a351 Mon Sep 17 00:00:00 2001 From: Tim Klever Date: Mon, 7 Apr 2025 18:51:17 -0700 Subject: [PATCH] feat: expand the configuration options for release manifests Ultimately, this paves the way for more configuration options with cli bump command when working in environments that are closed / air-gapped from the public internet. Signed-off-by: Tim Klever --- .../release-manifests/src/manifest.test.ts | 180 ++++++++++++++++++ packages/release-manifests/src/manifest.ts | 36 ++-- 2 files changed, 205 insertions(+), 11 deletions(-) diff --git a/packages/release-manifests/src/manifest.test.ts b/packages/release-manifests/src/manifest.test.ts index ad5bf13496..9cb100376a 100644 --- a/packages/release-manifests/src/manifest.test.ts +++ b/packages/release-manifests/src/manifest.test.ts @@ -86,6 +86,71 @@ describe('Release Manifests', () => { getManifestByVersion({ version: '0.0.0', fetch: mockFetch }), ).rejects.toThrow('No release found for 0.0.0 version'); }); + + it('should allow overriding the versions host', async () => { + const mockFetch = jest.fn().mockImplementation(async url => ({ + status: 200, + url, + json: () => ({ + packages: [{ name: '@backstage/core', version: '2.3.4' }], + }), + })); + + const pkgs = await getManifestByVersion({ + version: '0.0.0', + fetch: mockFetch, + versionsBaseUrl: 'https://versions.some-test-host.com', + }); + + expect(pkgs.packages).toEqual([ + { + name: '@backstage/core', + version: '2.3.4', + }, + ]); + + expect(mockFetch).toHaveBeenCalledWith( + 'https://versions.some-test-host.com/v1/releases/0.0.0/manifest.json', + expect.anything(), + ); + }); + + it('should allow overriding github host', async () => { + const mockFetch = jest.fn().mockImplementation(async url => { + if ( + url === + 'https://versions.some-test-host.com/v1/releases/0.0.0/manifest.json' + ) { + return { + status: 200, + url, + json: () => ({ + packages: [{ name: '@backstage/core', version: '2.3.4' }], + }), + }; + } + + throw new Error('Host not found'); + }); + + const pkgs = await getManifestByVersion({ + version: '0.0.0', + fetch: mockFetch, + gitHubRawBaseUrl: 'https://versions.some-test-host.com', + }); + + expect(pkgs.packages).toEqual([ + { + name: '@backstage/core', + version: '2.3.4', + }, + ]); + + expect(mockFetch).toHaveBeenCalledWith( + 'https://versions.some-test-host.com/v1/releases/0.0.0/manifest.json', + expect.anything(), + ); + }); }); describe('getManifestByReleaseLine', () => { @@ -119,6 +184,121 @@ describe('Release Manifests', () => { getManifestByReleaseLine({ releaseLine: 'foo' }), ).rejects.toThrow("No 'foo' release line found"); }); + + it('should allow overriding the fetch implementation', async () => { + const mockFetch = jest.fn().mockImplementation(async url => ({ + status: 200, + url, + json: () => ({ + packages: [{ name: '@backstage/core', version: '1.2.3' }], + }), + })); + + const pkgs = await getManifestByReleaseLine({ + releaseLine: 'main', + fetch: mockFetch, + }); + + expect(pkgs.packages).toEqual([ + { + name: '@backstage/core', + version: '1.2.3', + }, + ]); + + mockFetch.mockImplementation(async url => ({ + status: 404, + url, + })); + + await expect( + getManifestByReleaseLine({ releaseLine: 'foo', fetch: mockFetch }), + ).rejects.toThrow("No 'foo' release line found"); + }); + + it('should allow overriding the versions host', async () => { + const mockFetch = jest.fn().mockImplementation(async url => ({ + status: 200, + url, + json: () => ({ + packages: [{ name: '@backstage/core', version: '1.2.3' }], + }), + })); + + const pkgs = await getManifestByReleaseLine({ + releaseLine: 'main', + fetch: mockFetch, + versionsBaseUrl: 'https://versions.some-test-host.com', + }); + + expect(pkgs.packages).toEqual([ + { + name: '@backstage/core', + version: '1.2.3', + }, + ]); + + expect(mockFetch).toHaveBeenCalledWith( + 'https://versions.some-test-host.com/v1/tags/main/manifest.json', + expect.anything(), + ); + }); + + it('should allow overriding github host', async () => { + const mockFetch = jest.fn().mockImplementation(async url => { + if ( + url === + 'https://hosted.raw.internal-github.com/backstage/versions/main/v1/tags/main' + ) { + return { + ok: true, + status: 200, + url, + text: () => + 'https://hosted.raw.internal-github.com/backstage/versions/tags/main', + }; + } + + if ( + url === + 'https://hosted.raw.internal-github.com/backstage/versions/tags/main/manifest.json' + ) { + return { + status: 200, + url, + json: () => ({ + packages: [{ name: '@backstage/core', version: '1.2.3' }], + }), + }; + } + + throw new Error('Host Not Found'); + }); + + const pkgs = await getManifestByReleaseLine({ + releaseLine: 'main', + fetch: mockFetch, + gitHubRawBaseUrl: + 'https://hosted.raw.internal-github.com/backstage/versions/main', + }); + + expect(pkgs.packages).toEqual([ + { + name: '@backstage/core', + version: '1.2.3', + }, + ]); + + expect(mockFetch).toHaveBeenCalledWith( + 'https://hosted.raw.internal-github.com/backstage/versions/main/v1/tags/main', + expect.anything(), + ); + + expect(mockFetch).toHaveBeenCalledWith( + 'https://hosted.raw.internal-github.com/backstage/versions/tags/main/manifest.json', + expect.anything(), + ); + }); }); }); diff --git a/packages/release-manifests/src/manifest.ts b/packages/release-manifests/src/manifest.ts index 66a5f62151..8db9d59d39 100644 --- a/packages/release-manifests/src/manifest.ts +++ b/packages/release-manifests/src/manifest.ts @@ -37,6 +37,8 @@ export type GetManifestByVersionOptions = { url: string, options?: { signal?: AbortSignal }, ) => Promise>; + gitHubRawBaseUrl?: string; + versionsBaseUrl?: string; }; // Wait for waitMs, or until signal is aborted. @@ -88,19 +90,18 @@ export async function getManifestByVersion( const versionEnc = encodeURIComponent(options.version); const fetchFn = options.fetch ?? fetch; + const versionsHost = options.versionsBaseUrl ?? VERSIONS_BASE_URL; + const gitHubRawBaseUrl = options.gitHubRawBaseUrl ?? GITHUB_RAW_BASE_URL; const res = await withFallback( signal => - fetchFn(`${VERSIONS_BASE_URL}/v1/releases/${versionEnc}/manifest.json`, { + fetchFn(`${versionsHost}/v1/releases/${versionEnc}/manifest.json`, { signal, }), signal => - fetchFn( - `${GITHUB_RAW_BASE_URL}/v1/releases/${versionEnc}/manifest.json`, - { - signal, - }, - ), + fetchFn(`${gitHubRawBaseUrl}/v1/releases/${versionEnc}/manifest.json`, { + signal, + }), 500, ); if (res.status === 404) { @@ -120,6 +121,12 @@ export async function getManifestByVersion( */ export type GetManifestByReleaseLineOptions = { releaseLine: string; + fetch?: ( + url: string, + options?: { signal?: AbortSignal }, + ) => Promise>; + gitHubRawBaseUrl?: string; + versionsBaseUrl?: string; }; /** @@ -130,20 +137,27 @@ export async function getManifestByReleaseLine( options: GetManifestByReleaseLineOptions, ): Promise { const releaseEnc = encodeURIComponent(options.releaseLine); + + const fetchFn = options.fetch ?? fetch; + const versionsHost = options.versionsBaseUrl ?? VERSIONS_BASE_URL; + const gitHubRawBaseUrl = options.gitHubRawBaseUrl ?? GITHUB_RAW_BASE_URL; + const res = await withFallback( signal => - fetch(`${VERSIONS_BASE_URL}/v1/tags/${releaseEnc}/manifest.json`, { + fetchFn(`${versionsHost}/v1/tags/${releaseEnc}/manifest.json`, { signal, }), async signal => { // The release tags are symlinks, which we need to follow manually when fetching from GitHub. - const baseUrl = `${GITHUB_RAW_BASE_URL}/v1/tags/${releaseEnc}`; - const linkRes = await fetch(baseUrl, { signal }); + const baseUrl = `${gitHubRawBaseUrl}/v1/tags/${releaseEnc}`; + const linkRes = await fetchFn(baseUrl, { signal }); if (!linkRes.ok) { return linkRes; } const link = (await linkRes.text()).trim(); - return fetch(new URL(`${link}/manifest.json`, baseUrl), { signal }); + return fetchFn(new URL(`${link}/manifest.json`, baseUrl).toString(), { + signal, + }); }, 1000, );