Compare commits

...

9 commits

Author SHA1 Message Date
Ricardo Espinoza 59c5ff6135
fix(editor): Respect tag querystring filter when listing workflows (#11029)
Some checks are pending
Test Master / install-and-build (push) Waiting to run
Test Master / Unit tests (18.x) (push) Blocked by required conditions
Test Master / Unit tests (20.x) (push) Blocked by required conditions
Test Master / Unit tests (22.4) (push) Blocked by required conditions
Test Master / Lint (push) Blocked by required conditions
Test Master / Notify Slack on failure (push) Blocked by required conditions
Benchmark Docker Image CI / build (push) Waiting to run
2024-10-02 15:11:41 -04:00
Michael Kret 948edd1a04
fix(n8n Form Trigger Node): When clicking on a multiple choice label, the wrong one is selected (#11059) 2024-10-02 15:58:47 +01:00
github-actions[bot] 3c7556542c
🚀 Release 1.62.1 (#11068)
Co-authored-by: netroy <196144+netroy@users.noreply.github.com>
2024-10-02 16:37:44 +02:00
github-actions[bot] 86069321a1
🚀 Release 1.62.0 (#11056)
Co-authored-by: netroy <196144+netroy@users.noreply.github.com>
2024-10-02 16:19:12 +02:00
कारतोफ्फेलस्क्रिप्ट™ 49c71469f4
ci: @n8n/task-runner package setup (no-changelog) (#11067) 2024-10-02 16:14:57 +02:00
Tomi Turtiainen 4546649c61
feat: Separate task runner server from main http server (no-changelog) (#11062) 2024-10-02 16:38:42 +03:00
Iván Ovejero 8d9eb162ae
chore: Add verbose removal to breaking changes (#11053) 2024-10-02 14:35:31 +02:00
Ricardo Espinoza 9c43fb301d
fix(editor): Format action names properly when action is not defined (#11030) 2024-10-02 08:16:33 -04:00
Tomi Turtiainen 74fa259b37
feat: Make task runners work with n8n from npm (no-changelog) (#11015) 2024-10-02 15:16:02 +03:00
47 changed files with 946 additions and 497 deletions

View file

@ -1,3 +1,55 @@
## [1.62.1](https://github.com/n8n-io/n8n/compare/n8n@1.61.0...n8n@1.62.1) (2024-10-02)
### Bug Fixes
* **AI Agent Node:** Fix output parsing and empty tool input handling in AI Agent node ([#10970](https://github.com/n8n-io/n8n/issues/10970)) ([3a65bdc](https://github.com/n8n-io/n8n/commit/3a65bdc1f522932d463b4da0e67d29076887d06c))
* **API:** Fix workflow project transfer ([#10651](https://github.com/n8n-io/n8n/issues/10651)) ([5f89e3a](https://github.com/n8n-io/n8n/commit/5f89e3a01c1bbb3589ff0464fd5bc991426f55dc))
* **AwsS3 Node:** Fix search only using first input parameters ([#10998](https://github.com/n8n-io/n8n/issues/10998)) ([846cfde](https://github.com/n8n-io/n8n/commit/846cfde8dcaf7bf80f0a4ca7d65fc2a7b61d0e23))
* **Chat Trigger Node:** Fix Allowed Origins paramter ([#11011](https://github.com/n8n-io/n8n/issues/11011)) ([b5f4afe](https://github.com/n8n-io/n8n/commit/b5f4afe12ec77f527080a4b7f812e12f9f73f8df))
* **core:** Fix ownerless project case in statistics service ([#11051](https://github.com/n8n-io/n8n/issues/11051)) ([bdaadf1](https://github.com/n8n-io/n8n/commit/bdaadf10e058e2c0b1141289189d6526c030a2ca))
* **core:** Handle Redis disconnects gracefully ([#11007](https://github.com/n8n-io/n8n/issues/11007)) ([cd91648](https://github.com/n8n-io/n8n/commit/cd916480c2d2b55f2215c72309dc432340fc3f30))
* **core:** Prevent backend from loading duplicate copies of nodes packages ([#10979](https://github.com/n8n-io/n8n/issues/10979)) ([4584f22](https://github.com/n8n-io/n8n/commit/4584f22a9b16883779d8555cda309fd8bd113f6c))
* **core:** Upgrade @n8n/typeorm to address a rare mutex release issue ([#10993](https://github.com/n8n-io/n8n/issues/10993)) ([2af0fbf](https://github.com/n8n-io/n8n/commit/2af0fbf52f0b404697f5148f81ad0035c9ffb6b9))
* **editor:** Allow resources to move between personal and team projects ([#10683](https://github.com/n8n-io/n8n/issues/10683)) ([136d491](https://github.com/n8n-io/n8n/commit/136d49132567558b7d27069c857c0e0bfee70ce2))
* **editor:** Color scheme for a markdown code blocks in dark mode ([#11008](https://github.com/n8n-io/n8n/issues/11008)) ([b20d2eb](https://github.com/n8n-io/n8n/commit/b20d2eb403f71fe1dc21c92df118adcebef51ffe))
* **editor:** Fix filter execution by "Queued" ([#10987](https://github.com/n8n-io/n8n/issues/10987)) ([819d20f](https://github.com/n8n-io/n8n/commit/819d20fa2eee314b88a7ce1c4db632afac514704))
* **editor:** Fix performance issue in credentials list ([#10988](https://github.com/n8n-io/n8n/issues/10988)) ([7073ec6](https://github.com/n8n-io/n8n/commit/7073ec6fe5384cc8c50dcb242212999a1fbc9041))
* **editor:** Fix schema view pill highlighting ([#10936](https://github.com/n8n-io/n8n/issues/10936)) ([1b973dc](https://github.com/n8n-io/n8n/commit/1b973dcd8dbce598e6ada490fd48fad52f7b4f3a))
* **editor:** Fix workflow executions list page redirection ([#10981](https://github.com/n8n-io/n8n/issues/10981)) ([fe7d060](https://github.com/n8n-io/n8n/commit/fe7d0605681dc963f5e5d1607f9d40c5173e0f9f))
* **editor:** Format action names properly when action is not defined ([#11030](https://github.com/n8n-io/n8n/issues/11030)) ([9c43fb3](https://github.com/n8n-io/n8n/commit/9c43fb301d1ccb82e42f46833e19587289803cd3))
* **Elasticsearch Node:** Fix issue with self signed certificates not working ([#10954](https://github.com/n8n-io/n8n/issues/10954)) ([79622b5](https://github.com/n8n-io/n8n/commit/79622b5f267f2a4a53f3eb48e228939d6e3a9caa))
* **Facebook Lead Ads Trigger Node:** Pagination fix in RLC ([#10956](https://github.com/n8n-io/n8n/issues/10956)) ([6322372](https://github.com/n8n-io/n8n/commit/632237261087ada0177b67922f9f48ca02ef1d9e))
* **Github Document Loader Node:** Pass through apiUrl from credentials & fix log output ([#11049](https://github.com/n8n-io/n8n/issues/11049)) ([a7af981](https://github.com/n8n-io/n8n/commit/a7af98183c47a5e215869c8269729b0fb2f318b5))
* **Google Sheets Node:** Updating on row_number using automatic matching ([#10940](https://github.com/n8n-io/n8n/issues/10940)) ([ed91495](https://github.com/n8n-io/n8n/commit/ed91495ebc1e09b89533ffef4b775eaa0139f365))
* **HTTP Request Tool Node:** Remove default user agent header ([#10971](https://github.com/n8n-io/n8n/issues/10971)) ([5a99e93](https://github.com/n8n-io/n8n/commit/5a99e93f8d2c66d7dbcef382478badd63bc4a0b5))
* **Postgres Node:** Falsy query parameters ignored ([#10960](https://github.com/n8n-io/n8n/issues/10960)) ([4a63cff](https://github.com/n8n-io/n8n/commit/4a63cff5ec722c810e3ff2bd7b0bb1e32f7f403b))
* **Respond to Webhook Node:** Node does not work with Wait node ([#10992](https://github.com/n8n-io/n8n/issues/10992)) ([2df5a5b](https://github.com/n8n-io/n8n/commit/2df5a5b649f8ba3b747782d6d5045820aa74955d))
* **RSS Feed Trigger Node:** Fix regression on missing timestamps ([#10991](https://github.com/n8n-io/n8n/issues/10991)) ([d2bc076](https://github.com/n8n-io/n8n/commit/d2bc0760e2b5c977fcc683f0a0281f099a9c538d))
* **Supabase Node:** Fix issue with delete not always working ([#10952](https://github.com/n8n-io/n8n/issues/10952)) ([1944b46](https://github.com/n8n-io/n8n/commit/1944b46fd472bb59552b5fbf7783168a622a2bd2))
* **Text Classifier Node:** Default system prompt template ([#11018](https://github.com/n8n-io/n8n/issues/11018)) ([77fec19](https://github.com/n8n-io/n8n/commit/77fec195d92e0fe23c60552a72e8c030cf7e5e5c))
* **Todoist Node:** Fix listSearch filter bug in Todoist Node ([#10989](https://github.com/n8n-io/n8n/issues/10989)) ([c4b3272](https://github.com/n8n-io/n8n/commit/c4b327248d7aa1352e8d6acec5627ff406aea3d4))
* **Todoist Node:** Make Section Name optional in Move Task operation ([#10732](https://github.com/n8n-io/n8n/issues/10732)) ([799006a](https://github.com/n8n-io/n8n/commit/799006a3cce6abe210469c839ae392d0c1aec486))
### Features
* Add more context to support chat ([#11014](https://github.com/n8n-io/n8n/issues/11014)) ([8a30f92](https://github.com/n8n-io/n8n/commit/8a30f92156d6a4fe73113bd3cdfb751b8c9ce4b4))
* Add Sysdig API credentials for SecOps ([#7033](https://github.com/n8n-io/n8n/issues/7033)) ([a8d1a1e](https://github.com/n8n-io/n8n/commit/a8d1a1ea854fb2c69643b0a5738440b389121ca3))
* **core:** Filter executions by project ID in internal API ([#10976](https://github.com/n8n-io/n8n/issues/10976)) ([06d749f](https://github.com/n8n-io/n8n/commit/06d749ffa7ced503141d8b07e22c47d971eb1623))
* **core:** Implement Dynamic Parameters within regular nodes used as AI Tools ([#10862](https://github.com/n8n-io/n8n/issues/10862)) ([ef5b7cf](https://github.com/n8n-io/n8n/commit/ef5b7cf9b77b653111eb5b1d9de8116c9f6b9f92))
* **editor:** Do not show error for remote options when credentials aren't specified ([#10944](https://github.com/n8n-io/n8n/issues/10944)) ([9fc3699](https://github.com/n8n-io/n8n/commit/9fc3699beb0c150909889ed17740a5cd9e0461c3))
* **editor:** Enable drag and drop in code editors (Code/SQL/HTML) ([#10888](https://github.com/n8n-io/n8n/issues/10888)) ([af9e227](https://github.com/n8n-io/n8n/commit/af9e227ad4848995b9d82c72f814dbf9d1de506f))
* **editor:** Overhaul document title management ([#10999](https://github.com/n8n-io/n8n/issues/10999)) ([bb28956](https://github.com/n8n-io/n8n/commit/bb2895689fb006897bc244271aca6f0bfa1839b9))
* **editor:** Remove execution annotation feature flag ([#11020](https://github.com/n8n-io/n8n/issues/11020)) ([e7199db](https://github.com/n8n-io/n8n/commit/e7199dbfccdbdf1c4273f916e3006ca610c230e9))
* **editor:** Support node-creator actions for vector store nodes ([#11032](https://github.com/n8n-io/n8n/issues/11032)) ([72b70d9](https://github.com/n8n-io/n8n/commit/72b70d9d98daeba654baf6785ff1ae234c73c977))
* **Google BigQuery Node:** Return numeric values as integers ([#10943](https://github.com/n8n-io/n8n/issues/10943)) ([d7c1d24](https://github.com/n8n-io/n8n/commit/d7c1d24f74648740b2f425640909037ba06c5030))
* **Invoice Ninja Node:** Add more query params to getAll requests ([#9238](https://github.com/n8n-io/n8n/issues/9238)) ([50b7238](https://github.com/n8n-io/n8n/commit/50b723836e70bbe405594f690b73057f9c33fbe4))
* **Iterable Node:** Add support for EDC and USDC selection ([#10908](https://github.com/n8n-io/n8n/issues/10908)) ([0ca9c07](https://github.com/n8n-io/n8n/commit/0ca9c076ca51d313392e45c3b013f2e83aaea843))
* **Question and Answer Chain Node:** Customize question and answer system prompt ([#10385](https://github.com/n8n-io/n8n/issues/10385)) ([08a27b3](https://github.com/n8n-io/n8n/commit/08a27b3148aac2282f64339ddc33ac7c90835d84))
# [1.61.0](https://github.com/n8n-io/n8n/compare/n8n@1.60.0...n8n@1.61.0) (2024-09-25)

View file

@ -73,4 +73,28 @@ describe('Workflows', () => {
WorkflowsPage.getters.newWorkflowButtonCard().should('be.visible');
});
it('should respect tag querystring filter when listing workflows', () => {
WorkflowsPage.getters.newWorkflowButtonCard().click();
cy.createFixtureWorkflow('Test_workflow_2.json', getUniqueWorkflowName('My New Workflow'));
cy.visit(WorkflowsPage.url);
WorkflowsPage.getters.createWorkflowButton().click();
cy.createFixtureWorkflow('Test_workflow_1.json', 'Empty State Card Workflow');
cy.visit(WorkflowsPage.url);
WorkflowsPage.getters.workflowFilterButton().click();
WorkflowsPage.getters.workflowTagsDropdown().click();
WorkflowsPage.getters.workflowTagItem('some-tag-1').click();
cy.reload();
WorkflowsPage.getters.workflowCards().should('have.length', 1);
});
});

View file

@ -65,7 +65,7 @@ describe('Resource Locator', () => {
});
it('should show appropriate errors when search filter is required', () => {
workflowPage.actions.addNodeToCanvas('Github', true, true, 'On Pull Request');
workflowPage.actions.addNodeToCanvas('Github', true, true, 'On pull request');
ndv.getters.resourceLocator('owner').should('be.visible');
ndv.getters.resourceLocatorInput('owner').click();
ndv.getters.resourceLocatorErrorMessage().should('contain', NO_CREDENTIALS_MESSAGE);

View file

@ -1,6 +1,6 @@
{
"name": "n8n-monorepo",
"version": "1.61.0",
"version": "1.62.1",
"private": true,
"engines": {
"node": ">=20.15",

View file

@ -1,6 +1,6 @@
{
"name": "@n8n/n8n-benchmark",
"version": "1.5.0",
"version": "1.6.1",
"description": "Cli for running benchmark tests for n8n",
"main": "dist/index",
"scripts": {

View file

@ -1,6 +1,6 @@
{
"name": "@n8n/chat",
"version": "0.26.0",
"version": "0.27.1",
"scripts": {
"dev": "pnpm run storybook",
"build": "pnpm build:vite && pnpm build:bundle",

View file

@ -1,6 +1,6 @@
{
"name": "@n8n/config",
"version": "1.11.0",
"version": "1.12.1",
"scripts": {
"clean": "rimraf dist .turbo",
"dev": "pnpm watch",

View file

@ -11,4 +11,12 @@ export class TaskRunnersConfig {
@Env('N8N_RUNNERS_AUTH_TOKEN')
authToken: string = '';
/** IP address task runners server should listen on */
@Env('N8N_RUNNERS_SERVER_PORT')
port: number = 5679;
/** IP address task runners server should listen on */
@Env('N8N_RUNNERS_SERVER_LISTEN_ADDRESS')
listen_address: string = '127.0.0.1';
}

View file

@ -225,6 +225,8 @@ describe('GlobalConfig', () => {
disabled: true,
path: '/runners',
authToken: '',
listen_address: '127.0.0.1',
port: 5679,
},
sentry: {
backendDsn: '',

View file

@ -1,6 +1,6 @@
{
"name": "@n8n/n8n-nodes-langchain",
"version": "1.61.0",
"version": "1.62.1",
"description": "",
"main": "index.js",
"scripts": {

View file

@ -1,59 +0,0 @@
{
"name": "@n8n/task-runner",
"private": true,
"version": "0.1.0",
"description": "",
"main": "dist/index.js",
"scripts": {
"start": "node dist/start.js",
"dev": "pnpm build && pnpm start",
"build": "tsc -p ./tsconfig.build.json",
"test": "jest",
"lint": "eslint .",
"lintfix": "eslint . --fix",
"watch": "tsc -w -p ./tsconfig.build.json"
},
"engines": {
"node": ">=20.15",
"pnpm": ">=9.5"
},
"files": [
"src/",
"dist/",
"package.json",
"tsconfig.json"
],
"main": "dist/index.js",
"module": "src/index.ts",
"types": "dist/index.d.ts",
"packageManager": "pnpm@9.6.0",
"devDependencies": {
"@n8n_io/eslint-config": "^0.0.2",
"@types/jest": "^29.5.0",
"@types/node": "^18.13.0",
"@types/ws": "^8.5.12",
"@typescript-eslint/eslint-plugin": "^6.1.0",
"eslint": "^8.38.0",
"eslint-config-airbnb-typescript": "^17.1.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-n8n-local-rules": "^1.0.0",
"eslint-plugin-prettier": "^5.0.0",
"eslint-plugin-unicorn": "^48.0.0",
"eslint-plugin-unused-imports": "^3.0.0",
"jest": "^29.5.0",
"nodemon": "^2.0.20",
"prettier": "^3.0.0",
"ts-jest": "^29.1.0",
"ts-node": "^10.9.1",
"tsc-alias": "^1.8.7",
"typescript": "^5.0.0"
},
"dependencies": {
"jmespath": "^0.16.0",
"luxon": "^3.5.0",
"n8n-workflow": "workspace:*",
"n8n-core": "workspace:*",
"nanoid": "^3.3.6",
"ws": "^8.18.0"
}
}

View file

@ -1,34 +0,0 @@
import * as a from 'node:assert/strict';
import { JsTaskRunner } from './code';
import { authenticate } from './authenticator';
let _runner: JsTaskRunner;
type Config = {
n8nUri: string;
authToken: string;
};
function readAndParseConfig(): Config {
const authToken = process.env.N8N_RUNNERS_AUTH_TOKEN;
a.ok(authToken, 'Missing task runner auth token. Use N8N_RUNNERS_AUTH_TOKEN to configure it');
return {
n8nUri: process.env.N8N_RUNNERS_N8N_URI ?? 'localhost:5678',
authToken,
};
}
void (async function start() {
const config = readAndParseConfig();
const grantToken = await authenticate({
authToken: config.authToken,
n8nUri: config.n8nUri,
});
const wsUrl = `ws://${config.n8nUri}/rest/runners/_ws`;
_runner = new JsTaskRunner('javascript', wsUrl, grantToken, 5);
})();

View file

@ -0,0 +1,29 @@
{
"name": "@n8n/task-runner",
"version": "1.0.1",
"scripts": {
"clean": "rimraf dist .turbo",
"start": "node dist/start.js",
"dev": "pnpm build && pnpm start",
"typecheck": "tsc --noEmit",
"build": "tsc -p ./tsconfig.build.json",
"format": "biome format --write src",
"format:check": "biome ci src",
"test": "echo \"Error: no tests in this package\" && exit 0",
"lint": "eslint . --quiet",
"lintfix": "eslint . --fix",
"watch": "tsc -p tsconfig.build.json --watch"
},
"main": "dist/start.js",
"module": "src/start.ts",
"types": "dist/start.d.ts",
"files": [
"dist/**/*"
],
"dependencies": {
"n8n-workflow": "workspace:*",
"n8n-core": "workspace:*",
"nanoid": "^3.3.6",
"ws": "^8.18.0"
}
}

View file

@ -1,3 +1,4 @@
import { ApplicationError } from 'n8n-workflow';
import * as a from 'node:assert/strict';
export type AuthOpts = {
@ -23,7 +24,9 @@ export async function authenticate(opts: AuthOpts) {
});
if (!response.ok) {
throw new Error(`Invalid response status ${response.status}: ${await response.text()}`);
throw new ApplicationError(
`Invalid response status ${response.status}: ${await response.text()}`,
);
}
const { data } = (await response.json()) as { data: { token: string } };
@ -34,9 +37,11 @@ export async function authenticate(opts: AuthOpts) {
} catch (e) {
console.error(e);
const error = e as Error;
throw new Error(`Could not connect to n8n message broker ${opts.n8nUri}: ${error.message}`, {
cause: error,
});
throw new ApplicationError(
`Could not connect to n8n message broker ${opts.n8nUri}: ${error.message}`,
{
cause: error,
},
);
}
}

View file

@ -1,6 +1,4 @@
import { runInNewContext, type Context } from 'node:vm';
import * as a from 'node:assert';
import { getAdditionalKeys } from 'n8n-core';
import {
type INode,
type INodeType,
@ -8,8 +6,6 @@ import {
type IWorkflowExecuteAdditionalData,
WorkflowDataProxy,
type WorkflowParameters,
} from 'n8n-workflow';
import {
type IDataObject,
type IExecuteData,
type INodeExecutionData,
@ -19,7 +15,8 @@ import {
Workflow,
type WorkflowExecuteMode,
} from 'n8n-workflow';
import { getAdditionalKeys } from 'n8n-core';
import * as a from 'node:assert';
import { runInNewContext, type Context } from 'node:vm';
import type { TaskResultData } from './runner-types';
import { type Task, TaskRunner } from './task-runner';

View file

@ -0,0 +1,48 @@
import { ApplicationError, ensureError } from 'n8n-workflow';
import * as a from 'node:assert/strict';
import { authenticate } from './authenticator';
import { JsTaskRunner } from './code';
type Config = {
n8nUri: string;
authToken?: string;
grantToken?: string;
};
function readAndParseConfig(): Config {
const authToken = process.env.N8N_RUNNERS_AUTH_TOKEN;
const grantToken = process.env.N8N_RUNNERS_GRANT_TOKEN;
if (!authToken && !grantToken) {
throw new ApplicationError(
'Missing task runner authentication. Use either N8N_RUNNERS_AUTH_TOKEN or N8N_RUNNERS_GRANT_TOKEN to configure it',
);
}
return {
n8nUri: process.env.N8N_RUNNERS_N8N_URI ?? 'localhost:5678',
authToken,
grantToken,
};
}
void (async function start() {
const config = readAndParseConfig();
let grantToken = config.grantToken;
if (!grantToken) {
a.ok(config.authToken);
grantToken = await authenticate({
authToken: config.authToken,
n8nUri: config.n8nUri,
});
}
const wsUrl = `ws://${config.n8nUri}/runners/_ws`;
new JsTaskRunner('javascript', wsUrl, grantToken, 5);
})().catch((e) => {
const error = ensureError(e);
console.error('Task runner failed to start', { error });
process.exit(1);
});

View file

@ -1,7 +1,7 @@
import { URL } from 'node:url';
import { ApplicationError, ensureError } from 'n8n-workflow';
import { nanoid } from 'nanoid';
import { URL } from 'node:url';
import { type MessageEvent, WebSocket } from 'ws';
import { ensureError } from 'n8n-workflow';
import {
RPC_ALLOW_LIST,
@ -267,7 +267,7 @@ export abstract class TaskRunner {
// eslint-disable-next-line @typescript-eslint/naming-convention
async executeTask(_task: Task): Promise<TaskResultData> {
throw new Error('Unimplemented');
throw new ApplicationError('Unimplemented');
}
async requestData<T = unknown>(
@ -354,7 +354,7 @@ export abstract class TaskRunner {
obj = obj[s];
return;
}
obj[s] = async (...args: unknown[]) => this.makeRpcCall(taskId, r, args);
obj[s] = async (...args: unknown[]) => await this.makeRpcCall(taskId, r, args);
});
}
return rpcObject;

View file

@ -1,10 +1,11 @@
{
"extends": ["./tsconfig.json", "../../../tsconfig.build.json"],
"compilerOptions": {
"composite": true,
"rootDir": "src",
"outDir": "dist",
"tsBuildInfoFile": "dist/build.tsbuildinfo"
},
"include": ["src/**/*.ts"],
"exclude": ["test/**", "src/**/__tests__/**"]
"exclude": ["src/**/__tests__/**"]
}

View file

@ -2,13 +2,11 @@
"extends": ["../../../tsconfig.json", "../../../tsconfig.backend.json"],
"compilerOptions": {
"rootDir": ".",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"baseUrl": "src",
"paths": {
"@/*": ["./*"]
},
"tsBuildInfoFile": "dist/typecheck.tsbuildinfo"
},
"include": ["src/**/*.ts", "test/**/*.ts"]
"include": ["src/**/*.ts"]
}

View file

@ -2,6 +2,16 @@
This list shows all the versions which include breaking changes and how to upgrade.
## 1.57.0
### What changed?
The `verbose` log level was merged into the `debug` log level.
### When is action necessary?
If you are setting the env var `N8N_LOG_LEVEL=verbose`, please update your log level to `N8N_LOG_LEVEL=debug`.
## 1.55.0
### What changed?

View file

@ -1,6 +1,6 @@
{
"name": "n8n",
"version": "1.61.0",
"version": "1.62.1",
"description": "n8n Workflow Automation Tool",
"main": "dist/index",
"types": "dist/index.d.ts",

View file

@ -119,8 +119,6 @@ export abstract class AbstractServer {
protected setupPushServer() {}
protected setupRunnerServer() {}
private async setupHealthCheck() {
// main health check should not care about DB connections
this.app.get('/healthz', async (_req, res) => {
@ -184,10 +182,6 @@ export abstract class AbstractServer {
if (!inTest) {
await this.setupErrorHandlers();
this.setupPushServer();
if (!this.globalConfig.taskRunners.disabled) {
this.setupRunnerServer();
}
}
this.setupCommonMiddlewares();

View file

@ -225,6 +225,13 @@ export class Start extends BaseCommand {
if (!this.globalConfig.taskRunners.disabled) {
Container.set(TaskManager, new SingleMainTaskManager());
const { TaskRunnerServer } = await import('@/runners/task-runner-server');
const taskRunnerServer = Container.get(TaskRunnerServer);
await taskRunnerServer.start();
const { TaskRunnerProcess } = await import('@/runners/task-runner-process');
const runnerProcess = Container.get(TaskRunnerProcess);
await runnerProcess.start();
}
}

View file

@ -168,6 +168,8 @@ export const ARTIFICIAL_TASK_DATA = {
],
};
/** Lowest priority, meaning shut down happens after other groups */
export const LOWEST_SHUTDOWN_PRIORITY = 0;
export const DEFAULT_SHUTDOWN_PRIORITY = 100;
/** Highest priority, meaning shut down happens before all other groups */
export const HIGHEST_SHUTDOWN_PRIORITY = 200;

View file

@ -44,7 +44,7 @@ function getWsEndpoint(restEndpoint: string) {
@Service()
export class TaskRunnerService {
runnerConnections: Record<TaskRunner['id'], WebSocket> = {};
runnerConnections: Map<TaskRunner['id'], WebSocket> = new Map();
constructor(
private readonly logger: Logger,
@ -52,7 +52,7 @@ export class TaskRunnerService {
) {}
sendMessage(id: TaskRunner['id'], message: N8nMessage.ToRunner.All) {
this.runnerConnections[id]?.send(JSON.stringify(message));
this.runnerConnections.get(id)?.send(JSON.stringify(message));
}
add(id: TaskRunner['id'], connection: WebSocket) {
@ -75,7 +75,7 @@ export class TaskRunnerService {
this.removeConnection(id);
isConnected = true;
this.runnerConnections[id] = connection;
this.runnerConnections.set(id, connection);
this.taskBroker.registerRunner(
{
@ -117,10 +117,11 @@ export class TaskRunnerService {
}
removeConnection(id: TaskRunner['id']) {
if (id in this.runnerConnections) {
const connection = this.runnerConnections.get(id);
if (connection) {
this.taskBroker.deregisterRunner(id);
this.runnerConnections[id].close();
delete this.runnerConnections[id];
connection.close();
this.runnerConnections.delete(id);
}
}

View file

@ -0,0 +1,89 @@
import { GlobalConfig } from '@n8n/config';
import * as a from 'node:assert/strict';
import { spawn } from 'node:child_process';
import { Service } from 'typedi';
import { TaskRunnerAuthService } from './auth/task-runner-auth.service';
import { OnShutdown } from '../decorators/on-shutdown';
type ChildProcess = ReturnType<typeof spawn>;
/**
* Manages the JS task runner process as a child process
*/
@Service()
export class TaskRunnerProcess {
public get isRunning() {
return this.process !== null;
}
/** The process ID of the task runner process */
public get pid() {
return this.process?.pid;
}
private process: ChildProcess | null = null;
/** Promise that resolves after the process has exited */
private runPromise: Promise<void> | null = null;
private isShuttingDown = false;
constructor(
private readonly globalConfig: GlobalConfig,
private readonly authService: TaskRunnerAuthService,
) {}
async start() {
a.ok(!this.process, 'Task Runner Process already running');
const grantToken = await this.authService.createGrantToken();
const startScript = require.resolve('@n8n/task-runner');
this.process = spawn('node', [startScript], {
env: {
PATH: process.env.PATH,
N8N_RUNNERS_GRANT_TOKEN: grantToken,
N8N_RUNNERS_N8N_URI: `localhost:${this.globalConfig.taskRunners.port}`,
},
});
this.process.stdout?.pipe(process.stdout);
this.process.stderr?.pipe(process.stderr);
this.monitorProcess(this.process);
}
@OnShutdown()
async stop() {
if (!this.process) {
return;
}
this.isShuttingDown = true;
// TODO: Timeout & force kill
this.process.kill();
await this.runPromise;
this.isShuttingDown = false;
}
private monitorProcess(process: ChildProcess) {
this.runPromise = new Promise((resolve) => {
process.on('exit', (code) => {
this.onProcessExit(code, resolve);
});
});
}
private onProcessExit(_code: number | null, resolveFn: () => void) {
this.process = null;
resolveFn();
// If we are not shutting down, restart the process
if (!this.isShuttingDown) {
setImmediate(async () => await this.start());
}
}
}

View file

@ -0,0 +1,201 @@
import { GlobalConfig } from '@n8n/config';
import compression from 'compression';
import express from 'express';
import * as a from 'node:assert/strict';
import { randomBytes } from 'node:crypto';
import { ServerResponse, type Server, createServer as createHttpServer } from 'node:http';
import type { AddressInfo, Socket } from 'node:net';
import { parse as parseUrl } from 'node:url';
import { Service } from 'typedi';
import { Server as WSServer } from 'ws';
import { inTest, LOWEST_SHUTDOWN_PRIORITY } from '@/constants';
import { OnShutdown } from '@/decorators/on-shutdown';
import { Logger } from '@/logging/logger.service';
import { bodyParser, rawBodyReader } from '@/middlewares';
import { send } from '@/response-helper';
import { TaskRunnerAuthController } from '@/runners/auth/task-runner-auth.controller';
import type {
TaskRunnerServerInitRequest,
TaskRunnerServerInitResponse,
} from '@/runners/runner-types';
import { TaskRunnerService } from '@/runners/runner-ws-server';
/**
* Task Runner HTTP & WS server
*/
@Service()
export class TaskRunnerServer {
private server: Server | undefined;
private wsServer: WSServer | undefined;
readonly app: express.Application;
public get port() {
return (this.server?.address() as AddressInfo)?.port;
}
private get upgradeEndpoint() {
return `${this.getEndpointBasePath()}/_ws`;
}
constructor(
private readonly logger: Logger,
private readonly globalConfig: GlobalConfig,
private readonly taskRunnerAuthController: TaskRunnerAuthController,
private readonly taskRunnerService: TaskRunnerService,
) {
this.app = express();
this.app.disable('x-powered-by');
if (!this.globalConfig.taskRunners.authToken) {
// Generate an auth token if one is not set
this.globalConfig.taskRunners.authToken = randomBytes(32).toString('hex');
}
}
async start(): Promise<void> {
await this.setupHttpServer();
this.setupWsServer();
if (!inTest) {
await this.setupErrorHandlers();
}
this.setupCommonMiddlewares();
this.configureRoutes();
}
@OnShutdown(LOWEST_SHUTDOWN_PRIORITY)
async stop(): Promise<void> {
if (this.wsServer) {
this.wsServer.close();
this.wsServer = undefined;
}
if (this.server) {
await new Promise<void>((resolve) => this.server?.close(() => resolve()));
this.server = undefined;
}
}
/** Creates an HTTP server and listens to the configured port */
private async setupHttpServer() {
const { app } = this;
this.server = createHttpServer(app);
const {
taskRunners: { port, listen_address: address },
} = this.globalConfig;
this.server.on('error', (error: Error & { code: string }) => {
if (error.code === 'EADDRINUSE') {
this.logger.info(
`n8n Task Runner's port ${port} is already in use. Do you have another instance of n8n running already?`,
);
process.exit(1);
}
});
await new Promise<void>((resolve) => {
a.ok(this.server);
this.server.listen(port, address, () => resolve());
});
this.logger.info(`n8n Task Runner server ready on ${address}, port ${port}`);
}
/** Creates WebSocket server for handling upgrade requests */
private setupWsServer() {
const { authToken } = this.globalConfig.taskRunners;
a.ok(authToken);
a.ok(this.server);
this.wsServer = new WSServer({ noServer: true });
this.server.on('upgrade', this.handleUpgradeRequest);
}
private async setupErrorHandlers() {
const { app } = this;
// Augment errors sent to Sentry
const {
Handlers: { requestHandler, errorHandler },
} = await import('@sentry/node');
app.use(requestHandler());
app.use(errorHandler());
}
private setupCommonMiddlewares() {
// Compress the response data
this.app.use(compression());
this.app.use(rawBodyReader);
this.app.use(bodyParser);
}
private configureRoutes() {
this.app.use(
this.upgradeEndpoint,
// eslint-disable-next-line @typescript-eslint/unbound-method
this.taskRunnerAuthController.authMiddleware,
(req: TaskRunnerServerInitRequest, res: TaskRunnerServerInitResponse) =>
this.taskRunnerService.handleRequest(req, res),
);
const authEndpoint = `${this.getEndpointBasePath()}/auth`;
this.app.post(
authEndpoint,
send(async (req) => await this.taskRunnerAuthController.createGrantToken(req)),
);
}
private handleUpgradeRequest = (
request: TaskRunnerServerInitRequest,
socket: Socket,
head: Buffer,
) => {
if (parseUrl(request.url).pathname !== this.upgradeEndpoint) {
socket.write('HTTP/1.1 404 Not Found\r\n\r\n');
socket.destroy();
return;
}
if (!this.wsServer) {
// This might happen if the server is shutting down and we receive an upgrade request
socket.write('HTTP/1.1 503 Service Unavailable\r\n\r\n');
socket.destroy();
return;
}
this.wsServer.handleUpgrade(request, socket, head, (ws) => {
request.ws = ws;
const response = new ServerResponse(request);
response.writeHead = (statusCode) => {
if (statusCode > 200) ws.close(100);
return response;
};
// @ts-expect-error Delegate the request to the express app. This function is not exposed
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
this.app.handle(request, response);
});
};
/** Returns the normalized base path for the task runner endpoints */
private getEndpointBasePath() {
let path = this.globalConfig.taskRunners.path;
if (!path.startsWith('/')) {
path = `/${path}`;
}
if (path.endsWith('/')) {
path = path.slice(-1);
}
return path;
}
}

View file

@ -31,7 +31,6 @@ import { isApiEnabled, loadPublicApiVersions } from '@/public-api';
import { setupPushServer, setupPushHandler, Push } from '@/push';
import type { APIRequest } from '@/requests';
import * as ResponseHelper from '@/response-helper';
import { setupRunnerServer, setupRunnerHandler } from '@/runners/runner-ws-server';
import type { FrontendService } from '@/services/frontend.service';
import { OrchestrationService } from '@/services/orchestration.service';
@ -202,10 +201,6 @@ export class Server extends AbstractServer {
const { restEndpoint, app } = this;
setupPushHandler(restEndpoint, app);
if (!this.globalConfig.taskRunners.disabled) {
setupRunnerHandler(restEndpoint, app);
}
const push = Container.get(Push);
if (push.isBidirectional) {
const { CollaborationService } = await import('@/collaboration/collaboration.service');
@ -405,9 +400,4 @@ export class Server extends AbstractServer {
const { restEndpoint, server, app } = this;
setupPushServer(restEndpoint, server, app);
}
protected setupRunnerServer(): void {
const { restEndpoint, server, app } = this;
setupRunnerServer(restEndpoint, server, app);
}
}

View file

@ -0,0 +1,91 @@
import { GlobalConfig } from '@n8n/config';
import Container from 'typedi';
import { TaskRunnerService } from '@/runners/runner-ws-server';
import { TaskBroker } from '@/runners/task-broker.service';
import { TaskRunnerProcess } from '@/runners/task-runner-process';
import { TaskRunnerServer } from '@/runners/task-runner-server';
import { retryUntil } from '@test-integration/retry-until';
describe('TaskRunnerProcess', () => {
const authToken = 'token';
const globalConfig = Container.get(GlobalConfig);
globalConfig.taskRunners.authToken = authToken;
globalConfig.taskRunners.port = 0; // Use any port
const taskRunnerServer = Container.get(TaskRunnerServer);
const runnerProcess = Container.get(TaskRunnerProcess);
const taskBroker = Container.get(TaskBroker);
const taskRunnerService = Container.get(TaskRunnerService);
beforeAll(async () => {
await taskRunnerServer.start();
// Set the port to the actually used port
globalConfig.taskRunners.port = taskRunnerServer.port;
});
afterAll(async () => {
await taskRunnerServer.stop();
});
afterEach(async () => {
await runnerProcess.stop();
});
const getNumConnectedRunners = () => taskRunnerService.runnerConnections.size;
const getNumRegisteredRunners = () => taskBroker.getKnownRunners().size;
it('should start and connect the task runner', async () => {
// Act
await runnerProcess.start();
// Assert
expect(runnerProcess.isRunning).toBeTruthy();
// Wait until the runner has connected
await retryUntil(() => expect(getNumConnectedRunners()).toBe(1));
expect(getNumRegisteredRunners()).toBe(1);
});
it('should stop an disconnect the task runner', async () => {
// Arrange
await runnerProcess.start();
// Wait until the runner has connected
await retryUntil(() => expect(getNumConnectedRunners()).toBe(1));
expect(getNumRegisteredRunners()).toBe(1);
// Act
await runnerProcess.stop();
// Assert
// Wait until the runner has disconnected
await retryUntil(() => expect(getNumConnectedRunners()).toBe(0));
expect(runnerProcess.isRunning).toBeFalsy();
expect(getNumRegisteredRunners()).toBe(0);
});
it('should restart the task runner if it exits', async () => {
// Arrange
await runnerProcess.start();
// Wait until the runner has connected
await retryUntil(() => expect(getNumConnectedRunners()).toBe(1));
const processId = runnerProcess.pid;
// Act
// @ts-expect-error private property
runnerProcess.process?.kill('SIGKILL');
// Assert
// Wait until the runner is running again
await retryUntil(() => expect(runnerProcess.isRunning).toBeTruthy());
expect(runnerProcess.pid).not.toBe(processId);
// Wait until the runner has connected again
await retryUntil(() => expect(getNumConnectedRunners()).toBe(1));
expect(getNumConnectedRunners()).toBe(1);
expect(getNumRegisteredRunners()).toBe(1);
});
});

View file

@ -1,6 +1,6 @@
{
"name": "n8n-core",
"version": "1.61.0",
"version": "1.62.1",
"description": "Core functionality of n8n",
"main": "dist/index",
"types": "dist/index.d.ts",

View file

@ -1,6 +1,6 @@
{
"name": "n8n-design-system",
"version": "1.51.0",
"version": "1.52.1",
"main": "src/main.ts",
"import": "src/main.ts",
"scripts": {

View file

@ -1,6 +1,6 @@
{
"name": "n8n-editor-ui",
"version": "1.61.0",
"version": "1.62.1",
"description": "Workflow Editor UI for n8n",
"main": "index.js",
"scripts": {
@ -48,6 +48,7 @@
"@vueuse/core": "^10.11.0",
"axios": "catalog:",
"bowser": "2.11.0",
"change-case": "^5.4.4",
"chart.js": "^4.4.0",
"codemirror-lang-html-n8n": "^1.0.0",
"dateformat": "^3.0.3",

View file

@ -1,5 +1,5 @@
import type { SectionCreateElement } from '@/Interface';
import { groupItemsInSections, sortNodeCreateElements } from '../utils';
import { formatTriggerActionName, groupItemsInSections, sortNodeCreateElements } from '../utils';
import { mockActionCreateElement, mockNodeCreateElement, mockSectionCreateElement } from './utils';
describe('NodeCreator - utils', () => {
@ -62,4 +62,15 @@ describe('NodeCreator - utils', () => {
expect(sortNodeCreateElements([node1, node2, node3])).toEqual([node1, node2, node3]);
});
});
describe('formatTriggerActionName', () => {
test.each([
['project.created', 'project created'],
['Project Created', 'project created'],
['field.value.created', 'field value created'],
['attendee.checked_in', 'attendee checked in'],
])('Action name %i should become as %i', (actionName, expected) => {
expect(formatTriggerActionName(actionName)).toEqual(expected);
});
});
});

View file

@ -12,6 +12,7 @@ import type {
import { i18n } from '@/plugins/i18n';
import { getCredentialOnlyNodeType } from '@/utils/credentialOnlyNodes';
import { formatTriggerActionName } from '../utils';
const PLACEHOLDER_RECOMMENDED_ACTION_KEY = 'placeholder_recommended';
@ -168,7 +169,7 @@ function triggersCategory(nodeTypeDescription: INodeTypeDescription): ActionType
displayName:
categoryItem.action ??
cachedBaseText('nodeCreator.actionsCategory.onEvent', {
interpolate: { event: startCase(categoryItem.name) },
interpolate: { event: formatTriggerActionName(categoryItem.name) },
}),
description: categoryItem.description ?? '',
displayOptions: matchedProperty.displayOptions,

View file

@ -19,6 +19,7 @@ import { sublimeSearch } from '@/utils/sortUtils';
import type { NodeViewItemSection } from './viewsData';
import { i18n } from '@/plugins/i18n';
import { sortBy } from 'lodash-es';
import * as changeCase from 'change-case';
import { usePostHog } from '@/stores/posthog.store';
@ -177,3 +178,11 @@ export function groupItemsInSections(
return result;
}
export const formatTriggerActionName = (actionPropertyName: string) => {
let name = actionPropertyName;
if (actionPropertyName.includes('.')) {
name = actionPropertyName.split('.').join(' ');
}
return changeCase.noCase(name);
};

View file

@ -22,6 +22,7 @@ describe('WorkflowsView', () => {
let projectsStore: ReturnType<typeof useProjectsStore>;
const routerReplaceMock = vi.fn();
const routerPushMock = vi.fn();
const renderComponent = createComponentRenderer(WorkflowsView, {
global: {
@ -32,6 +33,7 @@ describe('WorkflowsView', () => {
},
$router: {
replace: routerReplaceMock,
push: routerPushMock,
},
},
},

View file

@ -26,6 +26,8 @@ interface Filters {
tags: string[];
}
type QueryFilters = Partial<Filters>;
const StatusFilter = {
ACTIVE: true,
DEACTIVATED: false,
@ -152,6 +154,9 @@ const WorkflowsView = defineComponent({
},
async mounted() {
this.documentTitle.set(this.$locale.baseText('workflows.heading'));
await this.tagsStore.fetchAll();
await this.setFiltersFromQueryString();
void this.usersStore.showPersonalizationSurvey();
@ -263,10 +268,20 @@ const WorkflowsView = defineComponent({
isValidProjectId(projectId: string) {
return this.projectsStore.availableProjects.some((project) => project.id === projectId);
},
async removeInvalidQueryFiltersFromUrl(filtersToApply: QueryFilters) {
await this.$router.push({
query: {
...(filtersToApply.tags && { tags: filtersToApply.tags?.join(',') }),
...(filtersToApply.status && { status: filtersToApply.status?.toString() }),
...(filtersToApply.search && { search: filtersToApply.search }),
...(filtersToApply.homeProject && { homeProject: filtersToApply.homeProject }),
},
});
},
async setFiltersFromQueryString() {
const { tags, status, search, homeProject } = this.$route.query;
const filtersToApply: { [key: string]: string | string[] | boolean } = {};
const filtersToApply: QueryFilters = {};
if (homeProject && typeof homeProject === 'string') {
await this.projectsStore.getAvailableProjects();
@ -295,6 +310,8 @@ const WorkflowsView = defineComponent({
filtersToApply.status = status === 'true';
}
await this.removeInvalidQueryFiltersFromUrl(filtersToApply);
if (Object.keys(filtersToApply).length) {
this.filters = {
...this.filters,

View file

@ -1,6 +1,6 @@
{
"name": "n8n-node-dev",
"version": "1.61.0",
"version": "1.62.1",
"description": "CLI to simplify n8n credentials/node development",
"main": "dist/src/index",
"types": "dist/src/index.d.ts",

View file

@ -211,9 +211,51 @@ describe('FormTrigger, prepareFormData', () => {
expect(result.formFields[0].isMultiSelect).toBe(true);
expect(result.formFields[0].multiSelectOptions).toEqual([
{ id: 'option0', label: 'Red' },
{ id: 'option1', label: 'Blue' },
{ id: 'option2', label: 'Green' },
{ id: 'option0_field-0', label: 'Red' },
{ id: 'option1_field-0', label: 'Blue' },
{ id: 'option2_field-0', label: 'Green' },
]);
});
it('should correctly handle multiselect fields with unique ids', () => {
const formFields: FormField[] = [
{
fieldLabel: 'Favorite Colors',
fieldType: 'text',
requiredField: true,
multiselect: true,
fieldOptions: { values: [{ option: 'Red' }, { option: 'Blue' }, { option: 'Green' }] },
},
{
fieldLabel: 'Favorite Colors',
fieldType: 'text',
requiredField: true,
multiselect: true,
fieldOptions: { values: [{ option: 'Red' }, { option: 'Blue' }, { option: 'Green' }] },
},
];
const query = { 'Favorite Colors': 'Red,Blue' };
const result = prepareFormData({
formTitle: 'Test Form',
formDescription: 'This is a test form',
formSubmittedText: 'Thank you',
redirectUrl: 'example.com',
formFields,
testRun: false,
query,
});
expect(result.formFields[0].isMultiSelect).toBe(true);
expect(result.formFields[0].multiSelectOptions).toEqual([
{ id: 'option0_field-0', label: 'Red' },
{ id: 'option1_field-0', label: 'Blue' },
{ id: 'option2_field-0', label: 'Green' },
]);
expect(result.formFields[1].multiSelectOptions).toEqual([
{ id: 'option0_field-1', label: 'Red' },
{ id: 'option1_field-1', label: 'Blue' },
{ id: 'option2_field-1', label: 'Green' },
]);
});
});

View file

@ -83,7 +83,7 @@ export function prepareFormData({
input.isMultiSelect = true;
input.multiSelectOptions =
field.fieldOptions?.values.map((e, i) => ({
id: `option${i}`,
id: `option${i}_${input.id}`,
label: e.option,
})) ?? [];
} else if (fieldType === 'file') {

View file

@ -1,6 +1,6 @@
{
"name": "n8n-nodes-base",
"version": "1.61.0",
"version": "1.62.1",
"description": "Base nodes of n8n",
"main": "index.js",
"scripts": {

View file

@ -1,6 +1,6 @@
{
"name": "n8n-workflow",
"version": "1.60.0",
"version": "1.61.1",
"description": "Workflow base code of n8n",
"main": "dist/index.js",
"module": "src/index.ts",

File diff suppressed because it is too large Load diff