Merge branch 'n8n-io:master' into feature/support-add-labels-for-gitlab-issue-api

This commit is contained in:
mmuenker 2024-09-17 12:06:17 +02:00 committed by GitHub
commit a1e177b021
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2878 changed files with 173041 additions and 39175 deletions

View file

@ -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:
```
```text
<type>(<scope>): <summary>
│ │ │
│ │ └─⫸ Summary: In imperative present tense.
| | Capitalized
| | 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
- type
- scope (*optional*)
- summary
- type
- scope (_optional_)
- summary
- PR description
- body (optional)
- blank line
- footer (optional)
- body (optional)
- blank line
- footer (optional)
The structure looks like this:
### **Type**
## Type
Must be one of the following:
- `feat` - A new feature
- `fix` - A bug fix
- `perf` - A code change that improves performance
- `test` - Adding missing tests or correcting existing tests
- `docs` - Documentation only changes
- `refactor` - A code change that neither fixes a bug nor adds a feature
- `build` - Changes that affect the build system or external dependencies (example scopes: broccoli, npm)
- `ci` - Changes to our CI configuration files and scripts (e.g. Github actions)
| type | description | appears in changelog |
| --- | --- | --- |
| `feat` | A new feature | ✅ |
| `fix` | A bug fix | ✅ |
| `perf` | A code change that improves performance | ✅ |
| `test` | Adding missing tests or correcting existing tests | ❌ |
| `docs` | Documentation only changes | ❌ |
| `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!)
- `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
- `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.
- mattermost → Mattermost Node
- microsoftToDo → Microsoft To Do Node
- n8n → n8n Node
- mattermost → Mattermost Node
- microsoftToDo → Microsoft To Do Node
- n8n → n8n Node
### **Summary**
## Summary
The summary contains succinct description of the change:
- use the imperative, present tense: "change" not "changed" nor "changes"
- capitalize the first letter
- *no* dot (.) at the end
- do *not* include Linear ticket IDs etc. (e.g. N8N-1234)
- _no_ dot (.) at the end
- 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.
### **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.
### **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:
```
```text
BREAKING CHANGE: <breaking change summary>
<BLANK LINE>
<breaking change description + migration instructions>
@ -83,7 +87,7 @@ Fixes #<issue number>
or
```
```text
DEPRECATED: <what is deprecated>
<BLANK LINE>
<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 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.
> 💡 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.
### **Revert commits**
### Revert commits
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:
- 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.

View file

@ -5,7 +5,7 @@
"debug": "4.3.4",
"glob": "10.3.10",
"p-limit": "3.1.0",
"picocolors": "1.0.0",
"picocolors": "1.0.1",
"semver": "7.5.4",
"tempfile": "5.0.0",
"typescript": "*"

View file

@ -16,7 +16,11 @@ const changelogStream = conventionalChangelog({
releaseCount: 1,
tagPrefix: 'n8n@',
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) => {
console.error(err.stack);

View file

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

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

@ -0,0 +1,97 @@
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
if: always()
uses: azure/login@v2.1.1
with:
client-id: ${{ env.ARM_CLIENT_ID }}
tenant-id: ${{ env.ARM_TENANT_ID }}
subscription-id: ${{ env.ARM_SUBSCRIPTION_ID }}
- name: Destroy the environment
if: always()
run: pnpm destroy-cloud-env ${{ env.DEBUG }}
working-directory: packages/@n8n/benchmark

View file

@ -26,7 +26,7 @@ jobs:
run: pnpm install --frozen-lockfile
- 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

View file

@ -7,8 +7,7 @@ on:
- edited
- synchronize
branches:
- '**'
- '!release/*'
- 'master'
jobs:
check-pr-title:
@ -29,6 +28,6 @@ jobs:
- name: 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:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View file

@ -4,19 +4,49 @@ on:
workflow_dispatch:
pull_request_review:
types: [submitted]
branch:
- 'master'
paths:
- packages/design-system/**
- .github/workflows/chromatic.yml
concurrency:
group: chromatic-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
get-metadata:
name: Get Metadata
runs-on: ubuntu-latest
steps:
- name: Check out current commit
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 2
- name: Determine changed files
uses: tomi/paths-filter-action@v3.0.2
id: changed
if: github.event_name == 'pull_request_review'
with:
filters: |
design_system:
- packages/design-system/**
- .github/workflows/chromatic.yml
outputs:
design_system_files_changed: ${{ steps.changed.outputs.design_system == 'true' }}
is_community_pr: ${{ contains(github.event.pull_request.labels.*.name, 'community') }}
is_pr_target_master: ${{ github.event.pull_request.base.ref == 'master' }}
is_dispatch: ${{ github.event_name == 'workflow_dispatch' }}
is_pr_approved: ${{ github.event.review.state == 'approved' }}
chromatic:
if: ${{ github.event.review.state == 'approved' && !contains(github.event.pull_request.labels.*.name, 'community') }}
needs: [get-metadata]
if: |
needs.get-metadata.outputs.is_dispatch == 'true' ||
(
needs.get-metadata.outputs.design_system_files_changed == 'true' &&
needs.get-metadata.outputs.is_community_pr == 'false' &&
needs.get-metadata.outputs.is_pr_target_master == 'true' &&
needs.get-metadata.outputs.is_pr_approved == 'true'
)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4.1.1

View file

@ -8,6 +8,8 @@ on:
paths:
- packages/cli/src/databases/**
- .github/workflows/ci-postgres-mysql.yml
pull_request_review:
types: [submitted]
concurrency:
group: db-${{ github.event.pull_request.number || github.ref }}
@ -17,6 +19,7 @@ jobs:
build:
name: Install & Build
runs-on: ubuntu-latest
if: github.event_name != 'pull_request_review' || startsWith(github.event.pull_request.base.ref, 'release/')
steps:
- uses: actions/checkout@v4.1.1
- run: corepack enable

View file

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

View file

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

View file

@ -6,10 +6,6 @@ on:
- cron: '0 1 * * *'
workflow_dispatch:
inputs:
repository:
description: 'GitHub repository to create image off.'
required: true
default: 'n8n-io/n8n'
branch:
description: 'GitHub branch to create image off.'
required: true
@ -36,6 +32,9 @@ on:
required: false
default: ''
env:
N8N_TAG: ${{ inputs.tag || 'nightly' }}
jobs:
build:
runs-on: ubuntu-latest
@ -49,7 +48,6 @@ jobs:
- name: Checkout
uses: actions/checkout@v4.1.1
with:
repository: ${{ github.event.inputs.repository || 'n8n-io/n8n' }}
ref: ${{ github.event.inputs.branch || 'master' }}
- 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 ""
shell: bash
- name: Build and push
- name: Build and push to DockerHub
uses: docker/build-push-action@v5.1.0
with:
context: .
@ -81,7 +79,22 @@ jobs:
push: true
cache-from: type=gha
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
run: |

View file

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

View file

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

View file

@ -3,33 +3,65 @@ name: PR E2E
on:
pull_request_review:
types: [submitted]
branch:
- 'master'
concurrency:
group: e2e-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
get-metadata:
name: Get Metadata
runs-on: ubuntu-latest
steps:
- name: Check out current commit
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 2
- name: Determine changed files
uses: tomi/paths-filter-action@v3.0.2
id: changed
with:
filters: |
not_ignored:
- '!.devcontainer/**'
- '!.github/*'
- '!.github/scripts/*'
- '!.github/workflows/benchmark-*'
- '!.github/workflows/check-*'
- '!.vscode/**'
- '!docker/**'
- '!packages/@n8n/benchmark/**'
- '!**/*.md'
predicate-quantifier: 'every'
outputs:
# The workflow should run when:
# - It has changes to files that are not ignored
# - It is not a community PR
# - It is targeting master or a release branch
should_run: ${{ steps.changed.outputs.not_ignored == 'true' && !contains(github.event.pull_request.labels.*.name, 'community') && (github.event.pull_request.base.ref == 'master' || startsWith(github.event.pull_request.base.ref, 'release/')) }}
run-e2e-tests:
name: E2E [Electron/Node 18]
uses: ./.github/workflows/e2e-reusable.yml
if: ${{ github.event.review.state == 'approved' && !contains(github.event.pull_request.labels.*.name, 'community') }}
needs: [get-metadata]
if: ${{ github.event.review.state == 'approved' && needs.get-metadata.outputs.should_run == 'true' }}
with:
pr_number: ${{ github.event.pull_request.number }}
user: ${{ github.event.pull_request.user.login || 'PR User' }}
spec: 'e2e/*'
secrets:
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
post-e2e-tests:
runs-on: ubuntu-latest
name: E2E [Electron/Node 18] - Checks
needs: [run-e2e-tests]
needs: [get-metadata, run-e2e-tests]
if: always()
steps:
- name: E2E success comment
if: ${{!contains(github.event.pull_request.labels.*.name, 'community') && needs.run-e2e-tests.outputs.tests_passed == 'true' }}
if: ${{ needs.get-metadata.outputs.should_run == 'true' && needs.run-e2e-tests.outputs.tests_passed == 'true' }}
uses: peter-evans/create-or-update-comment@v4.0.0
with:
issue-number: ${{ github.event.pull_request.number }}

View file

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

View file

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

View file

@ -8,18 +8,17 @@ on:
- 'release/*'
jobs:
publish-release:
if: github.event.pull_request.merged == true
publish-to-npm:
name: Publish to NPM
runs-on: ubuntu-latest
if: github.event.pull_request.merged == true
timeout-minutes: 10
permissions:
contents: write
id-token: write
timeout-minutes: 60
env:
NPM_CONFIG_PROVENANCE: true
outputs:
release: ${{ steps.set-release.outputs.release }}
steps:
- name: Checkout
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
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
uses: ncipollo/release-action@v1
with:
commit: ${{github.event.pull_request.base.ref}}
tag: 'n8n@${{env.RELEASE}}'
tag: 'n8n@${{ needs.publish-to-npm.outputs.release }}'
prerelease: true
makeLatest: false
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
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":"${{env.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":"${{ needs.publish-to-npm.outputs.release }}"}'
# - name: Merge Release into 'master'
# run: |
# git fetch origin
# git checkout --track origin/master
# git config user.name "Jan Oberhauser"
# git config user.email jan.oberhauser@gmail.com
# git merge --ff n8n@${{env.RELEASE}}
# git push origin master
# git push origin :${{github.event.pull_request.base.ref}}
merge-back-into-master:
name: Merge back into master
needs: [publish-to-npm, create-github-release]
if: ${{ github.event.pull_request.merged == true && !contains(github.event.pull_request.labels.*.name, 'release:patch') }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4.1.1
with:
fetch-depth: 0
- run: |
git checkout --track origin/master
git config user.name "github-actions[bot]"
git config user.email 41898282+github-actions[bot]@users.noreply.github.com
git merge --ff n8n@${{ needs.publish-to-npm.outputs.release }}
git push origin master
git push origin :${{github.event.pull_request.base.ref}}

View file

@ -73,6 +73,7 @@ jobs:
env:
N8N_ENCRYPTION_KEY: ${{secrets.ENCRYPTION_KEY}}
SKIP_STATISTICS_EVENTS: true
DB_SQLITE_POOL_SIZE: 4
# -
# name: Export credentials
# if: always()

View file

@ -36,7 +36,6 @@ jobs:
steps:
- uses: actions/checkout@v4.1.1
with:
repository: n8n-io/n8n
ref: ${{ inputs.ref }}
- run: corepack enable
@ -50,6 +49,7 @@ jobs:
run: pnpm install --frozen-lockfile
- name: Setup build cache
if: inputs.collectCoverage != true
uses: rharkor/caching-for-turbo@v1.5
- name: Build

View file

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

View file

@ -1,3 +1,290 @@
# [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)
### Bug Fixes
* Add better error handling for chat errors ([#10408](https://github.com/n8n-io/n8n/issues/10408)) ([f82b6e4](https://github.com/n8n-io/n8n/commit/f82b6e4ba9bf527b3a4c17872162d9ae124ead0d))
* **AI Agent Node:** Fix issues with some tools not populating ([#10406](https://github.com/n8n-io/n8n/issues/10406)) ([51a1edd](https://github.com/n8n-io/n8n/commit/51a1eddbf00393f3881c340cf37cfcca59566c99))
* **core:** Account for cancelling an execution with no workers available ([#10343](https://github.com/n8n-io/n8n/issues/10343)) ([b044e78](https://github.com/n8n-io/n8n/commit/b044e783e73a499dbd7532a5d489a782d3d021da))
* **core:** Account for owner when filtering by project ID in `GET /workflows` in Public API ([#10379](https://github.com/n8n-io/n8n/issues/10379)) ([5ac65b3](https://github.com/n8n-io/n8n/commit/5ac65b36bcb1351c6233b951f064f60862f790a5))
* **core:** Enforce shutdown timer and sequence on `SIGINT` for main ([#10346](https://github.com/n8n-io/n8n/issues/10346)) ([5255793](https://github.com/n8n-io/n8n/commit/5255793afee5653d8356b8e4d2e1009d5cf36164))
* **core:** Filter out prototype and constructor lookups in expressions ([#10382](https://github.com/n8n-io/n8n/issues/10382)) ([8e7d29a](https://github.com/n8n-io/n8n/commit/8e7d29ad3c4872b1cc147dfcfe9a864ba916692f))
* **core:** Fix duplicate Redis publisher ([#10392](https://github.com/n8n-io/n8n/issues/10392)) ([45813de](https://github.com/n8n-io/n8n/commit/45813debc963096f63cc0aabe82d9d9f853a39d7))
* **core:** Fix worker shutdown errors when active executions ([#10353](https://github.com/n8n-io/n8n/issues/10353)) ([e071b73](https://github.com/n8n-io/n8n/commit/e071b73bab34edd4b3e6aef6497514acc504cdc6))
* **core:** Prevent XSS in user update endpoints ([#10338](https://github.com/n8n-io/n8n/issues/10338)) ([7898498](https://github.com/n8n-io/n8n/commit/78984986a6b4add89df9743b94c113046f1d5ee8))
* **core:** Prevent XSS via static cache dir ([#10339](https://github.com/n8n-io/n8n/issues/10339)) ([4f392b5](https://github.com/n8n-io/n8n/commit/4f392b5e3e0ee166e85a2e060b3ec7fcf145229b))
* **core:** Rate limit MFA activation and verification endpoints ([#10330](https://github.com/n8n-io/n8n/issues/10330)) ([b6c47c0](https://github.com/n8n-io/n8n/commit/b6c47c0e3214878d42980d5c9535df52b3984b3c))
* **editor:** Connect up new project viewer role to the FE ([#9913](https://github.com/n8n-io/n8n/issues/9913)) ([117e2d9](https://github.com/n8n-io/n8n/commit/117e2d968fcc535f32c583ac8f2dc8a84e8cd6bd))
* **editor:** Enable credential sharing between all types of projects ([#10233](https://github.com/n8n-io/n8n/issues/10233)) ([1cf48cc](https://github.com/n8n-io/n8n/commit/1cf48cc3019c1cf27e2f3c9affd18426237e9064))
* **editor:** Fix rendering of SVG icons in public chat on iOS ([#10381](https://github.com/n8n-io/n8n/issues/10381)) ([7ab3811](https://github.com/n8n-io/n8n/commit/7ab38114dbf3881afba39287a061446ec4bf0431))
* **editor:** Fixing XSS vulnerability in toast messages ([#10329](https://github.com/n8n-io/n8n/issues/10329)) ([38bdd9f](https://github.com/n8n-io/n8n/commit/38bdd9f5d0d9ca06fab1a7e1a3e7a4a648a6a89a))
* **editor:** Revert change that hid swagger docs in the ui ([#10350](https://github.com/n8n-io/n8n/issues/10350)) ([bae49d3](https://github.com/n8n-io/n8n/commit/bae49d3198d4bcc27e7996cd4f7be3132becc98e))
* **n8n Form Trigger Node:** Fix issue preventing v1 node from working ([#10364](https://github.com/n8n-io/n8n/issues/10364)) ([9b647a9](https://github.com/n8n-io/n8n/commit/9b647a9837434e8b75e3ad754ff5136bb5ac760d))
* Require mfa code for password change if its enabled ([#10341](https://github.com/n8n-io/n8n/issues/10341)) ([9d7caac](https://github.com/n8n-io/n8n/commit/9d7caacc699f10962783393925a980ec6f1ca975))
* Require mfa code to disable mfa ([#10345](https://github.com/n8n-io/n8n/issues/10345)) ([3384f52](https://github.com/n8n-io/n8n/commit/3384f52a35b835ba1d8633dc94bab0ad6e7023b3))
### Features
* Add Ask assistant behind feature flag ([#9995](https://github.com/n8n-io/n8n/issues/9995)) ([5ed2a77](https://github.com/n8n-io/n8n/commit/5ed2a77740db1f02b27c571f4dfdfa206ebdb19c))
* **AI Transform Node:** New node ([#10405](https://github.com/n8n-io/n8n/issues/10405)) ([4d222ac](https://github.com/n8n-io/n8n/commit/4d222ac19d943b69fd9f87abe5e5c5f5141eed8d))
* **AI Transform Node:** New node ([#9990](https://github.com/n8n-io/n8n/issues/9990)) ([0de9d56](https://github.com/n8n-io/n8n/commit/0de9d56619ed1c055407353046b8a9ebe78da527))
* **core:** Allow overriding npm registry for community packages ([#10325](https://github.com/n8n-io/n8n/issues/10325)) ([33a2703](https://github.com/n8n-io/n8n/commit/33a2703429d9eaa41f72d2e7d2da5be60b6c620f))
* **editor:** Add schema view to expression modal ([#9976](https://github.com/n8n-io/n8n/issues/9976)) ([71b6c67](https://github.com/n8n-io/n8n/commit/71b6c671797024d7b516352fa9b7ecda101ce3b2))
* **MySQL Node:** Return decimal types as numbers ([#10313](https://github.com/n8n-io/n8n/issues/10313)) ([f744d7c](https://github.com/n8n-io/n8n/commit/f744d7c100be68669d9a3efd0033dd371a3cfaf7))
* **Okta Node:** Add Okta Node ([#10278](https://github.com/n8n-io/n8n/issues/10278)) ([5cac0f3](https://github.com/n8n-io/n8n/commit/5cac0f339d649cfe5857d33738210cbc1599370b))
# [1.54.0](https://github.com/n8n-io/n8n/compare/n8n@1.53.0...n8n@1.54.0) (2024-08-07)
### Bug Fixes
* **core:** Ensure OAuth token data is not stubbed in source control ([#10302](https://github.com/n8n-io/n8n/issues/10302)) ([98115e9](https://github.com/n8n-io/n8n/commit/98115e95df8289a8ec400a570a7f256382f8e286))
* **core:** Fix expressions in webhook nodes(Form, Webhook) to access previous node's data ([#10247](https://github.com/n8n-io/n8n/issues/10247)) ([88a1701](https://github.com/n8n-io/n8n/commit/88a170176a3447e7f847e9cf145aeb867b1c5fcf))
* **core:** Fix user telemetry bugs ([#10293](https://github.com/n8n-io/n8n/issues/10293)) ([42a0b59](https://github.com/n8n-io/n8n/commit/42a0b594d6ea2527c55a2aa9976c904cf70ecf92))
* **core:** Make execution and its data creation atomic ([#10276](https://github.com/n8n-io/n8n/issues/10276)) ([ae50bb9](https://github.com/n8n-io/n8n/commit/ae50bb95a8e5bf1cdbf9483da54b84094b82e260))
* **core:** Make OAuth1/OAuth2 callback not require auth ([#10263](https://github.com/n8n-io/n8n/issues/10263)) ([a8e2774](https://github.com/n8n-io/n8n/commit/a8e2774f5382e202556b5506c7788265786aa973))
* **core:** Revert transactions until we remove the legacy sqlite driver ([#10299](https://github.com/n8n-io/n8n/issues/10299)) ([1eba7c3](https://github.com/n8n-io/n8n/commit/1eba7c3c763ac5b6b28c1c6fc43fc8c215249292))
* **core:** Surface enterprise trial error message ([#10267](https://github.com/n8n-io/n8n/issues/10267)) ([432ac1d](https://github.com/n8n-io/n8n/commit/432ac1da59e173ce4c0f2abbc416743d9953ba70))
* **core:** Upgrade tournament to address some XSS vulnerabilities ([#10277](https://github.com/n8n-io/n8n/issues/10277)) ([43ae159](https://github.com/n8n-io/n8n/commit/43ae159ea40c574f8e41bdfd221ab2bf3268eee7))
* **core:** VM2 sandbox should not throw on `new Promise` ([#10298](https://github.com/n8n-io/n8n/issues/10298)) ([7e95f9e](https://github.com/n8n-io/n8n/commit/7e95f9e2e40a99871f1b6abcdacb39ac5f857332))
* **core:** Webhook and form baseUrl missing ([#10290](https://github.com/n8n-io/n8n/issues/10290)) ([8131d66](https://github.com/n8n-io/n8n/commit/8131d66f8ca1b1da00597a12859ee4372148a0c9))
* **editor:** Enable moving resources only if team projects are available by the license ([#10271](https://github.com/n8n-io/n8n/issues/10271)) ([42ba884](https://github.com/n8n-io/n8n/commit/42ba8841c401126c77158a53dc8fcbb45dfce8fd))
* **editor:** Fix execution retry button ([#10275](https://github.com/n8n-io/n8n/issues/10275)) ([55f2ffe](https://github.com/n8n-io/n8n/commit/55f2ffe256c91a028cee95c3bbb37a093a1c0f81))
* **editor:** Update design system Avatar component to show initials also when only firstName or lastName is given ([#10308](https://github.com/n8n-io/n8n/issues/10308)) ([46bbf09](https://github.com/n8n-io/n8n/commit/46bbf09beacad12472d91786b91d845fe2afb26d))
* **editor:** Update tags filter/editor to not show non existing tag as a selectable option ([#10297](https://github.com/n8n-io/n8n/issues/10297)) ([557a76e](https://github.com/n8n-io/n8n/commit/557a76ec2326de72fb7a8b46fc4353f8fd9b591d))
* **Invoice Ninja Node:** Fix payment types ([#10196](https://github.com/n8n-io/n8n/issues/10196)) ([c5acbb7](https://github.com/n8n-io/n8n/commit/c5acbb7ec0d24ec9b30c221fa3b2fb615fb9ec7f))
* Loop node no input data shown ([#10224](https://github.com/n8n-io/n8n/issues/10224)) ([c8ee852](https://github.com/n8n-io/n8n/commit/c8ee852159207be0cfe2c3e0ee8e7b29d838aa35))
### Features
* **core:** Allow filtering executions and users by project in Public API ([#10250](https://github.com/n8n-io/n8n/issues/10250)) ([7056e50](https://github.com/n8n-io/n8n/commit/7056e50b006bda665f64ce6234c5c1967891c415))
* **core:** Allow transferring credentials in Public API ([#10259](https://github.com/n8n-io/n8n/issues/10259)) ([07d7b24](https://github.com/n8n-io/n8n/commit/07d7b247f02a9d7185beca7817deb779a3d665dd))
* **core:** Show sub-node error on the logs pane. Open logs pane on sub-node error ([#10248](https://github.com/n8n-io/n8n/issues/10248)) ([57d1c9a](https://github.com/n8n-io/n8n/commit/57d1c9a99e97308f2f1b8ae05ac3861a835e8e5a))
* **core:** Support community packages in scaling-mode ([#10228](https://github.com/n8n-io/n8n/issues/10228)) ([88086a4](https://github.com/n8n-io/n8n/commit/88086a41ff5b804b35aa9d9503dc2d48836fe4ec))
* **core:** Support create, delete, edit role for users in Public API ([#10279](https://github.com/n8n-io/n8n/issues/10279)) ([84efbd9](https://github.com/n8n-io/n8n/commit/84efbd9b9c51f536b21a4f969ab607d277bef692))
* **core:** Support create, read, update, delete projects in Public API ([#10269](https://github.com/n8n-io/n8n/issues/10269)) ([489ce10](https://github.com/n8n-io/n8n/commit/489ce100634c3af678fb300e9a39d273042542e6))
* **editor:** Auto-add LLM chain for new LLM nodes on empty canvas ([#10245](https://github.com/n8n-io/n8n/issues/10245)) ([06419d9](https://github.com/n8n-io/n8n/commit/06419d9483ae916e79aace6d8c17e265b419b15d))
* **Elasticsearch Node:** Add bulk operations for Elasticsearch ([#9940](https://github.com/n8n-io/n8n/issues/9940)) ([bf8f848](https://github.com/n8n-io/n8n/commit/bf8f848645dfd31527713a55bd1fc93865327017))
* **Lemlist Trigger Node:** Update Trigger events ([#10311](https://github.com/n8n-io/n8n/issues/10311)) ([15f10ec](https://github.com/n8n-io/n8n/commit/15f10ec325cb5eda0f952bed3a5f171dd91bc639))
* **MongoDB Node:** Add projection to query options on Find ([#9972](https://github.com/n8n-io/n8n/issues/9972)) ([0a84e0d](https://github.com/n8n-io/n8n/commit/0a84e0d8b047669f5cf023c21383d01c929c5b4f))
* **Postgres Chat Memory, Redis Chat Memory, Xata:** Add support for context window length ([#10203](https://github.com/n8n-io/n8n/issues/10203)) ([e3edeaa](https://github.com/n8n-io/n8n/commit/e3edeaa03526f041d15d1099ea91869e38a0decc))
* **Stripe Trigger Node:** Add Stripe webhook descriptions based on the workflow ID and name ([#9956](https://github.com/n8n-io/n8n/issues/9956)) ([3433465](https://github.com/n8n-io/n8n/commit/34334651e0e6874736a437a894176bed4590e5a7))
* **Webflow Node:** Update to use the v2 API ([#9996](https://github.com/n8n-io/n8n/issues/9996)) ([6d8323f](https://github.com/n8n-io/n8n/commit/6d8323fadea8af04483eb1a873df0cf3ccc2a891))
# [1.53.0](https://github.com/n8n-io/n8n/compare/n8n@1.52.0...n8n@1.53.0) (2024-07-31)
### Bug Fixes
* Better error message when calling data transformation functions on a null value ([#10210](https://github.com/n8n-io/n8n/issues/10210)) ([1718125](https://github.com/n8n-io/n8n/commit/1718125c6d8589cf24dc8d34f6808dd6f1802691))
* **core:** Fix missing successful items on continueErrorOutput with multiple outputs ([#10218](https://github.com/n8n-io/n8n/issues/10218)) ([1a7713e](https://github.com/n8n-io/n8n/commit/1a7713ef263680da43f08b6c8a15aee7a0341493))
* **core:** Flush instance stopped event immediately ([#10238](https://github.com/n8n-io/n8n/issues/10238)) ([d6770b5](https://github.com/n8n-io/n8n/commit/d6770b5fcaec6438d677b918aaeb1669ad7424c2))
* **core:** Restore log event `n8n.workflow.failed` ([#10253](https://github.com/n8n-io/n8n/issues/10253)) ([3e96b29](https://github.com/n8n-io/n8n/commit/3e96b293329525c9d4b2fcef87b3803e458c8e7f))
* **core:** Upgrade @n8n/vm2 to address CVE202337466 ([#10265](https://github.com/n8n-io/n8n/issues/10265)) ([2a09a03](https://github.com/n8n-io/n8n/commit/2a09a036d2e916acff7ee50904f1d011a93758e1))
* **editor:** Defer `User saved credentials` telemetry event for OAuth credentials ([#10215](https://github.com/n8n-io/n8n/issues/10215)) ([40a5226](https://github.com/n8n-io/n8n/commit/40a5226e24448a4428143e69d80ebc78238365a1))
* **editor:** Fix custom API call notice ([#10227](https://github.com/n8n-io/n8n/issues/10227)) ([5b47c8b](https://github.com/n8n-io/n8n/commit/5b47c8b57b25528cd2d6f97bc6d98707d47f35bc))
* **editor:** Fix issue with existing credential not opening in HTTP agent tool ([#10167](https://github.com/n8n-io/n8n/issues/10167)) ([906b4c3](https://github.com/n8n-io/n8n/commit/906b4c3c7b2919111cf23eaa12b3c4d507969179))
* **editor:** Fix parameter input glitch when there was an error loading remote options ([#10209](https://github.com/n8n-io/n8n/issues/10209)) ([c0e3743](https://github.com/n8n-io/n8n/commit/c0e37439a87105a0e66c8ebced42c06dab30dc5e))
* **editor:** Fix workflow execution list scrolling after filter change ([#10226](https://github.com/n8n-io/n8n/issues/10226)) ([7e64358](https://github.com/n8n-io/n8n/commit/7e643589c67adc0218216ec4b89a95f0edfedbee))
* **Google BigQuery Node:** Send timeoutMs in query, pagination support ([#10205](https://github.com/n8n-io/n8n/issues/10205)) ([f5722e8](https://github.com/n8n-io/n8n/commit/f5722e8823ccd2bc2b5f43ba3c849797d5690a93))
* **Google Sheets Node:** Add column names row if sheet is empty ([#10200](https://github.com/n8n-io/n8n/issues/10200)) ([82eba9f](https://github.com/n8n-io/n8n/commit/82eba9fc5ff49b8e2a9db93c10b253fb67a8c644))
* **Google Sheets Node:** Do not insert row_number as a new column, do not checkForSchemaChanges in update operation ([#10201](https://github.com/n8n-io/n8n/issues/10201)) ([5136d10](https://github.com/n8n-io/n8n/commit/5136d10ca3492f92af67d4a1d4abc774419580cc))
* **Google Sheets Node:** Fix Google Sheet URL regex ([#10195](https://github.com/n8n-io/n8n/issues/10195)) ([e6fd996](https://github.com/n8n-io/n8n/commit/e6fd996973d4f40facf0ebf1eea3cc26acd0603d))
* **HTTP Request Node:** Resolve max pages expression ([#10192](https://github.com/n8n-io/n8n/issues/10192)) ([bfc8e1b](https://github.com/n8n-io/n8n/commit/bfc8e1b56f7714e1f52aae747d58d686b86e60f0))
* **LinkedIn Node:** Fix issue with some characters cutting off posts early ([#10185](https://github.com/n8n-io/n8n/issues/10185)) ([361b5e7](https://github.com/n8n-io/n8n/commit/361b5e7c37ba49b68dcf5b8122621aad4d8d96e0))
* **Postgres Node:** Expressions in query parameters for Postgres executeQuery operation ([#10217](https://github.com/n8n-io/n8n/issues/10217)) ([519fc4d](https://github.com/n8n-io/n8n/commit/519fc4d75325a80b84cc4dcacf52d6f4c02e3a44))
* **Postgres Node:** Option to treat query parameters enclosed in single quotas as text ([#10214](https://github.com/n8n-io/n8n/issues/10214)) ([00ec253](https://github.com/n8n-io/n8n/commit/00ec2533374d3def465efee718592fc4001d5602))
* **Read/Write Files from Disk Node:** Notice update in file selector, replace backslashes with forward slashes if windows path ([#10186](https://github.com/n8n-io/n8n/issues/10186)) ([3eac673](https://github.com/n8n-io/n8n/commit/3eac673b17986c5c74bd2adb5ad589ba0ca55319))
* **Text Classifier Node:** Use proper documentation URL and respect continueOnFail ([#10216](https://github.com/n8n-io/n8n/issues/10216)) ([452f52c](https://github.com/n8n-io/n8n/commit/452f52c124017e002e86c547ba42b1633b14beed))
* **Trello Node:** Use body for POST requests ([#10189](https://github.com/n8n-io/n8n/issues/10189)) ([7775d50](https://github.com/n8n-io/n8n/commit/7775d5059b7f69d9af22e7ad7d12c6cf9092a4e5))
* **Wait Node:** Authentication fix ([#10236](https://github.com/n8n-io/n8n/issues/10236)) ([f87854f](https://github.com/n8n-io/n8n/commit/f87854f8db360b7b870583753fcfb4af95adab8c))
### Features
* **Calendly Trigger Node:** Add OAuth Credentials Support ([#10251](https://github.com/n8n-io/n8n/issues/10251)) ([326c983](https://github.com/n8n-io/n8n/commit/326c983915a2c382e32398358e7dcadd022c0b77))
* **core:** Allow filtering workflows by project and transferring workflows in Public API ([#10231](https://github.com/n8n-io/n8n/issues/10231)) ([d719899](https://github.com/n8n-io/n8n/commit/d719899223907b20a17883a35e4ef637a3453532))
* **editor:** Show new executions as `Queued` in the UI, until they actually start ([#10204](https://github.com/n8n-io/n8n/issues/10204)) ([44728d7](https://github.com/n8n-io/n8n/commit/44728d72423f5549dda09589f4a618ebd80899cb))
* **HTTP Request Node:** Add option to disable lowercase headers ([#10154](https://github.com/n8n-io/n8n/issues/10154)) ([5aba69b](https://github.com/n8n-io/n8n/commit/5aba69bcf4d232d9860f3cd9fe57cb8839a2f96f))
* **Information Extractor Node:** Add new simplified AI-node for information extraction ([#10149](https://github.com/n8n-io/n8n/issues/10149)) ([3d235b0](https://github.com/n8n-io/n8n/commit/3d235b0b2df756df35ac60e3dcd87ad183a07167))
* Introduce Google Cloud Platform as external secrets provider ([#10146](https://github.com/n8n-io/n8n/issues/10146)) ([3ccb9df](https://github.com/n8n-io/n8n/commit/3ccb9df2f902e46f8cbb9c46c0727f29d752a773))
* **n8n Form Trigger Node:** Improvements ([#10092](https://github.com/n8n-io/n8n/issues/10092)) ([711b667](https://github.com/n8n-io/n8n/commit/711b667ebefe55740e5eb39f1f0f24ceee10e7b0))
* Recovery option for jsonParse helper ([#10182](https://github.com/n8n-io/n8n/issues/10182)) ([d165b33](https://github.com/n8n-io/n8n/commit/d165b33ceac4d24d0fc290bffe63b5f551204e38))
* **Sentiment Analysis Node:** Implement Sentiment Analysis node ([#10184](https://github.com/n8n-io/n8n/issues/10184)) ([8ef0a0c](https://github.com/n8n-io/n8n/commit/8ef0a0c58ac2a84aad649ccbe72aa907d005cc44))
* **Shopify Node:** Update Shopify API version ([#10155](https://github.com/n8n-io/n8n/issues/10155)) ([e2ee915](https://github.com/n8n-io/n8n/commit/e2ee91569a382bfbf787cf45204c72c821a860a0))
* Support create, read, delete variables in Public API ([#10241](https://github.com/n8n-io/n8n/issues/10241)) ([af695eb](https://github.com/n8n-io/n8n/commit/af695ebf934526d926ea87fe87df61aa73d70979))
# [1.52.0](https://github.com/n8n-io/n8n/compare/n8n@1.51.0...n8n@1.52.0) (2024-07-24)

View file

@ -95,8 +95,8 @@ development environment ready in minutes.
## License
n8n is [fair-code](https://faircode.io) distributed under the
[**Sustainable Use License**](https://github.com/n8n-io/n8n/blob/master/packages/cli/LICENSE.md) and the
[**n8n Enterprise License**](https://github.com/n8n-io/n8n/blob/master/packages/cli/LICENSE_EE.md).
[**Sustainable Use License**](https://github.com/n8n-io/n8n/blob/master/LICENSE.md) and the
[**n8n Enterprise License**](https://github.com/n8n-io/n8n/blob/master/LICENSE_EE.md).
Proprietary licenses are available for enterprise customers. [Get in touch](mailto:license@n8n.io)

View file

@ -1,41 +1,45 @@
import { CredentialsModal, WorkflowPage } from '../pages';
import { getVisibleSelect } from '../utils';
const workflowPage = new WorkflowPage();
const credentialsModal = new CredentialsModal();
export const getHomeButton = () => cy.getByTestId('project-home-menu-item');
export const getMenuItems = () => cy.getByTestId('project-menu-item');
export const getAddProjectButton = () => cy.getByTestId('add-project-menu-item');
export const getAddProjectButton = () =>
cy.getByTestId('add-project-menu-item').should('contain', 'Add project').should('be.visible');
export const getProjectTabs = () => cy.getByTestId('project-tabs').find('a');
export const getProjectTabWorkflows = () => getProjectTabs().filter('a[href$="/workflows"]');
export const getProjectTabCredentials = () => getProjectTabs().filter('a[href$="/credentials"]');
export const getProjectTabSettings = () => getProjectTabs().filter('a[href$="/settings"]');
export const getProjectSettingsNameInput = () => cy.getByTestId('project-settings-name-input');
export const getProjectSettingsNameInput = () =>
cy.getByTestId('project-settings-name-input').find('input');
export const getProjectSettingsSaveButton = () => cy.getByTestId('project-settings-save-button');
export const getProjectSettingsCancelButton = () =>
cy.getByTestId('project-settings-cancel-button');
export const getProjectSettingsDeleteButton = () =>
cy.getByTestId('project-settings-delete-button');
export const getProjectMembersSelect = () => cy.getByTestId('project-members-select');
export const addProjectMember = (email: string) => {
export const addProjectMember = (email: string, role?: string) => {
getProjectMembersSelect().click();
getProjectMembersSelect().get('.el-select-dropdown__item').contains(email.toLowerCase()).click();
if (role) {
cy.getByTestId(`user-list-item-${email}`)
.find('[data-test-id="projects-settings-user-role-select"]')
.click();
getVisibleSelect().find('li').contains(role).click();
}
};
export const getProjectNameInput = () => cy.get('#projectName').find('input');
export const getResourceMoveModal = () => cy.getByTestId('project-move-resource-modal');
export const getResourceMoveConfirmModal = () =>
cy.getByTestId('project-move-resource-confirm-modal');
export const getProjectMoveSelect = () => cy.getByTestId('project-move-resource-modal-select');
export function createProject(name: string) {
getAddProjectButton().should('be.visible').click();
getAddProjectButton().click();
getProjectNameInput()
.should('be.visible')
.should('be.focused')
.should('have.value', 'My project')
.clear()
.type(name);
getProjectSettingsNameInput().should('be.visible').clear().type(name);
getProjectSettingsSaveButton().click();
}
@ -46,7 +50,7 @@ export function createWorkflow(fixtureKey: string, name: string) {
workflowPage.actions.zoomToFit();
}
export function createCredential(name: string) {
export function createCredential(name: string, closeModal = true) {
credentialsModal.getters.newCredentialModal().should('be.visible');
credentialsModal.getters.newCredentialTypeSelect().should('be.visible');
credentialsModal.getters.newCredentialTypeOption('Notion API').click();
@ -54,13 +58,8 @@ export function createCredential(name: string) {
credentialsModal.getters.connectionParameter('Internal Integration Secret').type('1234567890');
credentialsModal.actions.setName(name);
credentialsModal.actions.save();
credentialsModal.actions.close();
}
export const actions = {
createProject: (name: string) => {
getAddProjectButton().click();
getProjectSettingsNameInput().type(name);
getProjectSettingsSaveButton().click();
},
};
if (closeModal) {
credentialsModal.actions.close();
}
}

View file

@ -37,6 +37,7 @@ export const INSTANCE_MEMBERS = [
export const MANUAL_TRIGGER_NODE_NAME = 'Manual Trigger';
export const MANUAL_TRIGGER_NODE_DISPLAY_NAME = 'When clicking Test workflow';
export const MANUAL_CHAT_TRIGGER_NODE_NAME = 'Chat Trigger';
export const CHAT_TRIGGER_NODE_DISPLAY_NAME = 'When chat message received';
export const SCHEDULE_TRIGGER_NODE_NAME = 'Schedule Trigger';
export const CODE_NODE_NAME = 'Code';
export const SET_NODE_NAME = 'Set';
@ -57,8 +58,10 @@ export const AI_TOOL_CODE_NODE_NAME = 'Code Tool';
export const AI_TOOL_WIKIPEDIA_NODE_NAME = 'Wikipedia';
export const AI_TOOL_HTTP_NODE_NAME = 'HTTP Request Tool';
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_OUTPUT_PARSER_AUTO_FIXING_NODE_NAME = 'Auto-fixing Output Parser';
export const WEBHOOK_NODE_NAME = 'Webhook';
export const EXECUTE_WORKFLOW_NODE_NAME = 'Execute Workflow';
export const META_KEY = Cypress.platform === 'darwin' ? 'meta' : 'ctrl';

View file

@ -11,6 +11,21 @@ describe('Inline expression editor', () => {
cy.on('uncaught:exception', (error) => error.name !== 'ExpressionError');
});
describe('Basic UI functionality', () => {
it('should open and close inline expression preview', () => {
WorkflowPage.actions.zoomToFit();
WorkflowPage.actions.openNode('Schedule');
WorkflowPage.actions.openInlineExpressionEditor();
WorkflowPage.getters.inlineExpressionEditorInput().clear();
WorkflowPage.getters.inlineExpressionEditorInput().click().type('{{');
WorkflowPage.getters.inlineExpressionEditorInput().type('123');
WorkflowPage.getters.inlineExpressionEditorOutput().contains(/^123$/);
// click outside to close
ndv.getters.outputPanel().click();
WorkflowPage.getters.inlineExpressionEditorOutput().should('not.exist');
});
});
describe('Static data', () => {
beforeEach(() => {
WorkflowPage.actions.addNodeToCanvas('Hacker News');

View file

@ -145,7 +145,16 @@ describe('Canvas Actions', () => {
});
});
it('should delete connections by pressing the delete button', () => {
it('should delete node by pressing keyboard backspace', () => {
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
WorkflowPage.getters.canvasNodeByName(CODE_NODE_NAME).click();
cy.get('body').type('{backspace}');
WorkflowPage.getters.nodeConnections().should('have.length', 0);
});
it('should delete connections by clicking on the delete button', () => {
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);

View file

@ -40,11 +40,13 @@ describe('Data mapping', () => {
ndv.actions.mapDataFromHeader(1, 'value');
ndv.getters.inlineExpressionEditorInput().should('have.text', '{{ $json.timestamp }}');
ndv.getters.inlineExpressionEditorInput().type('{esc}');
ndv.getters.parameterExpressionPreview('value').should('include.text', '2024');
ndv.actions.mapDataFromHeader(2, 'value');
ndv.getters
.inlineExpressionEditorInput()
.should('have.text', "{{ $json.timestamp }} {{ $json['Readable date'] }}");
.should('have.text', "{{ $json['Readable date'] }}{{ $json.timestamp }}");
});
it('maps expressions from table json, and resolves value based on hover', () => {
@ -133,6 +135,7 @@ describe('Data mapping', () => {
ndv.actions.mapToParameter('value');
ndv.getters.inlineExpressionEditorInput().should('have.text', '{{ $json.input[0].count }}');
ndv.getters.inlineExpressionEditorInput().type('{esc}');
ndv.getters.parameterExpressionPreview('value').should('include.text', '0');
ndv.getters
@ -145,8 +148,9 @@ describe('Data mapping', () => {
ndv.actions.mapToParameter('value');
ndv.getters
.inlineExpressionEditorInput()
.should('have.text', '{{ $json.input[0].count }} {{ $json.input }}');
ndv.actions.validateExpressionPreview('value', '0 [object Object]');
.should('have.text', '{{ $json.input }}{{ $json.input[0].count }}');
ndv.getters.inlineExpressionEditorInput().type('{esc}');
ndv.actions.validateExpressionPreview('value', '[object Object]0');
});
it('maps expressions from schema view', () => {
@ -163,6 +167,7 @@ describe('Data mapping', () => {
ndv.actions.mapToParameter('value');
ndv.getters.inlineExpressionEditorInput().should('have.text', '{{ $json.input[0].count }}');
ndv.getters.inlineExpressionEditorInput().type('{esc}');
ndv.actions.validateExpressionPreview('value', '0');
ndv.getters.inputDataContainer().find('span').contains('input').realMouseDown();
@ -170,8 +175,8 @@ describe('Data mapping', () => {
ndv.actions.mapToParameter('value');
ndv.getters
.inlineExpressionEditorInput()
.should('have.text', '{{ $json.input[0].count }} {{ $json.input }}');
ndv.actions.validateExpressionPreview('value', '0 [object Object]');
.should('have.text', '{{ $json.input }}{{ $json.input[0].count }}');
ndv.actions.validateExpressionPreview('value', '[object Object]0');
});
it('maps expressions from previous nodes', () => {
@ -192,6 +197,7 @@ describe('Data mapping', () => {
ndv.getters
.inlineExpressionEditorInput()
.should('have.text', `{{ $('${SCHEDULE_TRIGGER_NODE_NAME}').item.json.input[0].count }}`);
ndv.getters.inlineExpressionEditorInput().type('{esc}');
ndv.actions.switchInputMode('Table');
ndv.actions.selectInputNode(SCHEDULE_TRIGGER_NODE_NAME);
@ -200,17 +206,17 @@ describe('Data mapping', () => {
.inlineExpressionEditorInput()
.should(
'have.text',
`{{ $('${SCHEDULE_TRIGGER_NODE_NAME}').item.json.input[0].count }} {{ $('${SCHEDULE_TRIGGER_NODE_NAME}').item.json.input }}`,
`{{ $('${SCHEDULE_TRIGGER_NODE_NAME}').item.json.input }}{{ $('${SCHEDULE_TRIGGER_NODE_NAME}').item.json.input[0].count }}`,
);
ndv.actions.selectInputNode('Set');
ndv.getters.executingLoader().should('not.exist');
ndv.getters.inputDataContainer().should('exist');
ndv.actions.validateExpressionPreview('value', '0 [object Object]');
ndv.actions.validateExpressionPreview('value', '[object Object]0');
ndv.getters.inputTbodyCell(2, 0).realHover();
ndv.actions.validateExpressionPreview('value', '1 [object Object]');
ndv.actions.validateExpressionPreview('value', '[object Object]1');
});
it('maps keys to path', () => {
@ -271,12 +277,12 @@ describe('Data mapping', () => {
ndv.actions.typeIntoParameterInput('value', 'fun');
ndv.actions.clearParameterInput('value'); // keep focus on param
cy.wait(300);
ndv.getters.inputDataContainer().should('exist').find('span').contains('count').realMouseDown();
ndv.actions.mapToParameter('value');
ndv.getters.inlineExpressionEditorInput().should('have.text', '{{ $json.input[0].count }}');
ndv.getters.inlineExpressionEditorInput().type('{esc}');
ndv.actions.validateExpressionPreview('value', '0');
ndv.getters.inputDataContainer().find('span').contains('input').realMouseDown();
@ -284,8 +290,8 @@ describe('Data mapping', () => {
ndv.actions.mapToParameter('value');
ndv.getters
.inlineExpressionEditorInput()
.should('have.text', '{{ $json.input[0].count }} {{ $json.input }}');
ndv.actions.validateExpressionPreview('value', '0 [object Object]');
.should('have.text', '{{ $json.input }}{{ $json.input[0].count }}');
ndv.actions.validateExpressionPreview('value', '[object Object]0');
});
it('renders expression preview when a previous node is selected', () => {
@ -342,4 +348,31 @@ describe('Data mapping', () => {
.invoke('css', 'border')
.should('include', 'dashed rgb(90, 76, 194)');
});
it('maps expressions to a specific location in the editor', () => {
cy.fixture('Test_workflow_3.json').then((data) => {
cy.get('body').paste(JSON.stringify(data));
});
workflowPage.actions.zoomToFit();
workflowPage.actions.openNode('Set');
ndv.actions.typeIntoParameterInput('value', '=');
ndv.getters.inlineExpressionEditorInput().find('.cm-content').paste('hello world\n\nnewline');
ndv.getters.inlineExpressionEditorInput().type('{esc}');
ndv.getters.inputDataContainer().should('exist').find('span').contains('count').realMouseDown();
ndv.actions.mapToParameter('value');
ndv.getters
.inlineExpressionEditorInput()
.should('have.text', '{{ $json.input[0].count }}hello worldnewline');
ndv.getters.inlineExpressionEditorInput().type('{esc}');
ndv.actions.validateExpressionPreview('value', '0hello world\n\nnewline');
ndv.getters.inputDataContainer().find('span').contains('input').realMouseDown();
ndv.actions.mapToParameter('value', 'center');
ndv.getters
.inlineExpressionEditorInput()
.should('have.text', '{{ $json.input[0].count }}hello world{{ $json.input }}newline');
});
});

View file

@ -7,7 +7,7 @@ import {
WorkflowSharingModal,
WorkflowsPage,
} from '../pages';
import { getVisibleSelect } from '../utils';
import { getVisibleDropdown, getVisiblePopper, getVisibleSelect } from '../utils';
import * as projects from '../composables/projects';
/**
@ -180,7 +180,8 @@ describe('Sharing', { disableAutoLogin: true }, () => {
).should('be.visible');
credentialsModal.getters.usersSelect().click();
cy.getByTestId('project-sharing-info')
getVisiblePopper()
.find('[data-test-id="project-sharing-info"]')
.filter(':visible')
.should('have.length', 3)
.contains(INSTANCE_ADMIN.email)
@ -192,11 +193,79 @@ describe('Sharing', { disableAutoLogin: true }, () => {
credentialsModal.actions.saveSharing();
credentialsModal.actions.close();
});
it('credentials should work between team and personal projects', () => {
cy.resetDatabase();
cy.enableFeature('sharing');
cy.enableFeature('advancedPermissions');
cy.enableFeature('projectRole:admin');
cy.enableFeature('projectRole:editor');
cy.changeQuota('maxTeamProjects', -1);
cy.signinAsOwner();
cy.visit('/');
projects.createProject('Development');
projects.getHomeButton().click();
workflowsPage.getters.newWorkflowButtonCard().click();
projects.createWorkflow('Test_workflow_1.json', 'Test workflow');
projects.getHomeButton().click();
projects.getProjectTabCredentials().click();
credentialsPage.getters.emptyListCreateCredentialButton().click();
projects.createCredential('Notion API');
credentialsPage.getters.credentialCard('Notion API').click();
credentialsModal.actions.changeTab('Sharing');
credentialsModal.getters.usersSelect().click();
getVisibleSelect()
.find('li')
.should('have.length', 4)
.filter(':contains("Development")')
.should('have.length', 1)
.click();
credentialsModal.getters.saveButton().click();
credentialsModal.actions.close();
projects.getProjectTabWorkflows().click();
workflowsPage.getters.workflowCardActions('Test workflow').click();
getVisibleDropdown().find('li').contains('Share').click();
workflowSharingModal.getters.usersSelect().filter(':visible').click();
getVisibleSelect().find('li').should('have.length', 3).first().click();
workflowSharingModal.getters.saveButton().click();
projects.getMenuItems().first().click();
workflowsPage.getters.newWorkflowButtonCard().click();
projects.createWorkflow('Test_workflow_1.json', 'Test workflow 2');
workflowPage.actions.openShareModal();
workflowSharingModal.getters.usersSelect().should('not.exist');
cy.get('body').type('{esc}');
projects.getMenuItems().first().click();
projects.getProjectTabCredentials().click();
credentialsPage.getters.createCredentialButton().click();
projects.createCredential('Notion API 2', false);
credentialsModal.actions.changeTab('Sharing');
credentialsModal.getters.usersSelect().click();
getVisibleSelect().find('li').should('have.length', 4).first().click();
credentialsModal.getters.saveButton().click();
credentialsModal.actions.close();
credentialsPage.getters
.credentialCards()
.should('have.length', 2)
.filter(':contains("Owned by me")')
.should('have.length', 1);
});
});
describe('Credential Usage in Cross Shared Workflows', () => {
beforeEach(() => {
cy.resetDatabase();
cy.enableFeature('sharing');
cy.enableFeature('advancedPermissions');
cy.enableFeature('projectRole:admin');
cy.enableFeature('projectRole:editor');
@ -207,23 +276,18 @@ describe('Credential Usage in Cross Shared Workflows', () => {
});
it('should only show credentials from the same team project', () => {
cy.enableFeature('advancedPermissions');
cy.enableFeature('projectRole:admin');
cy.enableFeature('projectRole:editor');
cy.changeQuota('maxTeamProjects', -1);
// Create a notion credential in the home project
credentialsPage.getters.emptyListCreateCredentialButton().click();
credentialsModal.actions.createNewCredential('Notion API');
// Create a notion credential in one project
projects.actions.createProject('Development');
projects.createProject('Development');
projects.getProjectTabCredentials().click();
credentialsPage.getters.emptyListCreateCredentialButton().click();
credentialsModal.actions.createNewCredential('Notion API');
// Create a notion credential in another project
projects.actions.createProject('Test');
projects.createProject('Test');
projects.getProjectTabCredentials().click();
credentialsPage.getters.emptyListCreateCredentialButton().click();
credentialsModal.actions.createNewCredential('Notion API');
@ -238,10 +302,36 @@ describe('Credential Usage in Cross Shared Workflows', () => {
getVisibleSelect().find('li').should('have.length', 2);
});
it('should only show credentials in their personal project for members', () => {
// Create a notion credential as the owner
credentialsPage.getters.emptyListCreateCredentialButton().click();
credentialsModal.actions.createNewCredential('Notion API');
// Create another notion credential as the owner, but share it with member
// 0
credentialsPage.getters.createCredentialButton().click();
credentialsModal.actions.createNewCredential('Notion API', false);
credentialsModal.actions.changeTab('Sharing');
credentialsModal.actions.addUser(INSTANCE_MEMBERS[0].email);
credentialsModal.actions.saveSharing();
// As the member, create a new notion credential and a workflow
cy.signinAsMember();
cy.visit(credentialsPage.url);
credentialsPage.getters.createCredentialButton().click();
credentialsModal.actions.createNewCredential('Notion API');
cy.visit(workflowsPage.url);
workflowsPage.actions.createWorkflowFromCard();
workflowPage.actions.addNodeToCanvas(NOTION_NODE_NAME, true, true);
// Only the own credential the shared one (+ the 'Create new' option)
// should be in the dropdown
workflowPage.getters.nodeCredentialsSelect().click();
getVisibleSelect().find('li').should('have.length', 3);
});
it('should only show credentials in their personal project for members if the workflow was shared with them', () => {
const workflowName = 'Test workflow';
cy.enableFeature('sharing');
cy.reload();
// Create a notion credential as the owner and a workflow that is shared
// with member 0
@ -272,7 +362,6 @@ describe('Credential Usage in Cross Shared Workflows', () => {
it("should show all credentials from all personal projects the workflow's been shared into for the global owner", () => {
const workflowName = 'Test workflow';
cy.enableFeature('sharing');
// As member 1, create a new notion credential. This should not show up.
cy.signinAsMember(1);
@ -317,8 +406,6 @@ describe('Credential Usage in Cross Shared Workflows', () => {
});
it('should show all personal credentials if the global owner owns the workflow', () => {
cy.enableFeature('sharing');
// As member 0, create a new notion credential.
cy.signinAsMember();
cy.visit(credentialsPage.url);

View file

@ -1,4 +1,5 @@
import { WorkflowPage } from '../pages';
import { getVisibleSelect } from '../utils';
const wf = new WorkflowPage();
@ -64,10 +65,26 @@ describe('Workflow tags', () => {
it('should detach a tag inline by clicking on dropdown list item', () => {
wf.getters.createTagButton().click();
wf.actions.addTags(TEST_TAGS);
wf.getters.nthTagPill(1).click();
wf.getters.workflowTagsContainer().click();
wf.getters.tagsInDropdown().filter('.selected').first().click();
cy.get('body').click(0, 0);
wf.getters.workflowTags().click();
wf.getters.tagPills().should('have.length', TEST_TAGS.length - 1);
});
it('should not show non existing tag as a selectable option', () => {
const NON_EXISTING_TAG = 'My Test Tag';
wf.getters.createTagButton().click();
wf.actions.addTags(TEST_TAGS);
cy.get('body').click(0, 0);
wf.getters.workflowTags().click();
wf.getters.workflowTagsInput().type(NON_EXISTING_TAG);
getVisibleSelect()
.find('li')
.should('have.length', 2)
.filter(`:contains("${NON_EXISTING_TAG}")`)
.should('not.have.length');
});
});

View file

@ -0,0 +1,21 @@
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
const WorkflowPage = new WorkflowPageClass();
describe('PAY-1858 context menu', () => {
it('can use context menu on saved workflow', () => {
WorkflowPage.actions.visit();
cy.createFixtureWorkflow('Test_workflow_filter.json', 'test');
WorkflowPage.getters.canvasNodes().should('have.length', 5);
WorkflowPage.actions.deleteNodeFromContextMenu('Then');
WorkflowPage.getters.canvasNodes().should('have.length', 4);
WorkflowPage.actions.hitSaveWorkflow();
cy.reload();
WorkflowPage.getters.canvasNodes().should('have.length', 4);
WorkflowPage.actions.deleteNodeFromContextMenu('Code');
WorkflowPage.getters.canvasNodes().should('have.length', 3);
});
});

View file

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

View file

@ -112,13 +112,13 @@ describe('Credentials', () => {
workflowPage.getters.nodeCredentialsSelect().should('have.length', 2);
workflowPage.getters.nodeCredentialsSelect().first().click();
getVisibleSelect().find('li').last().click();
getVisibleSelect().find('li').contains('Create New Credential').click();
// This one should show auth type selector
credentialsModal.getters.credentialAuthTypeRadioButtons().should('have.length', 2);
cy.get('body').type('{esc}');
workflowPage.getters.nodeCredentialsSelect().last().click();
getVisibleSelect().find('li').last().click();
getVisibleSelect().find('li').contains('Create New Credential').click();
// This one should not show auth type selector
credentialsModal.getters.credentialsAuthTypeSelector().should('not.exist');
});

View file

@ -9,179 +9,252 @@ const executionsTab = new WorkflowExecutionsTab();
const executionsRefreshInterval = 4000;
// Test suite for executions tab
describe('Current Workflow Executions', () => {
beforeEach(() => {
workflowPage.actions.visit();
cy.createFixtureWorkflow('Test_workflow_4_executions_view.json', 'My test workflow');
});
describe('Workflow Executions', () => {
describe('when workflow is saved', () => {
beforeEach(() => {
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');
it('should render executions tab correctly', () => {
createMockExecutions();
cy.intercept('GET', '/rest/executions?filter=*').as('getExecutions');
executionsTab.actions.switchToExecutionsTab();
executionsTab.actions.switchToExecutionsTab();
cy.wait(['@getExecutions']);
cy.wait(['@getExecutions']);
executionsTab.getters.executionsList().scrollTo(0, 500).wait(0);
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_/);
});
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=*');
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');
});
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);
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.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');
cy.getByTestId('executions-filter-button').click();
cy.getByTestId('executions-filter-reset-button').should('be.visible').click();
executionsTab.getters.executionListItems().eq(11).should('be.visible');
});
});
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');
describe('when new workflow is not saved', () => {
beforeEach(() => {
workflowPage.actions.visit();
});
executionsTab.actions.switchToExecutionsTab();
cy.wait(['@getExecution']);
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();
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
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');
.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);
workflowPage.getters.saveButton().find('button').should('be.enabled').click();
workflowPage.getters.isWorkflowSaved();
workflowPage.getters.nodeViewRoot().should('be.visible');
});
});
});
@ -189,9 +262,11 @@ const createMockExecutions = () => {
executionsTab.actions.createManualExecutions(5);
// Make some failed executions by enabling Code node with syntax error
executionsTab.actions.toggleNodeEnabled('Error');
workflowPage.getters.disabledNodes().should('have.length', 0);
executionsTab.actions.createManualExecutions(2);
// Then add some more successful ones
executionsTab.actions.toggleNodeEnabled('Error');
workflowPage.getters.disabledNodes().should('have.length', 1);
executionsTab.actions.createManualExecutions(4);
};

View file

@ -0,0 +1,279 @@
import type { ExecutionError } from 'n8n-workflow/src';
import { NDV, WorkflowPage as WorkflowPageClass } from '../pages';
import {
addLanguageModelNodeToParent,
addMemoryNodeToParent,
addNodeToCanvas,
addToolNodeToParent,
navigateToNewWorkflowPage,
openNode,
} from '../composables/workflow';
import {
AGENT_NODE_NAME,
AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME,
AI_MEMORY_POSTGRES_NODE_NAME,
AI_TOOL_CALCULATOR_NODE_NAME,
CHAT_TRIGGER_NODE_DISPLAY_NAME,
MANUAL_CHAT_TRIGGER_NODE_NAME,
MANUAL_TRIGGER_NODE_DISPLAY_NAME,
MANUAL_TRIGGER_NODE_NAME,
} from '../constants';
import {
clickCreateNewCredential,
clickExecuteNode,
clickGetBackToCanvas,
} from '../composables/ndv';
import { setCredentialValues } from '../composables/modals/credential-modal';
import {
closeManualChatModal,
getManualChatMessages,
getManualChatModalLogs,
getManualChatModalLogsEntries,
sendManualChatMessage,
} from '../composables/modals/chat-modal';
import { createMockNodeExecutionData, getVisibleSelect, runMockWorkflowExecution } from '../utils';
const ndv = new NDV();
const WorkflowPage = new WorkflowPageClass();
function createRunDataWithError(inputMessage: string) {
return [
createMockNodeExecutionData(MANUAL_CHAT_TRIGGER_NODE_NAME, {
jsonData: {
main: { input: inputMessage },
},
}),
createMockNodeExecutionData(AI_MEMORY_POSTGRES_NODE_NAME, {
jsonData: {
ai_memory: {
json: {
action: 'loadMemoryVariables',
values: {
input: inputMessage,
system_message: 'You are a helpful assistant',
formatting_instructions:
'IMPORTANT: Always call `format_final_response` to format your final response!',
},
},
},
},
inputOverride: {
ai_memory: [
[
{
json: {
action: 'loadMemoryVariables',
values: {
input: inputMessage,
system_message: 'You are a helpful assistant',
formatting_instructions:
'IMPORTANT: Always call `format_final_response` to format your final response!',
},
},
},
],
],
},
error: {
message: 'Internal error',
timestamp: 1722591723244,
name: 'NodeOperationError',
description: 'Internal error',
context: {},
cause: {
name: 'error',
severity: 'FATAL',
code: '3D000',
file: 'postinit.c',
line: '885',
routine: 'InitPostgres',
} as unknown as Error,
} as ExecutionError,
}),
createMockNodeExecutionData(AGENT_NODE_NAME, {
executionStatus: 'error',
error: {
level: 'error',
tags: {
packageName: 'workflow',
},
context: {},
functionality: 'configuration-node',
name: 'NodeOperationError',
timestamp: 1722591723244,
node: {
parameters: {
notice: '',
sessionIdType: 'fromInput',
tableName: 'n8n_chat_histories',
},
id: '6b9141da-0135-4e9d-94d1-2d658cbf48b5',
name: 'Postgres Chat Memory',
type: '@n8n/n8n-nodes-langchain.memoryPostgresChat',
typeVersion: 1,
position: [1140, 500],
credentials: {
postgres: {
id: 'RkyZetVpGsSfEAhQ',
name: 'Postgres account',
},
},
},
messages: ['database "chat11" does not exist'],
description: 'Internal error',
message: 'Internal error',
} as unknown as ExecutionError,
metadata: {
subRun: [
{
node: 'Postgres Chat Memory',
runIndex: 0,
},
],
},
}),
];
}
function setupTestWorkflow(chatTrigger: boolean = false) {
// Setup test workflow with AI Agent, Postgres Memory Node (source of error), Calculator Tool, and OpenAI Chat Model
if (chatTrigger) {
addNodeToCanvas(MANUAL_CHAT_TRIGGER_NODE_NAME, true);
} else {
addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME, true);
}
addNodeToCanvas(AGENT_NODE_NAME, true);
if (!chatTrigger) {
// Remove chat trigger
WorkflowPage.getters
.canvasNodeByName(CHAT_TRIGGER_NODE_DISPLAY_NAME)
.find('[data-test-id="delete-node-button"]')
.click({ force: true });
// Set manual trigger to output standard pinned data
openNode(MANUAL_TRIGGER_NODE_DISPLAY_NAME);
ndv.actions.editPinnedData();
ndv.actions.savePinnedData();
ndv.actions.close();
}
// Calculator is added just to make OpenAI Chat Model work (tools can not be empty with OpenAI model)
addToolNodeToParent(AI_TOOL_CALCULATOR_NODE_NAME, AGENT_NODE_NAME);
clickGetBackToCanvas();
addMemoryNodeToParent(AI_MEMORY_POSTGRES_NODE_NAME, AGENT_NODE_NAME);
clickCreateNewCredential();
setCredentialValues({
password: 'testtesttest',
});
ndv.getters.parameterInput('sessionIdType').click();
getVisibleSelect().contains('Define below').click();
ndv.getters.parameterInput('sessionKey').type('asdasd');
clickGetBackToCanvas();
addLanguageModelNodeToParent(
AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME,
AGENT_NODE_NAME,
true,
);
clickCreateNewCredential();
setCredentialValues({
apiKey: 'sk_test_123',
});
clickGetBackToCanvas();
WorkflowPage.actions.zoomToFit();
}
function checkMessages(inputMessage: string, outputMessage: string) {
const messages = getManualChatMessages();
messages.should('have.length', 2);
messages.should('contain', inputMessage);
messages.should('contain', outputMessage);
getManualChatModalLogs().should('exist');
getManualChatModalLogsEntries()
.should('have.length', 1)
.should('contain', AI_MEMORY_POSTGRES_NODE_NAME);
}
describe("AI-233 Make root node's logs pane active in case of an error in sub-nodes", () => {
beforeEach(() => {
navigateToNewWorkflowPage();
});
it('should open logs tab by default when there was an error', () => {
setupTestWorkflow(true);
openNode(AGENT_NODE_NAME);
const inputMessage = 'Test the code tool';
clickExecuteNode();
runMockWorkflowExecution({
trigger: () => sendManualChatMessage(inputMessage),
runData: createRunDataWithError(inputMessage),
lastNodeExecuted: AGENT_NODE_NAME,
});
checkMessages(inputMessage, '[ERROR: Internal error]');
closeManualChatModal();
// Open the AI Agent node to see the logs
openNode(AGENT_NODE_NAME);
// Finally check that logs pane is opened by default
ndv.getters.outputDataContainer().should('be.visible');
ndv.getters.aiOutputModeToggle().should('be.visible');
ndv.getters
.aiOutputModeToggle()
.find('[role="radio"]')
.should('have.length', 2)
.eq(1)
.should('have.attr', 'aria-checked', 'true');
ndv.getters
.outputPanel()
.findChildByTestId('node-error-message')
.should('be.visible')
.should('contain', 'Error in sub-node');
});
it('should switch to logs tab on error, when NDV is already opened', () => {
setupTestWorkflow(false);
openNode(AGENT_NODE_NAME);
const inputMessage = 'Test the code tool';
runMockWorkflowExecution({
trigger: () => clickExecuteNode(),
runData: createRunDataWithError(inputMessage),
lastNodeExecuted: AGENT_NODE_NAME,
});
// Check that logs pane is opened by default
ndv.getters.outputDataContainer().should('be.visible');
ndv.getters.aiOutputModeToggle().should('be.visible');
ndv.getters
.aiOutputModeToggle()
.find('[role="radio"]')
.should('have.length', 2)
.eq(1)
.should('have.attr', 'aria-checked', 'true');
ndv.getters
.outputPanel()
.findChildByTestId('node-error-message')
.should('be.visible')
.should('contain', 'Error in sub-node');
});
});

View file

@ -10,7 +10,9 @@ import {
disableNode,
getExecuteWorkflowButton,
navigateToNewWorkflowPage,
getNodes,
openNode,
getConnectionBySourceAndTarget,
} from '../composables/workflow';
import {
clickCreateNewCredential,
@ -41,6 +43,7 @@ import {
AI_TOOL_WIKIPEDIA_NODE_NAME,
BASIC_LLM_CHAIN_NODE_NAME,
EDIT_FIELDS_SET_NODE_NAME,
CHAT_TRIGGER_NODE_DISPLAY_NAME,
} from './../constants';
describe('Langchain Integration', () => {
@ -331,4 +334,27 @@ describe('Langchain Integration', () => {
closeManualChatModal();
});
it('should auto-add chat trigger and basic LLM chain when adding LLM node', () => {
addNodeToCanvas(AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME, true);
getConnectionBySourceAndTarget(
CHAT_TRIGGER_NODE_DISPLAY_NAME,
BASIC_LLM_CHAIN_NODE_NAME,
).should('exist');
getConnectionBySourceAndTarget(
AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME,
BASIC_LLM_CHAIN_NODE_NAME,
).should('exist');
getNodes().should('have.length', 3);
});
it('should not auto-add nodes if AI nodes are already present', () => {
addNodeToCanvas(AGENT_NODE_NAME, true);
addNodeToCanvas(AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME, true);
getConnectionBySourceAndTarget(CHAT_TRIGGER_NODE_DISPLAY_NAME, AGENT_NODE_NAME).should('exist');
getNodes().should('have.length', 3);
});
});

View file

@ -35,13 +35,14 @@ describe('Personal Settings', () => {
successToast().find('.el-notification__closeBtn').click();
});
});
// eslint-disable-next-line n8n-local-rules/no-skipped-tests
it('not allow malicious values for personal data', () => {
cy.visit('/settings/personal');
INVALID_NAMES.forEach((name) => {
cy.getByTestId('personal-data-form').find('input[name="firstName"]').clear().type(name);
cy.getByTestId('personal-data-form').find('input[name="lastName"]').clear().type(name);
cy.getByTestId('save-settings-button').click();
errorToast().should('contain', 'Malicious firstName | Malicious lastName');
errorToast().should('contain', 'Potentially malicious string | Potentially malicious string');
errorToast().find('.el-notification__closeBtn').click();
});
});

View file

@ -1,9 +1,4 @@
import {
INSTANCE_MEMBERS,
INSTANCE_OWNER,
MANUAL_TRIGGER_NODE_NAME,
NOTION_NODE_NAME,
} from '../constants';
import { INSTANCE_MEMBERS, MANUAL_TRIGGER_NODE_NAME, NOTION_NODE_NAME } from '../constants';
import {
WorkflowsPage,
WorkflowPage,
@ -11,9 +6,10 @@ import {
CredentialsPage,
WorkflowExecutionsTab,
NDV,
MainSidebar,
} from '../pages';
import * as projects from '../composables/projects';
import { getVisibleSelect } from '../utils';
import { getVisibleDropdown, getVisibleModalOverlay, getVisibleSelect } from '../utils';
const workflowsPage = new WorkflowsPage();
const workflowPage = new WorkflowPage();
@ -21,6 +17,7 @@ const credentialsPage = new CredentialsPage();
const credentialsModal = new CredentialsModal();
const executionsTab = new WorkflowExecutionsTab();
const ndv = new NDV();
const mainSidebar = new MainSidebar();
describe('Projects', { disableAutoLogin: true }, () => {
before(() => {
@ -237,10 +234,30 @@ describe('Projects', { disableAutoLogin: true }, () => {
cy.signinAsMember(1);
cy.visit(workflowsPage.url);
projects.getAddProjectButton().should('not.exist');
cy.getByTestId('add-project-menu-item').should('not.exist');
projects.getMenuItems().should('not.exist');
});
it('should not show viewer role if not licensed', () => {
cy.signinAsOwner();
cy.visit(workflowsPage.url);
projects.getMenuItems().first().click();
projects.getProjectTabSettings().click();
cy.get(
`[data-test-id="user-list-item-${INSTANCE_MEMBERS[0].email}"] [data-test-id="projects-settings-user-role-select"]`,
).click();
cy.get('.el-select-dropdown__item.is-disabled')
.should('contain.text', 'Viewer')
.get('span:contains("Upgrade")')
.filter(':visible')
.click();
getVisibleModalOverlay().should('contain.text', 'Upgrade to unlock additional roles');
});
describe('when starting from scratch', () => {
beforeEach(() => {
cy.resetDatabase();
@ -257,7 +274,7 @@ describe('Projects', { disableAutoLogin: true }, () => {
// Create a project and add a credential to it
cy.intercept('POST', '/rest/projects').as('projectCreate');
projects.getAddProjectButton().should('contain', 'Add project').should('be.visible').click();
projects.getAddProjectButton().click();
cy.wait('@projectCreate');
projects.getMenuItems().should('have.length', 1);
projects.getMenuItems().first().click();
@ -418,7 +435,7 @@ describe('Projects', { disableAutoLogin: true }, () => {
});
it('should move resources between projects', () => {
cy.signin(INSTANCE_OWNER);
cy.signinAsOwner();
cy.visit(workflowsPage.url);
// Create a workflow and a credential in the Home project
@ -563,5 +580,80 @@ describe('Projects', { disableAutoLogin: true }, () => {
projects.getProjectTabCredentials().click();
credentialsPage.getters.credentialCards().should('have.length', 2);
});
it('should handle viewer role', () => {
cy.enableFeature('projectRole:viewer');
cy.signinAsOwner();
cy.visit(workflowsPage.url);
projects.createProject('Development');
projects.addProjectMember(INSTANCE_MEMBERS[0].email, 'Viewer');
projects.getProjectSettingsSaveButton().click();
projects.getProjectTabWorkflows().click();
workflowsPage.getters.newWorkflowButtonCard().click();
projects.createWorkflow('Test_workflow_4_executions_view.json', 'WF with random error');
executionsTab.actions.createManualExecutions(2);
executionsTab.actions.toggleNodeEnabled('Error');
executionsTab.actions.createManualExecutions(2);
workflowPage.actions.saveWorkflowUsingKeyboardShortcut();
projects.getMenuItems().first().click();
projects.getProjectTabCredentials().click();
credentialsPage.getters.emptyListCreateCredentialButton().click();
projects.createCredential('Notion API');
mainSidebar.actions.openUserMenu();
cy.getByTestId('user-menu-item-logout').click();
cy.get('input[name="email"]').type(INSTANCE_MEMBERS[0].email);
cy.get('input[name="password"]').type(INSTANCE_MEMBERS[0].password);
cy.getByTestId('form-submit-button').click();
mainSidebar.getters.executions().click();
cy.getByTestId('global-execution-list-item').first().find('td:last button').click();
getVisibleDropdown()
.find('li')
.filter(':contains("Retry")')
.should('have.class', 'is-disabled');
getVisibleDropdown()
.find('li')
.filter(':contains("Delete")')
.should('have.class', 'is-disabled');
projects.getMenuItems().first().click();
cy.getByTestId('workflow-card-name').should('be.visible').first().click();
workflowPage.getters.nodeViewRoot().should('be.visible');
workflowPage.getters.executeWorkflowButton().should('not.exist');
workflowPage.getters.nodeCreatorPlusButton().should('not.exist');
workflowPage.getters.canvasNodes().should('have.length', 3).last().click();
cy.get('body').type('{backspace}');
workflowPage.getters.canvasNodes().should('have.length', 3).last().rightclick();
getVisibleDropdown()
.find('li')
.should('be.visible')
.filter(
':contains("Open"), :contains("Copy"), :contains("Select all"), :contains("Clear selection")',
)
.should('not.have.class', 'is-disabled');
cy.get('body').type('{esc}');
executionsTab.actions.switchToExecutionsTab();
cy.getByTestId('retry-execution-button')
.should('be.visible')
.find('.is-disabled')
.should('exist');
cy.get('button:contains("Debug")').should('be.disabled');
cy.get('button[title="Retry execution"]').should('be.disabled');
cy.get('button[title="Delete this execution"]').should('be.disabled');
projects.getMenuItems().first().click();
projects.getProjectTabCredentials().click();
credentialsPage.getters.credentialCards().filter(':contains("Notion")').click();
cy.getByTestId('node-credentials-config-container')
.should('be.visible')
.find('input')
.should('not.have.length');
});
});
});

View file

@ -0,0 +1,431 @@
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';
const wf = new WorkflowPage();
const ndv = new NDV();
const aiAssistant = new AIAssistant();
const credentialsPage = new CredentialsPage();
const credentialsModal = new CredentialsModal();
describe('AI Assistant::disabled', () => {
beforeEach(() => {
aiAssistant.actions.disableAssistant();
wf.actions.visit();
});
it('does not show assistant button if feature is disabled', () => {
aiAssistant.getters.askAssistantFloatingButton().should('not.exist');
});
});
describe('AI Assistant::enabled', () => {
beforeEach(() => {
aiAssistant.actions.enableAssistant();
wf.actions.visit();
});
after(() => {
aiAssistant.actions.disableAssistant();
});
it('renders placeholder UI', () => {
aiAssistant.getters.askAssistantFloatingButton().should('be.visible');
aiAssistant.getters.askAssistantFloatingButton().click();
aiAssistant.getters.askAssistantChat().should('be.visible');
aiAssistant.getters.placeholderMessage().should('be.visible');
aiAssistant.getters.chatInput().should('be.visible');
aiAssistant.getters.sendMessageButton().should('be.disabled');
aiAssistant.getters.closeChatButton().should('be.visible');
aiAssistant.getters.closeChatButton().click();
aiAssistant.getters.askAssistantChat().should('not.be.visible');
});
it('should resize assistant chat up', () => {
aiAssistant.getters.askAssistantFloatingButton().click();
aiAssistant.getters.askAssistantSidebarResizer().should('be.visible');
aiAssistant.getters.askAssistantChat().then((element) => {
const { width, left } = element[0].getBoundingClientRect();
cy.drag(aiAssistant.getters.askAssistantSidebarResizer(), [left - 10, 0], {
abs: true,
clickToFinish: true,
});
aiAssistant.getters.askAssistantChat().then((newElement) => {
const newWidth = newElement[0].getBoundingClientRect().width;
expect(newWidth).to.be.greaterThan(width);
});
});
});
it('should resize assistant chat down', () => {
aiAssistant.getters.askAssistantFloatingButton().click();
aiAssistant.getters.askAssistantSidebarResizer().should('be.visible');
aiAssistant.getters.askAssistantChat().then((element) => {
const { width, left } = element[0].getBoundingClientRect();
cy.drag(aiAssistant.getters.askAssistantSidebarResizer(), [left + 10, 0], {
abs: true,
clickToFinish: true,
});
aiAssistant.getters.askAssistantChat().then((newElement) => {
const newWidth = newElement[0].getBoundingClientRect().width;
expect(newWidth).to.be.lessThan(width);
});
});
});
it('should start chat session from node error view', () => {
cy.intercept('POST', '/rest/ai-assistant/chat', {
statusCode: 200,
fixture: 'aiAssistant/simple_message_response.json',
}).as('chatRequest');
cy.createFixtureWorkflow('aiAssistant/test_workflow.json');
wf.actions.openNode('Stop and Error');
ndv.getters.nodeExecuteButton().click();
aiAssistant.getters.nodeErrorViewAssistantButton().click();
cy.wait('@chatRequest');
aiAssistant.getters.chatMessagesAll().should('have.length', 1);
aiAssistant.getters
.chatMessagesAll()
.eq(0)
.should('contain.text', 'Hey, this is an assistant message');
aiAssistant.getters.nodeErrorViewAssistantButton().should('be.disabled');
});
it('should render chat input correctly', () => {
cy.intercept('POST', '/rest/ai-assistant/chat', {
statusCode: 200,
fixture: 'aiAssistant/simple_message_response.json',
}).as('chatRequest');
cy.createFixtureWorkflow('aiAssistant/test_workflow.json');
wf.actions.openNode('Stop and Error');
ndv.getters.nodeExecuteButton().click();
aiAssistant.getters.nodeErrorViewAssistantButton().click();
cy.wait('@chatRequest');
// Send button should be disabled when input is empty
aiAssistant.getters.sendMessageButton().should('be.disabled');
aiAssistant.getters.chatInput().type('Yo ');
aiAssistant.getters.sendMessageButton().should('not.be.disabled');
aiAssistant.getters.chatInput().then((element) => {
const { height } = element[0].getBoundingClientRect();
// Shift + Enter should add a new line
aiAssistant.getters.chatInput().type('Hello{shift+enter}there');
aiAssistant.getters.chatInput().then((newElement) => {
const newHeight = newElement[0].getBoundingClientRect().height;
// Chat input should grow as user adds new lines
expect(newHeight).to.be.greaterThan(height);
aiAssistant.getters.sendMessageButton().click();
cy.wait('@chatRequest');
// New lines should be rendered as <br> in the chat
aiAssistant.getters.chatMessagesUser().should('have.length', 1);
aiAssistant.getters.chatMessagesUser().eq(0).find('br').should('have.length', 1);
// Chat input should be cleared now
aiAssistant.getters.chatInput().should('have.value', '');
});
});
});
it('should render and handle quick replies', () => {
cy.intercept('POST', '/rest/ai-assistant/chat', {
statusCode: 200,
fixture: 'aiAssistant/quick_reply_message_response.json',
}).as('chatRequest');
cy.createFixtureWorkflow('aiAssistant/test_workflow.json');
wf.actions.openNode('Stop and Error');
ndv.getters.nodeExecuteButton().click();
aiAssistant.getters.nodeErrorViewAssistantButton().click();
cy.wait('@chatRequest');
aiAssistant.getters.quickReplyButtons().should('have.length', 2);
aiAssistant.getters.quickReplyButtons().eq(0).click();
cy.wait('@chatRequest');
aiAssistant.getters.chatMessagesUser().should('have.length', 1);
aiAssistant.getters.chatMessagesUser().eq(0).should('contain.text', "Sure, let's do it");
});
it('should show quick replies when node is executed after new suggestion', () => {
cy.intercept('POST', '/rest/ai-assistant/chat', (req) => {
req.reply((res) => {
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');
cy.createFixtureWorkflow('aiAssistant/test_workflow.json');
wf.actions.openNode('Edit Fields');
ndv.getters.nodeExecuteButton().click();
aiAssistant.getters.nodeErrorViewAssistantButton().click();
cy.wait('@chatRequest');
aiAssistant.getters.chatMessagesAssistant().should('have.length', 1);
ndv.getters.nodeExecuteButton().click();
cy.wait('@chatRequest');
// 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', () => {
cy.intercept('POST', '/rest/ai-assistant/chat', {
statusCode: 200,
fixture: 'aiAssistant/simple_message_response.json',
}).as('chatRequest');
cy.createFixtureWorkflow('aiAssistant/test_workflow.json');
wf.actions.openNode('Edit Fields');
ndv.getters.nodeExecuteButton().click();
aiAssistant.getters.nodeErrorViewAssistantButton().click({ force: true });
cy.wait('@chatRequest');
aiAssistant.getters.closeChatButton().click();
ndv.getters.backToCanvas().click();
wf.actions.openNode('Stop and Error');
ndv.getters.nodeExecuteButton().click();
aiAssistant.getters.nodeErrorViewAssistantButton().click({ force: true });
// Since we already have an active session, a warning should be shown
aiAssistant.getters.newAssistantSessionModal().should('be.visible');
aiAssistant.getters
.newAssistantSessionModal()
.find('button')
.contains('Start new session')
.click();
cy.wait('@chatRequest');
// New session should start with initial assistant message
aiAssistant.getters.chatMessagesAll().should('have.length', 1);
});
it('should apply code diff to code node', () => {
cy.intercept('POST', '/rest/ai-assistant/chat', {
statusCode: 200,
fixture: 'aiAssistant/code_diff_suggestion_response.json',
}).as('chatRequest');
cy.intercept('POST', '/rest/ai-assistant/chat/apply-suggestion', {
statusCode: 200,
fixture: 'aiAssistant/apply_code_diff_response.json',
}).as('applySuggestion');
cy.createFixtureWorkflow('aiAssistant/test_workflow.json');
wf.actions.openNode('Code');
ndv.getters.nodeExecuteButton().click();
aiAssistant.getters.nodeErrorViewAssistantButton().click({ force: true });
cy.wait('@chatRequest');
// Should have two assistant messages
aiAssistant.getters.chatMessagesAll().should('have.length', 2);
aiAssistant.getters.codeDiffs().should('have.length', 1);
aiAssistant.getters.applyCodeDiffButtons().should('have.length', 1);
aiAssistant.getters.applyCodeDiffButtons().first().click();
cy.wait('@applySuggestion');
aiAssistant.getters.applyCodeDiffButtons().should('have.length', 0);
aiAssistant.getters.undoReplaceCodeButtons().should('have.length', 1);
aiAssistant.getters.codeReplacedMessage().should('be.visible');
ndv.getters
.parameterInput('jsCode')
.get('.cm-content')
.should('contain.text', 'item.json.myNewField = 1');
// Clicking undo should revert the code back but not call the assistant
aiAssistant.getters.undoReplaceCodeButtons().first().click();
aiAssistant.getters.applyCodeDiffButtons().should('have.length', 1);
aiAssistant.getters.codeReplacedMessage().should('not.exist');
cy.get('@applySuggestion.all').then((interceptions) => {
expect(interceptions).to.have.length(1);
});
ndv.getters
.parameterInput('jsCode')
.get('.cm-content')
.should('contain.text', 'item.json.myNewField = 1aaa');
// Replacing the code again should also not call the assistant
cy.get('@applySuggestion.all').then((interceptions) => {
expect(interceptions).to.have.length(1);
});
aiAssistant.getters.applyCodeDiffButtons().should('have.length', 1);
aiAssistant.getters.applyCodeDiffButtons().first().click();
ndv.getters
.parameterInput('jsCode')
.get('.cm-content')
.should('contain.text', 'item.json.myNewField = 1');
});
it('should end chat session when `end_session` event is received', () => {
cy.intercept('POST', '/rest/ai-assistant/chat', {
statusCode: 200,
fixture: 'aiAssistant/end_session_response.json',
}).as('chatRequest');
cy.createFixtureWorkflow('aiAssistant/test_workflow.json');
wf.actions.openNode('Stop and Error');
ndv.getters.nodeExecuteButton().click();
aiAssistant.getters.nodeErrorViewAssistantButton().click();
cy.wait('@chatRequest');
aiAssistant.getters.chatMessagesSystem().should('have.length', 1);
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');
});
});
describe('General help', () => {
beforeEach(() => {
aiAssistant.actions.enableAssistant();
wf.actions.visit();
});
it('assistant returns code snippet', () => {
cy.intercept('POST', '/rest/ai-assistant/chat', {
statusCode: 200,
fixture: 'aiAssistant/code_snippet_response.json',
}).as('chatRequest');
aiAssistant.getters.askAssistantFloatingButton().should('be.visible');
aiAssistant.getters.askAssistantFloatingButton().click();
aiAssistant.getters.askAssistantChat().should('be.visible');
aiAssistant.getters.placeholderMessage().should('be.visible');
aiAssistant.getters.chatInput().should('be.visible');
aiAssistant.getters.chatInput().type('Show me an expression');
aiAssistant.getters.sendMessageButton().click();
aiAssistant.getters.chatMessagesAll().should('have.length', 3);
aiAssistant.getters.chatMessagesUser().eq(0).should('contain.text', 'Show me an expression');
aiAssistant.getters
.chatMessagesAssistant()
.eq(0)
.should('contain.text', 'To use expressions in n8n, follow these steps:');
aiAssistant.getters
.chatMessagesAssistant()
.eq(0)
.should(
'include.html',
`<pre><code class="language-json">[
{
"headers": {
"host": "n8n.instance.address",
...
},
"params": {},
"query": {},
"body": {
"name": "Jim",
"age": 30,
"city": "New York"
}
}
]
</code></pre>`,
);
aiAssistant.getters.codeSnippet().should('have.text', '{{$json.body.city}}');
});
});

View file

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

View file

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

View file

@ -132,6 +132,10 @@ describe('NDV', () => {
'contains.text',
"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', () => {
@ -199,7 +203,7 @@ describe('NDV', () => {
.contains(key)
.should('be.visible');
});
getObjectValueItem().find('label').click();
getObjectValueItem().find('label').click({ force: true });
expandedObjectProps.forEach((key) => {
ndv.getters
.outputPanel()
@ -582,7 +586,13 @@ describe('NDV', () => {
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).click();
ndv.getters
.outputDisplayMode()
.find('label')
.eq(1)
.scrollIntoView()
.should('be.visible')
.click();
ndv.getters.outputDataContainer().find('.json-data').should('exist');
ndv.getters

View file

@ -38,6 +38,51 @@ describe('Code node', () => {
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', () => {

View file

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

View file

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

View file

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

View file

@ -0,0 +1,8 @@
{
"data": {
"sessionId": "f9130bd7-c078-4862-a38a-369b27b0ff20-e96eb9f7-d581-4684-b6a9-fd3dfe9fe1fb-emTezIGat7bQsDdtIlbti",
"parameters": {
"jsCode": "// Loop over input items and add a new field called 'myNewField' to the JSON of each one\nfor (const item of $input.all()) {\n item.json.myNewField = 1;\n}\n\nreturn $input.all();"
}
}
}

View file

@ -0,0 +1,23 @@
{
"sessionId": "1",
"messages": [
{
"role": "assistant",
"type": "message",
"text": "Hi there! Here is my top solution to fix the error in your **Code** node 👇"
},
{
"type": "code-diff",
"description": "Fix the syntax error by changing '1asd' to a valid value. In this case, it seems like '1' was intended.",
"suggestionId": "1",
"codeDiff": "@@ -2,2 +2,2 @@\n item.json.myNewField = 1asd;\n+ item.json.myNewField = 1;\n",
"role": "assistant",
"quickReplies": [
{
"text": "Give me another solution",
"type": "new-suggestion"
}
]
}
]
}

View file

@ -0,0 +1,28 @@
{
"sessionId": "f1d19ed5-0d55-4bad-b49a-f0c56bd6f76f-705b5dbf-12d4-4805-87a3-1e5b3c716d29-W1JgVNrpfitpSNF9rAjB4",
"messages": [
{
"role": "assistant",
"type": "message",
"text": "To use expressions in n8n, follow these steps:\n\n1. Hover over the parameter where you want to use an expression.\n2. Select **Expressions** in the **Fixed/Expression** toggle.\n3. Write your expression in the parameter, or select **Open expression editor** to open the expressions editor. You can browse the available data in the **Variable selector**. All expressions have the format `{{ your expression here }}`.\n\n### Example: Get data from webhook body\n\nIf your webhook data looks like this:\n\n```json\n[\n {\n \"headers\": {\n \"host\": \"n8n.instance.address\",\n ...\n },\n \"params\": {},\n \"query\": {},\n \"body\": {\n \"name\": \"Jim\",\n \"age\": 30,\n \"city\": \"New York\"\n }\n }\n]\n```\n\nYou can use the following expression to get the value of `city`:\n\n```js\n{{$json.body.city}}\n```\n\nThis expression accesses the incoming JSON-formatted data using n8n's custom `$json` variable and finds the value of `city` (in this example, \"New York\").",
"codeSnippet": "{{$json.body.city}}"
},
{
"role": "assistant",
"type": "message",
"text": "Did this answer solve your question?",
"quickReplies": [
{
"text": "Yes, thanks",
"type": "all-good",
"isFeedback": true
},
{
"text": "No, I am still stuck",
"type": "still-stuck",
"isFeedback": true
}
]
}
]
}

View file

@ -0,0 +1,16 @@
{
"sessionId": "1",
"messages": [
{
"role": "assistant",
"type": "message",
"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!"
},
{
"role": "assistant",
"type": "event",
"eventName": "end-session"
}
]
}

View file

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

View file

@ -0,0 +1,20 @@
{
"sessionId": "1",
"messages": [
{
"role": "assistant",
"type": "message",
"text": "Hey, this is an assistant message",
"quickReplies": [
{
"text": "Sure, let's do it",
"type": "yes"
},
{
"text": "Nah, doesn't sound good",
"type": "no"
}
]
}
]
}

View file

@ -0,0 +1,10 @@
{
"sessionId": "1",
"messages": [
{
"role": "assistant",
"type": "message",
"text": "Hey, this is an assistant message"
}
]
}

View file

@ -0,0 +1,88 @@
{
"nodes": [
{
"parameters": {},
"id": "ebfced75-2ce1-4c41-a971-6c3b83522c4d",
"name": "When clicking Test workflow",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
360,
220
]
},
{
"parameters": {
"errorMessage": "This is an error message"
},
"id": "f2e60459-401a-49d5-acfc-7b2b31cfdcf7",
"name": "Stop and Error",
"type": "n8n-nodes-base.stopAndError",
"typeVersion": 1,
"position": [
1020,
220
]
},
{
"parameters": {
"jsCode": "// Loop over input items and add a new field called 'myNewField' to the JSON of each one\nfor (const item of $input.all()) {\n item.json.myNewField = 1aaa;\n}\n\nreturn $input.all();"
},
"id": "b54d4db9-b257-41a8-862f-26d293115bad",
"name": "Code",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
840,
320
]
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "053ada73-f7db-4e6a-8cc8-85756cc6ca4e",
"name": "age",
"value": "={{ 32sad }}",
"type": "number"
}
]
},
"options": {}
},
"id": "5fd89612-a871-4679-b7b0-d659e09c6a0e",
"name": "Edit Fields",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
600,
100
]
}
],
"connections": {
"When clicking Test workflow": {
"main": [
[
{
"node": "Stop and Error",
"type": "main",
"index": 0
},
{
"node": "Code",
"type": "main",
"index": 0
},
{
"node": "Edit Fields",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {}
}

View file

@ -14,7 +14,7 @@
"start": "cd ..; pnpm start"
},
"devDependencies": {
"@types/lodash": "^4.14.195",
"@types/lodash": "catalog:",
"eslint-plugin-cypress": "^3.3.0",
"n8n-workflow": "workspace:*"
},
@ -24,8 +24,8 @@
"cypress": "^13.11.0",
"cypress-otp": "^1.0.3",
"cypress-real-events": "^1.12.0",
"lodash": "4.17.21",
"nanoid": "3.3.6",
"start-server-and-test": "^2.0.3"
"lodash": "catalog:",
"nanoid": "catalog:",
"start-server-and-test": "^2.0.5"
}
}

View file

@ -0,0 +1,64 @@
import { overrideFeatureFlag } from '../../composables/featureFlags';
import { BasePage } from '../base';
const AI_ASSISTANT_FEATURE = {
name: 'aiAssistant',
experimentName: '021_ai_debug_helper',
enabledFor: 'variant',
disabledFor: 'control',
};
export class AIAssistant extends BasePage {
url = '/workflows/new';
getters = {
askAssistantFloatingButton: () => cy.getByTestId('ask-assistant-floating-button'),
askAssistantSidebar: () => cy.getByTestId('ask-assistant-sidebar'),
askAssistantSidebarResizer: () =>
this.getters.askAssistantSidebar().find('[class^=_resizer][data-dir=left]').first(),
askAssistantChat: () => cy.getByTestId('ask-assistant-chat'),
placeholderMessage: () => cy.getByTestId('placeholder-message'),
closeChatButton: () => cy.getByTestId('close-chat-button'),
chatInputWrapper: () => cy.getByTestId('chat-input-wrapper'),
chatInput: () => cy.getByTestId('chat-input'),
sendMessageButton: () => cy.getByTestId('send-message-button'),
chatMessagesAll: () => cy.get('[data-test-id^=chat-message]'),
chatMessagesAssistant: () => cy.getByTestId('chat-message-assistant'),
chatMessagesUser: () => cy.getByTestId('chat-message-user'),
chatMessagesSystem: () => cy.getByTestId('chat-message-system'),
quickReplies: () => cy.getByTestId('quick-replies'),
quickReplyButtons: () => this.getters.quickReplies().find('button'),
newAssistantSessionModal: () => cy.getByTestId('new-assistant-session-modal'),
codeDiffs: () => cy.getByTestId('code-diff-suggestion'),
applyCodeDiffButtons: () => cy.getByTestId('replace-code-button'),
undoReplaceCodeButtons: () => cy.getByTestId('undo-replace-button'),
codeReplacedMessage: () => cy.getByTestId('code-replaced-message'),
nodeErrorViewAssistantButton: () =>
cy.getByTestId('node-error-view-ask-assistant-button').find('button').first(),
credentialEditAssistantButton: () =>
cy.getByTestId('credentail-edit-ask-assistant-button').find('button').first(),
codeSnippet: () => cy.getByTestId('assistant-code-snippet'),
};
actions = {
enableAssistant: () => {
overrideFeatureFlag(AI_ASSISTANT_FEATURE.experimentName, AI_ASSISTANT_FEATURE.enabledFor);
cy.enableFeature(AI_ASSISTANT_FEATURE.name);
},
disableAssistant: () => {
overrideFeatureFlag(AI_ASSISTANT_FEATURE.experimentName, AI_ASSISTANT_FEATURE.disabledFor);
cy.disableFeature(AI_ASSISTANT_FEATURE.name);
},
sendMessage: (message: string) => {
this.getters.chatInput().type(message).type('{enter}');
},
closeChat: () => {
this.getters.closeChatButton().click();
this.getters.askAssistantChat().should('not.be.visible');
},
openChat: () => {
this.getters.askAssistantFloatingButton().click();
this.getters.askAssistantChat().should('be.visible');
},
};
}

View file

@ -24,6 +24,7 @@ export class NDV extends BasePage {
editPinnedDataButton: () => cy.getByTestId('ndv-edit-pinned-data'),
pinnedDataEditor: () => this.getters.outputPanel().find('.cm-editor .cm-scroller .cm-content'),
runDataPaneHeader: () => cy.getByTestId('run-data-pane-header'),
aiOutputModeToggle: () => cy.getByTestId('ai-output-mode-select'),
nodeOutputHint: () => cy.getByTestId('ndv-output-run-node-hint'),
savePinnedDataButton: () =>
this.getters.runDataPaneHeader().find('button').filter(':visible').contains('Save'),
@ -137,6 +138,8 @@ export class NDV extends BasePage {
cy.getByTestId(`fixed-collection-${paramName}`),
schemaViewNode: () => cy.getByTestId('run-data-schema-node'),
schemaViewNodeName: () => cy.getByTestId('run-data-schema-node-name'),
expressionExpanders: () => cy.getByTestId('expander'),
expressionModalOutput: () => cy.getByTestId('expression-modal-output'),
};
actions = {
@ -174,7 +177,7 @@ export class NDV extends BasePage {
this.getters.editPinnedDataButton().click();
this.getters.pinnedDataEditor().click();
this.getters.pinnedDataEditor().type('{selectall}{backspace}').paste(JSON.stringify(data));
this.getters.pinnedDataEditor().invoke('text', '').paste(JSON.stringify(data));
this.actions.savePinnedData();
},
@ -204,9 +207,9 @@ export class NDV extends BasePage {
const droppable = `[data-test-id="parameter-input-${parameterName}"]`;
cy.draganddrop(draggable, droppable);
},
mapToParameter: (parameterName: string) => {
mapToParameter: (parameterName: string, position?: 'top' | 'center' | 'bottom') => {
const droppable = `[data-test-id="parameter-input-${parameterName}"]`;
cy.draganddrop('', droppable);
cy.draganddrop('', droppable, { position });
},
switchInputMode: (type: 'Schema' | 'Table' | 'JSON' | 'Binary') => {
this.getters.inputDisplayMode().find('label').contains(type).click({ force: true });

View file

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

View file

@ -59,14 +59,18 @@ Cypress.Commands.add('waitForLoad', (waitForIntercepts = true) => {
Cypress.Commands.add('signin', ({ email, password }) => {
void Cypress.session.clearAllSavedSessions();
cy.session([email, password], () =>
cy.request({
method: 'POST',
url: `${BACKEND_BASE_URL}/rest/login`,
body: { email, password },
failOnStatusCode: false,
}),
);
cy.session([email, password], () => {
return cy
.request({
method: 'POST',
url: `${BACKEND_BASE_URL}/rest/login`,
body: { email, password },
failOnStatusCode: false,
})
.then((response) => {
Cypress.env('currentUserId', response.body.data.id);
});
});
});
Cypress.Commands.add('signinAsOwner', () => cy.signin(INSTANCE_OWNER));
@ -175,7 +179,7 @@ Cypress.Commands.add('drag', (selector, pos, options) => {
});
});
Cypress.Commands.add('draganddrop', (draggableSelector, droppableSelector) => {
Cypress.Commands.add('draganddrop', (draggableSelector, droppableSelector, options) => {
if (draggableSelector) {
cy.get(draggableSelector).should('exist');
}
@ -197,7 +201,7 @@ Cypress.Commands.add('draganddrop', (draggableSelector, droppableSelector) => {
cy.get(droppableSelector).realMouseMove(0, 0);
cy.get(droppableSelector).realMouseMove(pageX, pageY);
cy.get(droppableSelector).realHover();
cy.get(droppableSelector).realMouseUp();
cy.get(droppableSelector).realMouseUp({ position: options?.position ?? 'top' });
if (draggableSelector) {
cy.get(draggableSelector).realMouseUp();
}

View file

@ -17,6 +17,8 @@ beforeEach(() => {
cy.window().then((win): void => {
win.localStorage.setItem('N8N_THEME', 'light');
win.localStorage.setItem('N8N_AUTOCOMPLETE_ONBOARDED', 'true');
win.localStorage.setItem('N8N_MAPPING_ONBOARDED', 'true');
});
cy.intercept('GET', '/rest/settings', (req) => {

View file

@ -12,6 +12,10 @@ interface SigninPayload {
password: string;
}
interface DragAndDropOptions {
position: 'top' | 'center' | 'bottom';
}
declare global {
namespace Cypress {
interface SuiteConfigOverrides {
@ -56,7 +60,11 @@ declare global {
target: [number, number],
options?: { abs?: boolean; index?: number; realMouse?: boolean; clickToFinish?: boolean },
): void;
draganddrop(draggableSelector: string, droppableSelector: string): void;
draganddrop(
draggableSelector: string,
droppableSelector: string,
options?: Partial<DragAndDropOptions>,
): void;
push(type: string, data: unknown): void;
shouldNotHaveConsoleErrors(): void;
window(): Chainable<

View file

@ -3,7 +3,7 @@ export function getPopper() {
}
export function getVisiblePopper() {
return getPopper().filter(':visible');
return getPopper().filter('[aria-hidden="false"]');
}
export function getVisibleSelect() {

View file

@ -230,6 +230,4 @@ Before you upgrade to the latest version make sure to check here if there are an
## License
n8n is [fair-code](https://faircode.io) distributed under the [**Sustainable Use License**](https://github.com/n8n-io/n8n/blob/master/packages/cli/LICENSE.md).
Additional information about the license can be found in the [docs](https://docs.n8n.io/reference/license/).
You can find the license information [here](https://github.com/n8n-io/n8n/blob/master/README.md#license)

View file

@ -1,22 +1,23 @@
{
"name": "n8n-monorepo",
"version": "1.52.0",
"version": "1.59.0",
"private": true,
"engines": {
"node": ">=18.10",
"pnpm": ">=9.1"
"node": ">=20.15",
"pnpm": ">=9.5"
},
"packageManager": "pnpm@9.1.4",
"packageManager": "pnpm@9.6.0",
"scripts": {
"preinstall": "node scripts/block-npm-install.js",
"build": "turbo run build",
"build:backend": "turbo run build:backend",
"build:frontend": "turbo run build:frontend",
"build:nodes": "turbo run build:nodes",
"typecheck": "turbo --filter=!n8n typecheck",
"typecheck": "turbo typecheck",
"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",
"clean": "turbo run clean --parallel",
"reset": "node scripts/ensure-zx.mjs && zx scripts/reset.mjs",
"format": "turbo run format && node scripts/format.mjs",
"lint": "turbo run lint",
"lintfix": "turbo run lintfix",
@ -40,7 +41,6 @@
"@n8n_io/eslint-config": "workspace:*",
"@types/jest": "^29.5.3",
"@types/supertest": "^6.0.2",
"@vitest/coverage-v8": "^1.6.0",
"jest": "^29.6.2",
"jest-environment-jsdom": "^29.6.2",
"jest-expect-message": "^1.1.3",
@ -57,10 +57,7 @@
"tsc-watch": "^6.0.4",
"turbo": "2.0.6",
"typescript": "*",
"vite": "^5.2.12",
"vitest": "^1.6.0",
"vitest-mock-extended": "^1.3.1",
"vue-tsc": "^2.0.19"
"zx": "^8.1.4"
},
"pnpm": {
"onlyBuiltDependencies": [
@ -68,7 +65,6 @@
],
"overrides": {
"@types/node": "^18.16.16",
"axios": "1.6.7",
"chokidar": "3.5.2",
"esbuild": "^0.20.2",
"formidable": "3.5.1",
@ -77,7 +73,7 @@
"semver": "^7.5.4",
"tslib": "^2.6.2",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.5.2",
"typescript": "^5.6.2",
"ws": ">=8.17.1"
},
"patchedDependencies": {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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