Merge pull request #23712 from backstage/philipph/moved-plugins-bump
CLI: Reference moved plugins `@backstage-community` scope
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/backend-dynamic-feature-service': patch
|
||||
---
|
||||
|
||||
Use `PackageRole` type explicitly
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/cli-node': patch
|
||||
'@backstage/cli': patch
|
||||
---
|
||||
|
||||
Added `versions:migrate` command to help move packages to the new `@backstage-community` namespace
|
||||
@@ -240,7 +240,9 @@ export interface NewBackendPluginInstaller {
|
||||
export type ScannedPluginManifest = BackstagePackageJson &
|
||||
Required<Pick<BackstagePackageJson, 'main'>> &
|
||||
Required<Pick<BackstagePackageJson, 'backstage'>> & {
|
||||
backstage: Required<BackstagePackageJson['backstage']>;
|
||||
backstage: {
|
||||
role: PackageRole;
|
||||
};
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { BackstagePackageJson } from '@backstage/cli-node';
|
||||
import { BackstagePackageJson, PackageRole } from '@backstage/cli-node';
|
||||
|
||||
/**
|
||||
* @public
|
||||
@@ -30,5 +30,7 @@ export interface ScannedPluginPackage {
|
||||
export type ScannedPluginManifest = BackstagePackageJson &
|
||||
Required<Pick<BackstagePackageJson, 'main'>> &
|
||||
Required<Pick<BackstagePackageJson, 'backstage'>> & {
|
||||
backstage: Required<BackstagePackageJson['backstage']>;
|
||||
backstage: {
|
||||
role: PackageRole;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -17,6 +17,7 @@ export interface BackstagePackageJson {
|
||||
// (undocumented)
|
||||
backstage?: {
|
||||
role?: PackageRole;
|
||||
moved?: string;
|
||||
};
|
||||
// (undocumented)
|
||||
bundled?: boolean;
|
||||
|
||||
@@ -45,6 +45,7 @@ export interface BackstagePackageJson {
|
||||
|
||||
backstage?: {
|
||||
role?: PackageRole;
|
||||
moved?: string;
|
||||
};
|
||||
|
||||
exports?: JsonValue;
|
||||
@@ -137,6 +138,7 @@ export class PackageGraph extends Map<string, PackageGraphNode> {
|
||||
*/
|
||||
static async listTargetPackages(): Promise<BackstagePackage[]> {
|
||||
const { packages } = await getPackages(paths.targetDir);
|
||||
|
||||
return packages as BackstagePackage[];
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ Commands:
|
||||
migrate [command]
|
||||
versions:bump [options]
|
||||
versions:check [options]
|
||||
versions:migrate [options]
|
||||
clean
|
||||
build-workspace [options] <workspace-dir> [packages...]
|
||||
create-github-app <github-org>
|
||||
@@ -600,6 +601,7 @@ Options:
|
||||
--pattern <glob>
|
||||
--release <version|next|main>
|
||||
--skip-install
|
||||
--skip-migrate
|
||||
-h, --help
|
||||
```
|
||||
|
||||
@@ -612,3 +614,13 @@ Options:
|
||||
--fix
|
||||
-h, --help
|
||||
```
|
||||
|
||||
### `backstage-cli versions:migrate`
|
||||
|
||||
```
|
||||
Usage: backstage-cli versions:migrate [options]
|
||||
|
||||
Options:
|
||||
--pattern <glob>
|
||||
-h, --help
|
||||
```
|
||||
|
||||
@@ -386,6 +386,7 @@ export function registerCommands(program: Command) {
|
||||
'main',
|
||||
)
|
||||
.option('--skip-install', 'Skips yarn install step')
|
||||
.option('--skip-migrate', 'Skips migration of any moved packages')
|
||||
.description('Bump Backstage packages to the latest versions')
|
||||
.action(lazy(() => import('./versions/bump').then(m => m.default)));
|
||||
|
||||
@@ -395,6 +396,17 @@ export function registerCommands(program: Command) {
|
||||
.description('Check Backstage package versioning')
|
||||
.action(lazy(() => import('./versions/lint').then(m => m.default)));
|
||||
|
||||
program
|
||||
.command('versions:migrate')
|
||||
.option(
|
||||
'--pattern <glob>',
|
||||
'Override glob for matching packages to upgrade',
|
||||
)
|
||||
.description(
|
||||
'Migrate any plugins that have been moved to the @backstage-community namespace automatically',
|
||||
)
|
||||
.action(lazy(() => import('./versions/migrate').then(m => m.default)));
|
||||
|
||||
// TODO(Rugvip): Deprecate in favor of package variant
|
||||
program
|
||||
.command('clean')
|
||||
|
||||
@@ -58,16 +58,16 @@ jest.mock('ora', () => ({
|
||||
}));
|
||||
|
||||
let mockDir: MockDirectory;
|
||||
|
||||
jest.mock('../../lib/paths', () => ({
|
||||
paths: {
|
||||
jest.mock('@backstage/cli-common', () => ({
|
||||
...jest.requireActual('@backstage/cli-common'),
|
||||
findPaths: () => ({
|
||||
resolveTargetRoot(filename: string) {
|
||||
return mockDir.resolve(filename);
|
||||
},
|
||||
get targetDir() {
|
||||
return mockDir.path;
|
||||
},
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('../../lib/run', () => {
|
||||
@@ -200,7 +200,7 @@ describe('bump', () => {
|
||||
),
|
||||
),
|
||||
);
|
||||
const { log: logs } = await withLogCollector(['log'], async () => {
|
||||
const { log: logs } = await withLogCollector(['log', 'warn'], async () => {
|
||||
await bump({ pattern: null, release: 'main' } as unknown as Command);
|
||||
});
|
||||
expectLogsToMatch(logs, [
|
||||
@@ -303,7 +303,7 @@ describe('bump', () => {
|
||||
),
|
||||
),
|
||||
);
|
||||
const { log: logs } = await withLogCollector(['log'], async () => {
|
||||
const { log: logs } = await withLogCollector(['log', 'warn'], async () => {
|
||||
await bump({
|
||||
pattern: null,
|
||||
release: 'main',
|
||||
@@ -419,7 +419,7 @@ describe('bump', () => {
|
||||
),
|
||||
),
|
||||
);
|
||||
const { log: logs } = await withLogCollector(['log'], async () => {
|
||||
const { log: logs } = await withLogCollector(['log', 'warn'], async () => {
|
||||
await bump({ pattern: null, release: 'main' } as unknown as Command);
|
||||
});
|
||||
expectLogsToMatch(logs, [
|
||||
@@ -517,7 +517,7 @@ describe('bump', () => {
|
||||
(_, res, ctx) => res(ctx.status(404), ctx.json({})),
|
||||
),
|
||||
);
|
||||
const { log: logs } = await withLogCollector(['log'], async () => {
|
||||
const { log: logs } = await withLogCollector(['log', 'warn'], async () => {
|
||||
await expect(
|
||||
bump({ pattern: null, release: '999.0.1' } as unknown as Command),
|
||||
).rejects.toThrow('No release found for 999.0.1 version');
|
||||
@@ -622,7 +622,7 @@ describe('bump', () => {
|
||||
),
|
||||
),
|
||||
);
|
||||
const { log: logs } = await withLogCollector(['log'], async () => {
|
||||
const { log: logs } = await withLogCollector(['log', 'warn'], async () => {
|
||||
await bump({ pattern: null, release: 'next' } as unknown as Command);
|
||||
});
|
||||
expectLogsToMatch(logs, [
|
||||
@@ -718,7 +718,7 @@ describe('bump', () => {
|
||||
),
|
||||
),
|
||||
);
|
||||
const { log: logs } = await withLogCollector(['log'], async () => {
|
||||
const { log: logs } = await withLogCollector(['log', 'warn'], async () => {
|
||||
await bump({
|
||||
pattern: '@{backstage,backstage-extra}/*',
|
||||
release: 'main',
|
||||
@@ -837,7 +837,7 @@ describe('bump', () => {
|
||||
),
|
||||
),
|
||||
);
|
||||
const { log: logs } = await withLogCollector(['log'], async () => {
|
||||
const { log: logs } = await withLogCollector(['log', 'warn'], async () => {
|
||||
await bump({ pattern: null, release: 'main' } as unknown as Command);
|
||||
});
|
||||
expectLogsToMatch(logs, [
|
||||
@@ -952,7 +952,7 @@ describe('bump', () => {
|
||||
),
|
||||
),
|
||||
);
|
||||
const { log: logs } = await withLogCollector(['log'], async () => {
|
||||
const { log: logs } = await withLogCollector(['log', 'warn'], async () => {
|
||||
await bump({ pattern: null, release: 'main' } as unknown as Command);
|
||||
});
|
||||
expectLogsToMatch(logs, [
|
||||
|
||||
@@ -40,6 +40,7 @@ import {
|
||||
} from '@backstage/release-manifests';
|
||||
import 'global-agent/bootstrap';
|
||||
import { PackageGraph } from '@backstage/cli-node';
|
||||
import { migrateMovedPackages } from './migrate';
|
||||
|
||||
const DEP_TYPES = [
|
||||
'dependencies',
|
||||
@@ -264,6 +265,15 @@ export default async (opts: OptionValues) => {
|
||||
console.log(chalk.yellow(`Skipping yarn install`));
|
||||
}
|
||||
|
||||
if (!opts.skipMigrate) {
|
||||
const changed = await migrateMovedPackages({
|
||||
pattern: opts.pattern,
|
||||
});
|
||||
if (changed && !opts.skipInstall) {
|
||||
await runYarnInstall();
|
||||
}
|
||||
}
|
||||
|
||||
if (breakingUpdates.size > 0) {
|
||||
console.log();
|
||||
console.log(
|
||||
@@ -470,7 +480,7 @@ export async function bumpBackstageJsonVersion(version: string) {
|
||||
);
|
||||
}
|
||||
|
||||
async function runYarnInstall() {
|
||||
export async function runYarnInstall() {
|
||||
const spinner = ora({
|
||||
prefixText: `Running ${chalk.blue('yarn install')} to install new versions`,
|
||||
spinner: 'arc',
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
/*
|
||||
* Copyright 2024 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 {
|
||||
MockDirectory,
|
||||
createMockDirectory,
|
||||
} from '@backstage/backend-test-utils';
|
||||
import * as run from '../../lib/run';
|
||||
import migrate from './migrate';
|
||||
import { withLogCollector } from '@backstage/test-utils';
|
||||
import fs from 'fs-extra';
|
||||
import { expectLogsToMatch } from '../../lib/new/factories/common/testUtils';
|
||||
|
||||
// Remove log coloring to simplify log matching
|
||||
jest.mock('chalk', () => ({
|
||||
red: (str: string) => str,
|
||||
blue: (str: string) => str,
|
||||
cyan: (str: string) => str,
|
||||
green: (str: string) => str,
|
||||
magenta: (str: string) => str,
|
||||
yellow: (str: string) => str,
|
||||
}));
|
||||
|
||||
let mockDir: MockDirectory;
|
||||
jest.mock('@backstage/cli-common', () => ({
|
||||
...jest.requireActual('@backstage/cli-common'),
|
||||
findPaths: () => ({
|
||||
resolveTargetRoot(filename: string) {
|
||||
return mockDir.resolve(filename);
|
||||
},
|
||||
get targetDir() {
|
||||
return mockDir.path;
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('../../lib/run', () => {
|
||||
return {
|
||||
run: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
describe('versions:migrate', () => {
|
||||
mockDir = createMockDirectory();
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should bump to the moved version when the package is moved', async () => {
|
||||
mockDir.setContent({
|
||||
'package.json': JSON.stringify({
|
||||
workspaces: {
|
||||
packages: ['packages/*'],
|
||||
},
|
||||
}),
|
||||
node_modules: {
|
||||
'@backstage': {
|
||||
custom: {
|
||||
'package.json': JSON.stringify({
|
||||
name: '@backstage-extra/custom',
|
||||
version: '1.0.1',
|
||||
backstage: {
|
||||
moved: '@backstage-community/custom',
|
||||
},
|
||||
}),
|
||||
},
|
||||
'custom-two': {
|
||||
'package.json': JSON.stringify({
|
||||
name: '@backstage-extra/custom-two',
|
||||
version: '1.0.0',
|
||||
backstage: {
|
||||
moved: '@backstage-community/custom-two',
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
packages: {
|
||||
a: {
|
||||
'package.json': JSON.stringify({
|
||||
name: 'a',
|
||||
dependencies: {
|
||||
'@backstage/core': '^1.0.5',
|
||||
'@backstage/custom': '^1.0.1',
|
||||
'@backstage/custom-two': '^1.0.0',
|
||||
},
|
||||
}),
|
||||
},
|
||||
b: {
|
||||
'package.json': JSON.stringify({
|
||||
name: 'b',
|
||||
dependencies: {
|
||||
'@backstage/core': '^1.0.3',
|
||||
'@backstage/theme': '^1.0.0',
|
||||
'@backstage/custom': '^1.1.0',
|
||||
'@backstage/custom-two': '^1.0.0',
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
jest.spyOn(run, 'run').mockResolvedValue(undefined);
|
||||
|
||||
const { warn, log: logs } = await withLogCollector(async () => {
|
||||
await migrate({});
|
||||
});
|
||||
|
||||
expectLogsToMatch(logs, [
|
||||
'Found a moved package @backstage/custom@^1.0.1 -> @backstage-community/custom in a (dependencies)',
|
||||
'Found a moved package @backstage/custom-two@^1.0.0 -> @backstage-community/custom-two in a (dependencies)',
|
||||
'Found a moved package @backstage/custom@^1.1.0 -> @backstage-community/custom in b (dependencies)',
|
||||
'Found a moved package @backstage/custom-two@^1.0.0 -> @backstage-community/custom-two in b (dependencies)',
|
||||
]);
|
||||
|
||||
expectLogsToMatch(warn, [
|
||||
'Could not find package.json for @backstage/core@^1.0.5 in a (dependencies)',
|
||||
'Could not find package.json for @backstage/core@^1.0.3 in b (dependencies)',
|
||||
'Could not find package.json for @backstage/theme@^1.0.0 in b (dependencies)',
|
||||
]);
|
||||
|
||||
expect(run.run).toHaveBeenCalledTimes(1);
|
||||
expect(run.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-community/custom': '^1.0.1',
|
||||
'@backstage-community/custom-two': '^1.0.0',
|
||||
'@backstage/core': '^1.0.5',
|
||||
},
|
||||
});
|
||||
const packageB = await fs.readJson(
|
||||
mockDir.resolve('packages/b/package.json'),
|
||||
);
|
||||
expect(packageB).toEqual({
|
||||
name: 'b',
|
||||
dependencies: {
|
||||
'@backstage-community/custom': '^1.1.0',
|
||||
'@backstage-community/custom-two': '^1.0.0',
|
||||
'@backstage/core': '^1.0.3',
|
||||
'@backstage/theme': '^1.0.0',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
* Copyright 2024 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 { BackstagePackageJson, PackageGraph } from '@backstage/cli-node';
|
||||
import chalk from 'chalk';
|
||||
import { resolve as resolvePath } from 'path';
|
||||
import { OptionValues } from 'commander';
|
||||
import { readJson, writeJson } from 'fs-extra';
|
||||
import { minimatch } from 'minimatch';
|
||||
import { runYarnInstall } from './bump';
|
||||
|
||||
export default async (options: OptionValues) => {
|
||||
const changed = await migrateMovedPackages({
|
||||
pattern: options.pattern,
|
||||
});
|
||||
|
||||
if (changed) {
|
||||
await runYarnInstall();
|
||||
}
|
||||
};
|
||||
|
||||
export async function migrateMovedPackages(options?: { pattern?: string }) {
|
||||
const packages = await PackageGraph.listTargetPackages();
|
||||
|
||||
const thingsThatHaveMoved = new Map<
|
||||
string,
|
||||
{
|
||||
dependencies: { [k: string]: string };
|
||||
devDependencies: { [k: string]: string };
|
||||
peerDependencies: { [k: string]: string };
|
||||
}
|
||||
>();
|
||||
|
||||
let didAnythingChange = false;
|
||||
|
||||
for (const pkg of packages) {
|
||||
const pkgName = pkg.packageJson.name;
|
||||
thingsThatHaveMoved.set(pkgName, {
|
||||
dependencies: {},
|
||||
devDependencies: {},
|
||||
peerDependencies: {},
|
||||
});
|
||||
|
||||
let didPackageChange = false;
|
||||
|
||||
for (const depType of [
|
||||
'dependencies',
|
||||
'devDependencies',
|
||||
'peerDependencies',
|
||||
] as const) {
|
||||
const depsObj = pkg.packageJson[depType];
|
||||
if (!depsObj) {
|
||||
continue;
|
||||
}
|
||||
for (const [depName, depVersion] of Object.entries(depsObj)) {
|
||||
if (options?.pattern && !minimatch(depName, options.pattern)) {
|
||||
continue;
|
||||
}
|
||||
let packageInfo: BackstagePackageJson;
|
||||
try {
|
||||
packageInfo = await readJson(
|
||||
require.resolve(`${depName}/package.json`, {
|
||||
paths: [pkg.dir],
|
||||
}),
|
||||
);
|
||||
} catch (ex) {
|
||||
console.warn(
|
||||
chalk.yellow(
|
||||
`Could not find package.json for ${depName}@${depVersion} in ${pkgName} (${depType})`,
|
||||
),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const movedPackageName = packageInfo.backstage?.moved;
|
||||
|
||||
if (movedPackageName) {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
`Found a moved package ${depName}@${depVersion} -> ${movedPackageName} in ${pkgName} (${depType})`,
|
||||
),
|
||||
);
|
||||
|
||||
didPackageChange = true;
|
||||
didAnythingChange = true;
|
||||
|
||||
depsObj[movedPackageName] = depsObj[depName];
|
||||
delete depsObj[depName];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (didPackageChange) {
|
||||
await writeJson(resolvePath(pkg.dir, 'package.json'), pkg.packageJson, {
|
||||
spaces: 2,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return didAnythingChange;
|
||||
}
|
||||
Reference in New Issue
Block a user