Skip to content

Commit 168ac0e

Browse files
authored
ci: Include THIRD_PARTY_LICENSES.md file with release (#18739)
1 parent daac88b commit 168ac0e

File tree

16 files changed

+729
-8
lines changed

16 files changed

+729
-8
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,4 @@ packages/testing/**/.cursor/rules/
4040
.venv
4141
.ruff_cache
4242
__pycache__
43+
packages/cli/THIRD_PARTY_LICENSES.md

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
"lint:affected": "turbo run lint --affected",
3636
"lint:fix": "turbo run lint:fix",
3737
"optimize-svg": "find ./packages -name '*.svg' ! -name 'pipedrive.svg' -print0 | xargs -0 -P16 -L20 npx svgo",
38+
"generate:third-party-licenses": "node scripts/generate-third-party-licenses.mjs",
3839
"setup-backend-module": "node scripts/ensure-zx.mjs && zx scripts/backend-module/setup.mjs",
3940
"start": "run-script-os",
4041
"start:default": "cd packages/cli/bin && ./n8n",
@@ -66,6 +67,7 @@
6667
"jest-mock": "^29.6.2",
6768
"jest-mock-extended": "^3.0.4",
6869
"lefthook": "^1.7.15",
70+
"license-checker": "^25.0.1",
6971
"nock": "^14.0.1",
7072
"nodemon": "^3.0.1",
7173
"npm-run-all2": "^7.0.2",

packages/cli/THIRD_PARTY_LICENSES.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Third-Party Licenses
2+
3+
**Note**: This is a placeholder file used during local development. The complete third-party licenses file is generated during production builds and contains detailed licensing information for all dependencies.
4+
5+
For the complete list of third-party licenses, please refer to the production build.
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { CLI_DIR } from '@/constants';
2+
import { Get, RestController } from '@n8n/decorators';
3+
import { Request, Response } from 'express';
4+
import { readFile } from 'fs/promises';
5+
import { resolve } from 'path';
6+
7+
@RestController('/third-party-licenses')
8+
export class ThirdPartyLicensesController {
9+
/**
10+
* Get third-party licenses content
11+
* Requires authentication to access
12+
*/
13+
@Get('/')
14+
async getThirdPartyLicenses(_: Request, res: Response) {
15+
const licenseFile = resolve(CLI_DIR, 'THIRD_PARTY_LICENSES.md');
16+
17+
try {
18+
const content = await readFile(licenseFile, 'utf-8');
19+
res.setHeader('Content-Type', 'text/markdown; charset=utf-8');
20+
res.send(content);
21+
} catch {
22+
res.status(404).send('Third-party licenses file not found');
23+
}
24+
}
25+
}

packages/cli/src/server.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ export class Server extends AbstractServer {
9191
const { FrontendService } = await import('@/services/frontend.service');
9292
this.frontendService = Container.get(FrontendService);
9393
await import('@/controllers/module-settings.controller');
94+
await import('@/controllers/third-party-licenses.controller');
9495
}
9596

9697
this.presetCredentialsLoaded = false;
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { createMember, createOwner } from '../shared/db/users';
2+
import type { SuperAgentTest } from '../shared/types';
3+
import { setupTestServer } from '../shared/utils';
4+
5+
jest.mock('fs/promises', () => ({
6+
readFile: jest.fn(),
7+
}));
8+
9+
import { readFile } from 'fs/promises';
10+
const mockReadFile = readFile as jest.MockedFunction<typeof readFile>;
11+
12+
describe('ThirdPartyLicensesController', () => {
13+
const testServer = setupTestServer({ endpointGroups: ['third-party-licenses'] });
14+
let ownerAgent: SuperAgentTest;
15+
let memberAgent: SuperAgentTest;
16+
17+
beforeAll(async () => {
18+
const owner = await createOwner();
19+
const member = await createMember();
20+
ownerAgent = testServer.authAgentFor(owner);
21+
memberAgent = testServer.authAgentFor(member);
22+
});
23+
24+
describe('GET /third-party-licenses', () => {
25+
beforeEach(() => {
26+
jest.resetAllMocks();
27+
});
28+
29+
it('should require authentication', async () => {
30+
await testServer.authlessAgent.get('/third-party-licenses').expect(401);
31+
});
32+
33+
describe('when license file exists', () => {
34+
beforeEach(() => {
35+
mockReadFile.mockResolvedValue('# Third Party Licenses\n\nSome license content...');
36+
});
37+
38+
it('should allow authenticated owner to get third-party licenses', async () => {
39+
const response = await ownerAgent.get('/third-party-licenses');
40+
expect(response.status).toBe(200);
41+
expect(response.headers['content-type']).toMatch(/text\/markdown/);
42+
expect(response.text).toBe('# Third Party Licenses\n\nSome license content...');
43+
});
44+
45+
it('should allow authenticated member to get third-party licenses', async () => {
46+
const response = await memberAgent.get('/third-party-licenses');
47+
expect(response.status).toBe(200);
48+
expect(response.headers['content-type']).toMatch(/text\/markdown/);
49+
expect(response.text).toBe('# Third Party Licenses\n\nSome license content...');
50+
});
51+
});
52+
53+
describe('when license file does not exist', () => {
54+
beforeEach(() => {
55+
mockReadFile.mockRejectedValue(new Error('ENOENT: no such file or directory'));
56+
});
57+
58+
it('should return 404 for authenticated owner', async () => {
59+
const response = await ownerAgent.get('/third-party-licenses');
60+
expect(response.status).toBe(404);
61+
expect(response.text).toBe('Third-party licenses file not found');
62+
});
63+
64+
it('should return 404 for authenticated member', async () => {
65+
const response = await memberAgent.get('/third-party-licenses');
66+
expect(response.status).toBe(404);
67+
expect(response.text).toBe('Third-party licenses file not found');
68+
});
69+
});
70+
71+
describe('when file read fails with other errors', () => {
72+
beforeEach(() => {
73+
mockReadFile.mockRejectedValue(new Error('EACCES: permission denied'));
74+
});
75+
76+
it('should return 404 for permission errors', async () => {
77+
const response = await ownerAgent.get('/third-party-licenses');
78+
expect(response.status).toBe(404);
79+
expect(response.text).toBe('Third-party licenses file not found');
80+
});
81+
});
82+
83+
describe('file path resolution', () => {
84+
it('should request the correct file path', async () => {
85+
mockReadFile.mockResolvedValue('test content');
86+
87+
await ownerAgent.get('/third-party-licenses');
88+
89+
expect(mockReadFile).toHaveBeenCalledWith(
90+
expect.stringMatching(/THIRD_PARTY_LICENSES\.md$/),
91+
'utf-8',
92+
);
93+
});
94+
});
95+
});
96+
});

packages/cli/test/integration/shared/types.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,10 @@ type EndpointGroup =
4343
| 'ai'
4444
| 'folder'
4545
| 'insights'
46+
| 'data-store'
47+
| 'module-settings'
4648
| 'data-table'
47-
| 'module-settings';
49+
| 'third-party-licenses';
4850

4951
type ModuleName = 'insights' | 'external-secrets' | 'community-packages' | 'data-table';
5052

packages/cli/test/integration/shared/utils/test-server.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,10 @@ export const setupTestServer = ({
313313
case 'module-settings':
314314
await import('@/controllers/module-settings.controller');
315315
break;
316+
317+
case 'third-party-licenses':
318+
await import('@/controllers/third-party-licenses.controller');
319+
break;
316320
}
317321
}
318322

packages/frontend/@n8n/i18n/src/locales/en.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,9 @@
123123
"about.debug.message": "Copy debug information",
124124
"about.debug.toast.title": "Debug info",
125125
"about.debug.toast.message": "Copied debug info to clipboard",
126+
"about.thirdPartyLicenses": "Third-Party Licenses",
127+
"about.thirdPartyLicensesLink": "View all third-party licenses",
128+
"about.thirdPartyLicenses.downloadError": "Failed to download third-party licenses file",
126129
"askAi.dialog.title": "'Ask AI' is almost ready",
127130
"askAi.dialog.body": "We’re still applying the finishing touches. Soon, you will be able to <strong>automatically generate code from simple text prompts</strong>. Join the waitlist to get early access to this feature.",
128131
"askAi.dialog.signup": "Join Waitlist",

packages/frontend/@n8n/rest-api-client/src/api/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export * from './module-settings';
1616
export * from './sso';
1717
export type * from './tags';
1818
export * from './templates';
19+
export * from './third-party-licenses';
1920
export * from './ui';
2021
export * from './users';
2122
export * from './versions';

0 commit comments

Comments
 (0)