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:
Zack Griesinger
2021-12-27 14:50:46 -06:00
parent 62b3cea48d
commit d9ea06a8b5
6 changed files with 167 additions and 18 deletions
+2
View File
@@ -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",
+2
View File
@@ -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)));
+142 -4
View File
@@ -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',
+14 -9
View File
@@ -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',
+3 -4
View File
@@ -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({