From 6b6b828e96fbb5895d7c7ecfdc7490fd5e5bea18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Fri, 19 Dec 2025 11:58:48 +0100 Subject: [PATCH] fix(core): Fix verified community packages reinstall (#23428) --- .../community-packages.service.test.ts | 126 ++++++++++++++++-- .../community-packages.service.ts | 49 +++++-- 2 files changed, 152 insertions(+), 23 deletions(-) diff --git a/packages/cli/src/modules/community-packages/__tests__/community-packages.service.test.ts b/packages/cli/src/modules/community-packages/__tests__/community-packages.service.test.ts index b752f896408..d3e60afc5f8 100644 --- a/packages/cli/src/modules/community-packages/__tests__/community-packages.service.test.ts +++ b/packages/cli/src/modules/community-packages/__tests__/community-packages.service.test.ts @@ -24,6 +24,7 @@ import { COMMUNITY_NODE_VERSION, COMMUNITY_PACKAGE_VERSION } from '@test-integra import { mockPackageName, mockPackagePair } from '@test-integration/utils'; import type { CommunityPackagesConfig } from '../community-packages.config'; +import { getCommunityNodeTypes } from '../community-node-types-utils'; import { CommunityPackagesService } from '../community-packages.service'; import type { CommunityPackages } from '../community-packages.types'; import { InstalledNodes } from '../installed-nodes.entity'; @@ -34,6 +35,9 @@ import { InstalledPackagesRepository } from '../installed-packages.repository'; jest.mock('node:fs/promises'); jest.mock('node:child_process'); jest.mock('axios'); +jest.mock('../community-node-types-utils', () => ({ + getCommunityNodeTypes: jest.fn().mockResolvedValue([]), +})); type ExecFileOptions = NonNullable[2]>; type ExecFileCallback = NonNullable[3]>; @@ -592,6 +596,7 @@ describe('CommunityPackagesService', () => { jest .spyOn(communityPackagesService, 'installPackage') .mockResolvedValue({} as InstalledPackages); + mocked(getCommunityNodeTypes).mockResolvedValue([]); }); test('should set missingPackages to empty array when no packages are missing', async () => { @@ -633,7 +638,11 @@ describe('CommunityPackagesService', () => { await communityPackagesService.checkForMissingPackages(); - expect(communityPackagesService.installPackage).toHaveBeenCalledWith('package-1', '1.0.0'); + expect(communityPackagesService.installPackage).toHaveBeenCalledWith( + 'package-1', + '1.0.0', + undefined, + ); expect(loadNodesAndCredentials.postProcessLoaders).toHaveBeenCalled(); expect(communityPackagesService.missingPackages).toEqual([]); expect(logger.info).toHaveBeenCalledWith( @@ -653,30 +662,125 @@ describe('CommunityPackagesService', () => { await communityPackagesService.checkForMissingPackages(); - expect(communityPackagesService.installPackage).toHaveBeenCalledWith('package-1', '1.0.0'); - expect(logger.error).toHaveBeenCalledWith('n8n was unable to install the missing packages.'); + expect(communityPackagesService.installPackage).toHaveBeenCalledWith( + 'package-1', + '1.0.0', + undefined, + ); + expect(logger.error).toHaveBeenCalledWith( + 'Failed to reinstall community package package-1: Installation failed', + ); expect(communityPackagesService.missingPackages).toEqual(['package-1@1.0.0']); }); - test('should handle multiple missing packages and stop reinstalling after first failure', async () => { + test('should continue reinstalling remaining packages after one fails', async () => { const installedPackages = [installedPackage1, installedPackage2]; installedPackageRepository.find.mockResolvedValue(installedPackages); loadNodesAndCredentials.isKnownNode.mockReturnValue(false); config.reinstallMissing = true; - // First installation succeeds, second fails + // First installation fails, second succeeds communityPackagesService.installPackage = jest .fn() - .mockResolvedValueOnce({} as InstalledPackages) - .mockRejectedValueOnce(new Error('Installation failed')); + .mockRejectedValueOnce(new Error('Installation failed')) + .mockResolvedValueOnce({} as InstalledPackages); await communityPackagesService.checkForMissingPackages(); - expect(communityPackagesService.installPackage).toHaveBeenCalledWith('package-1', '1.0.0'); - expect(communityPackagesService.installPackage).toHaveBeenCalledWith('package-2', '2.0.0'); - expect(logger.error).toHaveBeenCalledWith('n8n was unable to install the missing packages.'); - expect(communityPackagesService.missingPackages).toEqual(['package-2@2.0.0']); + expect(communityPackagesService.installPackage).toHaveBeenCalledWith( + 'package-1', + '1.0.0', + undefined, + ); + expect(communityPackagesService.installPackage).toHaveBeenCalledWith( + 'package-2', + '2.0.0', + undefined, + ); + expect(logger.error).toHaveBeenCalledWith( + 'Failed to reinstall community package package-1: Installation failed', + ); + // Only package-1 should be in missingPackages since package-2 succeeded + expect(communityPackagesService.missingPackages).toEqual(['package-1@1.0.0']); + expect(loadNodesAndCredentials.postProcessLoaders).toHaveBeenCalled(); + }); + + test('should pass checksum from vetted packages when reinstalling', async () => { + const installedPackages = [installedPackage1]; + + installedPackageRepository.find.mockResolvedValue(installedPackages); + loadNodesAndCredentials.isKnownNode.mockReturnValue(false); + config.reinstallMissing = true; + + mocked(getCommunityNodeTypes).mockResolvedValue([ + { + packageName: 'package-1', + checksum: 'sha512-abc123', + npmVersion: '1.0.0', + } as never, + ]); + + await communityPackagesService.checkForMissingPackages(); + + expect(communityPackagesService.installPackage).toHaveBeenCalledWith( + 'package-1', + '1.0.0', + 'sha512-abc123', + ); + }); + + test('should use version-specific checksum from nodeVersions when installed version differs from latest', async () => { + const installedPackages = [installedPackage1]; // version 1.0.0 + + installedPackageRepository.find.mockResolvedValue(installedPackages); + loadNodesAndCredentials.isKnownNode.mockReturnValue(false); + config.reinstallMissing = true; + + mocked(getCommunityNodeTypes).mockResolvedValue([ + { + packageName: 'package-1', + checksum: 'sha512-latest', + npmVersion: '2.0.0', + nodeVersions: [ + { npmVersion: '1.0.0', checksum: 'sha512-version-specific' }, + { npmVersion: '2.0.0', checksum: 'sha512-latest' }, + ], + } as never, + ]); + + await communityPackagesService.checkForMissingPackages(); + + expect(communityPackagesService.installPackage).toHaveBeenCalledWith( + 'package-1', + '1.0.0', + 'sha512-version-specific', + ); + }); + + test('should pass undefined checksum when installed version is not in vetted list', async () => { + const installedPackages = [installedPackage1]; // version 1.0.0 + + installedPackageRepository.find.mockResolvedValue(installedPackages); + loadNodesAndCredentials.isKnownNode.mockReturnValue(false); + config.reinstallMissing = true; + + mocked(getCommunityNodeTypes).mockResolvedValue([ + { + packageName: 'package-1', + checksum: 'sha512-latest', + npmVersion: '2.0.0', + nodeVersions: [{ npmVersion: '2.0.0', checksum: 'sha512-latest' }], + } as never, + ]); + + await communityPackagesService.checkForMissingPackages(); + + expect(communityPackagesService.installPackage).toHaveBeenCalledWith( + 'package-1', + '1.0.0', + undefined, + ); }); }); diff --git a/packages/cli/src/modules/community-packages/community-packages.service.ts b/packages/cli/src/modules/community-packages/community-packages.service.ts index f25d3b16a30..c9f1943e14c 100644 --- a/packages/cli/src/modules/community-packages/community-packages.service.ts +++ b/packages/cli/src/modules/community-packages/community-packages.service.ts @@ -5,7 +5,13 @@ import { Service } from '@n8n/di'; import axios from 'axios'; import type { PackageDirectoryLoader } from 'n8n-core'; import { InstanceSettings } from 'n8n-core'; -import { jsonParse, UnexpectedError, UserError, type PublicInstalledPackage } from 'n8n-workflow'; +import { + ensureError, + jsonParse, + UnexpectedError, + UserError, + type PublicInstalledPackage, +} from 'n8n-workflow'; import { execFile } from 'node:child_process'; import { access, constants, mkdir, readFile, rm, writeFile } from 'node:fs/promises'; import { join } from 'node:path'; @@ -26,6 +32,7 @@ import { toError } from '@/utils'; import { CommunityPackagesConfig } from './community-packages.config'; import type { CommunityPackages } from './community-packages.types'; +import { getCommunityNodeTypes } from './community-node-types-utils'; import { InstalledPackages } from './installed-packages.entity'; import { InstalledPackagesRepository } from './installed-packages.repository'; import { checkIfVersionExistsOrThrow, verifyIntegrity } from './npm-utils'; @@ -316,19 +323,37 @@ export class CommunityPackagesService { const { reinstallMissing } = this.config; if (reinstallMissing) { - this.logger.info('Attempting to reinstall missing packages', { missingPackages }); - try { - // Optimistic approach - stop if any installation fails - for (const missingPackage of missingPackages) { - await this.installPackage(missingPackage.packageName, missingPackage.version); + this.logger.info('Attempting to reinstall missing packages', { + missingPackages: [...missingPackages], + }); + const environment = process.env.ENVIRONMENT === 'staging' ? 'staging' : 'production'; + const vettedPackages = await getCommunityNodeTypes(environment); - missingPackages.delete(missingPackage); - } - this.logger.info('Packages reinstalled successfully. Resuming regular initialization.'); - await this.loadNodesAndCredentials.postProcessLoaders(); - } catch (error) { - this.logger.error('n8n was unable to install the missing packages.'); + const checksums = new Map>(); + for (const p of vettedPackages) { + const versionMap = new Map(); + versionMap.set(p.npmVersion, p.checksum); + for (const v of p.nodeVersions ?? []) versionMap.set(v.npmVersion, v.checksum); + checksums.set(p.packageName, versionMap); } + + for (const missingPackage of missingPackages) { + try { + const checksum = checksums.get(missingPackage.packageName)?.get(missingPackage.version); + await this.installPackage(missingPackage.packageName, missingPackage.version, checksum); + missingPackages.delete(missingPackage); + } catch (error) { + this.logger.error( + `Failed to reinstall community package ${missingPackage.packageName}: ${ensureError(error).message}`, + ); + } + } + + if (missingPackages.size === 0) { + this.logger.info('Packages reinstalled successfully. Resuming regular initialization.'); + } + + await this.loadNodesAndCredentials.postProcessLoaders(); } else { this.logger.warn( 'n8n detected that some packages are missing. For more information, visit https://docs.n8n.io/integrations/community-nodes/troubleshooting/',