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 <tim.v.klever@aexp.com>
This commit is contained in:
Tim Klever
2025-04-07 18:51:17 -07:00
parent 129dfdb588
commit b883f85f13
2 changed files with 205 additions and 11 deletions
@@ -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(),
);
});
});
});
+25 -11
View File
@@ -37,6 +37,8 @@ export type GetManifestByVersionOptions = {
url: string,
options?: { signal?: AbortSignal },
) => Promise<Pick<Response, 'status' | 'json' | 'url'>>;
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<Pick<Response, 'ok' | 'status' | 'text' | 'json' | 'url'>>;
gitHubRawBaseUrl?: string;
versionsBaseUrl?: string;
};
/**
@@ -130,20 +137,27 @@ export async function getManifestByReleaseLine(
options: GetManifestByReleaseLineOptions,
): Promise<ReleaseManifest> {
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,
);