cli: make versions:bump properly bump the versions of all @backstage packages

This commit is contained in:
Patrik Oldsberg
2020-11-26 13:33:06 +01:00
parent 01ea1cc6cc
commit 6321746e20
4 changed files with 78 additions and 41 deletions
@@ -25,6 +25,7 @@ import { withLogCollector } from '@backstage/test-utils';
const REGISTRY_VERSIONS: { [name: string]: string } = {
'@backstage/core': '1.0.6',
'@backstage/core-api': '1.0.7',
'@backstage/theme': '2.0.0',
};
@@ -54,11 +55,8 @@ const lockfileMock = `${HEADER}
version "1.0.3"
`;
// This resulting lockfile isn't a real world example, since it doesn't include the package bumps
// This is the lockfile that we produce to unlock versions before we run yarn install
const lockfileMockResult = `${HEADER}
"@backstage/core-api@^1.0.3", "@backstage/core-api@^1.0.6":
version "1.0.6"
"@backstage/core@^1.0.5":
version "1.0.6"
dependencies:
@@ -121,15 +119,16 @@ describe('bump', () => {
expect(logs.filter(Boolean)).toEqual([
'Checking for updates of @backstage/theme',
'Checking for updates of @backstage/core',
'Checking for updates of @backstage/core-api',
'Some packages are outdated, updating',
'Removing lockfile entry for @backstage/core@^1.0.3 to bump to 1.0.6',
'Removing lockfile entry for @backstage/core-api@^1.0.6 to bump to 1.0.7',
'Removing lockfile entry for @backstage/core-api@^1.0.3 to bump to 1.0.7',
'Bumping @backstage/theme in b to ^2.0.0',
"Running 'yarn install' to install new versions",
'Removing duplicate dependencies from yarn.lock',
"Running 'yarn install' to remove duplicates from node_modules",
]);
expect(runObj.runPlain).toHaveBeenCalledTimes(2);
expect(runObj.runPlain).toHaveBeenCalledTimes(3);
expect(runObj.runPlain).toHaveBeenCalledWith(
'yarn',
'info',
@@ -143,7 +142,7 @@ describe('bump', () => {
'@backstage/theme',
);
expect(runObj.run).toHaveBeenCalledTimes(2);
expect(runObj.run).toHaveBeenCalledTimes(1);
expect(runObj.run).toHaveBeenCalledWith('yarn', ['install']);
const lockfileContents = await fs.readFile('/yarn.lock', 'utf8');
+64 -32
View File
@@ -41,6 +41,9 @@ type PkgVersionInfo = {
export default async () => {
const lockfilePath = paths.resolveTargetRoot('yarn.lock');
const lockfile = await Lockfile.load(lockfilePath);
const findTargetVersion = createVersionFinder();
// First we discover all Backstage dependencies within our own repo
const dependencyMap = await mapDependencies(paths.targetDir);
@@ -48,20 +51,16 @@ export default async () => {
// 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 }>();
const unlocked = Array<{ name: string; range: string; target: 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;
if (!latest) {
throw new Error(`No latest version found for ${name}`);
}
const target = await findTargetVersion(name);
for (const pkg of pkgs) {
if (semver.satisfies(latest, pkg.range)) {
if (semver.minVersion(pkg.range)?.version !== latest) {
unlocked.push({ name, range: pkg.range, latest });
if (semver.satisfies(target, pkg.range)) {
if (semver.minVersion(pkg.range)?.version !== target) {
unlocked.push({ name, range: pkg.range, target });
}
continue;
}
versionBumps.set(
@@ -69,12 +68,32 @@ export default async () => {
(versionBumps.get(pkg.name) ?? []).concat({
name,
location: pkg.location,
range: `^${latest}`, // TODO(Rugvip): Option to use something else than ^?
range: `^${target}`, // TODO(Rugvip): Option to use something else than ^?
}),
);
}
});
// 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;
}
const target = await findTargetVersion(name);
for (const entry of lockfile.get(name) ?? []) {
// Ignore lockfile entries that don't satisfy the version range, since
// these can't cause the package to be locked to an older version
if (!semver.satisfies(target, entry.range)) {
continue;
}
// Unlock all entries that are within range but on the old version
unlocked.push({ name, range: entry.range, target });
}
});
console.log();
// Write all discovered version bumps to package.json in this repo
@@ -85,17 +104,21 @@ export default async () => {
console.log();
if (unlocked.length > 0) {
const lockfile = await Lockfile.load(lockfilePath);
for (const { name, range, latest } of unlocked) {
const removed = new Set<string>();
for (const { name, range, target } 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) {
if (existingEntry?.version === target) {
continue;
}
console.log(
`Removing lockfile entry for ${name}@${range} to bump to ${latest}`,
);
lockfile.remove(name, range);
const key = JSON.stringify({ name, range });
if (!removed.has(key)) {
removed.add(key);
console.log(
`Removing lockfile entry for ${name}@${range} to bump to ${target}`,
);
lockfile.remove(name, range);
}
}
await lockfile.save();
}
@@ -126,24 +149,13 @@ export default async () => {
console.log();
// Finally we make sure the new lockfile doesn't have any duplicates
const lockfile = await Lockfile.load(lockfilePath);
const result = lockfile.analyze({
const dedupLockfile = await Lockfile.load(lockfilePath);
const result = dedupLockfile.analyze({
filter: includedFilter,
});
if (result.newVersions.length > 0) {
console.log();
console.log('Removing duplicate dependencies from yarn.lock');
lockfile.replaceVersions(result.newVersions);
await lockfile.save();
console.log(
"Running 'yarn install' to remove duplicates from node_modules",
);
console.log();
await run('yarn', ['install']);
console.log();
throw new Error('Duplicate versions present after package bump');
}
const forbiddenNewRanges = result.newRanges.filter(({ name }) =>
@@ -158,6 +170,26 @@ export default async () => {
}
};
function createVersionFinder() {
const found = new Map<string, string>();
return async function findTargetVersion(name: string) {
const existing = found.get(name);
if (existing) {
return existing;
}
console.log(`Checking for updates of ${name}`);
const info = await fetchPackageInfo(name);
const latest = info['dist-tags'].latest;
if (!latest) {
throw new Error(`No latest version found for ${name}`);
}
found.set(name, latest);
return latest;
};
}
async function workerThreads<T>(
count: number,
items: IterableIterator<T>,
@@ -100,10 +100,16 @@ export class Lockfile {
private readonly data: LockfileData,
) {}
/** Get the entries for a single package in the lockfile */
get(name: string): LockfileQueryEntry[] | undefined {
return this.packages.get(name);
}
/** Returns the name of all packages available in the lockfile */
keys(): IterableIterator<string> {
return this.packages.keys();
}
/** Analyzes the lockfile to identify possible actions and warnings for the entries */
analyze(options?: { filter?: (name: string) => boolean }): AnalyzeResult {
const { filter } = options ?? {};