feat(cli): also support custom manifest in version bump
Signed-off-by: Hellgren Heikki <heikki.hellgren@op.fi>
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
---
|
||||
'@backstage/cli': patch
|
||||
---
|
||||
|
||||
Allow using custom manifest location in the yarn plugin and version bump.
|
||||
|
||||
The Backstage yarn plugin and version bump allows two new environment variables to configure custom manifest location:
|
||||
|
||||
- `BACKSTAGE_MANIFEST_BASE_URL`: The base URL for fetching the Backstage version
|
||||
manifest. Defaults to `https://versions.backstage.io/v1/releases/VERSION/manifest.json`.
|
||||
Useful for running the plugin in environment without direct access to the internet,
|
||||
for example by using a mirror of the versions API or a proxy.
|
||||
Note that the environment variable is just the host name, and the path is appended by
|
||||
the plugin. If you are using the yarn plugin, bump version command will also try
|
||||
to fetch the new version of the yarn plugin from the same base URL (defaults to
|
||||
`https://versions.backstage.io/v1/releases/RELEASE/yarn-plugin`)
|
||||
- `BACKSTAGE_MANIFEST_FILE`: Path to a local manifest file. If set, the plugin
|
||||
will not attempt to fetch the manifest from the network. Useful for running
|
||||
the plugin in environment without internet access and without mirror of the
|
||||
versions API.
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
* Copyright 2025 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.
|
||||
@@ -13,7 +13,6 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import fs from 'fs-extra';
|
||||
import { Command } from 'commander';
|
||||
import * as runObj from '../../../../lib/run';
|
||||
@@ -24,8 +23,8 @@ import { setupServer } from 'msw/node';
|
||||
import { rest } from 'msw';
|
||||
import { NotFoundError } from '@backstage/errors';
|
||||
import {
|
||||
MockDirectory,
|
||||
createMockDirectory,
|
||||
MockDirectory,
|
||||
} from '@backstage/backend-test-utils';
|
||||
|
||||
// Avoid mutating the global agents used in other tests
|
||||
@@ -1055,3 +1054,313 @@ describe('createVersionFinder', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('environment variables', () => {
|
||||
const worker = setupServer();
|
||||
registerMswTestHooks(worker);
|
||||
|
||||
beforeEach(() => {
|
||||
delete process.env.BACKSTAGE_MANIFEST_FILE;
|
||||
delete process.env.BACKSTAGE_MANIFEST_BASE_URL;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
delete process.env.BACKSTAGE_MANIFEST_FILE;
|
||||
delete process.env.BACKSTAGE_MANIFEST_BASE_URL;
|
||||
});
|
||||
|
||||
it('should use custom base URL when BACKSTAGE_MANIFEST_BASE_URL is set', async () => {
|
||||
process.env.BACKSTAGE_MANIFEST_BASE_URL = 'https://custom.example.com';
|
||||
|
||||
mockDir.setContent({
|
||||
'yarn.lock': lockfileMock,
|
||||
'package.json': JSON.stringify({
|
||||
workspaces: {
|
||||
packages: ['packages/*'],
|
||||
},
|
||||
}),
|
||||
packages: {
|
||||
a: {
|
||||
'package.json': JSON.stringify({
|
||||
name: 'a',
|
||||
dependencies: {
|
||||
'@backstage/core': '^1.0.5',
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
jest.spyOn(runObj, 'run').mockResolvedValue(undefined);
|
||||
worker.use(
|
||||
rest.get(
|
||||
'https://custom.example.com/v1/tags/main/manifest.json',
|
||||
(_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
releaseVersion: '1.5.0',
|
||||
packages: [
|
||||
{
|
||||
name: '@backstage/core',
|
||||
version: '1.5.0',
|
||||
},
|
||||
],
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
const { log: logs } = await withLogCollector(['log', 'warn'], async () => {
|
||||
await bump({ pattern: null, release: 'main' } as unknown as Command);
|
||||
});
|
||||
|
||||
expectLogsToMatch(logs, [
|
||||
'Using default pattern glob @backstage/*',
|
||||
'Checking for updates of @backstage/core',
|
||||
'Some packages are outdated, updating',
|
||||
'bumping @backstage/core in a to ^1.5.0',
|
||||
'Your project is now at version 1.5.0, which has been written to backstage.json',
|
||||
'Running yarn install to install new versions',
|
||||
'Checking for moved packages to the @backstage-community namespace...',
|
||||
'Version bump complete!',
|
||||
]);
|
||||
|
||||
expect(runObj.run).toHaveBeenCalledTimes(1);
|
||||
expect(runObj.run).toHaveBeenCalledWith(
|
||||
'yarn',
|
||||
['install'],
|
||||
expect.any(Object),
|
||||
);
|
||||
|
||||
const packageA = await fs.readJson(
|
||||
mockDir.resolve('packages/a/package.json'),
|
||||
);
|
||||
expect(packageA).toEqual({
|
||||
name: 'a',
|
||||
dependencies: {
|
||||
'@backstage/core': '^1.5.0',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should use custom manifest file when BACKSTAGE_MANIFEST_FILE is set', async () => {
|
||||
const manifestPath = mockDir.resolve('custom-manifest.json');
|
||||
process.env.BACKSTAGE_MANIFEST_FILE = manifestPath;
|
||||
|
||||
const customManifest = {
|
||||
releaseVersion: '2.0.0',
|
||||
packages: [
|
||||
{
|
||||
name: '@backstage/core',
|
||||
version: '2.0.0',
|
||||
},
|
||||
{
|
||||
name: '@backstage/theme',
|
||||
version: '3.0.0',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
mockDir.setContent({
|
||||
'custom-manifest.json': JSON.stringify(customManifest),
|
||||
'yarn.lock': lockfileMock,
|
||||
'package.json': JSON.stringify({
|
||||
workspaces: {
|
||||
packages: ['packages/*'],
|
||||
},
|
||||
}),
|
||||
packages: {
|
||||
a: {
|
||||
'package.json': JSON.stringify({
|
||||
name: 'a',
|
||||
dependencies: {
|
||||
'@backstage/core': '^1.0.5',
|
||||
'@backstage/theme': '^1.0.0',
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
jest.spyOn(runObj, 'run').mockResolvedValue(undefined);
|
||||
|
||||
const { log: logs } = await withLogCollector(['log', 'warn'], async () => {
|
||||
await bump({ pattern: null, release: 'main' } as unknown as Command);
|
||||
});
|
||||
|
||||
expectLogsToMatch(logs, [
|
||||
'Using default pattern glob @backstage/*',
|
||||
'Checking for updates of @backstage/core',
|
||||
'Checking for updates of @backstage/theme',
|
||||
'Some packages are outdated, updating',
|
||||
'bumping @backstage/core in a to ^2.0.0',
|
||||
'bumping @backstage/theme in a to ^3.0.0',
|
||||
'Your project is now at version 2.0.0, which has been written to backstage.json',
|
||||
'Running yarn install to install new versions',
|
||||
'Checking for moved packages to the @backstage-community namespace...',
|
||||
'⚠️ The following packages may have breaking changes:',
|
||||
' @backstage/core : 1.0.6 ~> 2.0.0',
|
||||
' https://github.com/backstage/backstage/blob/master/packages/core/CHANGELOG.md',
|
||||
' @backstage/theme : 1.0.0 ~> 3.0.0',
|
||||
' https://github.com/backstage/backstage/blob/master/packages/theme/CHANGELOG.md',
|
||||
'Version bump complete!',
|
||||
]);
|
||||
|
||||
// Should not make any HTTP requests since using local manifest
|
||||
expect(mockFetchPackageInfo).not.toHaveBeenCalled();
|
||||
|
||||
expect(runObj.run).toHaveBeenCalledTimes(1);
|
||||
expect(runObj.run).toHaveBeenCalledWith(
|
||||
'yarn',
|
||||
['install'],
|
||||
expect.any(Object),
|
||||
);
|
||||
|
||||
const packageA = await fs.readJson(
|
||||
mockDir.resolve('packages/a/package.json'),
|
||||
);
|
||||
expect(packageA).toEqual({
|
||||
name: 'a',
|
||||
dependencies: {
|
||||
'@backstage/core': '^2.0.0',
|
||||
'@backstage/theme': '^3.0.0',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should use custom base URL for yarn plugin when BACKSTAGE_MANIFEST_BASE_URL is set with yarn plugin', async () => {
|
||||
process.env.BACKSTAGE_MANIFEST_BASE_URL = 'https://custom.example.com';
|
||||
|
||||
mockDir.setContent({
|
||||
'.yarnrc.yml': yarnRcMock,
|
||||
'yarn.lock': lockfileMock,
|
||||
'package.json': JSON.stringify({
|
||||
workspaces: {
|
||||
packages: ['packages/*'],
|
||||
},
|
||||
}),
|
||||
packages: {
|
||||
a: {
|
||||
'package.json': JSON.stringify({
|
||||
name: 'a',
|
||||
dependencies: {
|
||||
'@backstage/core': '^1.0.5',
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
jest.spyOn(runObj, 'run').mockResolvedValue(undefined);
|
||||
worker.use(
|
||||
rest.get(
|
||||
'https://custom.example.com/v1/tags/main/manifest.json',
|
||||
(_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
releaseVersion: '1.5.0',
|
||||
packages: [
|
||||
{
|
||||
name: '@backstage/core',
|
||||
version: '1.5.0',
|
||||
},
|
||||
],
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
const { log: logs } = await withLogCollector(['log', 'warn'], async () => {
|
||||
await bump({ pattern: null, release: 'main' } as unknown as Command);
|
||||
});
|
||||
|
||||
expectLogsToMatch(logs, [
|
||||
'Using default pattern glob @backstage/*',
|
||||
'Checking for updates of @backstage/core',
|
||||
'NOTE: this bump used backstage:^ versions in package.json files, since the Backstage yarn plugin was detected in the repository. To migrate back to explicit npm versions, remove the plugin by running "yarn plugin remove @yarnpkg/plugin-backstage", then repeat this command.',
|
||||
'Some packages are outdated, updating',
|
||||
'bumping @backstage/core in a to ^1.5.0',
|
||||
'Updating yarn plugin to v1.5.0...',
|
||||
'Your project is now at version 1.5.0, which has been written to backstage.json',
|
||||
'Running yarn install to install new versions',
|
||||
'Checking for moved packages to the @backstage-community namespace...',
|
||||
'Version bump complete!',
|
||||
]);
|
||||
|
||||
expect(runObj.run).toHaveBeenCalledTimes(2);
|
||||
expect(runObj.run).toHaveBeenCalledWith('yarn', [
|
||||
'plugin',
|
||||
'import',
|
||||
'https://custom.example.com/v1/releases/1.5.0/yarn-plugin',
|
||||
]);
|
||||
expect(runObj.run).toHaveBeenCalledWith(
|
||||
'yarn',
|
||||
['install'],
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle missing manifest file when BACKSTAGE_MANIFEST_FILE is set', async () => {
|
||||
process.env.BACKSTAGE_MANIFEST_FILE = '/nonexistent/manifest.json';
|
||||
|
||||
mockDir.setContent({
|
||||
'yarn.lock': lockfileMock,
|
||||
'package.json': JSON.stringify({
|
||||
workspaces: {
|
||||
packages: ['packages/*'],
|
||||
},
|
||||
}),
|
||||
packages: {
|
||||
a: {
|
||||
'package.json': JSON.stringify({
|
||||
name: 'a',
|
||||
dependencies: {
|
||||
'@backstage/core': '^1.0.5',
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await expect(
|
||||
bump({ pattern: null, release: 'main' } as unknown as Command),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('should handle network errors when using custom base URL', async () => {
|
||||
process.env.BACKSTAGE_MANIFEST_BASE_URL = 'https://custom.example.com';
|
||||
|
||||
mockDir.setContent({
|
||||
'yarn.lock': lockfileMock,
|
||||
'package.json': JSON.stringify({
|
||||
workspaces: {
|
||||
packages: ['packages/*'],
|
||||
},
|
||||
}),
|
||||
packages: {
|
||||
a: {
|
||||
'package.json': JSON.stringify({
|
||||
name: 'a',
|
||||
dependencies: {
|
||||
'@backstage/core': '^1.0.5',
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
worker.use(
|
||||
rest.get(
|
||||
'https://custom.example.com/v1/tags/main/manifest.json',
|
||||
(_, res, ctx) => res(ctx.status(500), ctx.json({})),
|
||||
),
|
||||
);
|
||||
|
||||
await expect(
|
||||
bump({ pattern: null, release: 'main' } as unknown as Command),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
maybeBootstrapProxy();
|
||||
|
||||
import { env } from 'process';
|
||||
import fs from 'fs-extra';
|
||||
import chalk from 'chalk';
|
||||
import semver from 'semver';
|
||||
@@ -26,9 +27,9 @@ import { isError, NotFoundError } from '@backstage/errors';
|
||||
import { resolve as resolvePath } from 'path';
|
||||
import { paths } from '../../../../lib/paths';
|
||||
import {
|
||||
mapDependencies,
|
||||
fetchPackageInfo,
|
||||
Lockfile,
|
||||
mapDependencies,
|
||||
YarnInfoInspectData,
|
||||
} from '../../../../lib/versioning';
|
||||
import { BACKSTAGE_JSON } from '@backstage/cli-common';
|
||||
@@ -96,8 +97,14 @@ export default async (opts: OptionValues) => {
|
||||
|
||||
let findTargetVersion: (name: string) => Promise<string>;
|
||||
let releaseManifest: ReleaseManifest;
|
||||
// Specific release specified. Be strict when resolving versions
|
||||
if (semver.valid(opts.release)) {
|
||||
if (env.BACKSTAGE_MANIFEST_FILE) {
|
||||
// Use specific manifest file if provided
|
||||
releaseManifest = await fs.readJson(env.BACKSTAGE_MANIFEST_FILE);
|
||||
findTargetVersion = createStrictVersionFinder({
|
||||
releaseManifest,
|
||||
});
|
||||
} else if (semver.valid(opts.release)) {
|
||||
// Specific release specified. Be strict when resolving versions
|
||||
releaseManifest = await getManifestByVersion({ version: opts.release });
|
||||
findTargetVersion = createStrictVersionFinder({
|
||||
releaseManifest,
|
||||
@@ -107,9 +114,11 @@ export default async (opts: OptionValues) => {
|
||||
if (opts.release === 'next') {
|
||||
const next = await getManifestByReleaseLine({
|
||||
releaseLine: 'next',
|
||||
versionsBaseUrl: env.BACKSTAGE_MANIFEST_BASE_URL,
|
||||
});
|
||||
const main = await getManifestByReleaseLine({
|
||||
releaseLine: 'main',
|
||||
versionsBaseUrl: env.BACKSTAGE_MANIFEST_BASE_URL,
|
||||
});
|
||||
// Prefer manifest with the latest release version
|
||||
releaseManifest = semver.gt(next.releaseVersion, main.releaseVersion)
|
||||
@@ -118,6 +127,7 @@ export default async (opts: OptionValues) => {
|
||||
} else {
|
||||
releaseManifest = await getManifestByReleaseLine({
|
||||
releaseLine: opts.release,
|
||||
versionsBaseUrl: env.BACKSTAGE_MANIFEST_BASE_URL,
|
||||
});
|
||||
}
|
||||
findTargetVersion = createVersionFinder({
|
||||
@@ -132,11 +142,12 @@ export default async (opts: OptionValues) => {
|
||||
`Updating yarn plugin to v${releaseManifest.releaseVersion}...`,
|
||||
);
|
||||
console.log();
|
||||
await run('yarn', [
|
||||
'plugin',
|
||||
'import',
|
||||
`https://versions.backstage.io/v1/releases/${releaseManifest.releaseVersion}/yarn-plugin`,
|
||||
]);
|
||||
|
||||
const yarnPluginUrl = env.BACKSTAGE_MANIFEST_BASE_URL
|
||||
? `${env.BACKSTAGE_MANIFEST_BASE_URL}/v1/releases/${releaseManifest.releaseVersion}/yarn-plugin`
|
||||
: `https://versions.backstage.io/v1/releases/${releaseManifest.releaseVersion}/yarn-plugin`;
|
||||
|
||||
await run('yarn', ['plugin', 'import', yarnPluginUrl]);
|
||||
console.log();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user