diff --git a/.changeset/long-birds-rush.md b/.changeset/long-birds-rush.md new file mode 100644 index 0000000000..db4da34789 --- /dev/null +++ b/.changeset/long-birds-rush.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Make versions:bump install new versions of dependencies that were within the specified range diff --git a/packages/cli/src/commands/versions/bump.test.ts b/packages/cli/src/commands/versions/bump.test.ts index 687708b074..f4d69c52be 100644 --- a/packages/cli/src/commands/versions/bump.test.ts +++ b/packages/cli/src/commands/versions/bump.test.ts @@ -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', diff --git a/packages/cli/src/commands/versions/bump.ts b/packages/cli/src/commands/versions/bump.ts index 00944c5db9..1883d87bac 100644 --- a/packages/cli/src/commands/versions/bump.ts +++ b/packages/cli/src/commands/versions/bump.ts @@ -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(); - 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(); + // 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(); - 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(', ')}`, ); } }; diff --git a/packages/cli/src/commands/versions/lint.ts b/packages/cli/src/commands/versions/lint.ts index e2c36ba094..aa0bcd6f6a 100644 --- a/packages/cli/src/commands/versions/lint.ts +++ b/packages/cli/src/commands/versions/lint.ts @@ -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; diff --git a/packages/cli/src/lib/versioning/Lockfile.ts b/packages/cli/src/lib/versioning/Lockfile.ts index 893beca6e2..f9bc789d95 100644 --- a/packages/cli/src/lib/versioning/Lockfile.ts +++ b/packages/cli/src/lib/versioning/Lockfile.ts @@ -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) {