fix(core): Fix verified community packages reinstall (#23428)

This commit is contained in:
Iván Ovejero
2025-12-19 11:58:48 +01:00
committed by GitHub
parent 99f398e46e
commit 6b6b828e96
2 changed files with 152 additions and 23 deletions
@@ -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<Parameters<typeof execFile>[2]>;
type ExecFileCallback = NonNullable<Parameters<typeof execFile>[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,
);
});
});
@@ -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<string, Map<string, string>>();
for (const p of vettedPackages) {
const versionMap = new Map<string, string>();
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/',