Update CLI to allow passing in a prefix glob for versioning
Signed-off-by: Zack Griesinger <zack.griesinger@c2fo.com>
This commit is contained in:
@@ -83,6 +83,7 @@
|
||||
"json-schema": "^0.4.0",
|
||||
"jest-transform-yaml": "^0.1.1",
|
||||
"lodash": "^4.17.21",
|
||||
"minimatch": "3.0.4",
|
||||
"mini-css-extract-plugin": "^2.4.2",
|
||||
"node-libs-browser": "^2.2.1",
|
||||
"ora": "^5.3.0",
|
||||
@@ -127,6 +128,7 @@
|
||||
"@types/fs-extra": "^9.0.1",
|
||||
"@types/http-proxy": "^1.17.4",
|
||||
"@types/inquirer": "^8.1.3",
|
||||
"@types/minimatch": "^3.0.5",
|
||||
"@types/mock-fs": "^4.13.0",
|
||||
"@types/node": "^14.14.32",
|
||||
"@types/recursive-readdir": "^2.2.0",
|
||||
|
||||
@@ -214,11 +214,13 @@ export function registerCommands(program: CommanderStatic) {
|
||||
|
||||
program
|
||||
.command('versions:bump')
|
||||
.option('--prefix', 'Override glob for matching packages to upgrade')
|
||||
.description('Bump Backstage packages to the latest versions')
|
||||
.action(lazy(() => import('./versions/bump').then(m => m.default)));
|
||||
|
||||
program
|
||||
.command('versions:check')
|
||||
.option('--prefix <glob>', 'Override glob for matching packages to upgrade')
|
||||
.option('--fix', 'Fix any auto-fixable versioning problems')
|
||||
.description('Check Backstage package versioning')
|
||||
.action(lazy(() => import('./versions/lint').then(m => m.default)));
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
import fs from 'fs-extra';
|
||||
import mockFs from 'mock-fs';
|
||||
import { Command } from 'commander';
|
||||
import { resolve as resolvePath } from 'path';
|
||||
import { paths } from '../../lib/paths';
|
||||
import { mapDependencies } from '../../lib/versioning';
|
||||
@@ -36,6 +37,8 @@ const REGISTRY_VERSIONS: { [name: string]: string } = {
|
||||
'@backstage/core': '1.0.6',
|
||||
'@backstage/core-api': '1.0.7',
|
||||
'@backstage/theme': '2.0.0',
|
||||
'@backstage-extra/custom': '1.1.0',
|
||||
'@backstage-extra/custom-two': '2.0.0',
|
||||
};
|
||||
|
||||
const HEADER = `# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
@@ -83,7 +86,7 @@ describe('bump', () => {
|
||||
|
||||
it('should bump backstage dependencies', async () => {
|
||||
// Make sure all modules involved in package discovery are in the module cache before we mock fs
|
||||
await mapDependencies(paths.targetDir);
|
||||
await mapDependencies(paths.targetDir, '@backstage/*');
|
||||
|
||||
mockFs({
|
||||
'/yarn.lock': lockfileMock,
|
||||
@@ -123,7 +126,7 @@ describe('bump', () => {
|
||||
jest.spyOn(runObj, 'run').mockResolvedValue(undefined);
|
||||
|
||||
const { log: logs } = await withLogCollector(['log'], async () => {
|
||||
await bump();
|
||||
await bump({ prefix: null } as Command);
|
||||
});
|
||||
expect(logs.filter(Boolean)).toEqual([
|
||||
'Checking for updates of @backstage/theme',
|
||||
@@ -178,9 +181,144 @@ describe('bump', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should bump backstage dependencies and dependencies matching prefix glob', async () => {
|
||||
// Make sure all modules involved in package discovery are in the module cache before we mock fs
|
||||
await mapDependencies(paths.targetDir, '@backstage/*');
|
||||
const customLockfileMock = `${lockfileMock}
|
||||
"@backstage-extra/custom@^1.1.0":
|
||||
version "1.1.0"
|
||||
|
||||
"@backstage-extra/custom@^1.0.1":
|
||||
version "1.0.1"
|
||||
|
||||
"@backstage-extra/custom-two@^1.0.0":
|
||||
version "1.0.0"
|
||||
`;
|
||||
const customLockfileMockResult = `${HEADER}
|
||||
"@backstage-extra/custom-two@^1.0.0":
|
||||
version "1.0.0"
|
||||
|
||||
"@backstage-extra/custom@^1.1.0":
|
||||
version "1.1.0"
|
||||
|
||||
"@backstage/core@^1.0.5":
|
||||
version "1.0.6"
|
||||
dependencies:
|
||||
"@backstage/core-api" "^1.0.6"
|
||||
|
||||
"@backstage/theme@^1.0.0":
|
||||
version "1.0.0"
|
||||
`;
|
||||
mockFs({
|
||||
'/yarn.lock': customLockfileMock,
|
||||
'/lerna.json': JSON.stringify({
|
||||
packages: ['packages/*'],
|
||||
}),
|
||||
'/packages/a/package.json': JSON.stringify({
|
||||
name: 'a',
|
||||
dependencies: {
|
||||
'@backstage/core': '^1.0.5',
|
||||
'@backstage-extra/custom': '^1.0.1',
|
||||
'@backstage-extra/custom-two': '^1.0.0',
|
||||
},
|
||||
}),
|
||||
'/packages/b/package.json': JSON.stringify({
|
||||
name: 'b',
|
||||
dependencies: {
|
||||
'@backstage/core': '^1.0.3',
|
||||
'@backstage/theme': '^1.0.0',
|
||||
'@backstage-extra/custom': '^1.1.0',
|
||||
'@backstage-extra/custom-two': '^1.0.0',
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
paths.targetDir = '/';
|
||||
jest
|
||||
.spyOn(paths, 'resolveTargetRoot')
|
||||
.mockImplementation((...path) => resolvePath('/', ...path));
|
||||
jest.spyOn(runObj, 'runPlain').mockImplementation(async (...[, , , name]) =>
|
||||
JSON.stringify({
|
||||
type: 'inspect',
|
||||
data: {
|
||||
name: name,
|
||||
'dist-tags': {
|
||||
latest: REGISTRY_VERSIONS[name],
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
jest.spyOn(runObj, 'run').mockResolvedValue(undefined);
|
||||
|
||||
const { log: logs } = await withLogCollector(['log'], async () => {
|
||||
await bump({ prefix: '@{backstage,backstage-extra}/*' } as any);
|
||||
});
|
||||
expect(logs.filter(Boolean)).toEqual([
|
||||
'Checking for updates of @backstage/theme',
|
||||
'Checking for updates of @backstage-extra/custom-two',
|
||||
'Checking for updates of @backstage-extra/custom',
|
||||
'Checking for updates of @backstage/core',
|
||||
'Checking for updates of @backstage/core-api',
|
||||
'Some packages are outdated, updating',
|
||||
'unlocking @backstage-extra/custom@^1.0.1 ~> 1.1.0',
|
||||
'unlocking @backstage/core@^1.0.3 ~> 1.0.6',
|
||||
'unlocking @backstage/core-api@^1.0.6 ~> 1.0.7',
|
||||
'unlocking @backstage/core-api@^1.0.3 ~> 1.0.7',
|
||||
'bumping @backstage-extra/custom-two in a to ^2.0.0',
|
||||
'bumping @backstage/theme in b to ^2.0.0',
|
||||
'bumping @backstage-extra/custom-two in b to ^2.0.0',
|
||||
'Running yarn install to install new versions',
|
||||
'⚠️ The following packages may have breaking changes:',
|
||||
' @backstage-extra/custom-two : 1.0.0 ~> 2.0.0',
|
||||
' @backstage/theme : 1.0.0 ~> 2.0.0',
|
||||
' https://github.com/backstage/backstage/blob/master/packages/theme/CHANGELOG.md',
|
||||
'Version bump complete!',
|
||||
]);
|
||||
|
||||
expect(runObj.runPlain).toHaveBeenCalledTimes(6);
|
||||
expect(runObj.runPlain).toHaveBeenCalledWith(
|
||||
'yarn',
|
||||
'info',
|
||||
'--json',
|
||||
'@backstage/core',
|
||||
);
|
||||
expect(runObj.runPlain).toHaveBeenCalledWith(
|
||||
'yarn',
|
||||
'info',
|
||||
'--json',
|
||||
'@backstage/theme',
|
||||
);
|
||||
|
||||
expect(runObj.run).toHaveBeenCalledTimes(1);
|
||||
expect(runObj.run).toHaveBeenCalledWith('yarn', ['install']);
|
||||
|
||||
const lockfileContents = await fs.readFile('/yarn.lock', 'utf8');
|
||||
expect(lockfileContents).toEqual(customLockfileMockResult);
|
||||
|
||||
const packageA = await fs.readJson('/packages/a/package.json');
|
||||
expect(packageA).toEqual({
|
||||
name: 'a',
|
||||
dependencies: {
|
||||
'@backstage-extra/custom': '^1.0.1',
|
||||
'@backstage-extra/custom-two': '^2.0.0',
|
||||
'@backstage/core': '^1.0.5', // not bumped since new version is within range
|
||||
},
|
||||
});
|
||||
const packageB = await fs.readJson('/packages/b/package.json');
|
||||
expect(packageB).toEqual({
|
||||
name: 'b',
|
||||
dependencies: {
|
||||
'@backstage-extra/custom': '^1.1.0',
|
||||
'@backstage-extra/custom-two': '^2.0.0',
|
||||
'@backstage/core': '^1.0.3', // not bumped
|
||||
'@backstage/theme': '^2.0.0', // bumped since newer
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should ignore not found packages', async () => {
|
||||
// Make sure all modules involved in package discovery are in the module cache before we mock fs
|
||||
await mapDependencies(paths.targetDir);
|
||||
await mapDependencies(paths.targetDir, '@backstage/*');
|
||||
mockFs({
|
||||
'/yarn.lock': lockfileMockResult,
|
||||
'/lerna.json': JSON.stringify({
|
||||
@@ -209,7 +347,7 @@ describe('bump', () => {
|
||||
jest.spyOn(runObj, 'run').mockResolvedValue(undefined);
|
||||
|
||||
const { log: logs } = await withLogCollector(['log'], async () => {
|
||||
await bump();
|
||||
await bump({ prefix: null } as any);
|
||||
});
|
||||
expect(logs.filter(Boolean)).toEqual([
|
||||
'Checking for updates of @backstage/theme',
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
import fs from 'fs-extra';
|
||||
import chalk from 'chalk';
|
||||
import semver from 'semver';
|
||||
import minimatch from 'minimatch';
|
||||
import { Command } from 'commander';
|
||||
import { isError } from '@backstage/errors';
|
||||
import { resolve as resolvePath } from 'path';
|
||||
import { run } from '../../lib/run';
|
||||
@@ -26,7 +28,7 @@ import {
|
||||
fetchPackageInfo,
|
||||
Lockfile,
|
||||
} from '../../lib/versioning';
|
||||
import { includedFilter, forbiddenDuplicatesFilter } from './lint';
|
||||
import { forbiddenDuplicatesFilter } from './lint';
|
||||
import { BACKSTAGE_JSON } from '@backstage/cli-common';
|
||||
|
||||
const DEP_TYPES = [
|
||||
@@ -43,14 +45,19 @@ type PkgVersionInfo = {
|
||||
location: string;
|
||||
};
|
||||
|
||||
export default async () => {
|
||||
export default async (cmd: Command) => {
|
||||
const lockfilePath = paths.resolveTargetRoot('yarn.lock');
|
||||
const lockfile = await Lockfile.load(lockfilePath);
|
||||
let prefix = cmd.prefix;
|
||||
|
||||
if (!prefix) {
|
||||
prefix = '@backstage/*';
|
||||
}
|
||||
|
||||
const findTargetVersion = createVersionFinder();
|
||||
|
||||
// First we discover all Backstage dependencies within our own repo
|
||||
const dependencyMap = await mapDependencies(paths.targetDir);
|
||||
const dependencyMap = await mapDependencies(paths.targetDir, prefix);
|
||||
|
||||
// Next check with the package registry to see which dependency ranges we need to bump
|
||||
const versionBumps = new Map<string, PkgVersionInfo[]>();
|
||||
@@ -88,13 +95,11 @@ export default async () => {
|
||||
}
|
||||
});
|
||||
|
||||
// Only check @backstage packages and friends, we don't want this to do a full update of all deps
|
||||
const filter = (name: string) => minimatch(name, prefix);
|
||||
|
||||
// Check for updates of transitive backstage dependencies
|
||||
await workerThreads(16, lockfile.keys(), async name => {
|
||||
// Only check @backstage packages and friends, we don't want this to do a full update of all deps
|
||||
if (!includedFilter(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let target: string;
|
||||
try {
|
||||
target = await findTargetVersion(name);
|
||||
@@ -237,7 +242,7 @@ export default async () => {
|
||||
// Finally we make sure the new lockfile doesn't have any duplicates
|
||||
const dedupLockfile = await Lockfile.load(lockfilePath);
|
||||
const result = dedupLockfile.analyze({
|
||||
filter: includedFilter,
|
||||
filter,
|
||||
});
|
||||
|
||||
if (result.newVersions.length > 0) {
|
||||
|
||||
@@ -82,7 +82,10 @@ describe('mapDependencies', () => {
|
||||
}),
|
||||
});
|
||||
|
||||
const dependencyMap = await mapDependencies(paths.targetDir);
|
||||
const dependencyMap = await mapDependencies(
|
||||
paths.targetDir,
|
||||
'@backstage/*',
|
||||
);
|
||||
expect(Array.from(dependencyMap)).toEqual([
|
||||
[
|
||||
'@backstage/core',
|
||||
|
||||
@@ -13,12 +13,10 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import minimatch from 'minimatch';
|
||||
import { runPlain } from '../../lib/run';
|
||||
import { NotFoundError } from '../errors';
|
||||
|
||||
const PREFIX = '@backstage';
|
||||
|
||||
const DEP_TYPES = [
|
||||
'dependencies',
|
||||
'devDependencies',
|
||||
@@ -66,6 +64,7 @@ export async function fetchPackageInfo(
|
||||
/** Map all dependencies in the repo as dependency => dependents */
|
||||
export async function mapDependencies(
|
||||
targetDir: string,
|
||||
prefixGlob: string,
|
||||
): Promise<Map<string, PkgVersionInfo[]>> {
|
||||
const { Project } = require('@lerna/project');
|
||||
const project = new Project(targetDir);
|
||||
@@ -78,7 +77,7 @@ export async function mapDependencies(
|
||||
);
|
||||
|
||||
for (const [name, range] of deps) {
|
||||
if (name.startsWith(PREFIX)) {
|
||||
if (minimatch(name, prefixGlob)) {
|
||||
dependencyMap.set(
|
||||
name,
|
||||
(dependencyMap.get(name) ?? []).concat({
|
||||
|
||||
Reference in New Issue
Block a user