mirror of
https://github.com/n8n-io/n8n.git
synced 2026-09-24 23:22:38 +08:00
fix(core): Fix verified community packages reinstall (#23428)
This commit is contained in:
+115
-11
@@ -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/',
|
||||
|
||||
Reference in New Issue
Block a user