mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
Merge branch 'master' into node-1466-overhaul-code-node-p0
This commit is contained in:
commit
09a4673f46
72
.github/pull_request_title_conventions.md
vendored
72
.github/pull_request_title_conventions.md
vendored
|
@ -4,75 +4,79 @@ 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)
|
||||||
- blank line
|
- blank line
|
||||||
- footer (optional)
|
- footer (optional)
|
||||||
|
|
||||||
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.
|
||||||
- mattermost → Mattermost Node
|
- mattermost → Mattermost Node
|
||||||
- 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>
|
||||||
|
@ -95,18 +99,18 @@ Closes #<pr number>
|
||||||
A Breaking Change section should start with the phrase "`BREAKING CHANGE:` " followed by a summary of the breaking change, a blank line, and a detailed description of the breaking change that also includes migration instructions.
|
A Breaking Change section should start with the phrase "`BREAKING CHANGE:` " followed by a summary of the breaking change, a blank line, and a detailed description of the breaking change that also includes migration instructions.
|
||||||
|
|
||||||
> 💡 A breaking change can additionally also be marked by adding a “`!`” to the header, right before the “`:`”, e.g. `feat(editor)!: Remove support for dark mode`
|
> 💡 A breaking change can additionally also be marked by adding a “`!`” to the header, right before the “`:`”, e.g. `feat(editor)!: Remove support for dark mode`
|
||||||
>
|
>
|
||||||
> This makes locating breaking changes easier when just skimming through commit messages.
|
> This makes locating breaking changes easier when just skimming through commit messages.
|
||||||
|
|
||||||
> 💡 The breaking changes must also be added to the [packages/cli/BREAKING-CHANGES.md](https://github.com/n8n-io/n8n/blob/master/packages/cli/BREAKING-CHANGES.md) file located in the n8n repository.
|
> 💡 The breaking changes must also be added to the [packages/cli/BREAKING-CHANGES.md](https://github.com/n8n-io/n8n/blob/master/packages/cli/BREAKING-CHANGES.md) file located in the n8n repository.
|
||||||
|
|
||||||
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.
|
||||||
|
|
||||||
The content of the commit message body should contain:
|
The content of the commit message body should contain:
|
||||||
|
|
||||||
- information about the SHA of the commit being reverted in the following format: `This reverts commit <SHA>`,
|
- information about the SHA of the commit being reverted in the following format: `This reverts commit <SHA>`,
|
||||||
- a clear description of the reason for reverting the commit message.
|
- a clear description of the reason for reverting the commit message.
|
||||||
|
|
6
.github/scripts/update-changelog.mjs
vendored
6
.github/scripts/update-changelog.mjs
vendored
|
@ -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);
|
||||||
|
|
39
.github/workflows/benchmark-destroy-nightly.yml
vendored
Normal file
39
.github/workflows/benchmark-destroy-nightly.yml
vendored
Normal 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
96
.github/workflows/benchmark-nightly.yml
vendored
Normal 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
|
|
@ -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
|
||||||
|
|
||||||
|
|
5
.github/workflows/check-pr-title.yml
vendored
5
.github/workflows/check-pr-title.yml
vendored
|
@ -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 }}
|
||||||
|
|
2
.github/workflows/chromatic.yml
vendored
2
.github/workflows/chromatic.yml
vendored
|
@ -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/**
|
||||||
|
|
4
.github/workflows/ci-postgres-mysql.yml
vendored
4
.github/workflows/ci-postgres-mysql.yml
vendored
|
@ -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 }}
|
||||||
|
|
7
.github/workflows/ci-pull-requests.yml
vendored
7
.github/workflows/ci-pull-requests.yml
vendored
|
@ -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
|
||||||
|
|
43
.github/workflows/docker-images-benchmark.yml
vendored
Normal file
43
.github/workflows/docker-images-benchmark.yml
vendored
Normal 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
|
27
.github/workflows/docker-images-nightly.yml
vendored
27
.github/workflows/docker-images-nightly.yml
vendored
|
@ -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: |
|
||||||
|
|
48
.github/workflows/docker-images.yml
vendored
48
.github/workflows/docker-images.yml
vendored
|
@ -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 }}
|
|
11
.github/workflows/e2e-reusable.yml
vendored
11
.github/workflows/e2e-reusable.yml
vendored
|
@ -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
|
||||||
|
|
4
.github/workflows/e2e-tests-pr.yml
vendored
4
.github/workflows/e2e-tests-pr.yml
vendored
|
@ -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 }}
|
||||||
|
|
||||||
|
|
1
.github/workflows/linting-reusable.yml
vendored
1
.github/workflows/linting-reusable.yml
vendored
|
@ -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
|
||||||
|
|
6
.github/workflows/release-create-pr.yml
vendored
6
.github/workflows/release-create-pr.yml
vendored
|
@ -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'
|
||||||
|
|
109
.github/workflows/release-publish.yml
vendored
109
.github/workflows/release-publish.yml
vendored
|
@ -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}}
|
||||||
|
|
1
.github/workflows/units-tests-reusable.yml
vendored
1
.github/workflows/units-tests-reusable.yml
vendored
|
@ -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
|
||||||
|
|
1
.vscode/extensions.json
vendored
1
.vscode/extensions.json
vendored
|
@ -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"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
163
CHANGELOG.md
163
CHANGELOG.md
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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';
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -9,223 +9,252 @@ 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', () => {
|
||||||
beforeEach(() => {
|
describe('when workflow is saved', () => {
|
||||||
workflowPage.actions.visit();
|
beforeEach(() => {
|
||||||
cy.createFixtureWorkflow('Test_workflow_4_executions_view.json', 'My test workflow');
|
workflowPage.actions.visit();
|
||||||
});
|
cy.createFixtureWorkflow('Test_workflow_4_executions_view.json', 'My test workflow');
|
||||||
|
|
||||||
it('should render executions tab correctly', () => {
|
|
||||||
createMockExecutions();
|
|
||||||
cy.intercept('GET', '/rest/executions?filter=*').as('getExecutions');
|
|
||||||
|
|
||||||
executionsTab.actions.switchToExecutionsTab();
|
|
||||||
|
|
||||||
cy.wait(['@getExecutions']);
|
|
||||||
|
|
||||||
executionsTab.getters.executionsList().scrollTo(0, 500).wait(0);
|
|
||||||
|
|
||||||
executionsTab.getters.executionListItems().should('have.length', 11);
|
|
||||||
executionsTab.getters.successfulExecutionListItems().should('have.length', 9);
|
|
||||||
executionsTab.getters.failedExecutionListItems().should('have.length', 2);
|
|
||||||
executionsTab.getters
|
|
||||||
.executionListItems()
|
|
||||||
.first()
|
|
||||||
.invoke('attr', 'class')
|
|
||||||
.should('match', /_active_/);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not redirect back to execution tab when request is not done before leaving the page', () => {
|
|
||||||
cy.intercept('GET', '/rest/executions?filter=*');
|
|
||||||
cy.intercept('GET', '/rest/executions/active?filter=*');
|
|
||||||
|
|
||||||
executionsTab.actions.switchToExecutionsTab();
|
|
||||||
executionsTab.actions.switchToEditorTab();
|
|
||||||
cy.wait(executionsRefreshInterval);
|
|
||||||
cy.url().should('not.include', '/executions');
|
|
||||||
executionsTab.actions.switchToExecutionsTab();
|
|
||||||
executionsTab.actions.switchToEditorTab();
|
|
||||||
executionsTab.actions.switchToExecutionsTab();
|
|
||||||
executionsTab.actions.switchToEditorTab();
|
|
||||||
executionsTab.actions.switchToExecutionsTab();
|
|
||||||
executionsTab.actions.switchToEditorTab();
|
|
||||||
cy.wait(executionsRefreshInterval);
|
|
||||||
cy.url().should('not.include', '/executions');
|
|
||||||
executionsTab.actions.switchToExecutionsTab();
|
|
||||||
cy.wait(1000);
|
|
||||||
executionsTab.actions.switchToEditorTab();
|
|
||||||
cy.wait(executionsRefreshInterval);
|
|
||||||
cy.url().should('not.include', '/executions');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not redirect back to execution tab when slow request is not done before leaving the page', () => {
|
|
||||||
const throttleResponse: RouteHandler = async (req) => {
|
|
||||||
return await new Promise((resolve) => {
|
|
||||||
setTimeout(() => resolve(req.continue()), 2000);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
cy.intercept('GET', '/rest/executions?filter=*', throttleResponse);
|
|
||||||
cy.intercept('GET', '/rest/executions/active?filter=*', throttleResponse);
|
|
||||||
|
|
||||||
executionsTab.actions.switchToExecutionsTab();
|
|
||||||
executionsTab.actions.switchToEditorTab();
|
|
||||||
cy.wait(executionsRefreshInterval);
|
|
||||||
cy.url().should('not.include', '/executions');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should error toast when server error message returned without stack trace', () => {
|
|
||||||
executionsTab.actions.createManualExecutions(1);
|
|
||||||
const message = 'Workflow did not finish, possible out-of-memory issue';
|
|
||||||
cy.intercept('GET', '/rest/executions/*', {
|
|
||||||
statusCode: 200,
|
|
||||||
body: executionOutOfMemoryServerResponse,
|
|
||||||
}).as('getExecution');
|
|
||||||
|
|
||||||
executionsTab.actions.switchToExecutionsTab();
|
|
||||||
cy.wait(['@getExecution']);
|
|
||||||
|
|
||||||
executionsTab.getters
|
|
||||||
.workflowExecutionPreviewIframe()
|
|
||||||
.should('be.visible')
|
|
||||||
.its('0.contentDocument.body') // Access the body of the iframe document
|
|
||||||
.should('not.be.empty') // Ensure the body is not empty
|
|
||||||
|
|
||||||
.then(cy.wrap)
|
|
||||||
.find('.el-notification:has(.el-notification--error)')
|
|
||||||
.should('be.visible')
|
|
||||||
.filter(`:contains("${message}")`)
|
|
||||||
.should('be.visible');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should show workflow data in executions tab after hard reload and modify name and tags', () => {
|
|
||||||
executionsTab.actions.switchToExecutionsTab();
|
|
||||||
checkMainHeaderELements();
|
|
||||||
workflowPage.getters.saveButton().find('button').should('not.exist');
|
|
||||||
workflowPage.getters.tagPills().should('have.length', 2);
|
|
||||||
|
|
||||||
workflowPage.getters.workflowTags().click();
|
|
||||||
getVisibleSelect().find('li:contains("Manage tags")').click();
|
|
||||||
cy.get('button:contains("Add new")').click();
|
|
||||||
cy.getByTestId('tags-table').find('input').type('nutag').type('{enter}');
|
|
||||||
cy.get('button:contains("Done")').click();
|
|
||||||
|
|
||||||
cy.reload();
|
|
||||||
checkMainHeaderELements();
|
|
||||||
workflowPage.getters.saveButton().find('button').should('not.exist');
|
|
||||||
workflowPage.getters.workflowTags().click();
|
|
||||||
workflowPage.getters.tagsInDropdown().first().should('have.text', 'nutag').click();
|
|
||||||
workflowPage.getters.tagPills().should('have.length', 3);
|
|
||||||
|
|
||||||
let newWorkflowName = 'Renamed workflow';
|
|
||||||
workflowPage.actions.renameWorkflow(newWorkflowName);
|
|
||||||
workflowPage.getters.isWorkflowSaved();
|
|
||||||
workflowPage.getters
|
|
||||||
.workflowNameInputContainer()
|
|
||||||
.invoke('attr', 'title')
|
|
||||||
.should('eq', newWorkflowName);
|
|
||||||
|
|
||||||
executionsTab.actions.switchToEditorTab();
|
|
||||||
checkMainHeaderELements();
|
|
||||||
workflowPage.getters.saveButton().find('button').should('not.exist');
|
|
||||||
workflowPage.getters.tagPills().should('have.length', 3);
|
|
||||||
workflowPage.getters
|
|
||||||
.workflowNameInputContainer()
|
|
||||||
.invoke('attr', 'title')
|
|
||||||
.should('eq', newWorkflowName);
|
|
||||||
|
|
||||||
executionsTab.actions.switchToExecutionsTab();
|
|
||||||
checkMainHeaderELements();
|
|
||||||
workflowPage.getters.saveButton().find('button').should('not.exist');
|
|
||||||
workflowPage.getters.tagPills().should('have.length', 3);
|
|
||||||
workflowPage.getters
|
|
||||||
.workflowNameInputContainer()
|
|
||||||
.invoke('attr', 'title')
|
|
||||||
.should('eq', newWorkflowName);
|
|
||||||
|
|
||||||
executionsTab.actions.switchToEditorTab();
|
|
||||||
checkMainHeaderELements();
|
|
||||||
workflowPage.getters.saveButton().find('button').should('not.exist');
|
|
||||||
workflowPage.getters.tagPills().should('have.length', 3);
|
|
||||||
workflowPage.getters
|
|
||||||
.workflowNameInputContainer()
|
|
||||||
.invoke('attr', 'title')
|
|
||||||
.should('eq', newWorkflowName);
|
|
||||||
|
|
||||||
newWorkflowName = 'New workflow';
|
|
||||||
workflowPage.actions.renameWorkflow(newWorkflowName);
|
|
||||||
workflowPage.getters.isWorkflowSaved();
|
|
||||||
workflowPage.getters
|
|
||||||
.workflowNameInputContainer()
|
|
||||||
.invoke('attr', 'title')
|
|
||||||
.should('eq', newWorkflowName);
|
|
||||||
workflowPage.getters.workflowTags().click();
|
|
||||||
workflowPage.getters.tagsDropdown().find('.el-tag__close').first().click();
|
|
||||||
cy.get('body').click(0, 0);
|
|
||||||
workflowPage.getters.saveButton().find('button').should('not.exist');
|
|
||||||
workflowPage.getters.tagPills().should('have.length', 2);
|
|
||||||
|
|
||||||
executionsTab.actions.switchToExecutionsTab();
|
|
||||||
checkMainHeaderELements();
|
|
||||||
workflowPage.getters.saveButton().find('button').should('not.exist');
|
|
||||||
workflowPage.getters.tagPills().should('have.length', 2);
|
|
||||||
workflowPage.getters
|
|
||||||
.workflowNameInputContainer()
|
|
||||||
.invoke('attr', 'title')
|
|
||||||
.should('eq', newWorkflowName);
|
|
||||||
|
|
||||||
executionsTab.actions.switchToEditorTab();
|
|
||||||
checkMainHeaderELements();
|
|
||||||
workflowPage.getters.saveButton().find('button').should('not.exist');
|
|
||||||
workflowPage.getters.tagPills().should('have.length', 2);
|
|
||||||
workflowPage.getters
|
|
||||||
.workflowNameInputContainer()
|
|
||||||
.invoke('attr', 'title')
|
|
||||||
.should('eq', newWorkflowName);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should load items and auto scroll after filter change', () => {
|
|
||||||
createMockExecutions();
|
|
||||||
createMockExecutions();
|
|
||||||
cy.intercept('GET', '/rest/executions?filter=*').as('getExecutions');
|
|
||||||
|
|
||||||
executionsTab.actions.switchToExecutionsTab();
|
|
||||||
|
|
||||||
cy.wait(['@getExecutions']);
|
|
||||||
|
|
||||||
executionsTab.getters.executionsList().scrollTo(0, 500).wait(0);
|
|
||||||
|
|
||||||
executionsTab.getters.executionListItems().eq(10).click();
|
|
||||||
|
|
||||||
cy.getByTestId('executions-filter-button').click();
|
|
||||||
cy.getByTestId('executions-filter-status-select').should('be.visible').click();
|
|
||||||
getVisibleSelect().find('li:contains("Error")').click();
|
|
||||||
|
|
||||||
executionsTab.getters.executionListItems().should('have.length', 5);
|
|
||||||
executionsTab.getters.successfulExecutionListItems().should('have.length', 1);
|
|
||||||
executionsTab.getters.failedExecutionListItems().should('have.length', 4);
|
|
||||||
|
|
||||||
cy.getByTestId('executions-filter-button').click();
|
|
||||||
cy.getByTestId('executions-filter-status-select').should('be.visible').click();
|
|
||||||
getVisibleSelect().find('li:contains("Success")').click();
|
|
||||||
|
|
||||||
// check if the list is scrolled
|
|
||||||
executionsTab.getters.executionListItems().eq(10).should('be.visible');
|
|
||||||
executionsTab.getters.executionsList().then(($el) => {
|
|
||||||
const { scrollTop, scrollHeight, clientHeight } = $el[0];
|
|
||||||
expect(scrollTop).to.be.greaterThan(0);
|
|
||||||
expect(scrollTop + clientHeight).to.be.lessThan(scrollHeight);
|
|
||||||
|
|
||||||
// scroll to the bottom
|
|
||||||
$el[0].scrollTo(0, scrollHeight);
|
|
||||||
executionsTab.getters.executionListItems().should('have.length', 18);
|
|
||||||
executionsTab.getters.successfulExecutionListItems().should('have.length', 18);
|
|
||||||
executionsTab.getters.failedExecutionListItems().should('have.length', 0);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
cy.getByTestId('executions-filter-button').click();
|
it('should render executions tab correctly', () => {
|
||||||
cy.getByTestId('executions-filter-reset-button').should('be.visible').click();
|
createMockExecutions();
|
||||||
executionsTab.getters.executionListItems().eq(11).should('be.visible');
|
cy.intercept('GET', '/rest/executions?filter=*').as('getExecutions');
|
||||||
|
|
||||||
|
executionsTab.actions.switchToExecutionsTab();
|
||||||
|
|
||||||
|
cy.wait(['@getExecutions']);
|
||||||
|
|
||||||
|
executionsTab.getters.executionsList().scrollTo(0, 500).wait(0);
|
||||||
|
|
||||||
|
executionsTab.getters.executionListItems().should('have.length', 11);
|
||||||
|
executionsTab.getters.successfulExecutionListItems().should('have.length', 9);
|
||||||
|
executionsTab.getters.failedExecutionListItems().should('have.length', 2);
|
||||||
|
executionsTab.getters
|
||||||
|
.executionListItems()
|
||||||
|
.first()
|
||||||
|
.invoke('attr', 'class')
|
||||||
|
.should('match', /_active_/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not redirect back to execution tab when request is not done before leaving the page', () => {
|
||||||
|
cy.intercept('GET', '/rest/executions?filter=*');
|
||||||
|
cy.intercept('GET', '/rest/executions/active?filter=*');
|
||||||
|
|
||||||
|
executionsTab.actions.switchToExecutionsTab();
|
||||||
|
executionsTab.actions.switchToEditorTab();
|
||||||
|
cy.wait(executionsRefreshInterval);
|
||||||
|
cy.url().should('not.include', '/executions');
|
||||||
|
executionsTab.actions.switchToExecutionsTab();
|
||||||
|
executionsTab.actions.switchToEditorTab();
|
||||||
|
executionsTab.actions.switchToExecutionsTab();
|
||||||
|
executionsTab.actions.switchToEditorTab();
|
||||||
|
executionsTab.actions.switchToExecutionsTab();
|
||||||
|
executionsTab.actions.switchToEditorTab();
|
||||||
|
cy.wait(executionsRefreshInterval);
|
||||||
|
cy.url().should('not.include', '/executions');
|
||||||
|
executionsTab.actions.switchToExecutionsTab();
|
||||||
|
cy.wait(1000);
|
||||||
|
executionsTab.actions.switchToEditorTab();
|
||||||
|
cy.wait(executionsRefreshInterval);
|
||||||
|
cy.url().should('not.include', '/executions');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not redirect back to execution tab when slow request is not done before leaving the page', () => {
|
||||||
|
const throttleResponse: RouteHandler = async (req) => {
|
||||||
|
return await new Promise((resolve) => {
|
||||||
|
setTimeout(() => resolve(req.continue()), 2000);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
cy.intercept('GET', '/rest/executions?filter=*', throttleResponse);
|
||||||
|
cy.intercept('GET', '/rest/executions/active?filter=*', throttleResponse);
|
||||||
|
|
||||||
|
executionsTab.actions.switchToExecutionsTab();
|
||||||
|
executionsTab.actions.switchToEditorTab();
|
||||||
|
cy.wait(executionsRefreshInterval);
|
||||||
|
cy.url().should('not.include', '/executions');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should error toast when server error message returned without stack trace', () => {
|
||||||
|
executionsTab.actions.createManualExecutions(1);
|
||||||
|
const message = 'Workflow did not finish, possible out-of-memory issue';
|
||||||
|
cy.intercept('GET', '/rest/executions/*', {
|
||||||
|
statusCode: 200,
|
||||||
|
body: executionOutOfMemoryServerResponse,
|
||||||
|
}).as('getExecution');
|
||||||
|
|
||||||
|
executionsTab.actions.switchToExecutionsTab();
|
||||||
|
cy.wait(['@getExecution']);
|
||||||
|
|
||||||
|
executionsTab.getters
|
||||||
|
.workflowExecutionPreviewIframe()
|
||||||
|
.should('be.visible')
|
||||||
|
.its('0.contentDocument.body') // Access the body of the iframe document
|
||||||
|
.should('not.be.empty') // Ensure the body is not empty
|
||||||
|
|
||||||
|
.then(cy.wrap)
|
||||||
|
.find('.el-notification:has(.el-notification--error)')
|
||||||
|
.should('be.visible')
|
||||||
|
.filter(`:contains("${message}")`)
|
||||||
|
.should('be.visible');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show workflow data in executions tab after hard reload and modify name and tags', () => {
|
||||||
|
executionsTab.actions.switchToExecutionsTab();
|
||||||
|
checkMainHeaderELements();
|
||||||
|
workflowPage.getters.saveButton().find('button').should('not.exist');
|
||||||
|
workflowPage.getters.tagPills().should('have.length', 2);
|
||||||
|
|
||||||
|
workflowPage.getters.workflowTags().click();
|
||||||
|
getVisibleSelect().find('li:contains("Manage tags")').click();
|
||||||
|
cy.get('button:contains("Add new")').click();
|
||||||
|
cy.getByTestId('tags-table').find('input').type('nutag').type('{enter}');
|
||||||
|
cy.get('button:contains("Done")').click();
|
||||||
|
|
||||||
|
cy.reload();
|
||||||
|
checkMainHeaderELements();
|
||||||
|
workflowPage.getters.saveButton().find('button').should('not.exist');
|
||||||
|
workflowPage.getters.workflowTags().click();
|
||||||
|
workflowPage.getters.tagsInDropdown().first().should('have.text', 'nutag').click();
|
||||||
|
workflowPage.getters.tagPills().should('have.length', 3);
|
||||||
|
|
||||||
|
let newWorkflowName = 'Renamed workflow';
|
||||||
|
workflowPage.actions.renameWorkflow(newWorkflowName);
|
||||||
|
workflowPage.getters.isWorkflowSaved();
|
||||||
|
workflowPage.getters
|
||||||
|
.workflowNameInputContainer()
|
||||||
|
.invoke('attr', 'title')
|
||||||
|
.should('eq', newWorkflowName);
|
||||||
|
|
||||||
|
executionsTab.actions.switchToEditorTab();
|
||||||
|
checkMainHeaderELements();
|
||||||
|
workflowPage.getters.saveButton().find('button').should('not.exist');
|
||||||
|
workflowPage.getters.tagPills().should('have.length', 3);
|
||||||
|
workflowPage.getters
|
||||||
|
.workflowNameInputContainer()
|
||||||
|
.invoke('attr', 'title')
|
||||||
|
.should('eq', newWorkflowName);
|
||||||
|
|
||||||
|
executionsTab.actions.switchToExecutionsTab();
|
||||||
|
checkMainHeaderELements();
|
||||||
|
workflowPage.getters.saveButton().find('button').should('not.exist');
|
||||||
|
workflowPage.getters.tagPills().should('have.length', 3);
|
||||||
|
workflowPage.getters
|
||||||
|
.workflowNameInputContainer()
|
||||||
|
.invoke('attr', 'title')
|
||||||
|
.should('eq', newWorkflowName);
|
||||||
|
|
||||||
|
executionsTab.actions.switchToEditorTab();
|
||||||
|
checkMainHeaderELements();
|
||||||
|
workflowPage.getters.saveButton().find('button').should('not.exist');
|
||||||
|
workflowPage.getters.tagPills().should('have.length', 3);
|
||||||
|
workflowPage.getters
|
||||||
|
.workflowNameInputContainer()
|
||||||
|
.invoke('attr', 'title')
|
||||||
|
.should('eq', newWorkflowName);
|
||||||
|
|
||||||
|
newWorkflowName = 'New workflow';
|
||||||
|
workflowPage.actions.renameWorkflow(newWorkflowName);
|
||||||
|
workflowPage.getters.isWorkflowSaved();
|
||||||
|
workflowPage.getters
|
||||||
|
.workflowNameInputContainer()
|
||||||
|
.invoke('attr', 'title')
|
||||||
|
.should('eq', newWorkflowName);
|
||||||
|
workflowPage.getters.workflowTags().click();
|
||||||
|
workflowPage.getters.tagsDropdown().find('.el-tag__close').first().click();
|
||||||
|
cy.get('body').click(0, 0);
|
||||||
|
workflowPage.getters.saveButton().find('button').should('not.exist');
|
||||||
|
workflowPage.getters.tagPills().should('have.length', 2);
|
||||||
|
|
||||||
|
executionsTab.actions.switchToExecutionsTab();
|
||||||
|
checkMainHeaderELements();
|
||||||
|
workflowPage.getters.saveButton().find('button').should('not.exist');
|
||||||
|
workflowPage.getters.tagPills().should('have.length', 2);
|
||||||
|
workflowPage.getters
|
||||||
|
.workflowNameInputContainer()
|
||||||
|
.invoke('attr', 'title')
|
||||||
|
.should('eq', newWorkflowName);
|
||||||
|
|
||||||
|
executionsTab.actions.switchToEditorTab();
|
||||||
|
checkMainHeaderELements();
|
||||||
|
workflowPage.getters.saveButton().find('button').should('not.exist');
|
||||||
|
workflowPage.getters.tagPills().should('have.length', 2);
|
||||||
|
workflowPage.getters
|
||||||
|
.workflowNameInputContainer()
|
||||||
|
.invoke('attr', 'title')
|
||||||
|
.should('eq', newWorkflowName);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should load items and auto scroll after filter change', () => {
|
||||||
|
createMockExecutions();
|
||||||
|
createMockExecutions();
|
||||||
|
cy.intercept('GET', '/rest/executions?filter=*').as('getExecutions');
|
||||||
|
|
||||||
|
executionsTab.actions.switchToExecutionsTab();
|
||||||
|
|
||||||
|
cy.wait(['@getExecutions']);
|
||||||
|
|
||||||
|
executionsTab.getters.executionsList().scrollTo(0, 500).wait(0);
|
||||||
|
|
||||||
|
executionsTab.getters.executionListItems().eq(10).click();
|
||||||
|
|
||||||
|
cy.getByTestId('executions-filter-button').click();
|
||||||
|
cy.getByTestId('executions-filter-status-select').should('be.visible').click();
|
||||||
|
getVisibleSelect().find('li:contains("Error")').click();
|
||||||
|
|
||||||
|
executionsTab.getters.executionListItems().should('have.length', 5);
|
||||||
|
executionsTab.getters.successfulExecutionListItems().should('have.length', 1);
|
||||||
|
executionsTab.getters.failedExecutionListItems().should('have.length', 4);
|
||||||
|
|
||||||
|
cy.getByTestId('executions-filter-button').click();
|
||||||
|
cy.getByTestId('executions-filter-status-select').should('be.visible').click();
|
||||||
|
getVisibleSelect().find('li:contains("Success")').click();
|
||||||
|
|
||||||
|
// check if the list is scrolled
|
||||||
|
executionsTab.getters.executionListItems().eq(10).should('be.visible');
|
||||||
|
executionsTab.getters.executionsList().then(($el) => {
|
||||||
|
const { scrollTop, scrollHeight, clientHeight } = $el[0];
|
||||||
|
expect(scrollTop).to.be.greaterThan(0);
|
||||||
|
expect(scrollTop + clientHeight).to.be.lessThan(scrollHeight);
|
||||||
|
|
||||||
|
// scroll to the bottom
|
||||||
|
$el[0].scrollTo(0, scrollHeight);
|
||||||
|
executionsTab.getters.executionListItems().should('have.length', 18);
|
||||||
|
executionsTab.getters.successfulExecutionListItems().should('have.length', 18);
|
||||||
|
executionsTab.getters.failedExecutionListItems().should('have.length', 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.getByTestId('executions-filter-button').click();
|
||||||
|
cy.getByTestId('executions-filter-reset-button').should('be.visible').click();
|
||||||
|
executionsTab.getters.executionListItems().eq(11).should('be.visible');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -233,9 +262,11 @@ 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);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
82
cypress/e2e/45-workflow-selector-parameter.cy.ts
Normal file
82
cypress/e2e/45-workflow-selector-parameter.cy.ts
Normal 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');
|
||||||
|
});
|
||||||
|
});
|
35
cypress/e2e/46-n8n.io-iframe.cy.ts
Normal file
35
cypress/e2e/46-n8n.io-iframe.cy.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -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
|
||||||
|
|
|
@ -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', () => {
|
||||||
|
|
133
cypress/fixtures/Execution-pinned-data-check.json
Normal file
133
cypress/fixtures/Execution-pinned-data-check.json
Normal 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": []
|
||||||
|
}
|
53
cypress/fixtures/Test_Subworkflow_Get_Weather.json
Normal file
53
cypress/fixtures/Test_Subworkflow_Get_Weather.json
Normal 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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
64
cypress/fixtures/Test_Subworkflow_Search_DB.json
Normal file
64
cypress/fixtures/Test_Subworkflow_Search_DB.json
Normal 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": []
|
||||||
|
}
|
|
@ -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!"
|
||||||
},
|
},
|
||||||
|
|
|
@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -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');
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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'),
|
||||||
|
|
|
@ -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
|
||||||
method: 'POST',
|
.request({
|
||||||
url: `${BACKEND_BASE_URL}/rest/login`,
|
method: 'POST',
|
||||||
body: { email, password },
|
url: `${BACKEND_BASE_URL}/rest/login`,
|
||||||
failOnStatusCode: false,
|
body: { email, password },
|
||||||
}),
|
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));
|
||||||
|
|
|
@ -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": [
|
||||||
|
|
7
packages/@n8n/api-types/.eslintrc.js
Normal file
7
packages/@n8n/api-types/.eslintrc.js
Normal 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),
|
||||||
|
};
|
3
packages/@n8n/api-types/README.md
Normal file
3
packages/@n8n/api-types/README.md
Normal 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.
|
2
packages/@n8n/api-types/jest.config.js
Normal file
2
packages/@n8n/api-types/jest.config.js
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
/** @type {import('jest').Config} */
|
||||||
|
module.exports = require('../../../jest.config');
|
24
packages/@n8n/api-types/package.json
Normal file
24
packages/@n8n/api-types/package.json
Normal 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:*"
|
||||||
|
}
|
||||||
|
}
|
2
packages/@n8n/api-types/src/datetime.ts
Normal file
2
packages/@n8n/api-types/src/datetime.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
/** Date time in the ISO 8601 format, e.g. 2024-10-31T00:00:00.123Z */
|
||||||
|
export type Iso8601DateTimeString = string;
|
7
packages/@n8n/api-types/src/index.ts
Normal file
7
packages/@n8n/api-types/src/index.ts
Normal 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';
|
17
packages/@n8n/api-types/src/push/collaboration.ts
Normal file
17
packages/@n8n/api-types/src/push/collaboration.ts
Normal 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;
|
9
packages/@n8n/api-types/src/push/debug.ts
Normal file
9
packages/@n8n/api-types/src/push/debug.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
type SendConsoleMessage = {
|
||||||
|
type: 'sendConsoleMessage';
|
||||||
|
data: {
|
||||||
|
source: string;
|
||||||
|
messages: unknown[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DebugPushMessage = SendConsoleMessage;
|
53
packages/@n8n/api-types/src/push/execution.ts
Normal file
53
packages/@n8n/api-types/src/push/execution.ts
Normal 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;
|
21
packages/@n8n/api-types/src/push/hot-reload.ts
Normal file
21
packages/@n8n/api-types/src/push/hot-reload.ts
Normal 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;
|
20
packages/@n8n/api-types/src/push/index.ts
Normal file
20
packages/@n8n/api-types/src/push/index.ts
Normal 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'];
|
17
packages/@n8n/api-types/src/push/webhook.ts
Normal file
17
packages/@n8n/api-types/src/push/webhook.ts
Normal 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;
|
11
packages/@n8n/api-types/src/push/worker.ts
Normal file
11
packages/@n8n/api-types/src/push/worker.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import type { WorkerStatus } from '../scaling';
|
||||||
|
|
||||||
|
export type SendWorkerStatusMessage = {
|
||||||
|
type: 'sendWorkerStatusMessage';
|
||||||
|
data: {
|
||||||
|
workerId: string;
|
||||||
|
status: WorkerStatus;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type WorkerPushMessage = SendWorkerStatusMessage;
|
26
packages/@n8n/api-types/src/push/workflow.ts
Normal file
26
packages/@n8n/api-types/src/push/workflow.ts
Normal 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;
|
30
packages/@n8n/api-types/src/scaling.ts
Normal file
30
packages/@n8n/api-types/src/scaling.ts
Normal 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;
|
||||||
|
};
|
6
packages/@n8n/api-types/src/user.ts
Normal file
6
packages/@n8n/api-types/src/user.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
export type MinimalUser = {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
};
|
11
packages/@n8n/api-types/tsconfig.build.json
Normal file
11
packages/@n8n/api-types/tsconfig.build.json
Normal 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__/**"]
|
||||||
|
}
|
10
packages/@n8n/api-types/tsconfig.json
Normal file
10
packages/@n8n/api-types/tsconfig.json
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"extends": "../../../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"rootDir": ".",
|
||||||
|
"types": ["node", "jest"],
|
||||||
|
"baseUrl": "src",
|
||||||
|
"tsBuildInfoFile": "dist/typecheck.tsbuildinfo"
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts", "test/**/*.ts"]
|
||||||
|
}
|
31
packages/@n8n/benchmark/.eslintrc.js
Normal file
31
packages/@n8n/benchmark/.eslintrc.js
Normal 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
4
packages/@n8n/benchmark/.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
**/.terraform/*
|
||||||
|
**/*.tfstate*
|
||||||
|
**/*.tfvars
|
||||||
|
privatekey.pem
|
62
packages/@n8n/benchmark/Dockerfile
Normal file
62
packages/@n8n/benchmark/Dockerfile
Normal 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" ]
|
82
packages/@n8n/benchmark/README.md
Normal file
82
packages/@n8n/benchmark/README.md
Normal 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).
|
13
packages/@n8n/benchmark/bin/n8n-benchmark
Executable file
13
packages/@n8n/benchmark/bin/n8n-benchmark
Executable 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 });
|
||||||
|
})();
|
60
packages/@n8n/benchmark/infra/.terraform.lock.hcl
Normal file
60
packages/@n8n/benchmark/infra/.terraform.lock.hcl
Normal 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",
|
||||||
|
]
|
||||||
|
}
|
54
packages/@n8n/benchmark/infra/benchmark-env.tf
Normal file
54
packages/@n8n/benchmark/infra/benchmark-env.tf
Normal 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
|
||||||
|
}
|
11
packages/@n8n/benchmark/infra/modules/benchmark-vm/output.tf
Normal file
11
packages/@n8n/benchmark/infra/modules/benchmark-vm/output.tf
Normal 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
|
||||||
|
}
|
29
packages/@n8n/benchmark/infra/modules/benchmark-vm/vars.tf
Normal file
29
packages/@n8n/benchmark/infra/modules/benchmark-vm/vars.tf
Normal 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)
|
||||||
|
}
|
126
packages/@n8n/benchmark/infra/modules/benchmark-vm/vm.tf
Normal file
126
packages/@n8n/benchmark/infra/modules/benchmark-vm/vm.tf
Normal 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
|
||||||
|
}
|
16
packages/@n8n/benchmark/infra/output.tf
Normal file
16
packages/@n8n/benchmark/infra/output.tf
Normal 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
|
||||||
|
}
|
23
packages/@n8n/benchmark/infra/providers.tf
Normal file
23
packages/@n8n/benchmark/infra/providers.tf
Normal 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" {}
|
34
packages/@n8n/benchmark/infra/vars.tf
Normal file
34
packages/@n8n/benchmark/infra/vars.tf
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
55
packages/@n8n/benchmark/package.json
Normal file
55
packages/@n8n/benchmark/package.json
Normal 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": " "
|
||||||
|
}
|
||||||
|
}
|
|
@ -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": []
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
|
@ -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',
|
||||||
|
});
|
||||||
|
}
|
213
packages/@n8n/benchmark/scenarios/http-node/http-node.json
Normal file
213
packages/@n8n/benchmark/scenarios/http-node/http-node.json
Normal 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": []
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
|
@ -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": []
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
42
packages/@n8n/benchmark/scenarios/scenario.schema.json
Normal file
42
packages/@n8n/benchmark/scenarios/scenario.schema.json
Normal 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
|
||||||
|
}
|
|
@ -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": []
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
|
@ -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,
|
||||||
|
});
|
||||||
|
}
|
|
@ -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": []
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
|
@ -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,
|
||||||
|
});
|
||||||
|
}
|
52
packages/@n8n/benchmark/scripts/bootstrap.sh
Normal file
52
packages/@n8n/benchmark/scripts/bootstrap.sh
Normal 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
|
|
@ -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');
|
||||||
|
}
|
||||||
|
}
|
37
packages/@n8n/benchmark/scripts/clients/ssh-client.mjs
Normal file
37
packages/@n8n/benchmark/scripts/clients/ssh-client.mjs
Normal 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}`;
|
||||||
|
}
|
||||||
|
}
|
71
packages/@n8n/benchmark/scripts/clients/terraform-client.mjs
Normal file
71
packages/@n8n/benchmark/scripts/clients/terraform-client.mjs
Normal 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';
|
||||||
|
}
|
||||||
|
}
|
86
packages/@n8n/benchmark/scripts/destroy-cloud-env.mjs
Normal file
86
packages/@n8n/benchmark/scripts/destroy-cloud-env.mjs
Normal 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);
|
||||||
|
});
|
92110
packages/@n8n/benchmark/scripts/mock-api/mappings/mockApiData.json
Normal file
92110
packages/@n8n/benchmark/scripts/mock-api/mappings/mockApiData.json
Normal file
File diff suppressed because one or more lines are too long
|
@ -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}
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
|
@ -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}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
|
@ -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}
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
Loading…
Reference in a new issue