cli: update versions:bump to install new deps for accepted version ranges
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/cli': patch
|
||||
---
|
||||
|
||||
Make versions:bump install new versions of dependencies that were within the specified range
|
||||
@@ -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',
|
||||
|
||||
@@ -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(', ')}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user