diff --git a/.changeset/quote-plastic-monk.md b/.changeset/quote-plastic-monk.md new file mode 100644 index 0000000000..231afa33e7 --- /dev/null +++ b/.changeset/quote-plastic-monk.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Handle no npm info diff --git a/packages/cli/src/commands/versions/bump.test.ts b/packages/cli/src/commands/versions/bump.test.ts index 164a202385..987d4f6f31 100644 --- a/packages/cli/src/commands/versions/bump.test.ts +++ b/packages/cli/src/commands/versions/bump.test.ts @@ -177,4 +177,71 @@ describe('bump', () => { }, }); }); + + it('should ignore not found packages', async () => { + // Make sure all modules involved in package discovery are in the module cache before we mock fs + await mapDependencies(paths.targetDir); + mockFs({ + '/yarn.lock': lockfileMockResult, + '/lerna.json': JSON.stringify({ + packages: ['packages/*'], + }), + '/packages/a/package.json': JSON.stringify({ + name: 'a', + dependencies: { + '@backstage/core': '^1.0.5', + }, + }), + '/packages/b/package.json': JSON.stringify({ + name: 'b', + dependencies: { + '@backstage/core': '^1.0.3', + '@backstage/theme': '^2.0.0', + }, + }), + }); + + paths.targetDir = '/'; + jest + .spyOn(paths, 'resolveTargetRoot') + .mockImplementation((...paths) => resolvePath('/', ...paths)); + jest.spyOn(runObj, 'runPlain').mockImplementation(async () => ''); + jest.spyOn(runObj, 'run').mockResolvedValue(undefined); + + const { log: logs } = await withLogCollector(['log'], async () => { + await bump(); + }); + expect(logs.filter(Boolean)).toEqual([ + 'Checking for updates of @backstage/theme', + 'Checking for updates of @backstage/core', + 'Package info not found, ignoring package @backstage/theme', + 'Package info not found, ignoring package @backstage/core', + 'Checking for updates of @backstage/theme', + 'Checking for updates of @backstage/core', + 'Package info not found, ignoring package @backstage/theme', + 'Package info not found, ignoring package @backstage/core', + 'All Backstage packages are up to date!', + ]); + + expect(runObj.run).toHaveBeenCalledTimes(0); + + const lockfileContents = await fs.readFile('/yarn.lock', 'utf8'); + expect(lockfileContents).toBe(lockfileMockResult); + + const packageA = await fs.readJson('/packages/a/package.json'); + expect(packageA).toEqual({ + name: 'a', + dependencies: { + '@backstage/core': '^1.0.5', // not bumped + }, + }); + const packageB = await fs.readJson('/packages/b/package.json'); + expect(packageB).toEqual({ + name: 'b', + dependencies: { + '@backstage/core': '^1.0.3', // not bumped + '@backstage/theme': '^2.0.0', // not bumped + }, + }); + }); }); diff --git a/packages/cli/src/commands/versions/bump.ts b/packages/cli/src/commands/versions/bump.ts index 8b32cd5768..af670d4a8b 100644 --- a/packages/cli/src/commands/versions/bump.ts +++ b/packages/cli/src/commands/versions/bump.ts @@ -26,6 +26,7 @@ import { Lockfile, } from '../../lib/versioning'; import { includedFilter, forbiddenDuplicatesFilter } from './lint'; +import { NotFoundError } from '../../lib/errors'; const DEP_TYPES = [ 'dependencies', @@ -55,7 +56,16 @@ export default async () => { // 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 }>(); await workerThreads(16, dependencyMap.entries(), async ([name, pkgs]) => { - const target = await findTargetVersion(name); + let target: string; + try { + target = await findTargetVersion(name); + } catch (error) { + if (error instanceof NotFoundError) { + console.log(`Package info not found, ignoring package ${name}`); + return; + } + throw error; + } for (const pkg of pkgs) { if (semver.satisfies(target, pkg.range)) { @@ -84,7 +94,16 @@ export default async () => { return; } - const target = await findTargetVersion(name); + let target: string; + try { + target = await findTargetVersion(name); + } catch (error) { + if (error instanceof NotFoundError) { + console.log(`Package info not found, ignoring package ${name}`); + return; + } + throw error; + } for (const entry of lockfile.get(name) ?? []) { // Ignore lockfile entries that don't satisfy the version range, since diff --git a/packages/cli/src/lib/errors.ts b/packages/cli/src/lib/errors.ts index a1eab4c9e5..110a095fe3 100644 --- a/packages/cli/src/lib/errors.ts +++ b/packages/cli/src/lib/errors.ts @@ -44,3 +44,5 @@ export function exitWithError(error: Error): never { process.exit(1); } } + +export class NotFoundError extends CustomError {} diff --git a/packages/cli/src/lib/versioning/packages.test.ts b/packages/cli/src/lib/versioning/packages.test.ts index b0f1c46e8a..784be60632 100644 --- a/packages/cli/src/lib/versioning/packages.test.ts +++ b/packages/cli/src/lib/versioning/packages.test.ts @@ -19,6 +19,7 @@ import path from 'path'; import * as runObj from '../run'; import { paths } from '../paths'; import { fetchPackageInfo, mapDependencies } from './packages'; +import { NotFoundError } from '@backstage/backend-common'; describe('fetchPackageInfo', () => { afterEach(() => { @@ -40,6 +41,14 @@ describe('fetchPackageInfo', () => { 'my-package', ); }); + + it('should throw if no info', async () => { + jest.spyOn(runObj, 'runPlain').mockResolvedValue(''); + + await expect(fetchPackageInfo('my-package')).rejects.toThrow( + new NotFoundError(`No package information found for package my-package`), + ); + }); }); describe('mapDependencies', () => { diff --git a/packages/cli/src/lib/versioning/packages.ts b/packages/cli/src/lib/versioning/packages.ts index 76e5dc49b0..991a4d23ef 100644 --- a/packages/cli/src/lib/versioning/packages.ts +++ b/packages/cli/src/lib/versioning/packages.ts @@ -15,6 +15,7 @@ */ import { runPlain } from '../../lib/run'; +import { NotFoundError } from '../errors'; const PREFIX = '@backstage'; @@ -49,6 +50,11 @@ export async function fetchPackageInfo( name: string, ): Promise { const output = await runPlain('yarn', 'info', '--json', name); + + if (!output) { + throw new NotFoundError(`No package information found for package ${name}`); + } + const info = JSON.parse(output) as YarnInfo; if (info.type !== 'inspect') { throw new Error(`Received unknown yarn info for ${name}, ${output}`);