From ab14656446d78562990d362f92087e17c69ff5f5 Mon Sep 17 00:00:00 2001 From: blam Date: Mon, 15 Apr 2024 16:04:18 +0200 Subject: [PATCH] chore: include backstage-community deps in the bump Signed-off-by: blam --- .../cli/src/commands/versions/bump.test.ts | 161 +++++++++++++++++- packages/cli/src/commands/versions/bump.ts | 152 ++++------------- 2 files changed, 189 insertions(+), 124 deletions(-) diff --git a/packages/cli/src/commands/versions/bump.test.ts b/packages/cli/src/commands/versions/bump.test.ts index d00b4ce8da..f3eafb760a 100644 --- a/packages/cli/src/commands/versions/bump.test.ts +++ b/packages/cli/src/commands/versions/bump.test.ts @@ -92,6 +92,8 @@ const REGISTRY_VERSIONS: { [name: string]: string } = { '@backstage-extra/custom': '1.1.0', '@backstage-extra/custom-two': '2.0.0', '@backstage/create-app': '1.0.0', + '@backstage-community/custom': '1.1.0', + '@backstage-community/custom-two': '2.0.0', }; const HEADER = `# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. @@ -204,7 +206,7 @@ describe('bump', () => { await bump({ pattern: null, release: 'main' } as unknown as Command); }); expectLogsToMatch(logs, [ - 'Using default pattern glob @backstage/*', + 'Using default pattern glob @backstage?(-community)/*', 'Checking for updates of @backstage/core', 'Checking for updates of @backstage/theme', 'Checking for updates of @backstage/core-api', @@ -261,6 +263,151 @@ describe('bump', () => { }); }); + it('should bump backstage-community dependencies', async () => { + const customLockfileMock = `${lockfileMock} +"@backstage-community/custom@^1.1.0": + version "1.1.0" + +"@backstage-community/custom@^1.0.1": + version "1.0.1" + +"@backstage-community/custom-two@^1.0.0": + version "1.0.0" +`; + const customLockfileMockResult = `${HEADER} +"@backstage-community/custom-two@^1.0.0": + version "1.0.0" + +"@backstage-community/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" +`; + mockDir.setContent({ + 'yarn.lock': customLockfileMock, + 'package.json': JSON.stringify({ + workspaces: { + packages: ['packages/*'], + }, + }), + packages: { + a: { + 'package.json': JSON.stringify({ + name: 'a', + dependencies: { + '@backstage/core': '^1.0.5', + '@backstage-community/custom': '^1.0.1', + '@backstage-community/custom-two': '^1.0.0', + }, + }), + }, + b: { + 'package.json': JSON.stringify({ + name: 'b', + dependencies: { + '@backstage/core': '^1.0.3', + '@backstage/theme': '^1.0.0', + '@backstage-community/custom': '^1.1.0', + '@backstage-community/custom-two': '^1.0.0', + }, + }), + }, + }, + }); + + jest.spyOn(runObj, 'run').mockResolvedValue(undefined); + worker.use( + rest.get( + 'https://versions.backstage.io/v1/tags/main/manifest.json', + (_, res, ctx) => + res( + ctx.status(200), + ctx.json({ + packages: [], + }), + ), + ), + ); + const { log: logs } = await withLogCollector(['log'], async () => { + await bump({ + release: 'main', + } as any); + }); + expectLogsToMatch(logs, [ + 'Checking for updates of @backstage/core', + 'Checking for updates of @backstage-community/custom', + 'Checking for updates of @backstage-community/custom-two', + 'Checking for updates of @backstage/theme', + 'Checking for updates of @backstage/core-api', + 'Some packages are outdated, updating', + 'Using default pattern glob @backstage?(-community)/*', + 'unlocking @backstage/core@^1.0.3 ~> 1.0.6', + 'unlocking @backstage-community/custom@^1.0.1 ~> 1.1.0', + 'unlocking @backstage/core-api@^1.0.6 ~> 1.0.7', + 'unlocking @backstage/core-api@^1.0.3 ~> 1.0.7', + 'bumping @backstage/core in a to ^1.0.6', + 'bumping @backstage-community/custom in a to ^1.1.0', + 'bumping @backstage-community/custom-two in a to ^2.0.0', + 'bumping @backstage/core in b to ^1.0.6', + 'bumping @backstage-community/custom in b to ^1.1.0', + 'bumping @backstage-community/custom-two in b to ^2.0.0', + 'bumping @backstage/theme in b to ^2.0.0', + 'Running yarn install to install new versions', + '⚠️ The following packages may have breaking changes:', + ' @backstage-community/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(mockFetchPackageInfo).toHaveBeenCalledTimes(5); + expect(mockFetchPackageInfo).toHaveBeenCalledWith('@backstage/core'); + expect(mockFetchPackageInfo).toHaveBeenCalledWith('@backstage/theme'); + + expect(runObj.run).toHaveBeenCalledTimes(1); + expect(runObj.run).toHaveBeenCalledWith( + 'yarn', + ['install'], + expect.any(Object), + ); + + const lockfileContents = await fs.readFile( + mockDir.resolve('yarn.lock'), + 'utf8', + ); + expect(lockfileContents).toEqual(customLockfileMockResult); + + const packageA = await fs.readJson( + mockDir.resolve('packages/a/package.json'), + ); + expect(packageA).toEqual({ + name: 'a', + dependencies: { + '@backstage-community/custom': '^1.1.0', + '@backstage-community/custom-two': '^2.0.0', + '@backstage/core': '^1.0.6', + }, + }); + const packageB = await fs.readJson( + mockDir.resolve('packages/b/package.json'), + ); + expect(packageB).toEqual({ + name: 'b', + dependencies: { + '@backstage-community/custom': '^1.1.0', + '@backstage-community/custom-two': '^2.0.0', + '@backstage/core': '^1.0.6', + '@backstage/theme': '^2.0.0', + }, + }); + }); + it('should bump backstage dependencies but not install them', async () => { mockDir.setContent({ 'yarn.lock': lockfileMock, @@ -311,7 +458,7 @@ describe('bump', () => { } as unknown as Command); }); expectLogsToMatch(logs, [ - 'Using default pattern glob @backstage/*', + 'Using default pattern glob @backstage?(-community)/*', 'Checking for updates of @backstage/core', 'Checking for updates of @backstage/theme', 'Checking for updates of @backstage/core-api', @@ -423,7 +570,7 @@ describe('bump', () => { await bump({ pattern: null, release: 'main' } as unknown as Command); }); expectLogsToMatch(logs, [ - 'Using default pattern glob @backstage/*', + 'Using default pattern glob @backstage?(-community)/*', 'Checking for updates of @backstage/core', 'Checking for updates of @backstage/theme', 'Checking for updates of @backstage/theme', @@ -523,7 +670,7 @@ describe('bump', () => { ).rejects.toThrow('No release found for 999.0.1 version'); }); expect(logs.filter(Boolean)).toEqual([ - 'Using default pattern glob @backstage/*', + 'Using default pattern glob @backstage?(-community)/*', ]); expect(runObj.run).toHaveBeenCalledTimes(0); @@ -626,7 +773,7 @@ describe('bump', () => { await bump({ pattern: null, release: 'next' } as unknown as Command); }); expectLogsToMatch(logs, [ - 'Using default pattern glob @backstage/*', + 'Using default pattern glob @backstage?(-community)/*', 'Checking for updates of @backstage/core', 'Checking for updates of @backstage/theme', 'Checking for updates of @backstage/theme', @@ -841,7 +988,7 @@ describe('bump', () => { await bump({ pattern: null, release: 'main' } as unknown as Command); }); expectLogsToMatch(logs, [ - 'Using default pattern glob @backstage/*', + 'Using default pattern glob @backstage?(-community)/*', 'Checking for updates of @backstage/core', 'Checking for updates of @backstage/theme', 'Package info not found, ignoring package @backstage/core', @@ -956,7 +1103,7 @@ describe('bump', () => { await bump({ pattern: null, release: 'main' } as unknown as Command); }); expectLogsToMatch(logs, [ - 'Using default pattern glob @backstage/*', + 'Using default pattern glob @backstage?(-community)/*', 'Checking for updates of @backstage/core', 'Checking for updates of @backstage/theme', 'Checking for updates of @backstage/core-api', diff --git a/packages/cli/src/commands/versions/bump.ts b/packages/cli/src/commands/versions/bump.ts index a589e91fc7..c6a84208ab 100644 --- a/packages/cli/src/commands/versions/bump.ts +++ b/packages/cli/src/commands/versions/bump.ts @@ -14,32 +14,32 @@ * limitations under the License. */ -import { BACKSTAGE_JSON } from '@backstage/cli-common'; -import { PackageGraph } from '@backstage/cli-node'; +import fs from 'fs-extra'; +import chalk from 'chalk'; +import ora from 'ora'; +import semver from 'semver'; +import { minimatch } from 'minimatch'; +import { OptionValues } from 'commander'; import { isError, NotFoundError } from '@backstage/errors'; +import { resolve as resolvePath } from 'path'; +import { run } from '../../lib/run'; +import { paths } from '../../lib/paths'; +import { + mapDependencies, + fetchPackageInfo, + Lockfile, + YarnInfoInspectData, +} from '../../lib/versioning'; +import { forbiddenDuplicatesFilter } from './lint'; +import { BACKSTAGE_JSON } from '@backstage/cli-common'; +import { runParallelWorkers } from '../../lib/parallel'; import { getManifestByReleaseLine, getManifestByVersion, ReleaseManifest, } from '@backstage/release-manifests'; -import chalk from 'chalk'; -import { OptionValues } from 'commander'; -import fs from 'fs-extra'; import 'global-agent/bootstrap'; -import { minimatch } from 'minimatch'; -import ora from 'ora'; -import { resolve as resolvePath } from 'path'; -import semver from 'semver'; -import { runParallelWorkers } from '../../lib/parallel'; -import { paths } from '../../lib/paths'; -import { run } from '../../lib/run'; -import { - fetchPackageInfo, - Lockfile, - mapDependencies, - YarnInfoInspectData, -} from '../../lib/versioning'; -import { forbiddenDuplicatesFilter } from './lint'; +import { PackageGraph } from '@backstage/cli-node'; const DEP_TYPES = [ 'dependencies', @@ -48,7 +48,7 @@ const DEP_TYPES = [ 'optionalDependencies', ]; -const DEFAULT_PATTERN_GLOB = '@backstage/*'; +const DEFAULT_PATTERN_GLOB = '@backstage?(-community)/*'; type PkgVersionInfo = { range: string; @@ -108,13 +108,6 @@ export default async (opts: OptionValues) => { 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; target: string }>(); - // All backstage package versions in yarn.lock that are not in `unlocked` - const outsideRange = Array<{ name: string; range: string; target: string }>(); - // Moved packages & their new package version info - const moved = new Map< - string, - { name: string; range: string; target: string } - >(); await runParallelWorkers({ parallelismFactor: 4, @@ -172,7 +165,6 @@ export default async (opts: OptionValues) => { // 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)) { - outsideRange.push({ name, range: entry.range, target }); continue; } // Unlock all entries that are within range but on the old version @@ -181,32 +173,7 @@ export default async (opts: OptionValues) => { }, }); - await runParallelWorkers({ - parallelismFactor: 4, - items: [...unlocked, ...outsideRange], - async worker({ name }) { - if (!moved.get(name)) { - let info = await fetchPackageInfo(name); - - // TODO: To be removed - if (name === '@backstage/plugin-catalog-react') { - info.backstage = { - ...info.backstage, - moved: '@pagerduty/backstage-plugin', - }; - } - - if (info.backstage?.moved) { - info = await fetchPackageInfo(info.backstage.moved); - moved.set(name, { - name: info.name, - range: `^${info['dist-tags'].latest}`, - target: info['dist-tags'].latest, - }); - } - } - }, - }); + console.log(); // Write all discovered version bumps to package.json in this repo if (versionBumps.size === 0 && unlocked.length === 0) { @@ -215,59 +182,25 @@ export default async (opts: OptionValues) => { console.log(chalk.yellow('Some packages are outdated, updating')); console.log(); - if (unlocked.length > 0 || outsideRange.length > 0) { + if (unlocked.length > 0) { const removed = new Set(); - - for (const unlockedPackage of unlocked) { - const movedPackage = moved.get(unlockedPackage.name); - const { name, range, target } = movedPackage - ? movedPackage - : unlockedPackage; - + 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 === target && !movedPackage) { + if (existingEntry?.version === target) { continue; } const key = JSON.stringify({ name, range }); if (!removed.has(key)) { removed.add(key); - if (movedPackage) { - console.log( - `${chalk.yellow('moving')} ${unlockedPackage.name}@${ - unlockedPackage.range - } ~> ${chalk.yellow(`${name}@${range}`)}`, - ); - lockfile.remove(unlockedPackage.name, unlockedPackage.range); - } else { - console.log( - `${chalk.magenta('unlocking')} ${name}@${chalk.yellow( - range, - )} ~> ${chalk.yellow(target)}`, - ); - lockfile.remove(name, range); - } + console.log( + `${chalk.magenta('unlocking')} ${name}@${chalk.yellow( + range, + )} ~> ${chalk.yellow(target)}`, + ); + lockfile.remove(name, range); } } - - for (const outsideRangePackage of outsideRange) { - const movedPackage = moved.get(outsideRangePackage.name); - if (movedPackage) { - const { name, range } = movedPackage; - const key = JSON.stringify({ name, range }); - if (!removed.has(key)) { - removed.add(key); - // TODO: Does this need special instructions as there might be custom bumping needed? - console.log( - `${chalk.yellow('moving')} ${movedPackage.name}@${ - movedPackage.range - } ~> ${chalk.yellow(`${name}@${range}`)}`, - ); - lockfile.remove(movedPackage.name, movedPackage.range); - } - } - } - await lockfile.save(lockfilePath); } @@ -280,29 +213,14 @@ export default async (opts: OptionValues) => { const pkgJson = await fs.readJson(pkgPath); for (const dep of deps) { - const movedDep = moved.get(dep.name); - if (movedDep) { - console.log( - `${chalk.yellow('bumping')} ${dep.name} in ${chalk.cyan( - name, - )} to ${chalk.yellow(`${movedDep.name}@${movedDep.range}`)}`, - ); - } else { - console.log( - `${chalk.cyan('bumping')} ${dep.name} in ${chalk.cyan( - name, - )} to ${chalk.yellow(dep.range)}`, - ); - } + console.log( + `${chalk.cyan('bumping')} ${dep.name} in ${chalk.cyan( + name, + )} to ${chalk.yellow(dep.range)}`, + ); for (const depType of DEP_TYPES) { if (depType in pkgJson && dep.name in pkgJson[depType]) { - if (movedDep) { - delete pkgJson[depType][dep.name]; - pkgJson[depType][movedDep.name] = movedDep.range; - continue; - } - const oldRange = pkgJson[depType][dep.name]; pkgJson[depType][dep.name] = dep.range;