Merge branch 'master' into node-1466-overhaul-code-node-p0

This commit is contained in:
Elias Meire 2024-09-16 10:37:02 +02:00
commit 09a4673f46
No known key found for this signature in database
2589 changed files with 147170 additions and 30611 deletions

View file

@ -4,21 +4,21 @@ We have very precise rules over how Pull Requests (to the `master` branch) must
A PR title consists of these elements: A PR title consists of these elements:
``` ```text
<type>(<scope>): <summary> <type>(<scope>): <summary>
│ │ │ │ │ │
│ │ └─⫸ Summary: In imperative present tense. │ │ └─⫸ Summary: In imperative present tense.
| | Capitalized | | Capitalized
| | No period at the end. | | No period at the end.
│ │ │ │
│ └─⫸ Scope: API|core|editor|* Node │ └─⫸ Scope: API | benchmark | core | editor | * Node
└─⫸ Type: build|ci|docs|feat|fix|perf|refactor|test └─⫸ Type: build | ci | chore | docs | feat | fix | perf | refactor | test
``` ```
- PR title - PR title
- type - type
- scope (*optional*) - scope (_optional_)
- summary - summary
- PR description - PR description
- body (optional) - body (optional)
@ -27,26 +27,30 @@ A PR title consists of these elements:
The structure looks like this: The structure looks like this:
### **Type** ## Type
Must be one of the following: Must be one of the following:
- `feat` - A new feature | type | description | appears in changelog |
- `fix` - A bug fix | --- | --- | --- |
- `perf` - A code change that improves performance | `feat` | A new feature | ✅ |
- `test` - Adding missing tests or correcting existing tests | `fix` | A bug fix | ✅ |
- `docs` - Documentation only changes | `perf` | A code change that improves performance | ✅ |
- `refactor` - A code change that neither fixes a bug nor adds a feature | `test` | Adding missing tests or correcting existing tests | ❌ |
- `build` - Changes that affect the build system or external dependencies (example scopes: broccoli, npm) | `docs` | Documentation only changes | ❌ |
- `ci` - Changes to our CI configuration files and scripts (e.g. Github actions) | `refactor` | A behavior-neutral code change that neither fixes a bug nor adds a feature | ❌ |
| `build` | Changes that affect the build system or external dependencies (TypeScript, Jest, pnpm, etc.) | ❌ |
| `ci` | Changes to CI configuration files and scripts (e.g. Github actions) | ❌ |
| `chore` | Routine tasks, maintenance, and minor updates not covered by other types | ❌ |
If the prefix is `feat`, `fix` or `perf`, it will appear in the changelog. However if there is any BREAKING CHANGE (see Footer section below), the commit will always appear in the changelog. > BREAKING CHANGES (see Footer section below), will **always** appear in the changelog unless suffixed with `no-changelog`.
### **Scope (optional)** ## Scope (optional)
The scope should specify the place of the commit change as long as the commit clearly addresses one of the following supported scopes. (Otherwise, omit the scope!) The scope should specify the place of the commit change as long as the commit clearly addresses one of the following supported scopes. (Otherwise, omit the scope!)
- `API` - changes to the *public* API - `API` - changes to the _public_ API
- `benchmark` - changes to the benchmark cli
- `core` - changes to the core / private API / backend of n8n - `core` - changes to the core / private API / backend of n8n
- `editor` - changes to the Editor UI - `editor` - changes to the Editor UI
- `* Node` - changes to a specific node or trigger node (”`*`” to be replaced with the node name, not its display name), e.g. - `* Node` - changes to a specific node or trigger node (”`*`” to be replaced with the node name, not its display name), e.g.
@ -54,25 +58,25 @@ The scope should specify the place of the commit change as long as the commit cl
- microsoftToDo → Microsoft To Do Node - microsoftToDo → Microsoft To Do Node
- n8n → n8n Node - n8n → n8n Node
### **Summary** ## Summary
The summary contains succinct description of the change: The summary contains succinct description of the change:
- use the imperative, present tense: "change" not "changed" nor "changes" - use the imperative, present tense: "change" not "changed" nor "changes"
- capitalize the first letter - capitalize the first letter
- *no* dot (.) at the end - _no_ dot (.) at the end
- do *not* include Linear ticket IDs etc. (e.g. N8N-1234) - do _not_ include Linear ticket IDs etc. (e.g. N8N-1234)
- suffix with “(no-changelog)” for commits / PRs that should not get mentioned in the changelog. - suffix with “(no-changelog)” for commits / PRs that should not get mentioned in the changelog.
### **Body (optional)** ## Body (optional)
Just as in the **summary**, use the imperative, present tense: "change" not "changed" nor "changes". The body should include the motivation for the change and contrast this with previous behavior. Just as in the **summary**, use the imperative, present tense: "change" not "changed" nor "changes". The body should include the motivation for the change and contrast this with previous behavior.
### **Footer (optional)** ## Footer (optional)
The footer can contain information about breaking changes and deprecations and is also the place to [reference GitHub issues](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword), Linear tickets, and other PRs that this commit closes or is related to. For example: The footer can contain information about breaking changes and deprecations and is also the place to [reference GitHub issues](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword), Linear tickets, and other PRs that this commit closes or is related to. For example:
``` ```text
BREAKING CHANGE: <breaking change summary> BREAKING CHANGE: <breaking change summary>
<BLANK LINE> <BLANK LINE>
<breaking change description + migration instructions> <breaking change description + migration instructions>
@ -83,7 +87,7 @@ Fixes #<issue number>
or or
``` ```text
DEPRECATED: <what is deprecated> DEPRECATED: <what is deprecated>
<BLANK LINE> <BLANK LINE>
<deprecation description + recommended update path> <deprecation description + recommended update path>
@ -102,7 +106,7 @@ A Breaking Change section should start with the phrase "`BREAKING CHANGE:` " fol
Similarly, a Deprecation section should start with "`DEPRECATED:` " followed by a short description of what is deprecated, a blank line, and a detailed description of the deprecation that also mentions the recommended update path. Similarly, a Deprecation section should start with "`DEPRECATED:` " followed by a short description of what is deprecated, a blank line, and a detailed description of the deprecation that also mentions the recommended update path.
### **Revert commits** ### Revert commits
If the commit reverts a previous commit, it should begin with `revert:` , followed by the header of the reverted commit. If the commit reverts a previous commit, it should begin with `revert:` , followed by the header of the reverted commit.

View file

@ -16,7 +16,11 @@ const changelogStream = conventionalChangelog({
releaseCount: 1, releaseCount: 1,
tagPrefix: 'n8n@', tagPrefix: 'n8n@',
transform: (commit, callback) => { transform: (commit, callback) => {
callback(null, commit.header.includes('(no-changelog)') ? undefined : commit); const hasNoChangelogInHeader = commit.header.includes('(no-changelog)');
const isBenchmarkScope = commit.scope === 'benchmark';
// Ignore commits that have 'benchmark' scope or '(no-changelog)' in the header
callback(null, hasNoChangelogInHeader || isBenchmarkScope ? undefined : commit);
}, },
}).on('error', (err) => { }).on('error', (err) => {
console.error(err.stack); console.error(err.stack);

View file

@ -0,0 +1,39 @@
name: Destroy Benchmark Env
on:
schedule:
- cron: '0 4 * * *'
workflow_dispatch:
permissions:
id-token: write
contents: read
jobs:
build:
runs-on: ubuntu-latest
environment: benchmarking
steps:
- name: Checkout
uses: actions/checkout@v4.1.1
- name: Azure login
uses: azure/login@v2.1.1
with:
client-id: ${{ secrets.BENCHMARK_ARM_CLIENT_ID }}
tenant-id: ${{ secrets.BENCHMARK_ARM_TENANT_ID }}
subscription-id: ${{ secrets.BENCHMARK_ARM_SUBSCRIPTION_ID }}
- run: corepack enable
- uses: actions/setup-node@v4.0.2
with:
node-version: 20.x
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Destroy cloud env
run: pnpm destroy-cloud-env
working-directory: packages/@n8n/benchmark

96
.github/workflows/benchmark-nightly.yml vendored Normal file
View file

@ -0,0 +1,96 @@
name: Run Nightly Benchmark
run-name: Benchmark ${{ inputs.n8n_tag || 'nightly' }}
on:
schedule:
- cron: '30 1,2,3 * * *'
workflow_dispatch:
inputs:
debug:
description: 'Use debug logging'
required: true
default: 'false'
n8n_tag:
description: 'Name of the n8n docker tag to run the benchmark against.'
required: true
default: 'nightly'
benchmark_tag:
description: 'Name of the benchmark cli docker tag to run the benchmark with.'
required: true
default: 'latest'
env:
ARM_CLIENT_ID: ${{ secrets.BENCHMARK_ARM_CLIENT_ID }}
ARM_SUBSCRIPTION_ID: ${{ secrets.BENCHMARK_ARM_SUBSCRIPTION_ID }}
ARM_TENANT_ID: ${{ secrets.BENCHMARK_ARM_TENANT_ID }}
N8N_TAG: ${{ inputs.n8n_tag || 'nightly' }}
N8N_BENCHMARK_TAG: ${{ inputs.benchmark_tag || 'latest' }}
DEBUG: ${{ inputs.debug == 'true' && '--debug' || '' }}
permissions:
id-token: write
contents: read
jobs:
build:
runs-on: ubuntu-latest
environment: benchmarking
steps:
- name: Checkout
uses: actions/checkout@v4.1.1
- uses: hashicorp/setup-terraform@v3
with:
terraform_version: '1.8.5'
- run: corepack enable
- uses: actions/setup-node@v4.0.2
with:
node-version: 20.x
cache: pnpm
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Azure login
uses: azure/login@v2.1.1
with:
client-id: ${{ env.ARM_CLIENT_ID }}
tenant-id: ${{ env.ARM_TENANT_ID }}
subscription-id: ${{ env.ARM_SUBSCRIPTION_ID }}
- name: Destroy any existing environment
run: pnpm destroy-cloud-env
working-directory: packages/@n8n/benchmark
- name: Provision the environment
run: pnpm provision-cloud-env ${{ env.DEBUG }}
working-directory: packages/@n8n/benchmark
- name: Run the benchmark
env:
BENCHMARK_RESULT_WEBHOOK_URL: ${{ secrets.BENCHMARK_RESULT_WEBHOOK_URL }}
BENCHMARK_RESULT_WEBHOOK_AUTH_HEADER: ${{ secrets.BENCHMARK_RESULT_WEBHOOK_AUTH_HEADER }}
N8N_LICENSE_CERT: ${{ secrets.N8N_BENCHMARK_LICENSE_CERT }}
run: |
pnpm benchmark-in-cloud \
--vus 5 \
--duration 1m \
--n8nTag ${{ env.N8N_TAG }} \
--benchmarkTag ${{ env.N8N_BENCHMARK_TAG }} \
${{ env.DEBUG }}
working-directory: packages/@n8n/benchmark
# We need to login again because the access token expires
- name: Azure login
uses: azure/login@v2.1.1
with:
client-id: ${{ env.ARM_CLIENT_ID }}
tenant-id: ${{ env.ARM_TENANT_ID }}
subscription-id: ${{ env.ARM_SUBSCRIPTION_ID }}
- name: Destroy the environment
if: always()
run: pnpm destroy-cloud-env ${{ env.DEBUG }}
working-directory: packages/@n8n/benchmark

View file

@ -26,7 +26,7 @@ jobs:
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile
- name: Build relevant packages - name: Build relevant packages
run: pnpm --filter @n8n/client-oauth2 --filter @n8n/imap --filter n8n-workflow --filter n8n-core --filter n8n-nodes-base --filter @n8n/n8n-nodes-langchain build run: pnpm build:nodes
- run: npm install --prefix=.github/scripts --no-package-lock - run: npm install --prefix=.github/scripts --no-package-lock

View file

@ -7,8 +7,7 @@ on:
- edited - edited
- synchronize - synchronize
branches: branches:
- '**' - 'master'
- '!release/*'
jobs: jobs:
check-pr-title: check-pr-title:
@ -29,6 +28,6 @@ jobs:
- name: Validate PR title - name: Validate PR title
id: validate_pr_title id: validate_pr_title
uses: n8n-io/validate-n8n-pull-request-title@v2.0.1 uses: n8n-io/validate-n8n-pull-request-title@v2.2.0
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View file

@ -4,7 +4,7 @@ on:
workflow_dispatch: workflow_dispatch:
pull_request_review: pull_request_review:
types: [submitted] types: [submitted]
branch: branches:
- 'master' - 'master'
paths: paths:
- packages/design-system/** - packages/design-system/**

View file

@ -8,6 +8,10 @@ on:
paths: paths:
- packages/cli/src/databases/** - packages/cli/src/databases/**
- .github/workflows/ci-postgres-mysql.yml - .github/workflows/ci-postgres-mysql.yml
pull_request_review:
types: [submitted]
branches:
- 'release/*'
concurrency: concurrency:
group: db-${{ github.event.pull_request.number || github.ref }} group: db-${{ github.event.pull_request.number || github.ref }}

View file

@ -1,6 +1,10 @@
name: Build, unit test and lint branch name: Build, unit test and lint branch
on: [pull_request] on:
pull_request:
branches:
- '**'
- '!release/*'
jobs: jobs:
install-and-build: install-and-build:
@ -9,7 +13,6 @@ jobs:
steps: steps:
- uses: actions/checkout@v4.1.1 - uses: actions/checkout@v4.1.1
with: with:
repository: n8n-io/n8n
ref: refs/pull/${{ github.event.pull_request.number }}/merge ref: refs/pull/${{ github.event.pull_request.number }}/merge
- run: corepack enable - run: corepack enable

View file

@ -0,0 +1,43 @@
name: Benchmark Docker Image CI
on:
workflow_dispatch:
push:
branches:
- master
paths:
- 'packages/@n8n/benchmark/**'
- 'pnpm-lock.yaml'
- 'pnpm-workspace.yaml'
- '.github/workflows/docker-images-benchmark.yml'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4.1.1
- name: Set up QEMU
uses: docker/setup-qemu-action@v3.0.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.0.0
- name: Login to GitHub Container Registry
uses: docker/login-action@v3.0.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build
uses: docker/build-push-action@v5.1.0
with:
context: .
file: ./packages/@n8n/benchmark/Dockerfile
platforms: linux/amd64
provenance: false
push: true
tags: |
ghcr.io/${{ github.repository_owner }}/n8n-benchmark:latest

View file

@ -6,10 +6,6 @@ on:
- cron: '0 1 * * *' - cron: '0 1 * * *'
workflow_dispatch: workflow_dispatch:
inputs: inputs:
repository:
description: 'GitHub repository to create image off.'
required: true
default: 'n8n-io/n8n'
branch: branch:
description: 'GitHub branch to create image off.' description: 'GitHub branch to create image off.'
required: true required: true
@ -36,6 +32,9 @@ on:
required: false required: false
default: '' default: ''
env:
N8N_TAG: ${{ inputs.tag || 'nightly' }}
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -49,7 +48,6 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v4.1.1 uses: actions/checkout@v4.1.1
with: with:
repository: ${{ github.event.inputs.repository || 'n8n-io/n8n' }}
ref: ${{ github.event.inputs.branch || 'master' }} ref: ${{ github.event.inputs.branch || 'master' }}
- name: Set up QEMU - name: Set up QEMU
@ -69,7 +67,7 @@ jobs:
[[ "${{github.event.inputs.merge-master}}" == "true" ]] && git remote add upstream https://github.com/n8n-io/n8n.git -f; git merge upstream/master --allow-unrelated-histories || echo "" [[ "${{github.event.inputs.merge-master}}" == "true" ]] && git remote add upstream https://github.com/n8n-io/n8n.git -f; git merge upstream/master --allow-unrelated-histories || echo ""
shell: bash shell: bash
- name: Build and push - name: Build and push to DockerHub
uses: docker/build-push-action@v5.1.0 uses: docker/build-push-action@v5.1.0
with: with:
context: . context: .
@ -81,7 +79,22 @@ jobs:
push: true push: true
cache-from: type=gha cache-from: type=gha
cache-to: type=gha,mode=max cache-to: type=gha,mode=max
tags: ${{ secrets.DOCKER_USERNAME }}/n8n:${{ github.event.inputs.tag || 'nightly' }} tags: ${{ secrets.DOCKER_USERNAME }}/n8n:${{ env.N8N_TAG }}
- name: Login to GitHub Container Registry
if: env.N8N_TAG == 'nightly'
uses: docker/login-action@v3.0.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Push image to GHCR
if: env.N8N_TAG == 'nightly'
run: |
docker buildx imagetools create \
--tag ghcr.io/${{ github.repository_owner }}/n8n:nightly \
${{ secrets.DOCKER_USERNAME }}/n8n:nightly
- name: Call Success URL - optionally - name: Call Success URL - optionally
run: | run: |

View file

@ -1,48 +0,0 @@
name: Docker Image CI
on:
release:
types: [published]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4.1.1
- name: Get the version
id: vars
run: echo ::set-output name=tag::$(echo ${GITHUB_REF:14})
- name: Set up QEMU
uses: docker/setup-qemu-action@v3.0.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.0.0
- name: Login to GitHub Container Registry
uses: docker/login-action@v3.0.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to DockerHub
uses: docker/login-action@v3.0.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build
uses: docker/build-push-action@v5.1.0
with:
context: ./docker/images/n8n
build-args: |
N8N_VERSION=${{ steps.vars.outputs.tag }}
platforms: linux/amd64,linux/arm64
provenance: false
push: true
tags: |
${{ secrets.DOCKER_USERNAME }}/n8n:${{ steps.vars.outputs.tag }}
ghcr.io/${{ github.repository_owner }}/n8n:${{ steps.vars.outputs.tag }}

View file

@ -22,11 +22,6 @@ on:
required: false required: false
default: 'browsers:node18.12.0-chrome107' default: 'browsers:node18.12.0-chrome107'
type: string type: string
cache-key:
description: 'Cache key for modules and build artifacts.'
required: false
default: ${{ github.sha }}-${{ inputs.run-env }}-e2e-modules
type: string
record: record:
description: 'Record test run.' description: 'Record test run.'
required: false required: false
@ -78,7 +73,6 @@ jobs:
steps: steps:
- uses: actions/checkout@v4.1.1 - uses: actions/checkout@v4.1.1
with: with:
repository: n8n-io/n8n
ref: ${{ inputs.branch }} ref: ${{ inputs.branch }}
- name: Checkout PR - name: Checkout PR
@ -111,7 +105,7 @@ jobs:
/github/home/.cache /github/home/.cache
/github/home/.pnpm-store /github/home/.pnpm-store
./packages/**/dist ./packages/**/dist
key: ${{ inputs.cache-key }} key: ${{ github.sha }}-e2e
testing: testing:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -128,7 +122,6 @@ jobs:
steps: steps:
- uses: actions/checkout@v4.1.1 - uses: actions/checkout@v4.1.1
with: with:
repository: n8n-io/n8n
ref: ${{ inputs.branch }} ref: ${{ inputs.branch }}
- name: Checkout PR - name: Checkout PR
@ -146,7 +139,7 @@ jobs:
/github/home/.cache /github/home/.cache
/github/home/.pnpm-store /github/home/.pnpm-store
./packages/**/dist ./packages/**/dist
key: ${{ inputs.cache-key }} key: ${{ github.sha }}-e2e
- name: Install dependencies - name: Install dependencies
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile

View file

@ -3,8 +3,9 @@ name: PR E2E
on: on:
pull_request_review: pull_request_review:
types: [submitted] types: [submitted]
branch: branches:
- 'master' - 'master'
- 'release/*'
concurrency: concurrency:
group: e2e-${{ github.event.pull_request.number || github.ref }} group: e2e-${{ github.event.pull_request.number || github.ref }}
@ -18,7 +19,6 @@ jobs:
with: with:
pr_number: ${{ github.event.pull_request.number }} pr_number: ${{ github.event.pull_request.number }}
user: ${{ github.event.pull_request.user.login || 'PR User' }} user: ${{ github.event.pull_request.user.login || 'PR User' }}
spec: 'e2e/*'
secrets: secrets:
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}

View file

@ -21,7 +21,6 @@ jobs:
steps: steps:
- uses: actions/checkout@v4.1.1 - uses: actions/checkout@v4.1.1
with: with:
repository: n8n-io/n8n
ref: ${{ inputs.ref }} ref: ${{ inputs.ref }}
- run: corepack enable - run: corepack enable

View file

@ -56,12 +56,12 @@ jobs:
git push -f origin refs/remotes/origin/${{ github.event.inputs.base-branch }}:refs/heads/release/${{ env.NEXT_RELEASE }} git push -f origin refs/remotes/origin/${{ github.event.inputs.base-branch }}:refs/heads/release/${{ env.NEXT_RELEASE }}
- name: Push the release branch, and Create the PR - name: Push the release branch, and Create the PR
uses: peter-evans/create-pull-request@v5 uses: peter-evans/create-pull-request@v6
with: with:
base: 'release/${{ env.NEXT_RELEASE }}' base: 'release/${{ env.NEXT_RELEASE }}'
branch: '${{ env.NEXT_RELEASE }}-pr' branch: 'release-pr/${{ env.NEXT_RELEASE }}'
commit-message: ':rocket: Release ${{ env.NEXT_RELEASE }}' commit-message: ':rocket: Release ${{ env.NEXT_RELEASE }}'
delete-branch: true delete-branch: true
labels: 'release' labels: release,release:${{ github.event.inputs.release-type }}
title: ':rocket: Release ${{ env.NEXT_RELEASE }}' title: ':rocket: Release ${{ env.NEXT_RELEASE }}'
body-path: 'CHANGELOG-${{ env.NEXT_RELEASE }}.md' body-path: 'CHANGELOG-${{ env.NEXT_RELEASE }}.md'

View file

@ -8,18 +8,17 @@ on:
- 'release/*' - 'release/*'
jobs: jobs:
publish-release: publish-to-npm:
if: github.event.pull_request.merged == true name: Publish to NPM
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.event.pull_request.merged == true
timeout-minutes: 10
permissions: permissions:
contents: write
id-token: write id-token: write
timeout-minutes: 60
env: env:
NPM_CONFIG_PROVENANCE: true NPM_CONFIG_PROVENANCE: true
outputs:
release: ${{ steps.set-release.outputs.release }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4.1.1 uses: actions/checkout@v4.1.1
@ -51,25 +50,97 @@ jobs:
pnpm publish -r --publish-branch ${{github.event.pull_request.base.ref}} --access public --tag rc --no-git-checks pnpm publish -r --publish-branch ${{github.event.pull_request.base.ref}} --access public --tag rc --no-git-checks
npm dist-tag rm n8n rc npm dist-tag rm n8n rc
- id: set-release
run: echo "release=${{ env.RELEASE }}" >> $GITHUB_OUTPUT
publish-to-docker-hub:
name: Publish to DockerHub
needs: [publish-to-npm]
runs-on: ubuntu-latest
if: github.event.pull_request.merged == true
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@v4.1.1
with:
fetch-depth: 0
- name: Set up QEMU
uses: docker/setup-qemu-action@v3.0.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.0.0
- name: Login to GitHub Container Registry
uses: docker/login-action@v3.0.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to DockerHub
uses: docker/login-action@v3.0.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build
uses: docker/build-push-action@v5.1.0
with:
context: ./docker/images/n8n
build-args: |
N8N_VERSION=${{ needs.publish-to-npm.outputs.release }}
platforms: linux/amd64,linux/arm64
provenance: false
push: true
tags: |
${{ secrets.DOCKER_USERNAME }}/n8n:${{ needs.publish-to-npm.outputs.release }}
ghcr.io/${{ github.repository_owner }}/n8n:${{ needs.publish-to-npm.outputs.release }}
create-github-release:
name: Create a GitHub Release
needs: [publish-to-npm, publish-to-docker-hub]
runs-on: ubuntu-latest
if: github.event.pull_request.merged == true
timeout-minutes: 5
permissions:
contents: write
id-token: write
steps:
- name: Create a Release on GitHub - name: Create a Release on GitHub
uses: ncipollo/release-action@v1 uses: ncipollo/release-action@v1
with: with:
commit: ${{github.event.pull_request.base.ref}} commit: ${{github.event.pull_request.base.ref}}
tag: 'n8n@${{env.RELEASE}}' tag: 'n8n@${{ needs.publish-to-npm.outputs.release }}'
prerelease: true prerelease: true
makeLatest: false makeLatest: false
body: ${{github.event.pull_request.body}} body: ${{github.event.pull_request.body}}
trigger-release-note:
name: Trigger a release note
needs: [publish-to-npm, create-github-release]
if: github.event.pull_request.merged == true
runs-on: ubuntu-latest
steps:
- name: Trigger a release note - name: Trigger a release note
continue-on-error: true run: curl -u docsWorkflows:${{ secrets.N8N_WEBHOOK_DOCS_PASSWORD }} --request GET 'https://internal.users.n8n.cloud/webhook/trigger-release-note' --header 'Content-Type:application/json' --data '{"version":"${{ needs.publish-to-npm.outputs.release }}"}'
run: curl -u docsWorkflows:${{ secrets.N8N_WEBHOOK_DOCS_PASSWORD }} --request GET 'https://internal.users.n8n.cloud/webhook/trigger-release-note' --header 'Content-Type:application/json' --data '{"version":"${{env.RELEASE}}"}'
# - name: Merge Release into 'master' merge-back-into-master:
# run: | name: Merge back into master
# git fetch origin needs: [publish-to-npm, create-github-release]
# git checkout --track origin/master if: ${{ github.event.pull_request.merged == true && !contains(github.event.pull_request.labels.*.name, 'release:patch') }}
# git config user.name "Jan Oberhauser" runs-on: ubuntu-latest
# git config user.email jan.oberhauser@gmail.com steps:
# git merge --ff n8n@${{env.RELEASE}} - uses: actions/checkout@v4.1.1
# git push origin master with:
# git push origin :${{github.event.pull_request.base.ref}} fetch-depth: 0
- run: |
git checkout --track origin/master
git config user.name "github-actions[bot]"
git config user.email 41898282+github-actions[bot]@users.noreply.github.com
git merge --ff n8n@${{ needs.publish-to-npm.outputs.release }}
git push origin master
git push origin :${{github.event.pull_request.base.ref}}

View file

@ -36,7 +36,6 @@ jobs:
steps: steps:
- uses: actions/checkout@v4.1.1 - uses: actions/checkout@v4.1.1
with: with:
repository: n8n-io/n8n
ref: ${{ inputs.ref }} ref: ${{ inputs.ref }}
- run: corepack enable - run: corepack enable

View file

@ -5,6 +5,7 @@
"dbaeumer.vscode-eslint", "dbaeumer.vscode-eslint",
"EditorConfig.EditorConfig", "EditorConfig.EditorConfig",
"esbenp.prettier-vscode", "esbenp.prettier-vscode",
"mjmlio.vscode-mjml",
"Vue.volar" "Vue.volar"
] ]
} }

View file

@ -1,3 +1,166 @@
# [1.59.0](https://github.com/n8n-io/n8n/compare/n8n@1.58.0...n8n@1.59.0) (2024-09-11)
### Bug Fixes
* **Chat Trigger Node:** Fix auth in "Embedded Chat" mode ([#10734](https://github.com/n8n-io/n8n/issues/10734)) ([96db501](https://github.com/n8n-io/n8n/commit/96db501a615ff7ec91bb66ea49532a2c6ca2a172))
* **core:** Allow license:clear command to be used for licenses that failed renewal ([#10665](https://github.com/n8n-io/n8n/issues/10665)) ([a422c5a](https://github.com/n8n-io/n8n/commit/a422c5ac7b8f609eeab891230d9660f71bf225c5))
* **core:** Update subworkflow execution status correctly ([#10764](https://github.com/n8n-io/n8n/issues/10764)) ([4f94319](https://github.com/n8n-io/n8n/commit/4f94319cd93885ebe830fa1f0e6b757de80f7356))
* **editor:** Add arrow end to connection line ([#10704](https://github.com/n8n-io/n8n/issues/10704)) ([43713dc](https://github.com/n8n-io/n8n/commit/43713dcd89fcb98ea7e24d27127861fc4b0d7872))
* **editor:** Add sticky note readonly state in new canvas ([#10678](https://github.com/n8n-io/n8n/issues/10678)) ([c5bc8e6](https://github.com/n8n-io/n8n/commit/c5bc8e6eb9eadadf44f763e5e5aac4b35d03cc31))
* **editor:** Auto-focus expression input when switching from "fixed" mode ([#10686](https://github.com/n8n-io/n8n/issues/10686)) ([54ab2b1](https://github.com/n8n-io/n8n/commit/54ab2b14e41fe84a455c7e7d5c73d7347844d2fb))
* **editor:** Don't render pinned icon for disabled nodes ([#10712](https://github.com/n8n-io/n8n/issues/10712)) ([879b837](https://github.com/n8n-io/n8n/commit/879b8375812106b3f6909b7de27858175ba5575d))
* **editor:** Fix error rendering and indexing of LLM sub-node outputs ([#10688](https://github.com/n8n-io/n8n/issues/10688)) ([50459ba](https://github.com/n8n-io/n8n/commit/50459bacab517bacb97d2884fda69f8412c9960c))
* **editor:** Fix xss issues in toast usages ([#10733](https://github.com/n8n-io/n8n/issues/10733)) ([6df6f5f](https://github.com/n8n-io/n8n/commit/6df6f5f8df9a8fc0899524a1b69859815eeb341f))
* **editor:** Follow up fixes and improvements to viewer role ([#10684](https://github.com/n8n-io/n8n/issues/10684)) ([63548e6](https://github.com/n8n-io/n8n/commit/63548e6ead5c122732628b5feb1515f492d5e033))
* **editor:** Increase connector snap radius ([#10757](https://github.com/n8n-io/n8n/issues/10757)) ([297b668](https://github.com/n8n-io/n8n/commit/297b668f32f9ecfc82c1205ea4e915408cab482e))
* **editor:** Plus node button should not be visible on readonly mode ([#10692](https://github.com/n8n-io/n8n/issues/10692)) ([62cb189](https://github.com/n8n-io/n8n/commit/62cb189985035c447ad31c275337b3fb24089265))
* **editor:** Prevent action's panel flickering while dragging a node ([#10739](https://github.com/n8n-io/n8n/issues/10739)) ([efa5573](https://github.com/n8n-io/n8n/commit/efa5573278a60d55d5b509aac48cc112c79334d2))
* **editor:** Restrict when the collision avoidance algorithm is used ([#10755](https://github.com/n8n-io/n8n/issues/10755)) ([bf43d67](https://github.com/n8n-io/n8n/commit/bf43d673571b2fc18fe5d660171f0da165909dfc))
* **editor:** Show docs link in credential modal when docs sidebar is hidden ([#10750](https://github.com/n8n-io/n8n/issues/10750)) ([87333cb](https://github.com/n8n-io/n8n/commit/87333cbefebe652256fa1d60ba7a4b946fdfe17d))
* **Email Trigger (IMAP) Node:** Ensure connection close does not block deactivation ([#10689](https://github.com/n8n-io/n8n/issues/10689)) ([156eb72](https://github.com/n8n-io/n8n/commit/156eb72ebefa1d963ff46eff6652e2c947ef031b))
* Fix the issue in Trigger Nodes where poll time was not loaded ([#10695](https://github.com/n8n-io/n8n/issues/10695)) ([1dea8f4](https://github.com/n8n-io/n8n/commit/1dea8f4c7da2a04434c274faf8e0a9a7a693f5a4))
* **Gmail Trigger Node:** Change Gmail Trigger dedupe logic ([#10717](https://github.com/n8n-io/n8n/issues/10717)) ([9f3e03d](https://github.com/n8n-io/n8n/commit/9f3e03d728d8acda5ae4166c5837b00cb1311e96))
* Google Contacts node warm up request, Google Calendar node events>getAll fields option ([#10700](https://github.com/n8n-io/n8n/issues/10700)) ([22c70d5](https://github.com/n8n-io/n8n/commit/22c70d50697023cf448a379d7778695abb718ce9))
* **If Node:** Update copy for type conversion parameter ([#10769](https://github.com/n8n-io/n8n/issues/10769)) ([ee5fbc5](https://github.com/n8n-io/n8n/commit/ee5fbc543ce1d33a56cf118dbd048d6693a15875))
* **n8n Form Trigger Node:** Do not rerun trigger when it has run data ([#10687](https://github.com/n8n-io/n8n/issues/10687)) ([3adbcab](https://github.com/n8n-io/n8n/commit/3adbcab27de34ea5a2c7a88b2ad0d80e3f6d4a0b))
* **OpenAI Chat Model Node:** Prevent filtering of fine-tuned models in model selector ([#10662](https://github.com/n8n-io/n8n/issues/10662)) ([4e89912](https://github.com/n8n-io/n8n/commit/4e899125884bdd97c97446d90e89668688fe7573))
* Prevent AI assistant session reset when workflow is saved ([#10707](https://github.com/n8n-io/n8n/issues/10707)) ([91d9be2](https://github.com/n8n-io/n8n/commit/91d9be20667c20599f64a24fa99386c78476d425))
* Show a more user friendly error message if initial Db connection times out ([#10682](https://github.com/n8n-io/n8n/issues/10682)) ([4efcbc5](https://github.com/n8n-io/n8n/commit/4efcbc593685286837022e5600d81e67f3e0131c))
* **Webflow Node:** Update scopes to include forms ([#10554](https://github.com/n8n-io/n8n/issues/10554)) ([d3861b3](https://github.com/n8n-io/n8n/commit/d3861b31ceef16f566c525c7651453a1b84ed2a4))
* **YouTube Node:** Fix Date filters ([#10725](https://github.com/n8n-io/n8n/issues/10725)) ([21936c8](https://github.com/n8n-io/n8n/commit/21936c88a84b8c03a8d02391cb7112b0e4d9f1f9))
### Features
* **Code Tool Node:** Option to specify input schema ([#10693](https://github.com/n8n-io/n8n/issues/10693)) ([421aa71](https://github.com/n8n-io/n8n/commit/421aa712515d9beeae7c0201b173cb7324473f69))
* **editor:** Add lint for $('Node').item in runOnceForAllItems mode ([#10743](https://github.com/n8n-io/n8n/issues/10743)) ([1b04be1](https://github.com/n8n-io/n8n/commit/1b04be1240ec29151e79162680907710c71c6488))
* **editor:** Logs markdown block improvements ([#10681](https://github.com/n8n-io/n8n/issues/10681)) ([db6e832](https://github.com/n8n-io/n8n/commit/db6e8326c7119d90fa6a51f82099026f50587202))
* Filter parameter: Improve loose type validation for booleans ([#10702](https://github.com/n8n-io/n8n/issues/10702)) ([e9b8d99](https://github.com/n8n-io/n8n/commit/e9b8d99084f0ea2063a1d691928025e534980b4e))
* **Lemlist Node:** Add V2 to support more API operations ([#10615](https://github.com/n8n-io/n8n/issues/10615)) ([20b1cf2](https://github.com/n8n-io/n8n/commit/20b1cf2b7597c78e28f522945b8cbad2ee535cd7))
* **OpenAI Node:** Add Max Tools Iteration parameter and prevent tool calling after execution is aborted ([#10735](https://github.com/n8n-io/n8n/issues/10735)) ([5c47a5f](https://github.com/n8n-io/n8n/commit/5c47a5f691d42dae84a9df8a32a5ea600d83f6dd))
### Performance Improvements
* **editor:** Fix WorkflowDetails excessive re-rendering ([#10767](https://github.com/n8n-io/n8n/issues/10767)) ([00013a2](https://github.com/n8n-io/n8n/commit/00013a2069fff5e5d9398c5921c90d34dc384299))
# [1.58.0](https://github.com/n8n-io/n8n/compare/n8n@1.57.0...n8n@1.58.0) (2024-09-05)
### Bug Fixes
* **AI Agent Node:** Fix tools agent when using memory and Anthropic models ([#10513](https://github.com/n8n-io/n8n/issues/10513)) ([746e7b8](https://github.com/n8n-io/n8n/commit/746e7b89f7e9b99126fb69110773548dfe91b74f))
* **API:** Update express-openapi-validator to resolve AIKIDO-2024-10229 ([#10612](https://github.com/n8n-io/n8n/issues/10612)) ([1dcb814](https://github.com/n8n-io/n8n/commit/1dcb814ced7cfbc80eddbb4bc03108341a9f27f5))
* **core:** Declutter webhook insertion errors ([#10650](https://github.com/n8n-io/n8n/issues/10650)) ([36177b0](https://github.com/n8n-io/n8n/commit/36177b0943cf72bae3b0075453498dd1e41684d0))
* **core:** Flush responses for ai streaming endpoints ([#10633](https://github.com/n8n-io/n8n/issues/10633)) ([6bb6a5c](https://github.com/n8n-io/n8n/commit/6bb6a5c6cd1da3503a1a2b35bcf4c685cd3f964f))
* **core:** Tighten check for company size survey answer ([#10646](https://github.com/n8n-io/n8n/issues/10646)) ([e5aba60](https://github.com/n8n-io/n8n/commit/e5aba60afff93364d91f17c00ea18d38d9dbc970))
* **editor:** Add confirmation toast when changing user role ([#10592](https://github.com/n8n-io/n8n/issues/10592)) ([95da4d4](https://github.com/n8n-io/n8n/commit/95da4d4797e800c04b2b17c23c941c785dd62393))
* **editor:** Add pinned data only to manual executions in execution view ([#10605](https://github.com/n8n-io/n8n/issues/10605)) ([a12e9ed](https://github.com/n8n-io/n8n/commit/a12e9edac042957939c63f0a5c35572930632352))
* **editor:** Add tooltips to workflow history button ([#10570](https://github.com/n8n-io/n8n/issues/10570)) ([4a125f5](https://github.com/n8n-io/n8n/commit/4a125f511c5537977652900b7712a2ad908140e7))
* **editor:** Allow disabling SSO when config request fails ([#10635](https://github.com/n8n-io/n8n/issues/10635)) ([ce39933](https://github.com/n8n-io/n8n/commit/ce39933766fa18107f4082de0cba0b6702cbbbfa))
* **editor:** Fix notification rendering HTML as text ([#10642](https://github.com/n8n-io/n8n/issues/10642)) ([5eba534](https://github.com/n8n-io/n8n/commit/5eba5343191665cd4639632ba303464176c279c4))
* **editor:** Fix opening executions tab from a new, unsaved workflow ([#10652](https://github.com/n8n-io/n8n/issues/10652)) ([cd0891e](https://github.com/n8n-io/n8n/commit/cd0891e4f1cfdc90b2090958a39564ba99534627))
* **Gmail Trigger Node:** Don't return date instances, but date strings instead ([#10582](https://github.com/n8n-io/n8n/issues/10582)) ([9e1dac0](https://github.com/n8n-io/n8n/commit/9e1dac04655a20c5c7b99552742312fd9237604b))
* **HTTP Request Node:** Sanitize authorization headers ([#10607](https://github.com/n8n-io/n8n/issues/10607)) ([405c55a](https://github.com/n8n-io/n8n/commit/405c55a1f7cf34e7b6e46a86031ef9a41956ca78))
* **Wait Node:** Append n8n attribution option ([#10585](https://github.com/n8n-io/n8n/issues/10585)) ([81f4322](https://github.com/n8n-io/n8n/commit/81f4322d456773281aec4b47447465bdffd311fe))
### Features
* **core:** Execution curation ([#10342](https://github.com/n8n-io/n8n/issues/10342)) ([022ddcb](https://github.com/n8n-io/n8n/commit/022ddcbef9f1ac1b89bcfd5f7759d67325b07392))
* **core:** Implement wrapping of regular nodes as AI Tools ([#10641](https://github.com/n8n-io/n8n/issues/10641)) ([da44fe4](https://github.com/n8n-io/n8n/commit/da44fe4b8967055b7b1f849750e1fafa0ba67218))
* **core:** Introduce DB health check ([#10661](https://github.com/n8n-io/n8n/issues/10661)) ([a8e80d0](https://github.com/n8n-io/n8n/commit/a8e80d0c4b7531fe32be1d4057656885359f42fc))
* **core:** Make Postgres connection timeout configurable ([#10670](https://github.com/n8n-io/n8n/issues/10670)) ([8154031](https://github.com/n8n-io/n8n/commit/81540318b4c55f3a09c9776e23d2211abdbd36f7))
* **core:** Switch to MJML for email templates ([#10518](https://github.com/n8n-io/n8n/issues/10518)) ([dbc10fe](https://github.com/n8n-io/n8n/commit/dbc10fe9f522f31eb06add6f3f6863ce24510547))
* **editor:** Add A/B testing feature flag for credential docs modal ([#10664](https://github.com/n8n-io/n8n/issues/10664)) ([899b0a1](https://github.com/n8n-io/n8n/commit/899b0a19efc49c1c087f78bbb1a59d726a510965))
* **editor:** Add AI Assistant support chat ([#10656](https://github.com/n8n-io/n8n/issues/10656)) ([3a80780](https://github.com/n8n-io/n8n/commit/3a8078068e5c0b01dfd34ff838fe1b30d604abc6))
* **editor:** Implement new app layout ([#10548](https://github.com/n8n-io/n8n/issues/10548)) ([95a9cd2](https://github.com/n8n-io/n8n/commit/95a9cd2c739cf4f817eb8df6509a9112ac24a3b1))
* **editor:** Make highlighted data pane floating ([#10638](https://github.com/n8n-io/n8n/issues/10638)) ([8b5c333](https://github.com/n8n-io/n8n/commit/8b5c333d3dca03ba51a5873b75451fbfafc5ae15))
* More hints to nodes ([#10565](https://github.com/n8n-io/n8n/issues/10565)) ([66ddb4a](https://github.com/n8n-io/n8n/commit/66ddb4a6f367602c9aaad1bfb0cc6fac3facd15e))
* **Postgres PGVector Store Node:** Add PGVector vector store node ([#10517](https://github.com/n8n-io/n8n/issues/10517)) ([650389d](https://github.com/n8n-io/n8n/commit/650389d90763a45c037e74a1a1193c3cbe103a16))
* Reintroduce collaboration feature ([#10602](https://github.com/n8n-io/n8n/issues/10602)) ([2ea2bfe](https://github.com/n8n-io/n8n/commit/2ea2bfe762c02047e522f28dd97f197735b3fb46))
* **Text Classifier Node:** Add output fixing parser ([#10667](https://github.com/n8n-io/n8n/issues/10667)) ([aa37c32](https://github.com/n8n-io/n8n/commit/aa37c32f266ffff93cd903888b1c15caa0468830))
# [1.57.0](https://github.com/n8n-io/n8n/compare/n8n@1.56.0...n8n@1.57.0) (2024-08-28)
### Bug Fixes
* **AI Agent Node:** Allow AWS Bedrock Chat to be used with conversational agent ([#10489](https://github.com/n8n-io/n8n/issues/10489)) ([bdcc657](https://github.com/n8n-io/n8n/commit/bdcc657965af5f604aac1eaff7d937f69a08ce1c))
* **core:** Make boolean config value parsing backward-compatible ([#10560](https://github.com/n8n-io/n8n/issues/10560)) ([70b410f](https://github.com/n8n-io/n8n/commit/70b410f4b00dd599fcd4249aa105098aa262da66))
* **core:** Restore Redis cache key ([#10520](https://github.com/n8n-io/n8n/issues/10520)) ([873056a](https://github.com/n8n-io/n8n/commit/873056a92e52cc629d2873c960656d5f06d4728e))
* **core:** Scheduler tasks should not trigger on follower instances ([#10507](https://github.com/n8n-io/n8n/issues/10507)) ([3428f28](https://github.com/n8n-io/n8n/commit/3428f28a732f79e067b3cb515cc59d835de246a6))
* **core:** Stop explicit redis client disconnect on shutdown ([#10551](https://github.com/n8n-io/n8n/issues/10551)) ([f712812](https://github.com/n8n-io/n8n/commit/f71281221efb79d65d8d7610c292bc90cef13d7a))
* **editor:** Ensure `Datatable` component renders `All` option ([#10525](https://github.com/n8n-io/n8n/issues/10525)) ([bc27beb](https://github.com/n8n-io/n8n/commit/bc27beb6629883003a8991d7e840ffaa066d41ac))
* **editor:** Prevent Safari users from accessing the frontend over insecure contexts ([#10510](https://github.com/n8n-io/n8n/issues/10510)) ([a73b9a3](https://github.com/n8n-io/n8n/commit/a73b9a38d6c48e2f78593328e7d9933f2493dbb6))
* **editor:** Scale output item selector input width with value ([#10555](https://github.com/n8n-io/n8n/issues/10555)) ([52c574d](https://github.com/n8n-io/n8n/commit/52c574d83f344f03b0e39984bbc3ac0402e50791))
* **Google Sheets Trigger Node:** Show sheet name is too long error ([#10542](https://github.com/n8n-io/n8n/issues/10542)) ([4e15007](https://github.com/n8n-io/n8n/commit/4e1500757700ec984cdad8b9cfcd76ee00ae127e))
* **Wait Node:** Prevent waiting until invalid date ([#10523](https://github.com/n8n-io/n8n/issues/10523)) ([c0e7620](https://github.com/n8n-io/n8n/commit/c0e7620036738f8d0b382d0d0610b981dcbc29e0))
### Features
* Add new credentials for the HTTP Request node ([#9833](https://github.com/n8n-io/n8n/issues/9833)) ([26f1af3](https://github.com/n8n-io/n8n/commit/26f1af397b2b25e3394fc2dae91a5c281bf33d66))
* **AI Agent Node:** Add tutorial link to agent node ([#10493](https://github.com/n8n-io/n8n/issues/10493)) ([5c7cc36](https://github.com/n8n-io/n8n/commit/5c7cc36c23e58a47a1e71911e7303a1bd54f167e))
* **core:** Expose queue metrics for Prometheus ([#10559](https://github.com/n8n-io/n8n/issues/10559)) ([008c510](https://github.com/n8n-io/n8n/commit/008c510b7623fefb8c60730c7eac54dd9bb2e3fc))
* **editor:** Implement workflowSelector parameter type ([#10482](https://github.com/n8n-io/n8n/issues/10482)) ([84e54be](https://github.com/n8n-io/n8n/commit/84e54beac763f25399c9687f695f1e658e3ce434))
### Performance Improvements
* **core:** Make execution queries faster ([#9817](https://github.com/n8n-io/n8n/issues/9817)) ([dc7dc99](https://github.com/n8n-io/n8n/commit/dc7dc995d5e2ea8fbd0dcb54cfa8aa93ecb437c9))
### Other
* **Add user journey link to [n8n.io](https://n8n.io)** ([#10331](https://github.com/n8n-io/n8n/pull/10331))
# [1.56.0](https://github.com/n8n-io/n8n/compare/n8n@1.55.0...n8n@1.56.0) (2024-08-21)
### Bug Fixes
* Better errors in Switch, If and Filter nodes ([#10457](https://github.com/n8n-io/n8n/issues/10457)) ([aea82cb](https://github.com/n8n-io/n8n/commit/aea82cb74421d516919742127daf669808b57604))
* **Calendly Trigger Node:** Fix issue with webhook url matching ([#10378](https://github.com/n8n-io/n8n/issues/10378)) ([09c3a8b](https://github.com/n8n-io/n8n/commit/09c3a8b36733a9634ef5948922d6aa7a19bbb592))
* **core:** Fix payload property in `workflow-post-execute` event ([#10413](https://github.com/n8n-io/n8n/issues/10413)) ([d98e29e](https://github.com/n8n-io/n8n/commit/d98e29e3d53de87aec276260615fa60473a2692f))
* **core:** Fix XSS validation and separate URL validation ([#10424](https://github.com/n8n-io/n8n/issues/10424)) ([91467ab](https://github.com/n8n-io/n8n/commit/91467ab325e4c71c20c522f3143246d270101626))
* **core:** Replace `sanitize-html` with `xss` in XSS validator constraint ([#10479](https://github.com/n8n-io/n8n/issues/10479)) ([5dea51a](https://github.com/n8n-io/n8n/commit/5dea51aad7d9e7ffc676d16f4bbbdecce5876f0b))
* **core:** Use class-validator with XSS check for survey answers ([#10490](https://github.com/n8n-io/n8n/issues/10490)) ([547a606](https://github.com/n8n-io/n8n/commit/547a60642ce9e54819d4e600c822d87dabd59b2e))
* **core:** Use explicit types in configs to ensure valid decorator metadata ([#10433](https://github.com/n8n-io/n8n/issues/10433)) ([2043daa](https://github.com/n8n-io/n8n/commit/2043daa2570bc04b0b8d41f277901a8cc8a7b98f))
* **editor:** Add workflow scopes when initializing workflow ([#10455](https://github.com/n8n-io/n8n/issues/10455)) ([b857c2c](https://github.com/n8n-io/n8n/commit/b857c2cda0a9e4386a540d5e1e741570d9453588))
* **editor:** Buffer json chunks in stream response ([#10439](https://github.com/n8n-io/n8n/issues/10439)) ([37797f3](https://github.com/n8n-io/n8n/commit/37797f38d81b12d030ba85034baeb49192ea575c))
* **editor:** Fix flaky mapping tests ([#10453](https://github.com/n8n-io/n8n/issues/10453)) ([fc6d413](https://github.com/n8n-io/n8n/commit/fc6d4138d58282f676b32f1a6011b1b6d0184bf2))
* **editor:** Fix overflow in AI Assistant chat messages ([#10491](https://github.com/n8n-io/n8n/issues/10491)) ([4a6ca63](https://github.com/n8n-io/n8n/commit/4a6ca632100731f85875c639f2164bf1ef415009))
* **editor:** Highlight matching type in filter component ([#10425](https://github.com/n8n-io/n8n/issues/10425)) ([6bca879](https://github.com/n8n-io/n8n/commit/6bca879d4ae30c7f9a35e8d6672de42cf93be727))
* **editor:** Show item count in output panel schema view ([#10426](https://github.com/n8n-io/n8n/issues/10426)) ([4dee7cc](https://github.com/n8n-io/n8n/commit/4dee7cc36e5f7768d0b71095b194bf357c92e941))
* **editor:** Truncate long data pill labels in schema view ([#10427](https://github.com/n8n-io/n8n/issues/10427)) ([1bf2f4f](https://github.com/n8n-io/n8n/commit/1bf2f4f6171d666391bb3a3a312468bc083446e3))
* Filter component - improve errors ([#10456](https://github.com/n8n-io/n8n/issues/10456)) ([61ac0c7](https://github.com/n8n-io/n8n/commit/61ac0c77755210f570b887951fe6bbec1a323811))
* **Google Sheets Node:** Better error when column to match on is empty ([#10442](https://github.com/n8n-io/n8n/issues/10442)) ([ce46bf5](https://github.com/n8n-io/n8n/commit/ce46bf516a86d9779f37dd75b0c680d26d88e15d))
* **Google Sheets Node:** Update name and hint for useAppend option ([#10443](https://github.com/n8n-io/n8n/issues/10443)) ([c5a0c04](https://github.com/n8n-io/n8n/commit/c5a0c049eaf44419c690d151de42fb0c10bd406e))
* **Google Sheets Node:** Update to returnAllMatches option ([#10440](https://github.com/n8n-io/n8n/issues/10440)) ([f7fb02e](https://github.com/n8n-io/n8n/commit/f7fb02e92a756781f8e35bbbfc25d71c12cb70af))
* **Invoice Ninja Node:** Fix payment types ([#10462](https://github.com/n8n-io/n8n/issues/10462)) ([129245d](https://github.com/n8n-io/n8n/commit/129245da10be1d645f61e929e40b128bd7813f17))
* **n8n Form Trigger Node:** Show basic authentication modal on wrong credentials ([#10423](https://github.com/n8n-io/n8n/issues/10423)) ([0dc3e99](https://github.com/n8n-io/n8n/commit/0dc3e99b26bec45e747d83f383cfe5169d89e6b7))
* **OpenAI Node:** Throw node operations error in case of openAi client error ([#10448](https://github.com/n8n-io/n8n/issues/10448)) ([0d3ed46](https://github.com/n8n-io/n8n/commit/0d3ed461996bbad06015c455f133baab6506437f))
* Project Viewer always seeing a connection error when testing credentials ([#10417](https://github.com/n8n-io/n8n/issues/10417)) ([613cdd2](https://github.com/n8n-io/n8n/commit/613cdd2ba2c0f224c4857a5fc3eea36dbd683049))
* Remove unimplemented Postgres credentials options ([#10461](https://github.com/n8n-io/n8n/issues/10461)) ([17ac784](https://github.com/n8n-io/n8n/commit/17ac7844f29d819b91dfaf90b9fe386d98060c42))
* Rename Assistant back ([#10481](https://github.com/n8n-io/n8n/issues/10481)) ([c410aed](https://github.com/n8n-io/n8n/commit/c410aed4c22182943dc80ede63acda00b7898e10))
* Require mfa code to change email ([#10354](https://github.com/n8n-io/n8n/issues/10354)) ([39c8e50](https://github.com/n8n-io/n8n/commit/39c8e50ad0513649f5a8cef911b7d6cdd61c2372))
* **Respond to Webhook Node:** Fix issue preventing the chat trigger from working ([#9886](https://github.com/n8n-io/n8n/issues/9886)) ([9d6ad88](https://github.com/n8n-io/n8n/commit/9d6ad88c14a88fd0dfcb4f9981e38d19cf5f3067))
* Show input names when node has multiple inputs ([#10434](https://github.com/n8n-io/n8n/issues/10434)) ([973956c](https://github.com/n8n-io/n8n/commit/973956cc26c78c329ff6eb6934d4f0a24060c87c))
* **Toggl Trigger Node:** Update API version ([#10207](https://github.com/n8n-io/n8n/issues/10207)) ([9bdb1d6](https://github.com/n8n-io/n8n/commit/9bdb1d6dca43fe491c5eb96f093b7eec4509eaff))
### Features
* **core:** Support bidirectional communication between specific mains and specific workers ([#10377](https://github.com/n8n-io/n8n/issues/10377)) ([d0fc9de](https://github.com/n8n-io/n8n/commit/d0fc9dee0e17211c1ed130b19286e9573c9ebfbd))
* **Facebook Graph API Node:** Update node to support API v18 - v20 ([#10419](https://github.com/n8n-io/n8n/issues/10419)) ([e7ee10f](https://github.com/n8n-io/n8n/commit/e7ee10f243663d899d32e61bc6264b4df444e2af))
# [1.55.0](https://github.com/n8n-io/n8n/compare/n8n@1.54.0...n8n@1.55.0) (2024-08-14) # [1.55.0](https://github.com/n8n-io/n8n/compare/n8n@1.54.0...n8n@1.55.0) (2024-08-14)

View file

@ -61,6 +61,7 @@ export const AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME = 'OpenAI Chat Model'
export const AI_MEMORY_POSTGRES_NODE_NAME = 'Postgres Chat Memory'; export const AI_MEMORY_POSTGRES_NODE_NAME = 'Postgres Chat Memory';
export const AI_OUTPUT_PARSER_AUTO_FIXING_NODE_NAME = 'Auto-fixing Output Parser'; export const AI_OUTPUT_PARSER_AUTO_FIXING_NODE_NAME = 'Auto-fixing Output Parser';
export const WEBHOOK_NODE_NAME = 'Webhook'; export const WEBHOOK_NODE_NAME = 'Webhook';
export const EXECUTE_WORKFLOW_NODE_NAME = 'Execute Workflow';
export const META_KEY = Cypress.platform === 'darwin' ? 'meta' : 'ctrl'; export const META_KEY = Cypress.platform === 'darwin' ? 'meta' : 'ctrl';

View file

@ -7,7 +7,7 @@ import {
WorkflowSharingModal, WorkflowSharingModal,
WorkflowsPage, WorkflowsPage,
} from '../pages'; } from '../pages';
import { getVisibleDropdown, getVisibleSelect } from '../utils'; import { getVisibleDropdown, getVisiblePopper, getVisibleSelect } from '../utils';
import * as projects from '../composables/projects'; import * as projects from '../composables/projects';
/** /**
@ -180,7 +180,8 @@ describe('Sharing', { disableAutoLogin: true }, () => {
).should('be.visible'); ).should('be.visible');
credentialsModal.getters.usersSelect().click(); credentialsModal.getters.usersSelect().click();
cy.getByTestId('project-sharing-info') getVisiblePopper()
.find('[data-test-id="project-sharing-info"]')
.filter(':visible') .filter(':visible')
.should('have.length', 3) .should('have.length', 3)
.contains(INSTANCE_ADMIN.email) .contains(INSTANCE_ADMIN.email)

View file

@ -616,4 +616,45 @@ describe('Execution', () => {
errorToast().should('contain', 'Problem in node Telegram'); errorToast().should('contain', 'Problem in node Telegram');
}); });
it('should not show pinned data in production execution', () => {
cy.createFixtureWorkflow('Execution-pinned-data-check.json');
workflowPage.getters.zoomToFitButton().click();
cy.intercept('PATCH', '/rest/workflows/*').as('workflowActivate');
workflowPage.getters.activatorSwitch().click();
cy.wait('@workflowActivate');
cy.get('body').type('{esc}');
workflowPage.actions.openNode('Webhook');
cy.contains('label', 'Production URL').should('be.visible').click();
cy.grantBrowserPermissions('clipboardReadWrite', 'clipboardSanitizedWrite');
cy.get('.webhook-url').click();
ndv.getters.backToCanvas().click();
cy.readClipboard().then((url) => {
cy.request({
method: 'GET',
url,
}).then((resp) => {
expect(resp.status).to.eq(200);
});
});
cy.intercept('GET', '/rest/executions/*').as('getExecution');
executionsTab.actions.switchToExecutionsTab();
cy.wait('@getExecution');
executionsTab.getters
.workflowExecutionPreviewIframe()
.should('be.visible')
.its('0.contentDocument.body')
.should('not.be.empty')
.then(cy.wrap)
.find('.connection-run-items-label')
.filter(':contains("5 items")')
.should('have.length', 2);
});
}); });

View file

@ -9,7 +9,8 @@ const executionsTab = new WorkflowExecutionsTab();
const executionsRefreshInterval = 4000; const executionsRefreshInterval = 4000;
// Test suite for executions tab // Test suite for executions tab
describe('Current Workflow Executions', () => { describe('Workflow Executions', () => {
describe('when workflow is saved', () => {
beforeEach(() => { beforeEach(() => {
workflowPage.actions.visit(); workflowPage.actions.visit();
cy.createFixtureWorkflow('Test_workflow_4_executions_view.json', 'My test workflow'); cy.createFixtureWorkflow('Test_workflow_4_executions_view.json', 'My test workflow');
@ -229,13 +230,43 @@ describe('Current Workflow Executions', () => {
}); });
}); });
describe('when new workflow is not saved', () => {
beforeEach(() => {
workflowPage.actions.visit();
});
it('should open executions tab', () => {
executionsTab.actions.switchToExecutionsTab();
executionsTab.getters.executionsSidebar().should('be.visible');
executionsTab.getters.executionsEmptyList().should('be.visible');
cy.getByTestId('workflow-execution-no-trigger-content').should('be.visible');
cy.get('button:contains("Add first step")').should('be.visible').click();
cy.getByTestId('node-creator-item-name')
.should('be.visible')
.filter(':contains("Trigger")')
.click();
executionsTab.actions.switchToExecutionsTab();
executionsTab.getters.executionsSidebar().should('be.visible');
executionsTab.getters.executionsEmptyList().should('be.visible');
cy.getByTestId('workflow-execution-no-content').should('be.visible');
workflowPage.getters.saveButton().find('button').should('be.enabled').click();
workflowPage.getters.isWorkflowSaved();
workflowPage.getters.nodeViewRoot().should('be.visible');
});
});
});
const createMockExecutions = () => { const createMockExecutions = () => {
executionsTab.actions.createManualExecutions(5); executionsTab.actions.createManualExecutions(5);
// Make some failed executions by enabling Code node with syntax error // Make some failed executions by enabling Code node with syntax error
executionsTab.actions.toggleNodeEnabled('Error'); executionsTab.actions.toggleNodeEnabled('Error');
workflowPage.getters.disabledNodes().should('have.length', 0);
executionsTab.actions.createManualExecutions(2); executionsTab.actions.createManualExecutions(2);
// Then add some more successful ones // Then add some more successful ones
executionsTab.actions.toggleNodeEnabled('Error'); executionsTab.actions.toggleNodeEnabled('Error');
workflowPage.getters.disabledNodes().should('have.length', 1);
executionsTab.actions.createManualExecutions(4); executionsTab.actions.createManualExecutions(4);
}; };

View file

@ -1,9 +1,13 @@
import { NDV, WorkflowPage } from '../pages'; import { clickCreateNewCredential, openCredentialSelect } from '../composables/ndv';
import { GMAIL_NODE_NAME, SCHEDULE_TRIGGER_NODE_NAME } from '../constants';
import { CredentialsModal, CredentialsPage, NDV, WorkflowPage } from '../pages';
import { AIAssistant } from '../pages/features/ai-assistant'; import { AIAssistant } from '../pages/features/ai-assistant';
const wf = new WorkflowPage(); const wf = new WorkflowPage();
const ndv = new NDV(); const ndv = new NDV();
const aiAssistant = new AIAssistant(); const aiAssistant = new AIAssistant();
const credentialsPage = new CredentialsPage();
const credentialsModal = new CredentialsModal();
describe('AI Assistant::disabled', () => { describe('AI Assistant::disabled', () => {
beforeEach(() => { beforeEach(() => {
@ -31,10 +35,11 @@ describe('AI Assistant::enabled', () => {
aiAssistant.getters.askAssistantFloatingButton().click(); aiAssistant.getters.askAssistantFloatingButton().click();
aiAssistant.getters.askAssistantChat().should('be.visible'); aiAssistant.getters.askAssistantChat().should('be.visible');
aiAssistant.getters.placeholderMessage().should('be.visible'); aiAssistant.getters.placeholderMessage().should('be.visible');
aiAssistant.getters.chatInputWrapper().should('not.exist'); aiAssistant.getters.chatInput().should('be.visible');
aiAssistant.getters.sendMessageButton().should('be.disabled');
aiAssistant.getters.closeChatButton().should('be.visible'); aiAssistant.getters.closeChatButton().should('be.visible');
aiAssistant.getters.closeChatButton().click(); aiAssistant.getters.closeChatButton().click();
aiAssistant.getters.askAssistantChat().should('not.exist'); aiAssistant.getters.askAssistantChat().should('not.be.visible');
}); });
it('should resize assistant chat up', () => { it('should resize assistant chat up', () => {
@ -130,17 +135,24 @@ describe('AI Assistant::enabled', () => {
ndv.getters.nodeExecuteButton().click(); ndv.getters.nodeExecuteButton().click();
aiAssistant.getters.nodeErrorViewAssistantButton().click(); aiAssistant.getters.nodeErrorViewAssistantButton().click();
cy.wait('@chatRequest'); cy.wait('@chatRequest');
aiAssistant.getters.quickReplies().should('have.length', 2); aiAssistant.getters.quickReplyButtons().should('have.length', 2);
aiAssistant.getters.quickReplies().eq(0).click(); aiAssistant.getters.quickReplyButtons().eq(0).click();
cy.wait('@chatRequest'); cy.wait('@chatRequest');
aiAssistant.getters.chatMessagesUser().should('have.length', 1); aiAssistant.getters.chatMessagesUser().should('have.length', 1);
aiAssistant.getters.chatMessagesUser().eq(0).should('contain.text', "Sure, let's do it"); aiAssistant.getters.chatMessagesUser().eq(0).should('contain.text', "Sure, let's do it");
}); });
it('should send message to assistant when node is executed', () => { it('should show quick replies when node is executed after new suggestion', () => {
cy.intercept('POST', '/rest/ai-assistant/chat', { cy.intercept('POST', '/rest/ai-assistant/chat', (req) => {
statusCode: 200, req.reply((res) => {
fixture: 'aiAssistant/simple_message_response.json', if (['init-error-helper', 'message'].includes(req.body.payload.type)) {
res.send({ statusCode: 200, fixture: 'aiAssistant/simple_message_response.json' });
} else if (req.body.payload.type === 'event') {
res.send({ statusCode: 200, fixture: 'aiAssistant/node_execution_error_response.json' });
} else {
res.send({ statusCode: 500 });
}
});
}).as('chatRequest'); }).as('chatRequest');
cy.createFixtureWorkflow('aiAssistant/test_workflow.json'); cy.createFixtureWorkflow('aiAssistant/test_workflow.json');
wf.actions.openNode('Edit Fields'); wf.actions.openNode('Edit Fields');
@ -148,10 +160,17 @@ describe('AI Assistant::enabled', () => {
aiAssistant.getters.nodeErrorViewAssistantButton().click(); aiAssistant.getters.nodeErrorViewAssistantButton().click();
cy.wait('@chatRequest'); cy.wait('@chatRequest');
aiAssistant.getters.chatMessagesAssistant().should('have.length', 1); aiAssistant.getters.chatMessagesAssistant().should('have.length', 1);
// Executing the same node should sende a new message to the assistant automatically
ndv.getters.nodeExecuteButton().click(); ndv.getters.nodeExecuteButton().click();
cy.wait('@chatRequest'); cy.wait('@chatRequest');
aiAssistant.getters.chatMessagesAssistant().should('have.length', 2); // Respond 'Yes' to the quick reply (request new suggestion)
aiAssistant.getters.quickReplies().contains('Yes').click();
cy.wait('@chatRequest');
// No quick replies at this point
aiAssistant.getters.quickReplies().should('not.exist');
ndv.getters.nodeExecuteButton().click();
// But after executing the node again, quick replies should be shown
aiAssistant.getters.chatMessagesAssistant().should('have.length', 4);
aiAssistant.getters.quickReplies().should('have.length', 2);
}); });
it('should warn before starting a new session', () => { it('should warn before starting a new session', () => {
@ -162,13 +181,13 @@ describe('AI Assistant::enabled', () => {
cy.createFixtureWorkflow('aiAssistant/test_workflow.json'); cy.createFixtureWorkflow('aiAssistant/test_workflow.json');
wf.actions.openNode('Edit Fields'); wf.actions.openNode('Edit Fields');
ndv.getters.nodeExecuteButton().click(); ndv.getters.nodeExecuteButton().click();
aiAssistant.getters.nodeErrorViewAssistantButton().click(); aiAssistant.getters.nodeErrorViewAssistantButton().click({ force: true });
cy.wait('@chatRequest'); cy.wait('@chatRequest');
aiAssistant.getters.closeChatButton().click(); aiAssistant.getters.closeChatButton().click();
ndv.getters.backToCanvas().click(); ndv.getters.backToCanvas().click();
wf.actions.openNode('Stop and Error'); wf.actions.openNode('Stop and Error');
ndv.getters.nodeExecuteButton().click(); ndv.getters.nodeExecuteButton().click();
aiAssistant.getters.nodeErrorViewAssistantButton().click(); aiAssistant.getters.nodeErrorViewAssistantButton().click({ force: true });
// Since we already have an active session, a warning should be shown // Since we already have an active session, a warning should be shown
aiAssistant.getters.newAssistantSessionModal().should('be.visible'); aiAssistant.getters.newAssistantSessionModal().should('be.visible');
aiAssistant.getters aiAssistant.getters
@ -244,4 +263,114 @@ describe('AI Assistant::enabled', () => {
aiAssistant.getters.chatMessagesSystem().should('have.length', 1); aiAssistant.getters.chatMessagesSystem().should('have.length', 1);
aiAssistant.getters.chatMessagesSystem().first().should('contain.text', 'session has ended'); aiAssistant.getters.chatMessagesSystem().first().should('contain.text', 'session has ended');
}); });
it('should reset session after it ended and sidebar is closed', () => {
cy.intercept('POST', '/rest/ai-assistant/chat', (req) => {
req.reply((res) => {
if (['init-support-chat'].includes(req.body.payload.type)) {
res.send({ statusCode: 200, fixture: 'aiAssistant/simple_message_response.json' });
} else {
res.send({ statusCode: 200, fixture: 'aiAssistant/end_session_response.json' });
}
});
}).as('chatRequest');
aiAssistant.actions.openChat();
aiAssistant.actions.sendMessage('Hello');
cy.wait('@chatRequest');
aiAssistant.actions.closeChat();
aiAssistant.actions.openChat();
// After closing and reopening the chat, all messages should be still there
aiAssistant.getters.chatMessagesAll().should('have.length', 2);
// End the session
aiAssistant.actions.sendMessage('Thanks, bye');
cy.wait('@chatRequest');
aiAssistant.getters.chatMessagesSystem().should('have.length', 1);
aiAssistant.getters.chatMessagesSystem().first().should('contain.text', 'session has ended');
aiAssistant.actions.closeChat();
aiAssistant.actions.openChat();
// Now, session should be reset
aiAssistant.getters.placeholderMessage().should('be.visible');
});
it('Should not reset assistant session when workflow is saved', () => {
cy.intercept('POST', '/rest/ai-assistant/chat', {
statusCode: 200,
fixture: 'aiAssistant/simple_message_response.json',
}).as('chatRequest');
wf.actions.addInitialNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
aiAssistant.actions.openChat();
aiAssistant.actions.sendMessage('Hello');
wf.actions.openNode(SCHEDULE_TRIGGER_NODE_NAME);
ndv.getters.nodeExecuteButton().click();
wf.getters.isWorkflowSaved();
aiAssistant.getters.placeholderMessage().should('not.exist');
});
});
describe('AI Assistant Credential Help', () => {
beforeEach(() => {
aiAssistant.actions.enableAssistant();
wf.actions.visit();
});
after(() => {
aiAssistant.actions.disableAssistant();
});
it('should start credential help from node credential', () => {
cy.intercept('POST', '/rest/ai-assistant/chat', {
statusCode: 200,
fixture: 'aiAssistant/simple_message_response.json',
}).as('chatRequest');
wf.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
wf.actions.addNodeToCanvas(GMAIL_NODE_NAME);
wf.actions.openNode('Gmail');
openCredentialSelect();
clickCreateNewCredential();
aiAssistant.getters.credentialEditAssistantButton().should('be.visible');
aiAssistant.getters.credentialEditAssistantButton().click();
cy.wait('@chatRequest');
aiAssistant.getters.chatMessagesUser().should('have.length', 1);
aiAssistant.getters
.chatMessagesUser()
.eq(0)
.should('contain.text', 'How do I set up the credentials for Gmail OAuth2 API?');
aiAssistant.getters
.chatMessagesAssistant()
.eq(0)
.should('contain.text', 'Hey, this is an assistant message');
aiAssistant.getters.credentialEditAssistantButton().should('be.disabled');
});
it('should start credential help from credential list', () => {
cy.intercept('POST', '/rest/ai-assistant/chat', {
statusCode: 200,
fixture: 'aiAssistant/simple_message_response.json',
}).as('chatRequest');
cy.visit(credentialsPage.url);
credentialsPage.getters.emptyListCreateCredentialButton().click();
credentialsModal.getters.newCredentialModal().should('be.visible');
credentialsModal.getters.newCredentialTypeSelect().should('be.visible');
credentialsModal.getters.newCredentialTypeOption('Notion API').click();
credentialsModal.getters.newCredentialTypeButton().click();
aiAssistant.getters.credentialEditAssistantButton().should('be.visible');
aiAssistant.getters.credentialEditAssistantButton().click();
cy.wait('@chatRequest');
aiAssistant.getters.chatMessagesUser().should('have.length', 1);
aiAssistant.getters
.chatMessagesUser()
.eq(0)
.should('contain.text', 'How do I set up the credentials for Notion API?');
aiAssistant.getters
.chatMessagesAssistant()
.eq(0)
.should('contain.text', 'Hey, this is an assistant message');
aiAssistant.getters.credentialEditAssistantButton().should('be.disabled');
});
}); });

View file

@ -0,0 +1,82 @@
import { EXECUTE_WORKFLOW_NODE_NAME } from '../constants';
import { WorkflowPage as WorkflowPageClass, NDV } from '../pages';
import { getVisiblePopper } from '../utils';
const workflowPage = new WorkflowPageClass();
const ndv = new NDV();
describe('Workflow Selector Parameter', () => {
beforeEach(() => {
cy.resetDatabase();
cy.signinAsOwner();
['Get_Weather', 'Search_DB'].forEach((workflowName) => {
workflowPage.actions.visit();
cy.createFixtureWorkflow(`Test_Subworkflow_${workflowName}.json`, workflowName);
workflowPage.actions.saveWorkflowOnButtonClick();
});
workflowPage.actions.visit();
workflowPage.actions.addInitialNodeToCanvas(EXECUTE_WORKFLOW_NODE_NAME, {
keepNdvOpen: true,
action: 'Call Another Workflow',
});
});
it('should render sub-workflows list', () => {
ndv.getters.resourceLocator('workflowId').should('be.visible');
ndv.getters.resourceLocatorInput('workflowId').click();
getVisiblePopper()
.should('have.length', 1)
.findChildByTestId('rlc-item')
.should('have.length', 2);
});
it('should show required parameter warning', () => {
ndv.getters.resourceLocator('workflowId').should('be.visible');
ndv.getters.resourceLocatorInput('workflowId').click();
ndv.getters.parameterInputIssues('workflowId').should('exist');
});
it('should filter sub-workflows list', () => {
ndv.getters.resourceLocator('workflowId').should('be.visible');
ndv.getters.resourceLocatorInput('workflowId').click();
ndv.getters.resourceLocatorSearch('workflowId').type('Weather');
getVisiblePopper()
.should('have.length', 1)
.findChildByTestId('rlc-item')
.should('have.length', 1)
.click();
ndv.getters
.resourceLocatorInput('workflowId')
.find('input')
.should('have.value', 'Get_Weather');
});
it('should render sub-workflow links correctly', () => {
ndv.getters.resourceLocator('workflowId').should('be.visible');
ndv.getters.resourceLocatorInput('workflowId').click();
getVisiblePopper().findChildByTestId('rlc-item').first().click();
ndv.getters.resourceLocatorInput('workflowId').find('a').should('exist');
cy.getByTestId('radio-button-expression').eq(1).click();
ndv.getters.resourceLocatorInput('workflowId').find('a').should('not.exist');
});
it('should switch to ID mode on expression', () => {
ndv.getters.resourceLocator('workflowId').should('be.visible');
ndv.getters.resourceLocatorInput('workflowId').click();
getVisiblePopper().findChildByTestId('rlc-item').first().click();
ndv.getters
.resourceLocatorModeSelector('workflowId')
.find('input')
.should('have.value', 'From list');
cy.getByTestId('radio-button-expression').eq(1).click();
ndv.getters
.resourceLocatorModeSelector('workflowId')
.find('input')
.should('have.value', 'By ID');
});
});

View file

@ -0,0 +1,35 @@
import { WorkflowsPage } from '../pages';
const workflowsPage = new WorkflowsPage();
describe('n8n.io iframe', () => {
describe('when telemetry is disabled', () => {
it('should not load the iframe when visiting /home/workflows', () => {
cy.overrideSettings({ telemetry: { enabled: false } });
cy.visit(workflowsPage.url);
cy.get('iframe').should('not.exist');
});
});
describe('when telemetry is enabled', () => {
it('should load the iframe when visiting /home/workflows', () => {
const testInstanceId = 'test-instance-id';
cy.overrideSettings({ telemetry: { enabled: true }, instanceId: testInstanceId });
const testUserId = Cypress.env('currentUserId');
const iframeUrl = `https://n8n.io/self-install?instanceId=${testInstanceId}&userId=${testUserId}`;
cy.intercept(iframeUrl, (req) => req.reply(200)).as('iframeRequest');
cy.visit(workflowsPage.url);
cy.get('iframe').should('exist').and('have.attr', 'src', iframeUrl);
cy.wait('@iframeRequest').its('response.statusCode').should('eq', 200);
});
});
});

View file

@ -132,6 +132,10 @@ describe('NDV', () => {
'contains.text', 'contains.text',
"An expression here won't work because it uses .item and n8n can't figure out the matching item.", "An expression here won't work because it uses .item and n8n can't figure out the matching item.",
); );
ndv.getters.nodeRunErrorIndicator().should('be.visible');
// The error details should be hidden behind a tooltip
ndv.getters.nodeRunErrorIndicator().should('not.contain', 'Start Time');
ndv.getters.nodeRunErrorIndicator().should('not.contain', 'Execution Time');
}); });
it('should save workflow using keyboard shortcut from NDV', () => { it('should save workflow using keyboard shortcut from NDV', () => {
@ -199,7 +203,7 @@ describe('NDV', () => {
.contains(key) .contains(key)
.should('be.visible'); .should('be.visible');
}); });
getObjectValueItem().find('label').click(); getObjectValueItem().find('label').click({ force: true });
expandedObjectProps.forEach((key) => { expandedObjectProps.forEach((key) => {
ndv.getters ndv.getters
.outputPanel() .outputPanel()
@ -582,7 +586,13 @@ describe('NDV', () => {
ndv.getters.outputTableRow(1).find('mark').should('have.text', '<lib'); ndv.getters.outputTableRow(1).find('mark').should('have.text', '<lib');
ndv.getters.outputDisplayMode().find('label').eq(1).should('include.text', 'JSON'); ndv.getters.outputDisplayMode().find('label').eq(1).should('include.text', 'JSON');
ndv.getters.outputDisplayMode().find('label').eq(1).click(); ndv.getters
.outputDisplayMode()
.find('label')
.eq(1)
.scrollIntoView()
.should('be.visible')
.click();
ndv.getters.outputDataContainer().find('.json-data').should('exist'); ndv.getters.outputDataContainer().find('.json-data').should('exist');
ndv.getters ndv.getters

View file

@ -38,6 +38,51 @@ describe('Code node', () => {
successToast().contains('Node executed successfully'); successToast().contains('Node executed successfully');
}); });
it('should show lint errors in `runOnceForAllItems` mode', () => {
const getParameter = () => ndv.getters.parameterInput('jsCode').should('be.visible');
const getEditor = () => getParameter().find('.cm-content').should('exist');
getEditor().type('{selectall}').paste(`$input.itemMatching()
$input.item
$('When clicking Test workflow').item
$input.first(1)
for (const item of $input.all()) {
item.foo
}
return
`);
getParameter().get('.cm-lint-marker-error').should('have.length', 6);
getParameter().contains('itemMatching').realHover();
cy.get('.cm-tooltip-lint').should(
'have.text',
'`.itemMatching()` expects an item index to be passed in as its argument.',
);
});
it('should show lint errors in `runOnceForEachItem` mode', () => {
const getParameter = () => ndv.getters.parameterInput('jsCode').should('be.visible');
const getEditor = () => getParameter().find('.cm-content').should('exist');
ndv.getters.parameterInput('mode').click();
ndv.actions.selectOptionInParameterDropdown('mode', 'Run Once for Each Item');
getEditor().type('{selectall}').paste(`$input.itemMatching()
$input.all()
$input.first()
$input.item()
return []
`);
getParameter().get('.cm-lint-marker-error').should('have.length', 5);
getParameter().contains('all').realHover();
cy.get('.cm-tooltip-lint').should(
'have.text',
"Method `$input.all()` is only available in the 'Run Once for All Items' mode.",
);
});
}); });
describe('Ask AI', () => { describe('Ask AI', () => {

View file

@ -0,0 +1,133 @@
{
"name": "PAY-1707",
"nodes": [
{
"parameters": {
"options": {}
},
"id": "eaa428a8-eb9d-478a-b997-aed6ed298507",
"name": "Edit Fields",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
920,
380
]
},
{
"parameters": {
"options": {}
},
"id": "6b285c91-e7ea-4943-8ba3-59ce01a35d20",
"name": "Edit Fields1",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
920,
540
]
},
{
"parameters": {
"jsCode": "return Array.from({length: 5}, _ => ({}))"
},
"id": "70e682aa-dfef-4db7-a158-971ec7976d49",
"name": "Code",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
700,
380
]
},
{
"parameters": {
"jsCode": "return Array.from({length: 5}, _ => ({}))"
},
"id": "d5ee979e-9f53-4e62-8eb2-cdb92be8ea6e",
"name": "Code1",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
700,
540
]
},
{
"parameters": {
"path": "dd660366-ca4a-4736-8b1f-454560e87bfb",
"options": {}
},
"id": "20c33c8a-ab2f-4dd4-990f-6390feeb840c",
"name": "Webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [
480,
440
],
"webhookId": "dd660366-ca4a-4736-8b1f-454560e87bfb"
}
],
"pinData": {
"Code1": [
{
"json": {}
},
{
"json": {}
}
]
},
"connections": {
"Code1": {
"main": [
[
{
"node": "Edit Fields1",
"type": "main",
"index": 0
}
]
]
},
"Code": {
"main": [
[
{
"node": "Edit Fields",
"type": "main",
"index": 0
}
]
]
},
"Webhook": {
"main": [
[
{
"node": "Code",
"type": "main",
"index": 0
},
{
"node": "Code1",
"type": "main",
"index": 0
}
]
]
}
},
"active": true,
"settings": {
"executionOrder": "v1"
},
"versionId": "01e6693e-54f3-432d-9b1f-922ef92b4ab6",
"meta": {
"templateCredsSetupCompleted": true,
"instanceId": "8a47b83b4479b11330fdf21ccc96d4a8117035a968612e452b4c87bfd09c16c7"
},
"id": "hU0gp19G29ehWktc",
"tags": []
}

View file

@ -0,0 +1,53 @@
{
"name": "Get Weather",
"nodes": [
{
"parameters": {},
"id": "82eed1ba-179b-4f8f-8a85-b45f0d4e5857",
"name": "Execute Workflow Trigger",
"type": "n8n-nodes-base.executeWorkflowTrigger",
"typeVersion": 1,
"position": [
560,
340
]
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "6ad8dc55-20f3-45af-a724-c7ecac90d338",
"name": "response",
"value": "Weather is sunny",
"type": "string"
}
]
},
"options": {}
},
"id": "8f3e00f6-fc92-4aba-817b-93d206158bda",
"name": "Edit Fields",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
780,
340
]
}
],
"pinData": {},
"connections": {
"Execute Workflow Trigger": {
"main": [
[
{
"node": "Edit Fields",
"type": "main",
"index": 0
}
]
]
}
}
}

View file

@ -0,0 +1,64 @@
{
"name": "Search DB",
"nodes": [
{
"parameters": {},
"id": "64465f9b-63de-43f9-8d90-b5b2eb7a2dc7",
"name": "Execute Workflow Trigger",
"type": "n8n-nodes-base.executeWorkflowTrigger",
"typeVersion": 1,
"position": [
640,
380
]
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "6ad8dc55-20f3-45af-a724-c7ecac90d338",
"name": "response",
"value": "10 results found",
"type": "string"
}
]
},
"options": {}
},
"id": "b580fd2b-00c8-4a52-8acb-024f204c0947",
"name": "Edit Fields",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
860,
380
]
}
],
"pinData": {},
"connections": {
"Execute Workflow Trigger": {
"main": [
[
{
"node": "Edit Fields",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1"
},
"versionId": "6026f7a4-f5dc-4c27-9f83-3a02fc6e33ae",
"meta": {
"templateCredsSetupCompleted": true,
"instanceId": "27cc9b56542ad45b38725555722c50a1c3fee1670bbb67980558314ee08517c4"
},
"id": "BFFhCdBZmNSkx4qf",
"tags": []
}

View file

@ -1,9 +1,9 @@
{ {
"sessionId": "f9130bd7-c078-4862-a38a-369b27b0ff20-e96eb9f7-d581-4684-b6a9-fd3dfe9fe1fb-XCldJLlusGrEVku5I9cYT", "sessionId": "1",
"messages": [ "messages": [
{ {
"role": "assistant", "role": "assistant",
"type": "agent-suggestion", "type": "message",
"title": "Glad to Help", "title": "Glad to Help",
"text": "I'm glad I could help. If you have any more questions or need further assistance with your n8n workflows, feel free to ask!" "text": "I'm glad I could help. If you have any more questions or need further assistance with your n8n workflows, feel free to ask!"
}, },

View file

@ -0,0 +1,20 @@
{
"sessionId": "1",
"messages": [
{
"role": "assistant",
"type": "message",
"text": "It seems like my suggestion did not work. Do you want me to come up with a different suggestion? You can also provide more context via the chat.",
"quickReplies": [
{
"text": "Yes",
"type": "new-suggestion"
},
{
"text": "No, I don't think you can help",
"type": "event:end-session"
}
]
}
]
}

View file

@ -26,7 +26,8 @@ export class AIAssistant extends BasePage {
chatMessagesAssistant: () => cy.getByTestId('chat-message-assistant'), chatMessagesAssistant: () => cy.getByTestId('chat-message-assistant'),
chatMessagesUser: () => cy.getByTestId('chat-message-user'), chatMessagesUser: () => cy.getByTestId('chat-message-user'),
chatMessagesSystem: () => cy.getByTestId('chat-message-system'), chatMessagesSystem: () => cy.getByTestId('chat-message-system'),
quickReplies: () => cy.getByTestId('quick-replies').find('button'), quickReplies: () => cy.getByTestId('quick-replies'),
quickReplyButtons: () => this.getters.quickReplies().find('button'),
newAssistantSessionModal: () => cy.getByTestId('new-assistant-session-modal'), newAssistantSessionModal: () => cy.getByTestId('new-assistant-session-modal'),
codeDiffs: () => cy.getByTestId('code-diff-suggestion'), codeDiffs: () => cy.getByTestId('code-diff-suggestion'),
applyCodeDiffButtons: () => cy.getByTestId('replace-code-button'), applyCodeDiffButtons: () => cy.getByTestId('replace-code-button'),
@ -34,16 +35,29 @@ export class AIAssistant extends BasePage {
codeReplacedMessage: () => cy.getByTestId('code-replaced-message'), codeReplacedMessage: () => cy.getByTestId('code-replaced-message'),
nodeErrorViewAssistantButton: () => nodeErrorViewAssistantButton: () =>
cy.getByTestId('node-error-view-ask-assistant-button').find('button').first(), cy.getByTestId('node-error-view-ask-assistant-button').find('button').first(),
credentialEditAssistantButton: () =>
cy.getByTestId('credentail-edit-ask-assistant-button').find('button').first(),
}; };
actions = { actions = {
enableAssistant(): void { enableAssistant: () => {
overrideFeatureFlag(AI_ASSISTANT_FEATURE.experimentName, AI_ASSISTANT_FEATURE.enabledFor); overrideFeatureFlag(AI_ASSISTANT_FEATURE.experimentName, AI_ASSISTANT_FEATURE.enabledFor);
cy.enableFeature(AI_ASSISTANT_FEATURE.name); cy.enableFeature(AI_ASSISTANT_FEATURE.name);
}, },
disableAssistant(): void { disableAssistant: () => {
overrideFeatureFlag(AI_ASSISTANT_FEATURE.experimentName, AI_ASSISTANT_FEATURE.disabledFor); overrideFeatureFlag(AI_ASSISTANT_FEATURE.experimentName, AI_ASSISTANT_FEATURE.disabledFor);
cy.disableFeature(AI_ASSISTANT_FEATURE.name); cy.disableFeature(AI_ASSISTANT_FEATURE.name);
}, },
sendMessage: (message: string) => {
this.getters.chatInput().type(message).type('{enter}');
},
closeChat: () => {
this.getters.closeChatButton().click();
this.getters.askAssistantChat().should('not.be.visible');
},
openChat: () => {
this.getters.askAssistantFloatingButton().click();
this.getters.askAssistantChat().should('be.visible');
},
}; };
} }

View file

@ -7,6 +7,7 @@ export class WorkflowExecutionsTab extends BasePage {
getters = { getters = {
executionsTabButton: () => cy.getByTestId('radio-button-executions'), executionsTabButton: () => cy.getByTestId('radio-button-executions'),
executionsSidebar: () => cy.getByTestId('executions-sidebar'), executionsSidebar: () => cy.getByTestId('executions-sidebar'),
executionsEmptyList: () => cy.getByTestId('execution-list-empty'),
autoRefreshCheckBox: () => cy.getByTestId('auto-refresh-checkbox'), autoRefreshCheckBox: () => cy.getByTestId('auto-refresh-checkbox'),
executionsList: () => cy.getByTestId('current-executions-list'), executionsList: () => cy.getByTestId('current-executions-list'),
executionListItems: () => this.getters.executionsList().find('div.execution-card'), executionListItems: () => this.getters.executionsList().find('div.execution-card'),

View file

@ -59,14 +59,18 @@ Cypress.Commands.add('waitForLoad', (waitForIntercepts = true) => {
Cypress.Commands.add('signin', ({ email, password }) => { Cypress.Commands.add('signin', ({ email, password }) => {
void Cypress.session.clearAllSavedSessions(); void Cypress.session.clearAllSavedSessions();
cy.session([email, password], () => cy.session([email, password], () => {
cy.request({ return cy
.request({
method: 'POST', method: 'POST',
url: `${BACKEND_BASE_URL}/rest/login`, url: `${BACKEND_BASE_URL}/rest/login`,
body: { email, password }, body: { email, password },
failOnStatusCode: false, failOnStatusCode: false,
}), })
); .then((response) => {
Cypress.env('currentUserId', response.body.data.id);
});
});
}); });
Cypress.Commands.add('signinAsOwner', () => cy.signin(INSTANCE_OWNER)); Cypress.Commands.add('signinAsOwner', () => cy.signin(INSTANCE_OWNER));

View file

@ -1,6 +1,6 @@
{ {
"name": "n8n-monorepo", "name": "n8n-monorepo",
"version": "1.55.0", "version": "1.59.0",
"private": true, "private": true,
"engines": { "engines": {
"node": ">=20.15", "node": ">=20.15",
@ -17,6 +17,7 @@
"dev": "turbo run dev --parallel --env-mode=loose --filter=!n8n-design-system --filter=!@n8n/chat", "dev": "turbo run dev --parallel --env-mode=loose --filter=!n8n-design-system --filter=!@n8n/chat",
"dev:ai": "turbo run dev --parallel --env-mode=loose --filter=@n8n/nodes-langchain --filter=n8n --filter=n8n-core", "dev:ai": "turbo run dev --parallel --env-mode=loose --filter=@n8n/nodes-langchain --filter=n8n --filter=n8n-core",
"clean": "turbo run clean --parallel", "clean": "turbo run clean --parallel",
"reset": "node scripts/ensure-zx.mjs && zx scripts/reset.mjs",
"format": "turbo run format && node scripts/format.mjs", "format": "turbo run format && node scripts/format.mjs",
"lint": "turbo run lint", "lint": "turbo run lint",
"lintfix": "turbo run lintfix", "lintfix": "turbo run lintfix",
@ -55,7 +56,8 @@
"tsc-alias": "^1.8.7", "tsc-alias": "^1.8.7",
"tsc-watch": "^6.0.4", "tsc-watch": "^6.0.4",
"turbo": "2.0.6", "turbo": "2.0.6",
"typescript": "*" "typescript": "*",
"zx": "^8.1.4"
}, },
"pnpm": { "pnpm": {
"onlyBuiltDependencies": [ "onlyBuiltDependencies": [

View file

@ -0,0 +1,7 @@
const sharedOptions = require('@n8n_io/eslint-config/shared');
/** @type {import('@types/eslint').ESLint.ConfigData} */
module.exports = {
extends: ['@n8n_io/eslint-config/base'],
...sharedOptions(__dirname),
};

View file

@ -0,0 +1,3 @@
## @n8n/api-types
This package contains types and schema definitions for the n8n internal API, so that these can be shared between the backend and the frontend code.

View file

@ -0,0 +1,2 @@
/** @type {import('jest').Config} */
module.exports = require('../../../jest.config');

View file

@ -0,0 +1,24 @@
{
"name": "@n8n/api-types",
"version": "0.1.0",
"scripts": {
"clean": "rimraf dist .turbo",
"dev": "pnpm watch",
"typecheck": "tsc --noEmit",
"build": "tsc -p tsconfig.build.json",
"format": "prettier --write . --ignore-path ../../../.prettierignore",
"lint": "eslint .",
"lintfix": "eslint . --fix",
"watch": "tsc -p tsconfig.build.json --watch",
"test": "echo \"No tests yet\" && exit 0"
},
"main": "dist/index.js",
"module": "src/index.ts",
"types": "dist/index.d.ts",
"files": [
"dist/**/*"
],
"devDependencies": {
"n8n-workflow": "workspace:*"
}
}

View file

@ -0,0 +1,2 @@
/** Date time in the ISO 8601 format, e.g. 2024-10-31T00:00:00.123Z */
export type Iso8601DateTimeString = string;

View file

@ -0,0 +1,7 @@
export type * from './push';
export type * from './scaling';
export type * from './datetime';
export type * from './user';
export type { Collaborator } from './push/collaboration';
export type { SendWorkerStatusMessage } from './push/worker';

View file

@ -0,0 +1,17 @@
import type { Iso8601DateTimeString } from '../datetime';
import type { MinimalUser } from '../user';
export type Collaborator = {
user: MinimalUser;
lastSeen: Iso8601DateTimeString;
};
type CollaboratorsChanged = {
type: 'collaboratorsChanged';
data: {
workflowId: string;
collaborators: Collaborator[];
};
};
export type CollaborationPushMessage = CollaboratorsChanged;

View file

@ -0,0 +1,9 @@
type SendConsoleMessage = {
type: 'sendConsoleMessage';
data: {
source: string;
messages: unknown[];
};
};
export type DebugPushMessage = SendConsoleMessage;

View file

@ -0,0 +1,53 @@
import type { IRun, ITaskData, WorkflowExecuteMode } from 'n8n-workflow';
type ExecutionStarted = {
type: 'executionStarted';
data: {
executionId: string;
mode: WorkflowExecuteMode;
startedAt: Date;
workflowId: string;
workflowName?: string;
retryOf?: string;
};
};
type ExecutionFinished = {
type: 'executionFinished';
data: {
executionId: string;
data: IRun;
retryOf?: string;
};
};
type ExecutionRecovered = {
type: 'executionRecovered';
data: {
executionId: string;
};
};
type NodeExecuteBefore = {
type: 'nodeExecuteBefore';
data: {
executionId: string;
nodeName: string;
};
};
type NodeExecuteAfter = {
type: 'nodeExecuteAfter';
data: {
executionId: string;
nodeName: string;
data: ITaskData;
};
};
export type ExecutionPushMessage =
| ExecutionStarted
| ExecutionFinished
| ExecutionRecovered
| NodeExecuteBefore
| NodeExecuteAfter;

View file

@ -0,0 +1,21 @@
type NodeTypeData = {
name: string;
version: number;
};
type ReloadNodeType = {
type: 'reloadNodeType';
data: NodeTypeData;
};
type RemoveNodeType = {
type: 'removeNodeType';
data: NodeTypeData;
};
type NodeDescriptionUpdated = {
type: 'nodeDescriptionUpdated';
data: {};
};
export type HotReloadPushMessage = ReloadNodeType | RemoveNodeType | NodeDescriptionUpdated;

View file

@ -0,0 +1,20 @@
import type { ExecutionPushMessage } from './execution';
import type { WorkflowPushMessage } from './workflow';
import type { HotReloadPushMessage } from './hot-reload';
import type { WorkerPushMessage } from './worker';
import type { WebhookPushMessage } from './webhook';
import type { CollaborationPushMessage } from './collaboration';
import type { DebugPushMessage } from './debug';
export type PushMessage =
| ExecutionPushMessage
| WorkflowPushMessage
| HotReloadPushMessage
| WebhookPushMessage
| WorkerPushMessage
| CollaborationPushMessage
| DebugPushMessage;
export type PushType = PushMessage['type'];
export type PushPayload<T extends PushType> = Extract<PushMessage, { type: T }>['data'];

View file

@ -0,0 +1,17 @@
type TestWebhookDeleted = {
type: 'testWebhookDeleted';
data: {
executionId?: string;
workflowId: string;
};
};
type TestWebhookReceived = {
type: 'testWebhookReceived';
data: {
executionId: string;
workflowId: string;
};
};
export type WebhookPushMessage = TestWebhookDeleted | TestWebhookReceived;

View file

@ -0,0 +1,11 @@
import type { WorkerStatus } from '../scaling';
export type SendWorkerStatusMessage = {
type: 'sendWorkerStatusMessage';
data: {
workerId: string;
status: WorkerStatus;
};
};
export type WorkerPushMessage = SendWorkerStatusMessage;

View file

@ -0,0 +1,26 @@
type WorkflowActivated = {
type: 'workflowActivated';
data: {
workflowId: string;
};
};
type WorkflowFailedToActivate = {
type: 'workflowFailedToActivate';
data: {
workflowId: string;
errorMessage: string;
};
};
type WorkflowDeactivated = {
type: 'workflowDeactivated';
data: {
workflowId: string;
};
};
export type WorkflowPushMessage =
| WorkflowActivated
| WorkflowFailedToActivate
| WorkflowDeactivated;

View file

@ -0,0 +1,30 @@
import type { ExecutionStatus, WorkflowExecuteMode } from 'n8n-workflow';
export type RunningJobSummary = {
executionId: string;
workflowId: string;
workflowName: string;
mode: WorkflowExecuteMode;
startedAt: Date;
retryOf: string;
status: ExecutionStatus;
};
export type WorkerStatus = {
workerId: string;
runningJobsSummary: RunningJobSummary[];
freeMem: number;
totalMem: number;
uptime: number;
loadAvg: number[];
cpus: string;
arch: string;
platform: NodeJS.Platform;
hostname: string;
interfaces: Array<{
family: 'IPv4' | 'IPv6';
address: string;
internal: boolean;
}>;
version: string;
};

View file

@ -0,0 +1,6 @@
export type MinimalUser = {
id: string;
email: string;
firstName: string;
lastName: string;
};

View file

@ -0,0 +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__/**"]
}

View file

@ -0,0 +1,10 @@
{
"extends": "../../../tsconfig.json",
"compilerOptions": {
"rootDir": ".",
"types": ["node", "jest"],
"baseUrl": "src",
"tsBuildInfoFile": "dist/typecheck.tsbuildinfo"
},
"include": ["src/**/*.ts", "test/**/*.ts"]
}

View file

@ -0,0 +1,31 @@
const sharedOptions = require('@n8n_io/eslint-config/shared');
/**
* @type {import('@types/eslint').ESLint.ConfigData}
*/
module.exports = {
extends: ['@n8n_io/eslint-config/node'],
...sharedOptions(__dirname),
parserOptions: {
project: './tsconfig.json',
},
ignorePatterns: ['scenarios/**'],
rules: {
'unicorn/filename-case': ['error', { case: 'kebabCase' }],
'n8n-local-rules/no-plain-errors': 'off',
complexity: 'error',
},
overrides: [
{
files: ['./src/commands/*.ts'],
rules: {
'import/no-default-export': 'off',
},
},
],
};

4
packages/@n8n/benchmark/.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
**/.terraform/*
**/*.tfstate*
**/*.tfvars
privatekey.pem

View file

@ -0,0 +1,62 @@
# syntax=docker/dockerfile:1
FROM node:20.16.0 AS base
# Install required dependencies
RUN apt-get update && apt-get install -y gnupg2 curl
# Add k6 GPG key and repository
RUN mkdir -p /etc/apt/keyrings && \
curl -sS https://dl.k6.io/key.gpg | gpg --dearmor --yes -o /etc/apt/keyrings/k6.gpg && \
chmod a+x /etc/apt/keyrings/k6.gpg && \
echo "deb [signed-by=/etc/apt/keyrings/k6.gpg] https://dl.k6.io/deb stable main" | tee /etc/apt/sources.list.d/k6.list
# Update and install k6
RUN apt-get update && \
apt-get install -y k6 tini && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
#
# Builder
FROM base AS builder
WORKDIR /app
COPY --chown=node:node ./pnpm-lock.yaml /app/pnpm-lock.yaml
COPY --chown=node:node ./pnpm-workspace.yaml /app/pnpm-workspace.yaml
COPY --chown=node:node ./package.json /app/package.json
COPY --chown=node:node ./packages/@n8n/benchmark/package.json /app/packages/@n8n/benchmark/package.json
COPY --chown=node:node ./patches /app/patches
COPY --chown=node:node ./scripts /app/scripts
RUN pnpm install --frozen-lockfile
# TS config files
COPY --chown=node:node ./tsconfig.json /app/tsconfig.json
COPY --chown=node:node ./tsconfig.build.json /app/tsconfig.build.json
COPY --chown=node:node ./tsconfig.backend.json /app/tsconfig.backend.json
COPY --chown=node:node ./packages/@n8n/benchmark/tsconfig.json /app/packages/@n8n/benchmark/tsconfig.json
COPY --chown=node:node ./packages/@n8n/benchmark/tsconfig.build.json /app/packages/@n8n/benchmark/tsconfig.build.json
# Source files
COPY --chown=node:node ./packages/@n8n/benchmark/src /app/packages/@n8n/benchmark/src
COPY --chown=node:node ./packages/@n8n/benchmark/bin /app/packages/@n8n/benchmark/bin
COPY --chown=node:node ./packages/@n8n/benchmark/scenarios /app/packages/@n8n/benchmark/scenarios
WORKDIR /app/packages/@n8n/benchmark
RUN pnpm build
#
# Runner
FROM base AS runner
COPY --from=builder /app /app
WORKDIR /app/packages/@n8n/benchmark
USER node
ENTRYPOINT [ "/app/packages/@n8n/benchmark/bin/n8n-benchmark" ]

View file

@ -0,0 +1,82 @@
# n8n benchmarking tool
Tool for executing benchmarks against an n8n instance. The tool consists of these components:
## Directory structure
```text
packages/@n8n/benchmark
├── scenarios Benchmark scenarios
├── src Source code for the n8n-benchmark cli
├── Dockerfile Dockerfile for the n8n-benchmark cli
├── scripts Orchestration scripts
```
## Running the entire benchmark suite
The benchmark suite consists of [benchmark scenarios](#benchmark-scenarios) and different [n8n setups](#n8n-setups).
### locally
```sh
pnpm benchmark-locally
```
### In the cloud
```sh
pnpm benchmark-in-cloud
```
## Running the `n8n-benchmark` cli
The `n8n-benchmark` cli is a node.js program that runs one or more scenarios against a single n8n instance.
### Locally with Docker
Build the Docker image:
```sh
# Must be run in the repository root
# k6 doesn't have an arm64 build available for linux, we need to build against amd64
docker build --platform linux/amd64 -t n8n-benchmark -f packages/@n8n/benchmark/Dockerfile .
```
Run the image
```sh
docker run \
-e N8N_USER_EMAIL=user@n8n.io \
-e N8N_USER_PASSWORD=password \
# For macos, n8n running outside docker
-e N8N_BASE_URL=http://host.docker.internal:5678 \
n8n-benchmark
```
### Locally without Docker
Requirements:
- [k6](https://grafana.com/docs/k6/latest/set-up/install-k6/)
- Node.js v20 or higher
```sh
pnpm build
# Run tests against http://localhost:5678 with specified email and password
N8N_USER_EMAIL=user@n8n.io N8N_USER_PASSWORD=password ./bin/n8n-benchmark run
```
## Benchmark scenarios
A benchmark scenario defines one or multiple steps to execute and measure. It consists of:
- Manifest file which describes and configures the scenario
- Any test data that is imported before the scenario is run
- A [`k6`](https://grafana.com/docs/k6/latest/using-k6/http-requests/) script which executes the steps and receives `API_BASE_URL` environment variable in runtime.
Available scenarios are located in [`./scenarios`](./scenarios/).
## n8n setups
A n8n setup defines a single n8n runtime configuration using Docker compose. Different n8n setups are located in [`./scripts/n8nSetups`](./scripts/n8nSetups).

View file

@ -0,0 +1,13 @@
#!/usr/bin/env node
// Check if version should be displayed
const versionFlags = ['-v', '-V', '--version'];
if (versionFlags.includes(process.argv.slice(-1)[0])) {
console.log(require('../package').version);
process.exit(0);
}
(async () => {
const oclif = require('@oclif/core');
await oclif.execute({ dir: __dirname });
})();

View file

@ -0,0 +1,60 @@
# This file is maintained automatically by "terraform init".
# Manual edits may be lost in future updates.
provider "registry.terraform.io/hashicorp/azurerm" {
version = "3.115.0"
constraints = "~> 3.115.0"
hashes = [
"h1:O7C3Xb+MSOc9C/eAJ5C/CiJ4vuvUsYxxIzr9ZurmHNI=",
"zh:0ea93abd53cb872691bad6d5625bda88b5d9619ea813c208b36e0ee236308589",
"zh:26703cb9c2c38bc43e97bc83af03559d065750856ea85834b71fbcb2ef9d935c",
"zh:316255a3391c49fe9bd7c5b6aa53b56dd490e1083d19b722e7b8f956a2dfe004",
"zh:431637ae90c592126fb1ec813fee6390604275438a0d5e15904c65b0a6a0f826",
"zh:4cee0fa2e84f89853723c0bc72b7debf8ea2ffffc7ae34ff28d8a69269d3a879",
"zh:64a3a3c78ea877515365ed336bd0f3abbe71db7c99b3d2837915fbca168d429c",
"zh:7380d7b503b5a87fd71a31360c3eeab504f78e4f314824e3ceda724d9dc74cf0",
"zh:974213e05708037a6d2d8c58cc84981819138f44fe40e344034eb80e16ca6012",
"zh:9a91614de0476074e9c62bbf08d3bb9c64adbd1d3a4a2b5a3e8e41d9d6d5672f",
"zh:a438471c85b8788ab21bdef4cd5ca391a46cbae33bd0262668a80f5e6c4610e1",
"zh:bf823f2c941b336a1208f015466212b1a8fdf6da28abacf59bea708377709d9e",
"zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c",
]
}
provider "registry.terraform.io/hashicorp/random" {
version = "3.6.2"
hashes = [
"h1:VavG5unYCa3SYISMKF9pzc3718M0bhPlcbUZZGl7wuo=",
"zh:0ef01a4f81147b32c1bea3429974d4d104bbc4be2ba3cfa667031a8183ef88ec",
"zh:1bcd2d8161e89e39886119965ef0f37fcce2da9c1aca34263dd3002ba05fcb53",
"zh:37c75d15e9514556a5f4ed02e1548aaa95c0ecd6ff9af1119ac905144c70c114",
"zh:4210550a767226976bc7e57d988b9ce48f4411fa8a60cd74a6b246baf7589dad",
"zh:562007382520cd4baa7320f35e1370ffe84e46ed4e2071fdc7e4b1a9b1f8ae9b",
"zh:5efb9da90f665e43f22c2e13e0ce48e86cae2d960aaf1abf721b497f32025916",
"zh:6f71257a6b1218d02a573fc9bff0657410404fb2ef23bc66ae8cd968f98d5ff6",
"zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
"zh:9647e18f221380a85f2f0ab387c68fdafd58af6193a932417299cdcae4710150",
"zh:bb6297ce412c3c2fa9fec726114e5e0508dd2638cad6a0cb433194930c97a544",
"zh:f83e925ed73ff8a5ef6e3608ad9225baa5376446349572c2449c0c0b3cf184b7",
"zh:fbef0781cb64de76b1df1ca11078aecba7800d82fd4a956302734999cfd9a4af",
]
}
provider "registry.terraform.io/hashicorp/tls" {
version = "4.0.5"
hashes = [
"h1:zeG5RmggBZW/8JWIVrdaeSJa0OG62uFX5HY1eE8SjzY=",
"zh:01cfb11cb74654c003f6d4e32bbef8f5969ee2856394a96d127da4949c65153e",
"zh:0472ea1574026aa1e8ca82bb6df2c40cd0478e9336b7a8a64e652119a2fa4f32",
"zh:1a8ddba2b1550c5d02003ea5d6cdda2eef6870ece86c5619f33edd699c9dc14b",
"zh:1e3bb505c000adb12cdf60af5b08f0ed68bc3955b0d4d4a126db5ca4d429eb4a",
"zh:6636401b2463c25e03e68a6b786acf91a311c78444b1dc4f97c539f9f78de22a",
"zh:76858f9d8b460e7b2a338c477671d07286b0d287fd2d2e3214030ae8f61dd56e",
"zh:a13b69fb43cb8746793b3069c4d897bb18f454290b496f19d03c3387d1c9a2dc",
"zh:a90ca81bb9bb509063b736842250ecff0f886a91baae8de65c8430168001dad9",
"zh:c4de401395936e41234f1956ebadbd2ed9f414e6908f27d578614aaa529870d4",
"zh:c657e121af8fde19964482997f0de2d5173217274f6997e16389e7707ed8ece8",
"zh:d68b07a67fbd604c38ec9733069fbf23441436fecf554de6c75c032f82e1ef19",
"zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c",
]
}

View file

@ -0,0 +1,54 @@
data "azurerm_resource_group" "main" {
name = var.resource_group_name
}
# Random prefix for the resources
resource "random_string" "prefix" {
length = 8
special = false
}
# SSH key pair
resource "tls_private_key" "ssh_key" {
algorithm = "RSA"
rsa_bits = 4096
}
# Dedicated Host Group & Hosts
resource "azurerm_dedicated_host_group" "main" {
name = "${random_string.prefix.result}-hostgroup"
location = var.location
resource_group_name = data.azurerm_resource_group.main.name
platform_fault_domain_count = 1
automatic_placement_enabled = false
zone = 1
tags = local.common_tags
}
resource "azurerm_dedicated_host" "hosts" {
name = "${random_string.prefix.result}-host"
location = var.location
dedicated_host_group_id = azurerm_dedicated_host_group.main.id
sku_name = var.host_size_family
platform_fault_domain = 0
tags = local.common_tags
}
# VM
module "test_vm" {
source = "./modules/benchmark-vm"
location = var.location
resource_group_name = data.azurerm_resource_group.main.name
prefix = random_string.prefix.result
dedicated_host_id = azurerm_dedicated_host.hosts.id
ssh_public_key = tls_private_key.ssh_key.public_key_openssh
vm_size = var.vm_size
tags = local.common_tags
}

View file

@ -0,0 +1,11 @@
output "vm_name" {
value = azurerm_linux_virtual_machine.main.name
}
output "ip" {
value = azurerm_public_ip.main.ip_address
}
output "ssh_username" {
value = azurerm_linux_virtual_machine.main.admin_username
}

View file

@ -0,0 +1,29 @@
variable "location" {
description = "Region to deploy resources"
default = "East US"
}
variable "resource_group_name" {
description = "Name of the resource group"
}
variable "prefix" {
description = "Prefix to append to resources"
}
variable "dedicated_host_id" {
description = "Dedicated Host ID"
}
variable "ssh_public_key" {
description = "SSH Public Key"
}
variable "vm_size" {
description = "VM Size"
}
variable "tags" {
description = "Tags to apply to all resources created by this module"
type = map(string)
}

View file

@ -0,0 +1,126 @@
# Network
resource "azurerm_virtual_network" "main" {
name = "${var.prefix}-vnet"
location = var.location
resource_group_name = var.resource_group_name
address_space = ["10.0.0.0/16"]
tags = var.tags
}
resource "azurerm_subnet" "main" {
name = "${var.prefix}-subnet"
resource_group_name = var.resource_group_name
virtual_network_name = azurerm_virtual_network.main.name
address_prefixes = ["10.0.0.0/24"]
}
resource "azurerm_network_security_group" "ssh" {
name = "${var.prefix}-nsg"
location = var.location
resource_group_name = var.resource_group_name
security_rule {
name = "AllowSSH"
priority = 1001
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "22"
source_address_prefix = "*"
destination_address_prefix = "*"
}
tags = var.tags
}
resource "azurerm_public_ip" "main" {
name = "${var.prefix}-pip"
location = var.location
resource_group_name = var.resource_group_name
allocation_method = "Static"
sku = "Standard"
tags = var.tags
}
resource "azurerm_network_interface" "main" {
name = "${var.prefix}-nic"
location = var.location
resource_group_name = var.resource_group_name
ip_configuration {
name = "${var.prefix}-ipconfig"
subnet_id = azurerm_subnet.main.id
private_ip_address_allocation = "Dynamic"
public_ip_address_id = azurerm_public_ip.main.id
}
tags = var.tags
}
resource "azurerm_network_interface_security_group_association" "ssh" {
network_interface_id = azurerm_network_interface.main.id
network_security_group_id = azurerm_network_security_group.ssh.id
}
# Disk
resource "azurerm_managed_disk" "data" {
name = "${var.prefix}-disk"
location = var.location
resource_group_name = var.resource_group_name
storage_account_type = "PremiumV2_LRS"
create_option = "Empty"
disk_size_gb = "16"
zone = 1
tags = var.tags
}
resource "azurerm_virtual_machine_data_disk_attachment" "data" {
managed_disk_id = azurerm_managed_disk.data.id
virtual_machine_id = azurerm_linux_virtual_machine.main.id
lun = "1"
caching = "None"
}
# VM
resource "azurerm_linux_virtual_machine" "main" {
name = "${var.prefix}-vm"
location = var.location
resource_group_name = var.resource_group_name
network_interface_ids = [azurerm_network_interface.main.id]
dedicated_host_id = var.dedicated_host_id
zone = 1
size = var.vm_size
admin_username = "benchmark"
admin_ssh_key {
username = "benchmark"
public_key = var.ssh_public_key
}
os_disk {
caching = "ReadWrite"
storage_account_type = "Premium_LRS"
}
source_image_reference {
publisher = "Canonical"
offer = "0001-com-ubuntu-server-jammy"
sku = "22_04-lts-gen2"
version = "latest"
}
identity {
type = "SystemAssigned"
}
tags = var.tags
}

View file

@ -0,0 +1,16 @@
output "vm_name" {
value = module.test_vm.vm_name
}
output "ip" {
value = module.test_vm.ip
}
output "ssh_username" {
value = module.test_vm.ssh_username
}
output "ssh_private_key" {
value = tls_private_key.ssh_key.private_key_pem
sensitive = true
}

View file

@ -0,0 +1,23 @@
terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 3.115.0"
}
random = {
source = "hashicorp/random"
}
}
required_version = "~> 1.8.5"
}
provider "azurerm" {
features {}
skip_provider_registration = true
}
provider "random" {}

View file

@ -0,0 +1,34 @@
variable "location" {
description = "Region to deploy resources"
default = "East US"
}
variable "resource_group_name" {
description = "Name of the resource group"
default = "n8n-benchmarking"
}
variable "host_size_family" {
description = "Size Family for the Host Group"
default = "DCSv2-Type1"
}
variable "vm_size" {
description = "VM Size"
# 8 vCPUs, 32 GiB memory
default = "Standard_DC8_v2"
}
variable "number_of_vms" {
description = "Number of VMs to create"
default = 1
}
locals {
common_tags = {
Id = "N8nBenchmark"
Terraform = "true"
Owner = "Catalysts"
CreatedAt = timestamp()
}
}

View file

@ -0,0 +1,55 @@
{
"name": "@n8n/n8n-benchmark",
"version": "1.3.0",
"description": "Cli for running benchmark tests for n8n",
"main": "dist/index",
"scripts": {
"build": "tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json",
"lint": "eslint .",
"lintfix": "eslint . --fix",
"start": "./bin/n8n-benchmark",
"test": "echo \"Error: no test specified\" && exit 1",
"typecheck": "tsc --noEmit",
"benchmark": "zx scripts/run.mjs",
"benchmark-in-cloud": "pnpm benchmark --env cloud",
"benchmark-locally": "pnpm benchmark --env local",
"provision-cloud-env": "zx scripts/provision-cloud-env.mjs",
"destroy-cloud-env": "zx scripts/destroy-cloud-env.mjs",
"watch": "concurrently \"tsc -w -p tsconfig.build.json\" \"tsc-alias -w -p tsconfig.build.json\""
},
"engines": {
"node": ">=20.10"
},
"keywords": [
"automate",
"automation",
"IaaS",
"iPaaS",
"n8n",
"workflow",
"benchmark",
"performance"
],
"dependencies": {
"@oclif/core": "4.0.7",
"axios": "catalog:",
"dotenv": "8.6.0",
"nanoid": "catalog:",
"zx": "^8.1.4"
},
"devDependencies": {
"@types/convict": "^6.1.1",
"@types/k6": "^0.52.0",
"@types/node": "^20.14.8",
"tsc-alias": "^1.8.7",
"typescript": "^5.5.2"
},
"bin": {
"n8n-benchmark": "./bin/n8n-benchmark"
},
"oclif": {
"bin": "n8n-benchmark",
"commands": "./dist/commands",
"topicSeparator": " "
}
}

View file

@ -0,0 +1,67 @@
{
"createdAt": "2024-09-03T11:51:56.540Z",
"updatedAt": "2024-09-03T12:22:21.000Z",
"name": "Binary Data",
"active": true,
"nodes": [
{
"parameters": {
"httpMethod": "POST",
"path": "binary-files-benchmark",
"responseMode": "responseNode",
"options": {}
},
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [0, 0],
"id": "bfe19f12-3655-440f-be5c-8d71665c6353",
"name": "Webhook",
"webhookId": "109d7b13-93ad-42b0-a9ce-ca49e1817b35"
},
{
"parameters": { "respondWith": "binary", "options": {} },
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"position": [740, 0],
"id": "cd957c9b-6b7a-4423-aac3-6df4d8bb571e",
"name": "Respond to Webhook"
},
{
"parameters": {
"operation": "write",
"fileName": "=file-{{ Date.now() }}-{{ Math.random() }}.js",
"dataPropertyName": "file",
"options": {}
},
"type": "n8n-nodes-base.readWriteFile",
"typeVersion": 1,
"position": [260, 0],
"id": "f2ce4709-7697-4bc6-8eca-6c222485297a",
"name": "Write File to Disk"
},
{
"parameters": { "fileSelector": "={{ $json.fileName }}", "options": {} },
"type": "n8n-nodes-base.readWriteFile",
"typeVersion": 1,
"position": [500, 0],
"id": "198e8a6c-81a3-4b34-b099-501961a02006",
"name": "Read File from Disk"
}
],
"connections": {
"Webhook": { "main": [[{ "node": "Write File to Disk", "type": "main", "index": 0 }]] },
"Write File to Disk": {
"main": [[{ "node": "Read File from Disk", "type": "main", "index": 0 }]]
},
"Read File from Disk": {
"main": [[{ "node": "Respond to Webhook", "type": "main", "index": 0 }]]
}
},
"settings": { "executionOrder": "v1" },
"staticData": null,
"meta": null,
"pinData": {},
"versionId": "8dd197c0-d1ea-43c3-9f88-9d11e7b081a0",
"triggerCount": 1,
"tags": []
}

View file

@ -0,0 +1,7 @@
{
"$schema": "../scenario.schema.json",
"name": "BinaryData",
"description": "Send a binary file to a webhook, write it to FS, read it from FS and receive it back",
"scenarioData": { "workflowFiles": ["binary-data.json"] },
"scriptPath": "binary-data.script.js"
}

View file

@ -0,0 +1,22 @@
import http from 'k6/http';
import { check } from 'k6';
const apiBaseUrl = __ENV.API_BASE_URL;
const file = open(__ENV.SCRIPT_FILE_PATH, 'b');
const filename = String(__ENV.SCRIPT_FILE_PATH).split('/').pop();
export default function () {
const data = {
filename,
file: http.file(file, filename, 'application/javascript'),
};
const res = http.post(`${apiBaseUrl}/webhook/binary-files-benchmark`, data);
check(res, {
'is status 200': (r) => r.status === 200,
'has correct content type': (r) =>
r.headers['Content-Type'] === 'application/javascript; charset=utf-8',
});
}

View file

@ -0,0 +1,213 @@
{
"createdAt": "2024-09-04T07:18:29.011Z",
"updatedAt": "2024-09-04T07:27:58.000Z",
"id": "rUXzWNGsUDUmgaFS",
"name": "HTTP Request",
"active": false,
"nodes": [
{
"parameters": {
"httpMethod": "POST",
"path": "benchmark-http-node",
"responseMode": "responseNode",
"options": {}
},
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [-60, 20],
"id": "f11378b4-5f28-4a6c-9332-5878342cd3cf",
"name": "Webhook",
"webhookId": "c40014cc-4d64-4fcf-8c13-9e94b6792756"
},
{
"parameters": {
"respondWith": "allIncomingItems",
"options": {}
},
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"position": [1060, 20],
"id": "f42552c7-9c6e-4616-b9d5-ac79445ef4ed",
"name": "Respond to Webhook"
},
{
"parameters": {
"url": "http://mockapi:8080/users/clair.bahringer/received_events/public",
"options": {
"response": {
"response": {
"fullResponse": true
}
}
}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [300, -180],
"id": "20de816e-0fbe-4e28-bc53-f508a2dda117",
"name": "Mock public received events"
},
{
"parameters": {
"url": "http://mockapi:8080/repos/udke6pujoywnagxkcvab2riw23khzn2tibo2vincws32qexb50ey7h97d42vnzyol0rxypgsg4pomsf7sgnmdaihstljw8edcijrwmy7mfi76yif19c4/47i31dh737el215j62ts2f2782nw3ss26rul3s8jw13u3vu0xm349a5hyay5asmwnlnf7nx8p9h4g62so6s1cis7xv9puj5j98t4m980sbe2455fn1obccjl/events",
"options": {
"response": {
"response": {
"fullResponse": true
}
}
}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [300, 20],
"id": "083e02b3-a257-49a8-8f7d-42222cb9194c",
"name": "Mock repository events"
},
{
"parameters": {
"url": "http://mockapi:8080/orgs/g02pp066qoyithcjevhd6m1wfii3c4x51k39n9apybljhx69/events",
"options": {
"response": {
"response": {
"fullResponse": true
}
}
}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [300, 220],
"id": "f4c3b5d2-0257-4883-a585-ade4c3a1082c",
"name": "Mock organization events"
},
{
"parameters": {
"numberInputs": 3
},
"type": "n8n-nodes-base.merge",
"typeVersion": 3,
"position": [600, 20],
"id": "273985b7-b0ae-4cde-bbe9-7b3e4b29fe61",
"name": "Merge"
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "89608adb-f487-416f-a7d8-3ebb1f7b50e5",
"name": "statusCode",
"value": "={{ $json.statusCode }}",
"type": "number"
}
]
},
"options": {}
},
"id": "231275a7-44e7-47bb-8ccf-fe62dc48356b",
"name": "Select statusCode",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [820, 20]
}
],
"connections": {
"Webhook": {
"main": [
[
{
"node": "Mock public received events",
"type": "main",
"index": 0
},
{
"node": "Mock repository events",
"type": "main",
"index": 0
},
{
"node": "Mock organization events",
"type": "main",
"index": 0
}
]
]
},
"Mock public received events": {
"main": [
[
{
"node": "Merge",
"type": "main",
"index": 0
}
]
]
},
"Mock repository events": {
"main": [
[
{
"node": "Merge",
"type": "main",
"index": 1
}
]
]
},
"Mock organization events": {
"main": [
[
{
"node": "Merge",
"type": "main",
"index": 2
}
]
]
},
"Merge": {
"main": [
[
{
"node": "Select statusCode",
"type": "main",
"index": 0
}
]
]
},
"Select statusCode": {
"main": [
[
{
"node": "Respond to Webhook",
"type": "main",
"index": 0
}
]
]
}
},
"settings": { "executionOrder": "v1" },
"staticData": null,
"meta": null,
"pinData": {
"Webhook": [
{
"json": {
"headers": { "host": "localhost:5678", "user-agent": "curl/8.6.0", "accept": "*/*" },
"params": {},
"query": {},
"body": {},
"webhookUrl": "http://localhost:5678/webhook-test/benchmark-http-node",
"executionMode": "test"
}
}
]
},
"versionId": "9fa91e54-e73a-4a34-b781-d64f2b02f333",
"triggerCount": 0,
"tags": []
}

View file

@ -0,0 +1,7 @@
{
"$schema": "../scenario.schema.json",
"name": "HttpNode",
"description": "Webhook -> 3x HTTP request to a mock API -> Merge -> Respond to Webhook. Requires a mock API running at http://mockapi:8080",
"scenarioData": { "workflowFiles": ["http-node.json"] },
"scriptPath": "http-node.script.js"
}

View file

@ -0,0 +1,24 @@
import http from 'k6/http';
import { check } from 'k6';
const apiBaseUrl = __ENV.API_BASE_URL;
export default function () {
const res = http.post(`${apiBaseUrl}/webhook/benchmark-http-node`);
check(res, {
'is status 200': (r) => r.status === 200,
'http requests were OK': (r) => {
if (r.status !== 200) return false;
try {
// Response body is an array of the request status codes made with HttpNodes
const body = JSON.parse(r.body);
return Array.isArray(body) ? body.every((request) => request.statusCode === 200) : false;
} catch (error) {
console.error('Error parsing response body: ', error);
return false;
}
},
});
}

View file

@ -0,0 +1,97 @@
{
"createdAt": "2024-08-06T12:19:51.268Z",
"updatedAt": "2024-08-06T12:20:45.000Z",
"name": "JS Code Node Once For Each",
"active": true,
"nodes": [
{
"parameters": {
"httpMethod": "POST",
"path": "code-node-benchmark",
"responseMode": "responseNode",
"options": {}
},
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [0, 0],
"id": "849350b3-4212-4416-a462-1cf331157d37",
"name": "Webhook",
"webhookId": "34ca1895-ccf4-4a4a-8bb8-a042f5edb567"
},
{
"parameters": {
"respondWith": "allIncomingItems",
"options": {}
},
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"position": [660, 0],
"id": "f0660aa1-8a65-490f-b5cd-f8d134070c13",
"name": "Respond to Webhook"
},
{
"parameters": {
"category": "randomData",
"randomDataCount": 5
},
"type": "n8n-nodes-base.debugHelper",
"typeVersion": 1,
"position": [220, 0],
"id": "50f1efe8-bd2d-4061-9f51-b38c0e3daeb2",
"name": "DebugHelper"
},
{
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "// Add new field\n$input.item.json.age = 10 + Math.floor(Math.random() * 30);\n// Mutate existing field\n$input.item.json.password = $input.item.json.password.split('').map(() => '*').join(\"\")\n// Remove field\ndelete $input.item.json.lastname\n// New object field\nconst emailParts = $input.item.json.email.split(\"@\")\n$input.item.json.emailData = {\n user: emailParts[0],\n domain: emailParts[1]\n}\n\nreturn $input.item;"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [440, 0],
"id": "f9f2f865-e228-403d-8e47-72308359e207",
"name": "OnceForEachItemJSCode"
}
],
"connections": {
"Webhook": {
"main": [
[
{
"node": "DebugHelper",
"type": "main",
"index": 0
}
]
]
},
"DebugHelper": {
"main": [
[
{
"node": "OnceForEachItemJSCode",
"type": "main",
"index": 0
}
]
]
},
"OnceForEachItemJSCode": {
"main": [
[
{
"node": "Respond to Webhook",
"type": "main",
"index": 0
}
]
]
}
},
"settings": { "executionOrder": "v1" },
"staticData": null,
"meta": { "templateCredsSetupCompleted": true, "responseMode": "lastNode", "options": {} },
"pinData": {},
"versionId": "840a38a1-ba37-433d-9f20-de73f5131a2b",
"triggerCount": 1,
"tags": []
}

View file

@ -0,0 +1,7 @@
{
"$schema": "../scenario.schema.json",
"name": "CodeNodeJsOnceForEach",
"description": "A JS Code Node that runs once for each item and adds, modifies and removes properties. The data of 5 items is generated using DebugHelper Node, and returned with RespondToWebhook Node.",
"scenarioData": { "workflowFiles": ["js-code-node-once-for-each.json"] },
"scriptPath": "js-code-node-once-for-each.script.js"
}

View file

@ -0,0 +1,22 @@
import http from 'k6/http';
import { check } from 'k6';
const apiBaseUrl = __ENV.API_BASE_URL;
export default function () {
const res = http.post(`${apiBaseUrl}/webhook/code-node-benchmark`, {});
check(res, {
'is status 200': (r) => r.status === 200,
'has items in response': (r) => {
if (r.status !== 200) return false;
try {
const body = JSON.parse(r.body);
return Array.isArray(body) ? body.length === 5 : false;
} catch (error) {
console.error('Error parsing response body: ', error);
return false;
}
},
});
}

View file

@ -0,0 +1,42 @@
{
"definitions": {
"ScenarioData": {
"type": "object",
"properties": {
"workflowFiles": {
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [],
"additionalProperties": false
}
},
"type": "object",
"properties": {
"$schema": {
"type": "string",
"description": "The JSON schema to validate this file"
},
"name": {
"type": "string",
"description": "The name of the scenario"
},
"description": {
"type": "string",
"description": "A longer description of the scenario"
},
"scriptPath": {
"type": "string",
"description": "Relative path to the k6 test script"
},
"scenarioData": {
"$ref": "#/definitions/ScenarioData",
"description": "Data to import before running the scenario"
}
},
"required": ["name", "description", "scriptPath", "scenarioData"],
"additionalProperties": false
}

View file

@ -0,0 +1,91 @@
{
"createdAt": "2024-09-03T11:30:26.333Z",
"updatedAt": "2024-09-03T11:42:52.000Z",
"name": "Set Node Expressions",
"active": false,
"nodes": [
{
"parameters": {
"httpMethod": "POST",
"path": "set-expressions-benchmark",
"responseMode": "responseNode",
"options": {}
},
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [40, 0],
"id": "5babc228-2b89-48cb-8337-28416e867874",
"name": "Webhook",
"webhookId": "f6f1750d-b734-496f-afe8-26e8e393ca87"
},
{
"parameters": { "respondWith": "allIncomingItems", "options": {} },
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"position": [640, 0],
"id": "4146a3fb-403c-4cfc-9d38-8af4d16a8440",
"name": "Respond to Webhook"
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "48c46098-f411-41f7-8f0a-1da372340a4e",
"name": "oneToOneCopy",
"value": "={{ $json.headers.host }}",
"type": "string"
},
{
"id": "5d90808b-1c1a-4065-ac51-6d61bd03e564",
"name": "={{ $json.headers['user-agent'].slice(0, 4) }}",
"value": "Set key with expression",
"type": "string"
},
{
"id": "8a74ac24-1f43-43ba-969d-87bfd2f401ce",
"name": "Multiple variables",
"value": "={{ $json.executionMode + ' ' + $json.webhookUrl }}",
"type": "string"
},
{
"id": "93eba201-79d9-4305-a246-f9c8ec50ebab",
"name": "Static value",
"value": 42,
"type": "number"
},
{
"id": "0470a712-c795-44ab-9dcc-05a3f67698bb",
"name": "Object",
"value": "={{ $json.headers }}",
"type": "object"
},
{
"id": "eb671167-da14-4b55-8eea-31ab7bedae10",
"name": "Array",
"value": "={{ Object.values($json.headers) }}",
"type": "array"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [360, 0],
"id": "0cb5e82d-f61e-4d91-8fa9-365e382a4d75",
"name": "Edit Fields"
}
],
"connections": {
"Webhook": { "main": [[{ "node": "Edit Fields", "type": "main", "index": 0 }]] },
"Edit Fields": { "main": [[{ "node": "Respond to Webhook", "type": "main", "index": 0 }]] }
},
"settings": { "executionOrder": "v1" },
"staticData": null,
"meta": null,
"pinData": {},
"versionId": "04fd543e-3923-4092-8c2b-2b4262ccbb38",
"triggerCount": 0,
"tags": []
}

View file

@ -0,0 +1,7 @@
{
"$schema": "../scenario.schema.json",
"name": "SetNodeExpressions",
"description": "Expressions in a Set node",
"scenarioData": { "workflowFiles": ["set-node-expressions.json"] },
"scriptPath": "set-node-expressions.script.js"
}

View file

@ -0,0 +1,11 @@
import http from 'k6/http';
import { check } from 'k6';
const apiBaseUrl = __ENV.API_BASE_URL;
export default function () {
const res = http.post(`${apiBaseUrl}/webhook/set-expressions-benchmark`, {});
check(res, {
'is status 200': (r) => r.status === 200,
});
}

View file

@ -0,0 +1,25 @@
{
"createdAt": "2024-08-06T12:19:51.268Z",
"updatedAt": "2024-08-06T12:20:45.000Z",
"name": "Single Webhook",
"active": true,
"nodes": [
{
"parameters": { "path": "single-webhook", "options": {} },
"id": "7587ab0e-cc15-424f-83c0-c887a0eb97fb",
"name": "Webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [760, 400],
"webhookId": "fa563fc2-c73f-4631-99a1-39c16f1f858f"
}
],
"connections": {},
"settings": { "executionOrder": "v1" },
"staticData": null,
"meta": { "templateCredsSetupCompleted": true, "responseMode": "lastNode", "options": {} },
"pinData": {},
"versionId": "840a38a1-ba37-433d-9f20-de73f5131a2b",
"triggerCount": 1,
"tags": []
}

View file

@ -0,0 +1,7 @@
{
"$schema": "../scenario.schema.json",
"name": "SingleWebhook",
"description": "A single webhook trigger that responds with a 200 status code",
"scenarioData": { "workflowFiles": ["single-webhook.json"] },
"scriptPath": "single-webhook.script.ts"
}

View file

@ -0,0 +1,11 @@
import http from 'k6/http';
import { check } from 'k6';
const apiBaseUrl = __ENV.API_BASE_URL;
export default function () {
const res = http.get(`${apiBaseUrl}/webhook/single-webhook`);
check(res, {
'is status 200': (r) => r.status === 200,
});
}

View file

@ -0,0 +1,52 @@
#!/bin/bash
#
# Script to initialize the benchmark environment on a VM
#
set -euo pipefail;
CURRENT_USER=$(whoami)
# Mount the data disk
# First wait for the disk to become available
WAIT_TIME=0
MAX_WAIT_TIME=60
while [ ! -e /dev/sdc ]; do
if [ $WAIT_TIME -ge $MAX_WAIT_TIME ]; then
echo "Error: /dev/sdc did not become available within $MAX_WAIT_TIME seconds."
exit 1
fi
echo "Waiting for /dev/sdc to be available... ($WAIT_TIME/$MAX_WAIT_TIME)"
sleep 1
WAIT_TIME=$((WAIT_TIME + 1))
done
# Then mount it
if [ -d "/n8n" ]; then
echo "Data disk already mounted. Clearing it..."
sudo rm -rf /n8n/*
sudo rm -rf /n8n/.[!.]*
else
sudo mkdir -p /n8n
sudo parted /dev/sdc --script mklabel gpt mkpart xfspart xfs 0% 100%
sudo mkfs.xfs /dev/sdc1
sudo partprobe /dev/sdc1
sudo mount /dev/sdc1 /n8n
sudo chown -R "$CURRENT_USER":"$CURRENT_USER" /n8n
fi
# Include nodejs v20 repository
curl -fsSL https://deb.nodesource.com/setup_20.x -o nodesource_setup.sh
sudo -E bash nodesource_setup.sh
# Install docker, docker compose and nodejs
sudo DEBIAN_FRONTEND=noninteractive apt-get update -yq
sudo DEBIAN_FRONTEND=noninteractive apt-get install -yq docker.io docker-compose nodejs
# Add the current user to the docker group
sudo usermod -aG docker "$CURRENT_USER"
# Install zx
npm install zx

View file

@ -0,0 +1,45 @@
import { which } from 'zx';
export class DockerComposeClient {
/**
*
* @param {{ $: Shell; verbose?: boolean }} opts
*/
constructor({ $ }) {
this.$$ = $;
}
async $(...args) {
await this.resolveExecutableIfNeeded();
if (this.isCompose) {
return await this.$$`docker-compose ${args}`;
} else {
return await this.$$`docker compose ${args}`;
}
}
async resolveExecutableIfNeeded() {
if (this.isResolved) {
return;
}
// The VM deployment doesn't have `docker compose` available,
// so try to resolve the `docker-compose` first
const compose = await which('docker-compose', { nothrow: true });
if (compose) {
this.isResolved = true;
this.isCompose = true;
return;
}
const docker = await which('docker', { nothrow: true });
if (docker) {
this.isResolved = true;
this.isCompose = false;
return;
}
throw new Error('Could not resolve docker-compose or docker');
}
}

View file

@ -0,0 +1,37 @@
// @ts-check
import { $ } from 'zx';
export class SshClient {
/**
*
* @param {{ privateKeyPath: string; ip: string; username: string; verbose?: boolean }} param0
*/
constructor({ privateKeyPath, ip, username, verbose = false }) {
this.verbose = verbose;
this.privateKeyPath = privateKeyPath;
this.ip = ip;
this.username = username;
this.$$ = $({
verbose,
});
}
/**
* @param {string} command
* @param {{ verbose?: boolean }} [options]
*/
async ssh(command, options = {}) {
const $$ = options?.verbose ? $({ verbose: true }) : this.$$;
const target = `${this.username}@${this.ip}`;
await $$`ssh -i ${this.privateKeyPath} -o StrictHostKeyChecking=accept-new ${target} ${command}`;
}
async scp(source, destination) {
const target = `${this.username}@${this.ip}:${destination}`;
await this
.$$`scp -i ${this.privateKeyPath} -o StrictHostKeyChecking=accept-new ${source} ${target}`;
}
}

View file

@ -0,0 +1,71 @@
// @ts-check
import path from 'path';
import { $, fs } from 'zx';
const paths = {
infraCodeDir: path.resolve('infra'),
terraformStateFile: path.join(path.resolve('infra'), 'terraform.tfstate'),
};
export class TerraformClient {
constructor({ isVerbose = false }) {
this.isVerbose = isVerbose;
this.$$ = $({
cwd: paths.infraCodeDir,
verbose: isVerbose,
});
}
/**
* Provisions the environment
*/
async provisionEnvironment() {
console.log('Provisioning cloud environment...');
await this.$$`terraform init`;
await this.$$`terraform apply -input=false -auto-approve`;
}
/**
* @typedef {Object} BenchmarkEnv
* @property {string} vmName
* @property {string} ip
* @property {string} sshUsername
* @property {string} sshPrivateKeyPath
*
* @returns {Promise<BenchmarkEnv>}
*/
async getTerraformOutputs() {
const privateKeyName = await this.extractPrivateKey();
return {
ip: await this.getTerraformOutput('ip'),
sshUsername: await this.getTerraformOutput('ssh_username'),
sshPrivateKeyPath: path.join(paths.infraCodeDir, privateKeyName),
vmName: await this.getTerraformOutput('vm_name'),
};
}
hasTerraformState() {
return fs.existsSync(paths.terraformStateFile);
}
async destroyEnvironment() {
console.log('Destroying cloud environment...');
await this.$$`terraform destroy -input=false -auto-approve`;
}
async getTerraformOutput(key) {
const output = await this.$$`terraform output -raw ${key}`;
return output.stdout.trim();
}
async extractPrivateKey() {
await this.$$`terraform output -raw ssh_private_key > privatekey.pem`;
await this.$$`chmod 600 privatekey.pem`;
return 'privatekey.pem';
}
}

View file

@ -0,0 +1,86 @@
#!/usr/bin/env zx
/**
* Script that deletes all resources created by the benchmark environment.
*
* This scripts tries to delete resources created by Terraform. If Terraform
* state file is not found, it will try to delete resources using Azure CLI.
* The terraform state is not persisted, so we want to support both cases.
*/
// @ts-check
import { $, minimist } from 'zx';
import { TerraformClient } from './clients/terraform-client.mjs';
const RESOURCE_GROUP_NAME = 'n8n-benchmarking';
const args = minimist(process.argv.slice(3), {
boolean: ['debug'],
});
const isVerbose = !!args.debug;
async function main() {
const terraformClient = new TerraformClient({ isVerbose });
if (terraformClient.hasTerraformState()) {
await terraformClient.destroyEnvironment();
} else {
await destroyUsingAz();
}
}
async function destroyUsingAz() {
const resourcesResult =
await $`az resource list --resource-group ${RESOURCE_GROUP_NAME} --query "[?tags.Id == 'N8nBenchmark'].{id:id, createdAt:tags.CreatedAt}" -o json`;
const resources = JSON.parse(resourcesResult.stdout);
const resourcesToDelete = resources.map((resource) => resource.id);
if (resourcesToDelete.length === 0) {
console.log('No resources found in the resource group.');
return;
}
await deleteResources(resourcesToDelete);
}
async function deleteResources(resourceIds) {
// We don't know the order in which resource should be deleted.
// Here's a poor person's approach to try deletion until all complete
const MAX_ITERATIONS = 100;
let i = 0;
const toDelete = [...resourceIds];
console.log(`Deleting ${resourceIds.length} resources...`);
while (toDelete.length > 0) {
const resourceId = toDelete.shift();
const deleted = await deleteById(resourceId);
if (!deleted) {
toDelete.push(resourceId);
}
if (i++ > MAX_ITERATIONS) {
console.log(
`Max iterations reached. Exiting. Could not delete ${toDelete.length} resources.`,
);
process.exit(1);
}
}
}
async function deleteById(id) {
try {
await $`az resource delete --ids ${id}`;
return true;
} catch (error) {
return false;
}
}
main().catch((error) => {
console.error('An error occurred destroying cloud env:');
console.error(error);
process.exit(1);
});

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,59 @@
services:
mockapi:
image: wiremock/wiremock:3.9.1
ports:
- '8088:8080'
volumes:
- ${MOCK_API_DATA_PATH}/mappings:/home/wiremock/mappings
postgres:
image: postgres:16
restart: always
user: root:root
environment:
- POSTGRES_DB=n8n
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=password
- PGDATA=/var/lib/postgresql/data/pgdata
volumes:
- ${RUN_DIR}/postgres:/var/lib/postgresql/data
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U postgres']
interval: 5s
timeout: 5s
retries: 5
n8n:
image: ghcr.io/n8n-io/n8n:${N8N_VERSION:-latest}
user: root:root
environment:
- N8N_DIAGNOSTICS_ENABLED=false
- N8N_USER_FOLDER=/n8n
- DB_TYPE=postgresdb
- DB_POSTGRESDB_HOST=postgres
- DB_POSTGRESDB_PASSWORD=password
ports:
- 5678:5678
volumes:
- ${RUN_DIR}/n8n:/n8n
depends_on:
postgres:
condition: service_healthy
mockapi:
condition: service_started
healthcheck:
test: ['CMD-SHELL', 'wget --spider -q http://n8n:5678/healthz || exit 1']
interval: 5s
timeout: 5s
retries: 10
benchmark:
image: ghcr.io/n8n-io/n8n-benchmark:${N8N_BENCHMARK_VERSION:-latest}
depends_on:
n8n:
condition: service_healthy
environment:
- N8N_BASE_URL=http://n8n:5678
- K6_API_TOKEN=${K6_API_TOKEN}
- BENCHMARK_RESULT_WEBHOOK_URL=${BENCHMARK_RESULT_WEBHOOK_URL}
- BENCHMARK_RESULT_WEBHOOK_AUTH_HEADER=${BENCHMARK_RESULT_WEBHOOK_AUTH_HEADER}

View file

@ -0,0 +1,15 @@
#!/usr/bin/env zx
import path from 'path';
import { fs } from 'zx';
/**
* Creates the needed directories so the permissions get set correctly.
*/
export function setup({ runDir }) {
const neededDirs = ['n8n', 'postgres'];
for (const dir of neededDirs) {
fs.ensureDirSync(path.join(runDir, dir));
}
}

View file

@ -0,0 +1,196 @@
services:
mockapi:
image: wiremock/wiremock:3.9.1
ports:
- '8088:8080'
volumes:
- ${MOCK_API_DATA_PATH}/mappings:/home/wiremock/mappings
redis:
image: redis:6-alpine
restart: always
ports:
- 6379:6379
healthcheck:
test: ['CMD', 'redis-cli', 'ping']
interval: 1s
timeout: 3s
postgres:
image: postgres:16
restart: always
environment:
- POSTGRES_DB=n8n
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=password
- PGDATA=/var/lib/postgresql/data/pgdata
volumes:
- ${RUN_DIR}/postgres:/var/lib/postgresql/data
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U postgres']
interval: 5s
timeout: 5s
retries: 10
n8n_worker1:
image: ghcr.io/n8n-io/n8n:${N8N_VERSION:-latest}
environment:
- N8N_DIAGNOSTICS_ENABLED=false
- N8N_USER_FOLDER=/n8n/worker1
- N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}
- N8N_LICENSE_CERT=${N8N_LICENSE_CERT}
- N8N_LICENSE_ACTIVATION_KEY=${N8N_LICENSE_ACTIVATION_KEY}
- N8N_LICENSE_TENANT_ID=${N8N_LICENSE_TENANT_ID}
# Scaling mode config
- EXECUTIONS_MODE=queue
- QUEUE_BULL_REDIS_HOST=redis
- QUEUE_HEALTH_CHECK_ACTIVE=true
- N8N_CONCURRENCY_PRODUCTION_LIMIT=10
# DB config
- DB_TYPE=postgresdb
- DB_POSTGRESDB_HOST=postgres
- DB_POSTGRESDB_PASSWORD=password
command: worker
volumes:
- ${RUN_DIR}/n8n-worker1:/n8n
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
healthcheck:
test: ['CMD-SHELL', 'wget --spider -q http://localhost:5678/healthz || exit 1']
interval: 5s
timeout: 5s
retries: 10
n8n_worker2:
image: ghcr.io/n8n-io/n8n:${N8N_VERSION:-latest}
environment:
- N8N_DIAGNOSTICS_ENABLED=false
- N8N_USER_FOLDER=/n8n/worker2
- N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}
- N8N_LICENSE_CERT=${N8N_LICENSE_CERT}
- N8N_LICENSE_ACTIVATION_KEY=${N8N_LICENSE_ACTIVATION_KEY}
- N8N_LICENSE_TENANT_ID=${N8N_LICENSE_TENANT_ID}
# Scaling mode config
- EXECUTIONS_MODE=queue
- QUEUE_BULL_REDIS_HOST=redis
- QUEUE_HEALTH_CHECK_ACTIVE=true
- N8N_CONCURRENCY_PRODUCTION_LIMIT=10
# DB config
- DB_TYPE=postgresdb
- DB_POSTGRESDB_HOST=postgres
- DB_POSTGRESDB_PASSWORD=password
command: worker
volumes:
- ${RUN_DIR}/n8n-worker2:/n8n
depends_on:
# We let the worker 1 start first so it can run the DB migrations
n8n_worker1:
condition: service_healthy
postgres:
condition: service_healthy
redis:
condition: service_healthy
healthcheck:
test: ['CMD-SHELL', 'wget --spider -q http://localhost:5678/healthz || exit 1']
interval: 5s
timeout: 5s
retries: 10
n8n_main2:
image: ghcr.io/n8n-io/n8n:${N8N_VERSION:-latest}
environment:
- N8N_DIAGNOSTICS_ENABLED=false
- N8N_USER_FOLDER=/n8n
- N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}
- N8N_LICENSE_CERT=${N8N_LICENSE_CERT}
- N8N_LICENSE_ACTIVATION_KEY=${N8N_LICENSE_ACTIVATION_KEY}
- N8N_LICENSE_TENANT_ID=${N8N_LICENSE_TENANT_ID}
# Scaling mode config
- EXECUTIONS_MODE=queue
- QUEUE_BULL_REDIS_HOST=redis
- N8N_MULTI_MAIN_SETUP_ENABLED=true
# DB config
- DB_TYPE=postgresdb
- DB_POSTGRESDB_HOST=postgres
- DB_POSTGRESDB_PASSWORD=password
volumes:
- ${RUN_DIR}/n8n-main2:/n8n
depends_on:
n8n_worker1:
condition: service_healthy
n8n_worker2:
condition: service_healthy
postgres:
condition: service_healthy
redis:
condition: service_healthy
mockapi:
condition: service_started
healthcheck:
test: ['CMD-SHELL', 'wget --spider -q http://n8n_main2:5678/healthz || exit 1']
interval: 5s
timeout: 5s
retries: 10
n8n_main1:
image: ghcr.io/n8n-io/n8n:${N8N_VERSION:-latest}
environment:
- N8N_DIAGNOSTICS_ENABLED=false
- N8N_USER_FOLDER=/n8n
- N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}
- N8N_LICENSE_CERT=${N8N_LICENSE_CERT}
- N8N_LICENSE_ACTIVATION_KEY=${N8N_LICENSE_ACTIVATION_KEY}
- N8N_LICENSE_TENANT_ID=${N8N_LICENSE_TENANT_ID}
# Scaling mode config
- EXECUTIONS_MODE=queue
- QUEUE_BULL_REDIS_HOST=redis
- N8N_MULTI_MAIN_SETUP_ENABLED=true
# DB config
- DB_TYPE=postgresdb
- DB_POSTGRESDB_HOST=postgres
- DB_POSTGRESDB_PASSWORD=password
volumes:
- ${RUN_DIR}/n8n-main1:/n8n
depends_on:
n8n_worker1:
condition: service_healthy
n8n_worker2:
condition: service_healthy
postgres:
condition: service_healthy
redis:
condition: service_healthy
mockapi:
condition: service_started
healthcheck:
test: ['CMD-SHELL', 'wget --spider -q http://n8n_main1:5678/healthz || exit 1']
interval: 5s
timeout: 5s
retries: 10
# Load balancer that acts as an entry point for n8n
n8n:
image: nginx:latest
ports:
- '5678:80'
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
depends_on:
n8n_main1:
condition: service_healthy
n8n_main2:
condition: service_healthy
benchmark:
image: ghcr.io/n8n-io/n8n-benchmark:${N8N_BENCHMARK_VERSION:-latest}
depends_on:
- n8n
environment:
- N8N_BASE_URL=http://n8n:80
- K6_API_TOKEN=${K6_API_TOKEN}
- BENCHMARK_RESULT_WEBHOOK_URL=${BENCHMARK_RESULT_WEBHOOK_URL}
- BENCHMARK_RESULT_WEBHOOK_AUTH_HEADER=${BENCHMARK_RESULT_WEBHOOK_AUTH_HEADER}

View file

@ -0,0 +1,20 @@
events {}
http {
upstream backend {
server n8n_main1:5678;
server n8n_main2:5678;
}
server {
listen 80;
location / {
proxy_pass http://backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
}

View file

@ -0,0 +1,15 @@
#!/usr/bin/env zx
import path from 'path';
import { fs } from 'zx';
/**
* Creates the needed directories so the permissions get set correctly.
*/
export function setup({ runDir }) {
const neededDirs = ['n8n-worker1', 'n8n-worker2', 'n8n-main1', 'n8n-main2', 'postgres'];
for (const dir of neededDirs) {
fs.ensureDirSync(path.join(runDir, dir));
}
}

View file

@ -0,0 +1,142 @@
services:
mockapi:
image: wiremock/wiremock:3.9.1
ports:
- '8088:8080'
volumes:
- ${MOCK_API_DATA_PATH}/mappings:/home/wiremock/mappings
redis:
image: redis:6-alpine
ports:
- 6379:6379
healthcheck:
test: ['CMD', 'redis-cli', 'ping']
interval: 1s
timeout: 3s
postgres:
image: postgres:16
user: root:root
restart: always
environment:
- POSTGRES_DB=n8n
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=password
- PGDATA=/var/lib/postgresql/data/pgdata
volumes:
- ${RUN_DIR}/postgres:/var/lib/postgresql/data
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U postgres']
interval: 5s
timeout: 5s
retries: 10
n8n_worker1:
image: ghcr.io/n8n-io/n8n:${N8N_VERSION:-latest}
user: root:root
environment:
- N8N_DIAGNOSTICS_ENABLED=false
- N8N_USER_FOLDER=/n8n/worker1
- N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}
# Queue mode config
- EXECUTIONS_MODE=queue
- QUEUE_BULL_REDIS_HOST=redis
- QUEUE_HEALTH_CHECK_ACTIVE=true
- N8N_CONCURRENCY_PRODUCTION_LIMIT=10
# DB config
- DB_TYPE=postgresdb
- DB_POSTGRESDB_HOST=postgres
- DB_POSTGRESDB_PASSWORD=password
command: worker
volumes:
- ${RUN_DIR}/n8n-worker1:/n8n
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
healthcheck:
test: ['CMD-SHELL', 'wget --spider -q http://n8n_worker1:5678/healthz || exit 1']
interval: 5s
timeout: 5s
retries: 10
n8n_worker2:
image: ghcr.io/n8n-io/n8n:${N8N_VERSION:-latest}
user: root:root
environment:
- N8N_DIAGNOSTICS_ENABLED=false
- N8N_USER_FOLDER=/n8n/worker2
- N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}
# Queue mode config
- EXECUTIONS_MODE=queue
- QUEUE_BULL_REDIS_HOST=redis
- QUEUE_HEALTH_CHECK_ACTIVE=true
- N8N_CONCURRENCY_PRODUCTION_LIMIT=10
# DB config
- DB_TYPE=postgresdb
- DB_POSTGRESDB_HOST=postgres
- DB_POSTGRESDB_PASSWORD=password
command: worker
volumes:
- ${RUN_DIR}/n8n-worker2:/n8n
depends_on:
# We let the worker 1 start first so it can run the DB migrations
n8n_worker1:
condition: service_healthy
postgres:
condition: service_healthy
redis:
condition: service_healthy
healthcheck:
test: ['CMD-SHELL', 'wget --spider -q http://n8n_worker2:5678/healthz || exit 1']
interval: 5s
timeout: 5s
retries: 10
n8n:
image: ghcr.io/n8n-io/n8n:${N8N_VERSION:-latest}
user: root:root
environment:
- N8N_DIAGNOSTICS_ENABLED=false
- N8N_USER_FOLDER=/n8n/main
- N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}
# Queue mode config
- EXECUTIONS_MODE=queue
- QUEUE_BULL_REDIS_HOST=redis
# DB config
- DB_TYPE=postgresdb
- DB_POSTGRESDB_HOST=postgres
- DB_POSTGRESDB_PASSWORD=password
ports:
- 5678:5678
volumes:
- ${RUN_DIR}/n8n-main:/n8n
depends_on:
n8n_worker1:
condition: service_healthy
n8n_worker2:
condition: service_healthy
postgres:
condition: service_healthy
redis:
condition: service_healthy
mockapi:
condition: service_started
healthcheck:
test: ['CMD-SHELL', 'wget --spider -q http://n8n:5678/healthz || exit 1']
interval: 5s
timeout: 5s
retries: 10
benchmark:
image: ghcr.io/n8n-io/n8n-benchmark:${N8N_BENCHMARK_VERSION:-latest}
depends_on:
n8n:
condition: service_healthy
environment:
- N8N_BASE_URL=http://n8n:5678
- K6_API_TOKEN=${K6_API_TOKEN}
- BENCHMARK_RESULT_WEBHOOK_URL=${BENCHMARK_RESULT_WEBHOOK_URL}
- BENCHMARK_RESULT_WEBHOOK_AUTH_HEADER=${BENCHMARK_RESULT_WEBHOOK_AUTH_HEADER}

View file

@ -0,0 +1,15 @@
#!/usr/bin/env zx
import path from 'path';
import { fs } from 'zx';
/**
* Creates the needed directories so the permissions get set correctly.
*/
export function setup({ runDir }) {
const neededDirs = ['n8n-worker1', 'n8n-worker2', 'n8n-main', 'postgres'];
for (const dir of neededDirs) {
fs.ensureDirSync(path.join(runDir, dir));
}
}

View file

@ -0,0 +1,37 @@
services:
mockapi:
image: wiremock/wiremock:3.9.1
ports:
- '8088:8080'
volumes:
- ${MOCK_API_DATA_PATH}/mappings:/home/wiremock/mappings
n8n:
image: ghcr.io/n8n-io/n8n:${N8N_VERSION:-latest}
user: root:root
environment:
- N8N_DIAGNOSTICS_ENABLED=false
- N8N_USER_FOLDER=/n8n
ports:
- 5678:5678
volumes:
- ${RUN_DIR}:/n8n
healthcheck:
test: ['CMD-SHELL', 'wget --spider -q http://n8n:5678/healthz || exit 1']
interval: 5s
timeout: 5s
retries: 10
depends_on:
mockapi:
condition: service_started
benchmark:
image: ghcr.io/n8n-io/n8n-benchmark:${N8N_BENCHMARK_VERSION:-latest}
depends_on:
n8n:
condition: service_healthy
environment:
- N8N_BASE_URL=http://n8n:5678
- K6_API_TOKEN=${K6_API_TOKEN}
- BENCHMARK_RESULT_WEBHOOK_URL=${BENCHMARK_RESULT_WEBHOOK_URL}
- BENCHMARK_RESULT_WEBHOOK_AUTH_HEADER=${BENCHMARK_RESULT_WEBHOOK_AUTH_HEADER}

Some files were not shown because too many files have changed in this diff Show more