cli: update versions:bump to install new deps for accepted version ranges

This commit is contained in:
Patrik Oldsberg
2020-11-25 10:33:45 +01:00
parent a7974a6769
commit f538e2c560
5 changed files with 86 additions and 34 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/cli': patch
---
Make versions:bump install new versions of dependencies that were within the specified range
+19 -12
View File
@@ -24,7 +24,7 @@ import bump from './bump';
import { withLogCollector } from '@backstage/test-utils';
const REGISTRY_VERSIONS: { [name: string]: string } = {
'@backstage/core': '1.0.7',
'@backstage/core': '1.0.6',
'@backstage/theme': '2.0.0',
};
@@ -36,30 +36,36 @@ const HEADER = `# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
const lockfileMock = `${HEADER}
"@backstage/core@^1.0.5":
version "1.0.6"
resolved "https://my-registry/a-1.0.01.tgz#abc123"
integrity sha512-xyz
dependencies:
"@backstage/core-api" "^1.0.6"
"@backstage/core@^1.0.3":
version "1.0.3"
resolved "https://my-registry/a-1.0.01.tgz#abc123"
integrity sha512-xyz
dependencies:
"@backstage/core-api" "^1.0.3"
"@backstage/theme@^1.0.0":
version "1.0.0"
resolved "https://my-registry/a-1.0.01.tgz#abc123"
integrity sha512-xyz
"@backstage/core-api@^1.0.6":
version "1.0.6"
"@backstage/core-api@^1.0.3":
version "1.0.3"
`;
// This resulting lockfile isn't a real world example, since it doesn't include the package bumps
const lockfileMockResult = `${HEADER}
"@backstage/core@^1.0.3", "@backstage/core@^1.0.5":
"@backstage/core-api@^1.0.3", "@backstage/core-api@^1.0.6":
version "1.0.6"
resolved "https://my-registry/a-1.0.01.tgz#abc123"
integrity sha512-xyz
"@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"
resolved "https://my-registry/a-1.0.01.tgz#abc123"
integrity sha512-xyz
`;
describe('bump', () => {
@@ -116,6 +122,7 @@ describe('bump', () => {
'Checking for updates of @backstage/theme',
'Checking for updates of @backstage/core',
'Some packages are outdated, updating',
'Removing lockfile entry for @backstage/core@^1.0.3 to bump to 1.0.6',
'Bumping @backstage/theme in b to ^2.0.0',
"Running 'yarn install' to install new versions",
'Removing duplicate dependencies from yarn.lock',
+40 -19
View File
@@ -24,6 +24,7 @@ import {
fetchPackageInfo,
Lockfile,
} from '../../lib/versioning';
import { includedFilter, forbiddenDuplicatesFilter } from './lint';
const DEP_TYPES = [
'dependencies',
@@ -39,12 +40,16 @@ type PkgVersionInfo = {
};
export default async () => {
const lockfilePath = paths.resolveTargetRoot('yarn.lock');
// First we discover all Backstage dependencies within our own repo
const dependencyMap = await mapDependencies(paths.targetDir);
// Next check with the package registry what the latest version of all of those dependencies are
const targetVersions = new Map<string, string>();
await workerThreads(16, dependencyMap.keys(), async name => {
// Next check with the package registry to see which dependency ranges we need to bump
const versionBumps = new Map<string, PkgVersionInfo[]>();
// Track package versions that we want to remove from yarn.lock in order to trigger a bump
const unlocked = Array<{ name: string; range: string; latest: string }>();
await workerThreads(16, dependencyMap.entries(), async ([name, pkgs]) => {
console.log(`Checking for updates of ${name}`);
const info = await fetchPackageInfo(name);
const latest = info['dist-tags'].latest;
@@ -52,38 +57,49 @@ export default async () => {
throw new Error(`No latest version found for ${name}`);
}
targetVersions.set(name, latest);
});
// Then figure out which local packages need to have their dependencies bumped
const versionBumps = new Map<string, PkgVersionInfo[]>();
for (const [name, pkgs] of dependencyMap) {
const targetVersion = targetVersions.get(name)!;
for (const pkg of pkgs) {
if (semver.satisfies(targetVersion, pkg.range)) {
if (semver.satisfies(latest, pkg.range)) {
if (semver.minVersion(pkg.range)?.version !== latest) {
unlocked.push({ name, range: pkg.range, latest });
}
continue;
}
versionBumps.set(
pkg.name,
(versionBumps.get(pkg.name) ?? []).concat({
name,
location: pkg.location,
range: `^${targetVersion}`, // TODO(Rugvip): Option to use something else than ^?
range: `^${latest}`, // TODO(Rugvip): Option to use something else than ^?
}),
);
}
}
});
console.log();
// Write all discovered version bumps to package.json in this repo
if (versionBumps.size === 0) {
if (versionBumps.size === 0 && unlocked.length === 0) {
console.log('All Backstage packages are up to date!');
} else {
console.log('Some packages are outdated, updating');
console.log();
if (unlocked.length > 0) {
const lockfile = await Lockfile.load(lockfilePath);
for (const { name, range, latest } of unlocked) {
// Don't bother removing lockfile entries if they're already on the correct version
const existingEntry = lockfile.get(name)?.find(e => e.range === range);
if (existingEntry?.version === latest) {
continue;
}
console.log(
`Removing lockfile entry for ${name}@${range} to bump to ${latest}`,
);
lockfile.remove(name, range);
}
await lockfile.save();
}
await workerThreads(16, versionBumps.entries(), async ([name, deps]) => {
const pkgPath = resolvePath(deps[0].location, 'package.json');
const pkgJson = await fs.readJson(pkgPath);
@@ -110,9 +126,9 @@ export default async () => {
console.log();
// Finally we make sure the new lockfile doesn't have any duplicates
const lockfile = await Lockfile.load(paths.resolveTargetRoot('yarn.lock'));
const lockfile = await Lockfile.load(lockfilePath);
const result = lockfile.analyze({
filter: name => dependencyMap.has(name),
filter: includedFilter,
});
if (result.newVersions.length > 0) {
@@ -130,9 +146,14 @@ export default async () => {
console.log();
}
if (result.newRanges.length > 0) {
const forbiddenNewRanges = result.newRanges.filter(({ name }) =>
forbiddenDuplicatesFilter(name),
);
if (forbiddenNewRanges.length > 0) {
throw new Error(
`Version bump failed for ${result.newRanges.map(i => i.name).join(', ')}`,
`Version bump failed for ${forbiddenNewRanges
.map(i => i.name)
.join(', ')}`,
);
}
};
+9 -3
View File
@@ -22,6 +22,9 @@ import partition from 'lodash/partition';
// Packages that we try to avoid duplicates for
const INCLUDED = [/^@backstage\//];
export const includedFilter = (name: string) =>
INCLUDED.some(pattern => pattern.test(name));
// Packages that are not allowed to have any duplicates
const FORBID_DUPLICATES = [
/^@backstage\/core$/,
@@ -29,6 +32,9 @@ const FORBID_DUPLICATES = [
/^@backstage\/plugin-/,
];
export const forbiddenDuplicatesFilter = (name: string) =>
FORBID_DUPLICATES.some(pattern => pattern.test(name));
export default async (cmd: Command) => {
const fix = Boolean(cmd.fix);
@@ -36,7 +42,7 @@ export default async (cmd: Command) => {
const lockfile = await Lockfile.load(paths.resolveTargetRoot('yarn.lock'));
const result = lockfile.analyze({
filter: name => INCLUDED.some(pattern => pattern.test(name)),
filter: includedFilter,
});
logArray(
@@ -53,7 +59,7 @@ export default async (cmd: Command) => {
newVersionsForbidden,
newVersionsAllowed,
] = partition(result.newVersions, ({ name }) =>
FORBID_DUPLICATES.some(pattern => pattern.test(name)),
forbiddenDuplicatesFilter(name),
);
if (newVersionsForbidden.length && !fix) {
success = false;
@@ -75,7 +81,7 @@ export default async (cmd: Command) => {
const [newRangesForbidden, newRangesAllowed] = partition(
result.newRanges,
({ name }) => FORBID_DUPLICATES.some(pattern => pattern.test(name)),
({ name }) => forbiddenDuplicatesFilter(name),
);
if (newRangesForbidden.length) {
success = false;
@@ -202,6 +202,19 @@ export class Lockfile {
return result;
}
remove(name: string, range: string): boolean {
const query = `${name}@${range}`;
const existed = Boolean(this.data[query]);
delete this.data[query];
const newEntries = this.packages.get(name)?.filter(e => e.range !== range);
if (newEntries) {
this.packages.set(name, newEntries);
}
return existed;
}
/** Modifies the lockfile by bumping packages to the suggested versions */
replaceVersions(results: AnalyzeResultNewVersion[]) {
for (const { name, range, oldVersion, newVersion } of results) {