From cdb93086a496e342570d1efbacd3c1cb5ed3020b Mon Sep 17 00:00:00 2001 From: Tomi Turtiainen <10324676+tomi@users.noreply.github.com> Date: Wed, 14 Aug 2024 16:53:54 +0300 Subject: [PATCH 01/17] feat: Add n8n-benchmark cli Adds a new cli tool for executing benchmark tests against any n8n instance. The idea is to treat n8n as a black box and use webhook triggers to initiate workflow executions. The tool consists of following parts: - Test scenarios - Consists of test data (workflow(s)) and k6 script to run the benchmark - In the `testScenarios` folder - n8n-benchmark cli - In the `src` folder - Based on oclif - Two commands: - list : lists all available test scenarios - run : runs all available test scenarios (or specified ones in the future) Test scenario execution works as follows: - Setup: setup owner if needed, login - For each test scenario: - Import the specified test data using n8n internal API - Run the benchmark and print out results This is the first step in creating an automated benchmark test setup. Next step is to add the test environment setup and orchestration to run the benchmarks there. The feasibility of the approach has been validated in [CAT-26](https://linear.app/n8n/issue/CAT-26/spike-select-infra-to-run-the-benchmarks) See the [linear project](https://linear.app/n8n/project/n8n-benchmarking-suite-3e7679d176b4/overview) for more details --- packages/benchmark/Dockerfile | 67 ++++ packages/benchmark/README.md | 33 ++ packages/benchmark/bin/n8n-benchmark | 13 + packages/benchmark/nodemon.json | 6 + packages/benchmark/package.json | 41 ++ packages/benchmark/src/commands/list.ts | 21 + packages/benchmark/src/commands/run.ts | 39 ++ packages/benchmark/src/config/config.ts | 50 +++ .../n8nApiClient/authenticatedN8nApiClient.ts | 68 ++++ .../src/n8nApiClient/n8nApiClient.ts | 78 ++++ .../src/n8nApiClient/n8nApiClient.types.ts | 8 + .../src/n8nApiClient/workflowsApiClient.ts | 50 +++ .../benchmark/src/testExecution/k6Executor.ts | 28 ++ .../src/testExecution/testDataImporter.ts | 57 +++ .../src/testExecution/testScenarioRunner.ts | 50 +++ .../src/testScenario/testDataLoader.ts | 31 ++ .../src/testScenario/testScenarioLoader.ts | 67 ++++ packages/benchmark/src/types/testScenario.ts | 26 ++ .../testScenarios/codeNode/codeNode.json | 36 ++ .../codeNode/codeNode.manifest.json | 9 + .../testScenarios/codeNode/codeNode.script.ts | 11 + .../singleWebhook/singleWebhook.json | 25 ++ .../singleWebhook/singleWebhook.manifest.json | 6 + .../singleWebhook/singleWebhook.script.ts | 11 + .../testScenarios/testScenario.schema.json | 42 ++ packages/benchmark/tsconfig.build.json | 9 + packages/benchmark/tsconfig.json | 13 + pnpm-lock.yaml | 378 +++++++++++++----- 28 files changed, 1163 insertions(+), 110 deletions(-) create mode 100644 packages/benchmark/Dockerfile create mode 100644 packages/benchmark/README.md create mode 100755 packages/benchmark/bin/n8n-benchmark create mode 100644 packages/benchmark/nodemon.json create mode 100644 packages/benchmark/package.json create mode 100644 packages/benchmark/src/commands/list.ts create mode 100644 packages/benchmark/src/commands/run.ts create mode 100644 packages/benchmark/src/config/config.ts create mode 100644 packages/benchmark/src/n8nApiClient/authenticatedN8nApiClient.ts create mode 100644 packages/benchmark/src/n8nApiClient/n8nApiClient.ts create mode 100644 packages/benchmark/src/n8nApiClient/n8nApiClient.types.ts create mode 100644 packages/benchmark/src/n8nApiClient/workflowsApiClient.ts create mode 100644 packages/benchmark/src/testExecution/k6Executor.ts create mode 100644 packages/benchmark/src/testExecution/testDataImporter.ts create mode 100644 packages/benchmark/src/testExecution/testScenarioRunner.ts create mode 100644 packages/benchmark/src/testScenario/testDataLoader.ts create mode 100644 packages/benchmark/src/testScenario/testScenarioLoader.ts create mode 100644 packages/benchmark/src/types/testScenario.ts create mode 100644 packages/benchmark/testScenarios/codeNode/codeNode.json create mode 100644 packages/benchmark/testScenarios/codeNode/codeNode.manifest.json create mode 100644 packages/benchmark/testScenarios/codeNode/codeNode.script.ts create mode 100644 packages/benchmark/testScenarios/singleWebhook/singleWebhook.json create mode 100644 packages/benchmark/testScenarios/singleWebhook/singleWebhook.manifest.json create mode 100644 packages/benchmark/testScenarios/singleWebhook/singleWebhook.script.ts create mode 100644 packages/benchmark/testScenarios/testScenario.schema.json create mode 100644 packages/benchmark/tsconfig.build.json create mode 100644 packages/benchmark/tsconfig.json diff --git a/packages/benchmark/Dockerfile b/packages/benchmark/Dockerfile new file mode 100644 index 0000000000000..18673141fd565 --- /dev/null +++ b/packages/benchmark/Dockerfile @@ -0,0 +1,67 @@ +# NOTE: +# This Dockerfile needs to be built in the root of the repository +# `docker build -t n8n-benchmark -f packages/benchmark/Dockerfile .` +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/benchmark/package.json /app/packages/benchmark/package.json +COPY --chown=node:node ./patches /app/patches +COPY --chown=node:node ./scripts /app/scripts + +RUN pnpm install --frozen-lockfile + +# Package.json +COPY --chown=node:node ./packages/benchmark/package.json /app/packages/benchmark/package.json + +# 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/benchmark/tsconfig.json /app/packages/benchmark/tsconfig.json +COPY --chown=node:node ./packages/benchmark/tsconfig.build.json /app/packages/benchmark/tsconfig.build.json + +# Source files +COPY --chown=node:node ./packages/benchmark/src /app/packages/benchmark/src +COPY --chown=node:node ./packages/benchmark/bin /app/packages/benchmark/bin +COPY --chown=node:node ./packages/benchmark/testScenarios /app/packages/benchmark/testScenarios + +WORKDIR /app/packages/benchmark +RUN pnpm build + +# +# Runner +FROM base AS runner + +COPY --from=builder /app /app + +WORKDIR /app/packages/benchmark +USER node + +ENTRYPOINT [ "/app/packages/benchmark/bin/n8n-benchmark" ] diff --git a/packages/benchmark/README.md b/packages/benchmark/README.md new file mode 100644 index 0000000000000..eaaa36631371b --- /dev/null +++ b/packages/benchmark/README.md @@ -0,0 +1,33 @@ +# n8n benchmarking tool + +Tool for executing benchmarks against an n8n instance. + +## Requirements + +- [k6](https://grafana.com/docs/k6/latest/) +- Node.js v20 or higher + +## Running locally + +```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 + +# If you installed k6 using brew, you might have to specify it explicitly +K6_PATH=/opt/homebrew/bin/k6 N8N_USER_EMAIL=user@n8n.io N8N_USER_PASSWORD=password ./bin/n8n-benchmark run +``` + +## Configuration + +The configuration options the cli accepts can be seen from [config.ts](./src/config/config.ts) + +## Docker build + +Because k6 doesn't have an arm64 build available for linux, we need to build against amd64 + +```sh +# In the repository root +docker build --platform linux/amd64 -f packages/benchmark/Dockerfile -t n8n-benchmark:latest . +``` diff --git a/packages/benchmark/bin/n8n-benchmark b/packages/benchmark/bin/n8n-benchmark new file mode 100755 index 0000000000000..c7f0996f09476 --- /dev/null +++ b/packages/benchmark/bin/n8n-benchmark @@ -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 }); +})(); diff --git a/packages/benchmark/nodemon.json b/packages/benchmark/nodemon.json new file mode 100644 index 0000000000000..393762d234d2d --- /dev/null +++ b/packages/benchmark/nodemon.json @@ -0,0 +1,6 @@ +{ + "ignore": ["**/*.spec.ts", ".git", "node_modules"], + "watch": ["commands", "index.ts", "src"], + "exec": "npm start", + "ext": "ts" +} diff --git a/packages/benchmark/package.json b/packages/benchmark/package.json new file mode 100644 index 0000000000000..c1d2ba6b4b0cd --- /dev/null +++ b/packages/benchmark/package.json @@ -0,0 +1,41 @@ +{ + "name": "n8n-benchmark", + "version": "1.0.0", + "description": "", + "main": "dist/index", + "scripts": { + "build": "tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json", + "start": "./bin/n8n-benchmark", + "test": "echo \"Error: no test specified\" && exit 1", + "typecheck": "tsc --noEmit", + "watch": "concurrently \"tsc -w -p tsconfig.build.json\" \"tsc-alias -w -p tsconfig.build.json\"" + }, + "engines": { + "node": ">=20.10" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "@oclif/core": "4.0.7", + "axios": "catalog:", + "convict": "6.2.4", + "dotenv": "8.6.0", + "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": " " + } +} diff --git a/packages/benchmark/src/commands/list.ts b/packages/benchmark/src/commands/list.ts new file mode 100644 index 0000000000000..603f746f286ae --- /dev/null +++ b/packages/benchmark/src/commands/list.ts @@ -0,0 +1,21 @@ +import { Command } from '@oclif/core'; +import { TestScenarioLoader } from '@/testScenario/testScenarioLoader'; +import { loadConfig } from '@/config/config'; + +export default class ListCommand extends Command { + static description = 'List all available test scenarios'; + + async run() { + const config = loadConfig(); + const testScenarioLoader = new TestScenarioLoader(); + + const allScenarios = testScenarioLoader.loadAllTestScenarios(config.get('testScenariosPath')); + + console.log('Available test scenarios:'); + console.log(''); + + for (const testCase of allScenarios) { + console.log('\t', testCase.name, ':', testCase.description); + } + } +} diff --git a/packages/benchmark/src/commands/run.ts b/packages/benchmark/src/commands/run.ts new file mode 100644 index 0000000000000..0b524906e3e20 --- /dev/null +++ b/packages/benchmark/src/commands/run.ts @@ -0,0 +1,39 @@ +import { Command, Flags } from '@oclif/core'; +import { loadConfig } from '@/config/config'; +import { TestScenarioLoader } from '@/testScenario/testScenarioLoader'; +import { TestScenarioRunner } from '@/testExecution/testScenarioRunner'; +import { N8nApiClient } from '@/n8nApiClient/n8nApiClient'; +import { TestDataFileLoader } from '@/testScenario/testDataLoader'; +import { K6Executor } from '@/testExecution/k6Executor'; + +export default class RunCommand extends Command { + static description = 'Run all (default) or specified test scenarios'; + + // TODO: Add support for filtering scenarios + static flags = { + scenarios: Flags.string({ + char: 't', + description: 'Comma-separated list of test scenarios to run', + required: false, + }), + }; + + async run() { + const config = loadConfig(); + const testScenarioLoader = new TestScenarioLoader(); + + const testScenarioRunner = new TestScenarioRunner( + new N8nApiClient(config.get('n8n.baseUrl')), + new TestDataFileLoader(), + new K6Executor(config.get('k6ExecutablePath'), config.get('n8n.baseUrl')), + { + email: config.get('n8n.user.email'), + password: config.get('n8n.user.password'), + }, + ); + + const allScenarios = testScenarioLoader.loadAllTestScenarios(config.get('testScenariosPath')); + + await testScenarioRunner.runManyTestScenarios(allScenarios); + } +} diff --git a/packages/benchmark/src/config/config.ts b/packages/benchmark/src/config/config.ts new file mode 100644 index 0000000000000..d20bfbe8779f8 --- /dev/null +++ b/packages/benchmark/src/config/config.ts @@ -0,0 +1,50 @@ +import convict from 'convict'; +import dotenv from 'dotenv'; + +dotenv.config(); + +const configSchema = { + testScenariosPath: { + doc: 'The path to the test scenarios', + format: String, + default: 'testScenarios', + }, + n8n: { + baseUrl: { + doc: 'The base URL for the n8n instance', + format: String, + default: 'http://localhost:5678', + env: 'N8N_BASE_URL', + }, + user: { + email: { + doc: 'The email address of the n8n user', + format: String, + default: 'benchmark-user@n8n.io', + env: 'N8N_USER_EMAIL', + }, + password: { + doc: 'The password of the n8n user', + format: String, + default: 'VerySecret!123', + env: 'N8N_USER_PASSWORD', + }, + }, + }, + k6ExecutablePath: { + doc: 'The path to the k6 binary', + format: String, + default: 'k6', + env: 'K6_PATH', + }, +}; + +export type Config = ReturnType; + +export function loadConfig() { + const config = convict(configSchema); + + config.validate({ allowed: 'strict' }); + + return config; +} diff --git a/packages/benchmark/src/n8nApiClient/authenticatedN8nApiClient.ts b/packages/benchmark/src/n8nApiClient/authenticatedN8nApiClient.ts new file mode 100644 index 0000000000000..16f5058e18aa2 --- /dev/null +++ b/packages/benchmark/src/n8nApiClient/authenticatedN8nApiClient.ts @@ -0,0 +1,68 @@ +import { strict as assert } from 'node:assert'; +import { N8nApiClient } from './n8nApiClient'; +import { AxiosRequestConfig } from 'axios'; + +export class AuthenticatedN8nApiClient extends N8nApiClient { + constructor( + apiBaseUrl: string, + private readonly authCookie: string, + ) { + super(apiBaseUrl); + } + + static async createUsingUsernameAndPassword( + apiClient: N8nApiClient, + loginDetails: { + email: string; + password: string; + }, + ) { + const response = await apiClient.restApiRequest('/login', { + method: 'POST', + data: loginDetails, + }); + + const cookieHeader = response.headers['set-cookie']; + const authCookie = Array.isArray(cookieHeader) ? cookieHeader.join('; ') : cookieHeader; + assert(authCookie); + + return new AuthenticatedN8nApiClient(apiClient.apiBaseUrl, authCookie); + } + + async get(endpoint: string) { + return await this.authenticatedRequest(endpoint, { + method: 'GET', + }); + } + + async post(endpoint: string, data: unknown) { + return await this.authenticatedRequest(endpoint, { + method: 'POST', + data, + }); + } + + async patch(endpoint: string, data: unknown) { + return await this.authenticatedRequest(endpoint, { + method: 'PATCH', + data, + }); + } + + async delete(endpoint: string, data?: unknown) { + return await this.authenticatedRequest(endpoint, { + method: 'DELETE', + data, + }); + } + + protected async authenticatedRequest(endpoint: string, init: Omit) { + return await this.restApiRequest(endpoint, { + ...init, + headers: { + ...init.headers, + cookie: this.authCookie, + }, + }); + } +} diff --git a/packages/benchmark/src/n8nApiClient/n8nApiClient.ts b/packages/benchmark/src/n8nApiClient/n8nApiClient.ts new file mode 100644 index 0000000000000..5c5c94b555cf4 --- /dev/null +++ b/packages/benchmark/src/n8nApiClient/n8nApiClient.ts @@ -0,0 +1,78 @@ +import axios, { AxiosError, AxiosRequestConfig } from 'axios'; + +export class N8nApiClient { + constructor(public readonly apiBaseUrl: string) {} + + async waitForInstanceToBecomeOnline(): Promise { + const healthEndpoint = 'healthz'; + const startTime = Date.now(); + const intervalMs = 1000; + const timeout = 60000; + + while (Date.now() - startTime < timeout) { + try { + const response = await axios.request({ + url: `${this.apiBaseUrl}/${healthEndpoint}`, + method: 'GET', + }); + + if (response.status === 200 && response.data.status === 'ok') { + return; + } + } catch {} + + console.log(`n8n instance not online yet, retrying in ${intervalMs / 1000} seconds...`); + await this.delay(intervalMs); + } + + throw new Error(`n8n instance did not come online within ${timeout / 1000} seconds`); + } + + async setupOwnerIfNeeded(loginDetails: { email: string; password: string }) { + const response = await this.restApiRequest<{ message: string }>('/owner/setup', { + method: 'POST', + data: { + email: loginDetails.email, + password: loginDetails.password, + firstName: 'Test', + lastName: 'User', + }, + // Don't throw on non-2xx responses + validateStatus: () => true, + }); + + const responsePayload = response.data; + + if (response.status === 200) { + console.log('Owner setup successful'); + } else if (response.status === 400) { + if (responsePayload.message === 'Instance owner already setup') + console.log('Owner already set up'); + } else { + throw new Error( + `Owner setup failed with status ${response.status}: ${responsePayload.message}`, + ); + } + } + + async restApiRequest(endpoint: string, init: Omit) { + try { + return await axios.request({ + ...init, + url: this.getRestEndpointUrl(endpoint), + }); + } catch (e) { + const error = e as AxiosError; + console.error(`[ERROR] Request failed ${init.method} ${endpoint}`, error?.response?.data); + throw error; + } + } + + protected getRestEndpointUrl(endpoint: string) { + return `${this.apiBaseUrl}/rest${endpoint}`; + } + + private delay(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); + } +} diff --git a/packages/benchmark/src/n8nApiClient/n8nApiClient.types.ts b/packages/benchmark/src/n8nApiClient/n8nApiClient.types.ts new file mode 100644 index 0000000000000..9758d66b32d4b --- /dev/null +++ b/packages/benchmark/src/n8nApiClient/n8nApiClient.types.ts @@ -0,0 +1,8 @@ +/** + * n8n workflow. This is a simplified version of the actual workflow object. + */ +export interface Workflow { + id: string; + name: string; + tags?: string[]; +} diff --git a/packages/benchmark/src/n8nApiClient/workflowsApiClient.ts b/packages/benchmark/src/n8nApiClient/workflowsApiClient.ts new file mode 100644 index 0000000000000..5d871a8d17194 --- /dev/null +++ b/packages/benchmark/src/n8nApiClient/workflowsApiClient.ts @@ -0,0 +1,50 @@ +import { Workflow } from '@/n8nApiClient/n8nApiClient.types'; +import { AuthenticatedN8nApiClient } from './authenticatedN8nApiClient'; + +export class WorkflowApiClient { + private readonly apiEndpoint = '/workflows'; + + constructor(private readonly apiClient: AuthenticatedN8nApiClient) {} + + async getAllWorkflows(): Promise { + const response = await this.apiClient.get<{ count: number; data: Workflow[] }>( + this.apiEndpoint, + ); + + return response.data.data; + } + + async createWorkflow(workflow: unknown): Promise { + const response = await this.apiClient.post<{ data: Workflow }>(this.apiEndpoint, workflow); + + return response.data.data; + } + + async activateWorkflow(workflow: Workflow): Promise { + const response = await this.apiClient.patch<{ data: Workflow }>( + `${this.apiEndpoint}/${workflow.id}`, + { + ...workflow, + active: true, + }, + ); + + return response.data.data; + } + + async deleteWorkflow(workflowId: Workflow['id']): Promise { + await this.apiClient.delete(`${this.apiEndpoint}/${workflowId}`); + } + + async tagWorkflow(workflow: Workflow, tagId: string): Promise { + const response = await this.apiClient.patch<{ data: Workflow }>( + `${this.apiEndpoint}/${workflow.id}`, + { + ...workflow, + tags: [...(workflow.tags ?? []), tagId], + }, + ); + + return response.data.data; + } +} diff --git a/packages/benchmark/src/testExecution/k6Executor.ts b/packages/benchmark/src/testExecution/k6Executor.ts new file mode 100644 index 0000000000000..f2f1768331db1 --- /dev/null +++ b/packages/benchmark/src/testExecution/k6Executor.ts @@ -0,0 +1,28 @@ +import { $ } from 'zx'; +import { TestScenario } from '@/types/testScenario'; + +/** + * Executes test scenarios using k6 + */ +export class K6Executor { + constructor( + private readonly k6ExecutablePath: string, + private readonly n8nApiBaseUrl: string, + ) {} + + async executeTestScenario(testCase: TestScenario) { + // For 1 min with 5 virtual users + const stage = '1m:5'; + + const processPromise = $({ + cwd: testCase.testScenarioPath, + env: { + API_BASE_URL: this.n8nApiBaseUrl, + }, + })`${this.k6ExecutablePath} run --quiet --stage ${stage} ${testCase.testScriptPath}`; + + for await (const chunk of processPromise.stdout) { + console.log(chunk.toString()); + } + } +} diff --git a/packages/benchmark/src/testExecution/testDataImporter.ts b/packages/benchmark/src/testExecution/testDataImporter.ts new file mode 100644 index 0000000000000..a56224ee7fbce --- /dev/null +++ b/packages/benchmark/src/testExecution/testDataImporter.ts @@ -0,0 +1,57 @@ +import { AuthenticatedN8nApiClient } from '@/n8nApiClient/authenticatedN8nApiClient'; +import { Workflow } from '@/n8nApiClient/n8nApiClient.types'; +import { WorkflowApiClient } from '@/n8nApiClient/workflowsApiClient'; + +/** + * Imports test data into an n8n instance + */ +export class TestDataImporter { + private readonly workflowApiClient: WorkflowApiClient; + + constructor(n8nApiClient: AuthenticatedN8nApiClient) { + this.workflowApiClient = new WorkflowApiClient(n8nApiClient); + } + + async importTestScenarioData(workflows: Workflow[]) { + const existingWorkflows = await this.workflowApiClient.getAllWorkflows(); + + for (const workflow of workflows) { + await this.importWorkflow({ existingWorkflows, workflow }); + } + } + + /** + * Imports a single workflow into n8n and tags it with the given testCaseId + */ + private async importWorkflow(opts: { existingWorkflows: Workflow[]; workflow: Workflow }) { + const existingWorkflows = this.tryFindExistingWorkflows(opts.existingWorkflows, opts.workflow); + if (existingWorkflows.length > 0) { + for (const toDelete of existingWorkflows) { + await this.workflowApiClient.deleteWorkflow(toDelete.id); + } + } + + const createdWorkflow = await this.workflowApiClient.createWorkflow({ + ...opts.workflow, + name: this.getBenchmarkWorkflowName(opts.workflow), + }); + const activeWorkflow = await this.workflowApiClient.activateWorkflow(createdWorkflow); + + return activeWorkflow; + } + + private tryFindExistingWorkflows( + existingWorkflows: Workflow[], + workflowToImport: Workflow, + ): Workflow[] { + const benchmarkWorkflowName = this.getBenchmarkWorkflowName(workflowToImport); + + return existingWorkflows.filter( + (existingWorkflow) => existingWorkflow.name === benchmarkWorkflowName, + ); + } + + private getBenchmarkWorkflowName(workflow: Workflow) { + return `[BENCHMARK] ${workflow.name}`; + } +} diff --git a/packages/benchmark/src/testExecution/testScenarioRunner.ts b/packages/benchmark/src/testExecution/testScenarioRunner.ts new file mode 100644 index 0000000000000..2078864880f2f --- /dev/null +++ b/packages/benchmark/src/testExecution/testScenarioRunner.ts @@ -0,0 +1,50 @@ +import { TestScenario } from '@/types/testScenario'; +import { N8nApiClient } from '@/n8nApiClient/n8nApiClient'; +import { TestDataFileLoader } from '@/testScenario/testDataLoader'; +import { K6Executor } from './k6Executor'; +import { TestDataImporter } from '@/testExecution/testDataImporter'; +import { AuthenticatedN8nApiClient } from '@/n8nApiClient/authenticatedN8nApiClient'; + +/** + * Runs test scenarios + */ +export class TestScenarioRunner { + constructor( + private readonly n8nClient: N8nApiClient, + private readonly testDataLoader: TestDataFileLoader, + private readonly k6Executor: K6Executor, + private readonly ownerConfig: { + email: string; + password: string; + }, + ) {} + + async runManyTestScenarios(testCases: TestScenario[]) { + console.log(`Waiting for n8n ${this.n8nClient.apiBaseUrl} to become online`); + await this.n8nClient.waitForInstanceToBecomeOnline(); + + console.log('Setting up owner'); + await this.n8nClient.setupOwnerIfNeeded(this.ownerConfig); + + const authenticatedN8nClient = await AuthenticatedN8nApiClient.createUsingUsernameAndPassword( + this.n8nClient, + this.ownerConfig, + ); + const testDataImporter = new TestDataImporter(authenticatedN8nClient); + + for (const testCase of testCases) { + await this.runSingleTestScenario(testDataImporter, testCase); + } + } + + private async runSingleTestScenario(testDataImporter: TestDataImporter, scenario: TestScenario) { + console.log('Running scenario:', scenario.name); + + console.log('Loading and importing test data'); + const testData = await this.testDataLoader.loadTestDataForScenario(scenario); + await testDataImporter.importTestScenarioData(testData.workflows); + + console.log('Executing test script'); + await this.k6Executor.executeTestScenario(scenario); + } +} diff --git a/packages/benchmark/src/testScenario/testDataLoader.ts b/packages/benchmark/src/testScenario/testDataLoader.ts new file mode 100644 index 0000000000000..3badad5e0cd32 --- /dev/null +++ b/packages/benchmark/src/testScenario/testDataLoader.ts @@ -0,0 +1,31 @@ +import fs from 'fs'; +import path from 'path'; +import { TestScenario } from '@/types/testScenario'; +import { Workflow } from '@/n8nApiClient/n8nApiClient.types'; + +/** + * Loads test data files from FS + */ +export class TestDataFileLoader { + async loadTestDataForScenario(testScenario: TestScenario): Promise<{ + workflows: Workflow[]; + }> { + const workflows = await Promise.all( + testScenario.testData.workflowFiles?.map((workflowFilePath) => + this.loadSingleWorkflowFromFile(path.join(testScenario.testScenarioPath, workflowFilePath)), + ) ?? [], + ); + + return { + workflows, + }; + } + + private loadSingleWorkflowFromFile(workflowFilePath: string): Workflow { + const fileContent = fs.readFileSync(workflowFilePath, 'utf8'); + + const workflow = JSON.parse(fileContent); + + return workflow; + } +} diff --git a/packages/benchmark/src/testScenario/testScenarioLoader.ts b/packages/benchmark/src/testScenario/testScenarioLoader.ts new file mode 100644 index 0000000000000..b0a6d8ca5e6ba --- /dev/null +++ b/packages/benchmark/src/testScenario/testScenarioLoader.ts @@ -0,0 +1,67 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import { createHash } from 'node:crypto'; +import type { TestScenario, TestScenarioManifest } from '@/types/testScenario'; + +export class TestScenarioLoader { + /** + * Loads all test scenarios from the given path + */ + loadAllTestScenarios(testScenariosPath: string): TestScenario[] { + testScenariosPath = path.resolve(testScenariosPath); + const testScenarioFolders = fs + .readdirSync(testScenariosPath, { withFileTypes: true }) + .filter((dirent) => dirent.isDirectory()) + .map((dirent) => dirent.name); + + const scenarios: TestScenario[] = []; + + for (const folder of testScenarioFolders) { + const scenarioPath = path.join(testScenariosPath, folder); + const manifestFileName = `${folder}.manifest.json`; + const scenarioManifestPath = path.join(testScenariosPath, folder, manifestFileName); + if (!fs.existsSync(scenarioManifestPath)) { + console.warn(`Test scenario at ${scenarioPath} is missing the ${manifestFileName} file`); + continue; + } + + // Load the scenario manifest file + const [testCase, validationErrors] = + this.loadAndValidateTestScenarioManifest(scenarioManifestPath); + if (validationErrors) { + console.warn( + `Test case at ${scenarioPath} has the following validation errors: ${validationErrors.join(', ')}`, + ); + continue; + } + + scenarios.push({ + ...testCase, + id: this.formTestScenarioId(scenarioPath), + testScenarioPath: scenarioPath, + }); + } + + return scenarios; + } + + private loadAndValidateTestScenarioManifest( + testScenarioManifestPath: string, + ): [TestScenarioManifest, null] | [null, string[]] { + const testCase = JSON.parse(fs.readFileSync(testScenarioManifestPath, 'utf8')); + const validationErrors: string[] = []; + + if (!testCase.name) { + validationErrors.push(`Test case at ${testScenarioManifestPath} is missing a name`); + } + if (!testCase.description) { + validationErrors.push(`Test case at ${testScenarioManifestPath} is missing a description`); + } + + return validationErrors.length === 0 ? [testCase, null] : [null, validationErrors]; + } + + private formTestScenarioId(testCasePath: string): string { + return createHash('sha256').update(testCasePath).digest('hex'); + } +} diff --git a/packages/benchmark/src/types/testScenario.ts b/packages/benchmark/src/types/testScenario.ts new file mode 100644 index 0000000000000..7946301bd12f5 --- /dev/null +++ b/packages/benchmark/src/types/testScenario.ts @@ -0,0 +1,26 @@ +export type TestData = { + /** Relative paths to the workflow files */ + workflowFiles?: string[]; +}; + +/** + * Configuration that defines the test scenario + */ +export type TestScenarioManifest = { + /** The name of the test scenario */ + name: string; + /** A longer description of the test scenario */ + description: string; + /** Relative path to the k6 test script */ + testScriptPath: string; + /** Test data to import before running the scenario */ + testData: TestData; +}; + +/** + * A test scenario with additional metadata + */ +export type TestScenario = TestScenarioManifest & { + id: string; + testScenarioPath: string; +}; diff --git a/packages/benchmark/testScenarios/codeNode/codeNode.json b/packages/benchmark/testScenarios/codeNode/codeNode.json new file mode 100644 index 0000000000000..6831d22068493 --- /dev/null +++ b/packages/benchmark/testScenarios/codeNode/codeNode.json @@ -0,0 +1,36 @@ +{ + "createdAt": "2024-08-06T13:37:07.119Z", + "updatedAt": "2024-08-06T13:39:27.000Z", + "name": "Code Node", + "active": true, + "nodes": [ + { + "parameters": { + "mode": "runOnceForEachItem", + "jsCode": "$input.item.json.myNewField = Math.random();\n\nreturn $input.item;" + }, + "id": "7d100719-8d9e-497d-a365-78c793c244d6", + "name": "Code", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [980, 400] + }, + { + "parameters": { "path": "code-node", "responseMode": "lastNode", "options": {} }, + "id": "1e172d8f-f6cd-43ec-8257-e6a8212add00", + "name": "Webhook", + "type": "n8n-nodes-base.webhook", + "typeVersion": 2, + "position": [760, 400], + "webhookId": "224a56e0-f929-4f01-b49e-d91861b1b8d7" + } + ], + "connections": { "Webhook": { "main": [[{ "node": "Code", "type": "main", "index": 0 }]] } }, + "settings": { "executionOrder": "v1" }, + "staticData": null, + "meta": { "templateCredsSetupCompleted": true }, + "pinData": {}, + "versionId": "a91dfe67-971d-425f-9066-feab46150f95", + "triggerCount": 1, + "tags": [] +} diff --git a/packages/benchmark/testScenarios/codeNode/codeNode.manifest.json b/packages/benchmark/testScenarios/codeNode/codeNode.manifest.json new file mode 100644 index 0000000000000..a44b49af8f1d7 --- /dev/null +++ b/packages/benchmark/testScenarios/codeNode/codeNode.manifest.json @@ -0,0 +1,9 @@ +{ + "$schema": "../testScenario.schema.json", + "name": "CodeNode", + "description": "Workflow with a single code node that is triggered using a webhook", + "testData": { + "workflowFiles": ["codeNode.json"] + }, + "testScriptPath": "codeNode.script.ts" +} diff --git a/packages/benchmark/testScenarios/codeNode/codeNode.script.ts b/packages/benchmark/testScenarios/codeNode/codeNode.script.ts new file mode 100644 index 0000000000000..b9ec9b345ec98 --- /dev/null +++ b/packages/benchmark/testScenarios/codeNode/codeNode.script.ts @@ -0,0 +1,11 @@ +import http from 'k6/http'; +import { check } from 'k6'; + +const apiBaseUrl = __ENV.API_BASE_URL; + +export default function () { + const res = http.get(`${apiBaseUrl}/webhook/code-node`); + check(res, { + 'is status 200': (r) => r.status === 200, + }); +} diff --git a/packages/benchmark/testScenarios/singleWebhook/singleWebhook.json b/packages/benchmark/testScenarios/singleWebhook/singleWebhook.json new file mode 100644 index 0000000000000..cba1aa5832f70 --- /dev/null +++ b/packages/benchmark/testScenarios/singleWebhook/singleWebhook.json @@ -0,0 +1,25 @@ +{ + "createdAt": "2024-08-06T12:19:51.268Z", + "updatedAt": "2024-08-06T12:20:45.000Z", + "name": "Single Webhook", + "active": true, + "nodes": [ + { + "parameters": { "path": "single-webhook", "options": {} }, + "id": "7587ab0e-cc15-424f-83c0-c887a0eb97fb", + "name": "Webhook", + "type": "n8n-nodes-base.webhook", + "typeVersion": 2, + "position": [760, 400], + "webhookId": "fa563fc2-c73f-4631-99a1-39c16f1f858f" + } + ], + "connections": {}, + "settings": { "executionOrder": "v1" }, + "staticData": null, + "meta": { "templateCredsSetupCompleted": true, "responseMode": "lastNode", "options": {} }, + "pinData": {}, + "versionId": "840a38a1-ba37-433d-9f20-de73f5131a2b", + "triggerCount": 1, + "tags": [] +} diff --git a/packages/benchmark/testScenarios/singleWebhook/singleWebhook.manifest.json b/packages/benchmark/testScenarios/singleWebhook/singleWebhook.manifest.json new file mode 100644 index 0000000000000..9314cf510a173 --- /dev/null +++ b/packages/benchmark/testScenarios/singleWebhook/singleWebhook.manifest.json @@ -0,0 +1,6 @@ +{ + "name": "SingleWebhook", + "description": "A single webhook trigger that responds with a 200 status code", + "testData": { "workflowFiles": ["singleWebhook.json"] }, + "testScriptPath": "singleWebhook.script.ts" +} diff --git a/packages/benchmark/testScenarios/singleWebhook/singleWebhook.script.ts b/packages/benchmark/testScenarios/singleWebhook/singleWebhook.script.ts new file mode 100644 index 0000000000000..72e2563cbea9f --- /dev/null +++ b/packages/benchmark/testScenarios/singleWebhook/singleWebhook.script.ts @@ -0,0 +1,11 @@ +import http from 'k6/http'; +import { check } from 'k6'; + +const apiBaseUrl = __ENV.API_BASE_URL; + +export default function () { + const res = http.get(`${apiBaseUrl}/webhook/single-webhook`); + check(res, { + 'is status 200': (r) => r.status === 200, + }); +} diff --git a/packages/benchmark/testScenarios/testScenario.schema.json b/packages/benchmark/testScenarios/testScenario.schema.json new file mode 100644 index 0000000000000..da02b9d35ca3f --- /dev/null +++ b/packages/benchmark/testScenarios/testScenario.schema.json @@ -0,0 +1,42 @@ +{ + "definitions": { + "TestData": { + "type": "object", + "properties": { + "workflowFiles": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [], + "additionalProperties": false + } + }, + "type": "object", + "properties": { + "$schema": { + "type": "string", + "description": "The JSON schema to validate this file" + }, + "name": { + "type": "string", + "description": "The name of the test scenario" + }, + "description": { + "type": "string", + "description": "A longer description of the test scenario" + }, + "testScriptPath": { + "type": "string", + "description": "Relative path to the k6 test script" + }, + "testData": { + "$ref": "#/definitions/TestData", + "description": "Test data to import before running the scenario" + } + }, + "required": ["name", "description", "testScriptPath", "testData"], + "additionalProperties": false +} diff --git a/packages/benchmark/tsconfig.build.json b/packages/benchmark/tsconfig.build.json new file mode 100644 index 0000000000000..cb80f3c918e4d --- /dev/null +++ b/packages/benchmark/tsconfig.build.json @@ -0,0 +1,9 @@ +{ + "extends": ["./tsconfig.json", "../../tsconfig.build.json"], + "compilerOptions": { + "rootDir": "src", + "outDir": "dist", + "tsBuildInfoFile": "dist/build.tsbuildinfo" + }, + "include": ["src/**/*.ts"] +} diff --git a/packages/benchmark/tsconfig.json b/packages/benchmark/tsconfig.json new file mode 100644 index 0000000000000..d2e1fe362751a --- /dev/null +++ b/packages/benchmark/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": ["../../tsconfig.json", "../../tsconfig.backend.json"], + "compilerOptions": { + "rootDir": ".", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "baseUrl": "src", + "paths": { + "@/*": ["./*"] + } + }, + "include": ["src/**/*.ts"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e3b834bfa4025..931037a3f2717 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -123,7 +123,7 @@ importers: version: 6.0.2 jest: specifier: ^29.6.2 - version: 29.6.2(@types/node@18.16.16) + version: 29.6.2(@types/node@18.16.16)(ts-node@10.9.2(@types/node@18.16.16)(typescript@5.5.2)) jest-environment-jsdom: specifier: ^29.6.2 version: 29.6.2 @@ -135,7 +135,7 @@ importers: version: 29.6.2 jest-mock-extended: specifier: ^3.0.4 - version: 3.0.4(jest@29.6.2(@types/node@18.16.16))(typescript@5.5.2) + version: 3.0.4(jest@29.6.2(@types/node@18.16.16)(ts-node@10.9.2(@types/node@18.16.16)(typescript@5.5.2)))(typescript@5.5.2) nock: specifier: ^13.3.2 version: 13.3.2 @@ -156,7 +156,7 @@ importers: version: 7.0.0 ts-jest: specifier: ^29.1.1 - version: 29.1.1(@babel/core@7.24.0)(@jest/types@29.6.1)(babel-jest@29.6.2(@babel/core@7.24.0))(jest@29.6.2(@types/node@18.16.16))(typescript@5.5.2) + version: 29.1.1(@babel/core@7.24.0)(@jest/types@29.6.1)(babel-jest@29.6.2(@babel/core@7.24.0))(jest@29.6.2(@types/node@18.16.16)(ts-node@10.9.2(@types/node@18.16.16)(typescript@5.5.2)))(typescript@5.5.2) tsc-alias: specifier: ^1.8.7 version: 1.8.7 @@ -388,7 +388,7 @@ importers: version: 0.5.0 '@n8n/typeorm': specifier: 0.3.20-10 - version: 0.3.20-10(@sentry/node@7.87.0)(ioredis@5.3.2)(mssql@10.0.2)(mysql2@3.11.0)(pg@8.12.0)(redis@4.6.12)(sqlite3@5.1.7) + version: 0.3.20-10(@sentry/node@7.87.0)(ioredis@5.3.2)(mssql@10.0.2)(mysql2@3.11.0)(pg@8.12.0)(redis@4.6.12)(sqlite3@5.1.7)(ts-node@10.9.2(@types/node@18.16.16)(typescript@5.5.2)) '@n8n/vm2': specifier: 3.9.25 version: 3.9.25 @@ -520,7 +520,7 @@ importers: version: 8.1.4(@types/react@18.0.27)(encoding@0.1.13)(prettier@3.2.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@storybook/addon-interactions': specifier: ^8.1.4 - version: 8.1.4(@jest/globals@29.6.2)(@types/jest@29.5.3)(jest@29.6.2(@types/node@18.16.16))(vitest@1.6.0(@types/node@18.16.16)(sass@1.64.1)(terser@5.16.1)) + version: 8.1.4(@jest/globals@29.6.2)(@types/jest@29.5.3)(jest@29.6.2(@types/node@18.16.16)(ts-node@10.9.2(@types/node@18.16.16)(typescript@5.5.2)))(vitest@1.6.0(@types/node@18.16.16)(sass@1.64.1)(terser@5.16.1)) '@storybook/addon-links': specifier: ^8.1.4 version: 8.1.4(react@18.2.0) @@ -532,7 +532,7 @@ importers: version: 8.1.4(@types/react@18.0.27)(encoding@0.1.13)(prettier@3.2.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@storybook/test': specifier: ^8.1.4 - version: 8.1.4(@jest/globals@29.6.2)(@types/jest@29.5.3)(jest@29.6.2(@types/node@18.16.16))(vitest@1.6.0(@types/node@18.16.16)(sass@1.64.1)(terser@5.16.1)) + version: 8.1.4(@jest/globals@29.6.2)(@types/jest@29.5.3)(jest@29.6.2(@types/node@18.16.16)(ts-node@10.9.2(@types/node@18.16.16)(typescript@5.5.2)))(vitest@1.6.0(@types/node@18.16.16)(sass@1.64.1)(terser@5.16.1)) '@storybook/vue3': specifier: ^8.1.4 version: 8.1.4(encoding@0.1.13)(prettier@3.2.5)(vue@3.4.21(typescript@5.5.2)) @@ -600,6 +600,40 @@ importers: specifier: ^9.4.2 version: 9.4.2(eslint@8.57.0) + packages/benchmark: + dependencies: + '@oclif/core': + specifier: 4.0.7 + version: 4.0.7 + axios: + specifier: 1.7.3 + version: 1.7.3(debug@3.2.7) + convict: + specifier: 6.2.4 + version: 6.2.4 + dotenv: + specifier: 8.6.0 + version: 8.6.0 + zx: + specifier: ^8.1.4 + version: 8.1.4 + devDependencies: + '@types/convict': + specifier: ^6.1.1 + version: 6.1.1 + '@types/k6': + specifier: ^0.52.0 + version: 0.52.0 + '@types/node': + specifier: ^18.16.16 + version: 18.16.16 + tsc-alias: + specifier: ^1.8.7 + version: 1.8.7 + typescript: + specifier: ^5.5.2 + version: 5.5.2 + packages/cli: dependencies: '@azure/identity': @@ -628,7 +662,7 @@ importers: version: link:../@n8n/permissions '@n8n/typeorm': specifier: 0.3.20-10 - version: 0.3.20-10(@sentry/node@7.87.0)(ioredis@5.3.2)(mssql@10.0.2)(mysql2@3.11.0)(pg@8.12.0)(redis@4.6.14)(sqlite3@5.1.7) + version: 0.3.20-10(@sentry/node@7.87.0)(ioredis@5.3.2)(mssql@10.0.2)(mysql2@3.11.0)(pg@8.12.0)(redis@4.6.14)(sqlite3@5.1.7)(ts-node@10.9.2(@types/node@18.16.16)(typescript@5.5.2)) '@n8n_io/ai-assistant-sdk': specifier: 1.9.4 version: 1.9.4 @@ -1102,7 +1136,7 @@ importers: version: link:../@n8n/storybook '@testing-library/jest-dom': specifier: ^6.1.5 - version: 6.1.5(@jest/globals@29.6.2)(@types/jest@29.5.3)(jest@29.6.2(@types/node@18.16.16))(vitest@1.6.0(@types/node@18.16.16)(jsdom@23.0.1)(sass@1.64.1)(terser@5.16.1)) + version: 6.1.5(@jest/globals@29.6.2)(@types/jest@29.5.3)(jest@29.6.2(@types/node@18.16.16)(ts-node@10.9.2(@types/node@18.16.16)(typescript@5.5.2)))(vitest@1.6.0(@types/node@18.16.16)(jsdom@23.0.1)(sass@1.64.1)(terser@5.16.1)) '@testing-library/user-event': specifier: ^14.5.1 version: 14.5.1(@testing-library/dom@9.3.4) @@ -1141,7 +1175,7 @@ importers: version: 1.64.1 tailwindcss: specifier: ^3.4.3 - version: 3.4.3 + version: 3.4.3(ts-node@10.9.2(@types/node@18.16.16)(typescript@5.5.2)) unplugin-icons: specifier: ^0.19.0 version: 0.19.0(@vue/compiler-sfc@3.4.21)(vue-template-compiler@2.7.14) @@ -3079,6 +3113,10 @@ packages: resolution: {integrity: sha512-dkk7FX8L/JLia5pi+IQ11lCw2D6FTmbWL2iWTHgCbP40/deeXgknlkEQcQ/rOkjwQbqp8RZ4ey/anR17K66sqw==} engines: {node: '>=16'} + '@cspotcode/source-map-support@0.8.1': + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + '@ctrl/tinycolor@3.6.0': resolution: {integrity: sha512-/Z3l6pXthq0JvMYdUFyX9j0MaCltlIn6mfh9jLyQwg5aPKxkyNa0PTHtU1AlFXLNk55ZuAeJRcpvq+tmLfKmaQ==} engines: {node: '>=10'} @@ -3578,6 +3616,9 @@ packages: '@jridgewell/trace-mapping@0.3.25': resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + '@jridgewell/trace-mapping@0.3.9': + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + '@js-joda/core@5.6.1': resolution: {integrity: sha512-Xla/d7ZMMR6+zRd6lTio0wRZECfcfFJP7GGe9A9L4tDOlD5CX4YcZ4YZle9w58bBYzssojVapI84RraKWDQZRg==} @@ -5259,6 +5300,18 @@ packages: resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==} engines: {node: '>= 10'} + '@tsconfig/node10@1.0.11': + resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==} + + '@tsconfig/node12@1.0.11': + resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} + + '@tsconfig/node14@1.0.3': + resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} + + '@tsconfig/node16@1.0.4': + resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + '@types/amqplib@0.10.1': resolution: {integrity: sha512-j6ANKT79ncUDnAs/+9r9eDujxbeJoTjoVu33gHHcaPfmLQaMhvfbH2GqSe8KUM444epAp1Vl3peVOQfZk3UIqA==} @@ -5376,6 +5429,9 @@ packages: '@types/formidable@3.4.5': resolution: {integrity: sha512-s7YPsNVfnsng5L8sKnG/Gbb2tiwwJTY1conOkJzTMRvJAlLFW1nEua+ADsJQu8N1c0oTHx9+d5nqg10WuT9gHQ==} + '@types/fs-extra@11.0.4': + resolution: {integrity: sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==} + '@types/ftp@0.3.33': resolution: {integrity: sha512-L7wFlX3t9GsGgNS0oxLt6zbAZZGgsdptMmciL4cdxHmbL3Hz4Lysh8YqAR34eHsJ1uacJITcZBBDl5XpQlxPpQ==} @@ -5433,12 +5489,18 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/jsonfile@6.1.4': + resolution: {integrity: sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==} + '@types/jsonpath@0.2.0': resolution: {integrity: sha512-v7qlPA0VpKUlEdhghbDqRoKMxFB3h3Ch688TApBJ6v+XLDdvWCGLJIYiPKGZnS6MAOie+IorCfNYVHOPIHSWwQ==} '@types/jsonwebtoken@9.0.6': resolution: {integrity: sha512-/5hndP5dCjloafCXns6SZyESp3Ldq7YjH3zwzwczYnjxIT0Fqzk5ROSYVGfFyczIue7IUEj8hkvLbPoLQ18vQw==} + '@types/k6@0.52.0': + resolution: {integrity: sha512-yaw2wg61nKQtToDML+nngzgXVjZ6wNA4R0Q3jKDTeadG5EqfZgis5a1Q2hwY7kjuGuXmu8eM6gHg3tgnOj4vNw==} + '@types/linkify-it@3.0.5': resolution: {integrity: sha512-yg6E+u0/+Zjva+buc3EIb+29XEg4wltq7cSmd4Uc2EE/1nUVmxyzpX6gUXD0V8jIrG0r7YeOGVIbYRkxeooCtw==} @@ -6145,6 +6207,9 @@ packages: engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} deprecated: This package is no longer supported. + arg@4.1.3: + resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + arg@5.0.2: resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} @@ -6709,10 +6774,6 @@ packages: resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} engines: {node: '>=8'} - cli-spinners@2.9.0: - resolution: {integrity: sha512-4/aL9X3Wh0yiMQlE+eeRhWP6vclO3QRtw1JHKIT0FFUs5FjpFmESqtMvYZ0+lbzBw900b95mS0hohy+qn2VK/g==} - engines: {node: '>=6'} - cli-spinners@2.9.2: resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} engines: {node: '>=6'} @@ -6952,6 +7013,9 @@ packages: resolution: {integrity: sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==} engines: {node: '>=10.0.0'} + create-require@1.1.1: + resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + crelt@1.0.5: resolution: {integrity: sha512-+BO9wPPi+DWTDcNYhr/W90myha8ptzftZT+LwcmUbbok0rcP/fequmFYCw8NMoH7pkAZQzU78b3kYrlua5a9eA==} @@ -7324,6 +7388,10 @@ packages: resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + diff@4.0.2: + resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} + engines: {node: '>=0.3.1'} + diff@5.2.0: resolution: {integrity: sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==} engines: {node: '>=0.3.1'} @@ -8370,6 +8438,7 @@ packages: google-p12-pem@4.0.1: resolution: {integrity: sha512-WPkN4yGtz05WZ5EhtlxNDWPhC4JIic6G8ePitwUWy4l+XPVYec+a0j0Ts47PDtW59y3RwAhUd9/h9ZZ63px6RQ==} engines: {node: '>=12.0.0'} + deprecated: Package is no longer maintained hasBin: true gopd@1.0.1: @@ -9607,10 +9676,6 @@ packages: resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} engines: {node: '>=10'} - lilconfig@3.0.0: - resolution: {integrity: sha512-K2U4W2Ff5ibV7j7ydLr+zLAkIg5JJ4lPn1Ltsdt+Tz/IjQ8buJ55pZAxoP34lqIiwtF9iAvtLv3JGv7CAyAg+g==} - engines: {node: '>=14'} - lilconfig@3.1.2: resolution: {integrity: sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==} engines: {node: '>=14'} @@ -9983,10 +10048,6 @@ packages: resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} engines: {node: '>=16 || 14 >=14.17'} - minimatch@9.0.4: - resolution: {integrity: sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==} - engines: {node: '>=16 || 14 >=14.17'} - minimatch@9.0.5: resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} engines: {node: '>=16 || 14 >=14.17'} @@ -12355,6 +12416,20 @@ packages: ts-map@1.0.3: resolution: {integrity: sha512-vDWbsl26LIcPGmDpoVzjEP6+hvHZkBkLW7JpvwbCv/5IYPJlsbzCVXY3wsCeAxAUeTclNOUZxnLdGh3VBD/J6w==} + ts-node@10.9.2: + resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} + hasBin: true + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': ^18.16.16 + typescript: ^5.5.2 + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true + ts-toolbelt@9.6.0: resolution: {integrity: sha512-nsZd8ZeNUzukXPlJmTBwUAuABDe/9qtVDelJeT/qW0ow3ZS3BsQJtNkan1802aM9Uf68/Y8ljw86Hu0h5IUW3w==} @@ -12759,6 +12834,9 @@ packages: v3-infinite-loading@1.2.2: resolution: {integrity: sha512-MWJc6yChnqeUasBFJ3Enu8IGPcQgRMSTrAEtT1MsHBEx+QjwvNTaY8o+8V9DgVt1MVhQSl3MC55hsaWLJmpRMw==} + v8-compile-cache-lib@3.0.1: + resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} + v8-to-istanbul@9.1.0: resolution: {integrity: sha512-6z3GW9x8G1gd+JIIgQQQxXuiJtCXeAjp6RaPEPLv62mH3iPHPxV6W3robxtCzNErRo6ZwTmzWhsbNvjyEBKzKA==} engines: {node: '>=10.12.0'} @@ -13281,6 +13359,10 @@ packages: yauzl@2.10.0: resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} + yn@3.1.1: + resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} + engines: {node: '>=6'} + yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -13309,6 +13391,11 @@ packages: zod@3.23.8: resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==} + zx@8.1.4: + resolution: {integrity: sha512-QFDYYpnzdpRiJ3dL2102Cw26FpXpWshW4QLTGxiYfIcwdAqg084jRCkK/kuP/NOSkxOjydRwNFG81qzA5r1a6w==} + engines: {node: '>= 12.17.0'} + hasBin: true + snapshots: '@aashutoshrathi/word-wrap@1.2.6': {} @@ -13316,7 +13403,7 @@ snapshots: '@acuminous/bitsyntax@0.1.2': dependencies: buffer-more-ints: 1.0.0 - debug: 4.3.5(supports-color@8.1.1) + debug: 4.3.6(supports-color@8.1.1) safe-buffer: 5.1.2 transitivePeerDependencies: - supports-color @@ -14433,7 +14520,7 @@ snapshots: '@babel/traverse': 7.24.0 '@babel/types': 7.24.6 convert-source-map: 2.0.0 - debug: 4.3.6 + debug: 4.3.6(supports-color@8.1.1) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 7.6.0 @@ -14453,7 +14540,7 @@ snapshots: '@babel/traverse': 7.24.6 '@babel/types': 7.24.6 convert-source-map: 2.0.0 - debug: 4.3.5(supports-color@8.1.1) + debug: 4.3.5 gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 7.6.0 @@ -14554,7 +14641,7 @@ snapshots: '@babel/core': 7.24.6 '@babel/helper-compilation-targets': 7.24.6 '@babel/helper-plugin-utils': 7.24.6 - debug: 4.3.6 + debug: 4.3.6(supports-color@8.1.1) lodash.debounce: 4.0.8 resolve: 1.22.8 transitivePeerDependencies: @@ -14565,7 +14652,7 @@ snapshots: '@babel/core': 7.24.6 '@babel/helper-compilation-targets': 7.24.6 '@babel/helper-plugin-utils': 7.24.6 - debug: 4.3.6 + debug: 4.3.6(supports-color@8.1.1) lodash.debounce: 4.0.8 resolve: 1.22.8 transitivePeerDependencies: @@ -15449,7 +15536,7 @@ snapshots: '@babel/helper-split-export-declaration': 7.22.6 '@babel/parser': 7.24.6 '@babel/types': 7.24.6 - debug: 4.3.6 + debug: 4.3.6(supports-color@8.1.1) globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -15464,7 +15551,7 @@ snapshots: '@babel/helper-split-export-declaration': 7.24.6 '@babel/parser': 7.24.6 '@babel/types': 7.24.6 - debug: 4.3.5(supports-color@8.1.1) + debug: 4.3.6(supports-color@8.1.1) globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -15601,6 +15688,11 @@ snapshots: '@common.js/is-network-error@1.0.1': {} + '@cspotcode/source-map-support@0.8.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + optional: true + '@ctrl/tinycolor@3.6.0': {} '@curlconverter/yargs-parser@0.0.1': {} @@ -15746,7 +15838,7 @@ snapshots: '@eslint/eslintrc@2.1.4': dependencies: ajv: 6.12.6 - debug: 4.3.5(supports-color@8.1.1) + debug: 4.3.6(supports-color@8.1.1) espree: 9.6.1 globals: 13.20.0 ignore: 5.2.4 @@ -15906,7 +15998,7 @@ snapshots: '@humanwhocodes/config-array@0.11.14': dependencies: '@humanwhocodes/object-schema': 2.0.2 - debug: 4.3.5(supports-color@8.1.1) + debug: 4.3.6(supports-color@8.1.1) minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -15936,7 +16028,7 @@ snapshots: '@antfu/install-pkg': 0.1.1 '@antfu/utils': 0.7.10 '@iconify/types': 2.0.0 - debug: 4.3.5(supports-color@8.1.1) + debug: 4.3.6(supports-color@8.1.1) kolorist: 1.8.0 local-pkg: 0.5.0 mlly: 1.7.1 @@ -15998,7 +16090,7 @@ snapshots: jest-util: 29.6.2 slash: 3.0.0 - '@jest/core@29.6.2': + '@jest/core@29.6.2(ts-node@10.9.2(@types/node@18.16.16)(typescript@5.5.2))': dependencies: '@jest/console': 29.6.2 '@jest/reporters': 29.6.2 @@ -16012,7 +16104,7 @@ snapshots: exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.5.0 - jest-config: 29.6.2(@types/node@18.16.16) + jest-config: 29.6.2(@types/node@18.16.16)(ts-node@10.9.2(@types/node@18.16.16)(typescript@5.5.2)) jest-haste-map: 29.6.2 jest-message-util: 29.6.2 jest-regex-util: 29.4.3 @@ -16190,6 +16282,12 @@ snapshots: '@jridgewell/resolve-uri': 3.1.0 '@jridgewell/sourcemap-codec': 1.4.15 + '@jridgewell/trace-mapping@0.3.9': + dependencies: + '@jridgewell/resolve-uri': 3.1.0 + '@jridgewell/sourcemap-codec': 1.4.15 + optional: true + '@js-joda/core@5.6.1': {} '@js-sdsl/ordered-map@4.4.2': {} @@ -16230,7 +16328,7 @@ snapshots: '@kwsites/file-exists@1.1.1': dependencies: - debug: 4.3.5(supports-color@8.1.1) + debug: 4.3.6(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -16641,7 +16739,7 @@ snapshots: '@n8n/localtunnel@3.0.0': dependencies: axios: 1.7.3(debug@4.3.6) - debug: 4.3.6 + debug: 4.3.6(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -16658,7 +16756,7 @@ snapshots: esprima-next: 5.8.4 recast: 0.22.0 - '@n8n/typeorm@0.3.20-10(@sentry/node@7.87.0)(ioredis@5.3.2)(mssql@10.0.2)(mysql2@3.11.0)(pg@8.12.0)(redis@4.6.12)(sqlite3@5.1.7)': + '@n8n/typeorm@0.3.20-10(@sentry/node@7.87.0)(ioredis@5.3.2)(mssql@10.0.2)(mysql2@3.11.0)(pg@8.12.0)(redis@4.6.12)(sqlite3@5.1.7)(ts-node@10.9.2(@types/node@18.16.16)(typescript@5.5.2))': dependencies: '@n8n/p-retry': 6.2.0-2 '@sqltools/formatter': 1.2.5 @@ -16667,7 +16765,7 @@ snapshots: buffer: 6.0.3 chalk: 4.1.2 dayjs: 1.11.10 - debug: 4.3.6 + debug: 4.3.6(supports-color@8.1.1) dotenv: 16.3.1 glob: 10.3.10 mkdirp: 2.1.3 @@ -16685,10 +16783,11 @@ snapshots: pg: 8.12.0 redis: 4.6.12 sqlite3: 5.1.7 + ts-node: 10.9.2(@types/node@18.16.16)(typescript@5.5.2) transitivePeerDependencies: - supports-color - '@n8n/typeorm@0.3.20-10(@sentry/node@7.87.0)(ioredis@5.3.2)(mssql@10.0.2)(mysql2@3.11.0)(pg@8.12.0)(redis@4.6.14)(sqlite3@5.1.7)': + '@n8n/typeorm@0.3.20-10(@sentry/node@7.87.0)(ioredis@5.3.2)(mssql@10.0.2)(mysql2@3.11.0)(pg@8.12.0)(redis@4.6.14)(sqlite3@5.1.7)(ts-node@10.9.2(@types/node@18.16.16)(typescript@5.5.2))': dependencies: '@n8n/p-retry': 6.2.0-2 '@sqltools/formatter': 1.2.5 @@ -16697,7 +16796,7 @@ snapshots: buffer: 6.0.3 chalk: 4.1.2 dayjs: 1.11.10 - debug: 4.3.6 + debug: 4.3.6(supports-color@8.1.1) dotenv: 16.3.1 glob: 10.3.10 mkdirp: 2.1.3 @@ -16715,6 +16814,7 @@ snapshots: pg: 8.12.0 redis: 4.6.14 sqlite3: 5.1.7 + ts-node: 10.9.2(@types/node@18.16.16)(typescript@5.5.2) transitivePeerDependencies: - supports-color @@ -16783,14 +16883,14 @@ snapshots: ansis: 3.2.0 clean-stack: 3.0.1 cli-spinners: 2.9.2 - debug: 4.3.5(supports-color@8.1.1) + debug: 4.3.6(supports-color@8.1.1) ejs: 3.1.10 get-package-type: 0.1.0 globby: 11.1.0 indent-string: 4.0.0 is-wsl: 2.2.0 lilconfig: 3.1.2 - minimatch: 9.0.4 + minimatch: 9.0.5 string-width: 4.2.3 supports-color: 8.1.1 widest-line: 3.1.0 @@ -17775,11 +17875,11 @@ snapshots: dependencies: '@storybook/global': 5.0.0 - '@storybook/addon-interactions@8.1.4(@jest/globals@29.6.2)(@types/jest@29.5.3)(jest@29.6.2(@types/node@18.16.16))(vitest@1.6.0(@types/node@18.16.16)(sass@1.64.1)(terser@5.16.1))': + '@storybook/addon-interactions@8.1.4(@jest/globals@29.6.2)(@types/jest@29.5.3)(jest@29.6.2(@types/node@18.16.16)(ts-node@10.9.2(@types/node@18.16.16)(typescript@5.5.2)))(vitest@1.6.0(@types/node@18.16.16)(sass@1.64.1)(terser@5.16.1))': dependencies: '@storybook/global': 5.0.0 '@storybook/instrumenter': 8.1.4 - '@storybook/test': 8.1.4(@jest/globals@29.6.2)(@types/jest@29.5.3)(jest@29.6.2(@types/node@18.16.16))(vitest@1.6.0(@types/node@18.16.16)(sass@1.64.1)(terser@5.16.1)) + '@storybook/test': 8.1.4(@jest/globals@29.6.2)(@types/jest@29.5.3)(jest@29.6.2(@types/node@18.16.16)(ts-node@10.9.2(@types/node@18.16.16)(typescript@5.5.2)))(vitest@1.6.0(@types/node@18.16.16)(sass@1.64.1)(terser@5.16.1)) '@storybook/types': 8.1.4 polished: 4.2.2 ts-dedent: 2.2.0 @@ -18227,14 +18327,14 @@ snapshots: - prettier - supports-color - '@storybook/test@8.1.4(@jest/globals@29.6.2)(@types/jest@29.5.3)(jest@29.6.2(@types/node@18.16.16))(vitest@1.6.0(@types/node@18.16.16)(sass@1.64.1)(terser@5.16.1))': + '@storybook/test@8.1.4(@jest/globals@29.6.2)(@types/jest@29.5.3)(jest@29.6.2(@types/node@18.16.16)(ts-node@10.9.2(@types/node@18.16.16)(typescript@5.5.2)))(vitest@1.6.0(@types/node@18.16.16)(sass@1.64.1)(terser@5.16.1))': dependencies: '@storybook/client-logger': 8.1.4 '@storybook/core-events': 8.1.4 '@storybook/instrumenter': 8.1.4 '@storybook/preview-api': 8.1.4 '@testing-library/dom': 9.3.4 - '@testing-library/jest-dom': 6.4.2(@jest/globals@29.6.2)(@types/jest@29.5.3)(jest@29.6.2(@types/node@18.16.16))(vitest@1.6.0(@types/node@18.16.16)(sass@1.64.1)(terser@5.16.1)) + '@testing-library/jest-dom': 6.4.2(@jest/globals@29.6.2)(@types/jest@29.5.3)(jest@29.6.2(@types/node@18.16.16)(ts-node@10.9.2(@types/node@18.16.16)(typescript@5.5.2)))(vitest@1.6.0(@types/node@18.16.16)(sass@1.64.1)(terser@5.16.1)) '@testing-library/user-event': 14.5.2(@testing-library/dom@9.3.4) '@vitest/expect': 1.3.1 '@vitest/spy': 1.3.1 @@ -18381,7 +18481,7 @@ snapshots: lz-string: 1.5.0 pretty-format: 27.5.1 - '@testing-library/jest-dom@6.1.5(@jest/globals@29.6.2)(@types/jest@29.5.3)(jest@29.6.2(@types/node@18.16.16))(vitest@1.6.0(@types/node@18.16.16)(jsdom@23.0.1)(sass@1.64.1)(terser@5.16.1))': + '@testing-library/jest-dom@6.1.5(@jest/globals@29.6.2)(@types/jest@29.5.3)(jest@29.6.2(@types/node@18.16.16)(ts-node@10.9.2(@types/node@18.16.16)(typescript@5.5.2)))(vitest@1.6.0(@types/node@18.16.16)(jsdom@23.0.1)(sass@1.64.1)(terser@5.16.1))': dependencies: '@adobe/css-tools': 4.3.2 '@babel/runtime': 7.22.6 @@ -18394,10 +18494,10 @@ snapshots: optionalDependencies: '@jest/globals': 29.6.2 '@types/jest': 29.5.3 - jest: 29.6.2(@types/node@18.16.16) + jest: 29.6.2(@types/node@18.16.16)(ts-node@10.9.2(@types/node@18.16.16)(typescript@5.5.2)) vitest: 1.6.0(@types/node@18.16.16)(jsdom@23.0.1)(sass@1.64.1)(terser@5.16.1) - '@testing-library/jest-dom@6.4.2(@jest/globals@29.6.2)(@types/jest@29.5.3)(jest@29.6.2(@types/node@18.16.16))(vitest@1.6.0(@types/node@18.16.16)(sass@1.64.1)(terser@5.16.1))': + '@testing-library/jest-dom@6.4.2(@jest/globals@29.6.2)(@types/jest@29.5.3)(jest@29.6.2(@types/node@18.16.16)(ts-node@10.9.2(@types/node@18.16.16)(typescript@5.5.2)))(vitest@1.6.0(@types/node@18.16.16)(sass@1.64.1)(terser@5.16.1))': dependencies: '@adobe/css-tools': 4.3.2 '@babel/runtime': 7.23.6 @@ -18410,7 +18510,7 @@ snapshots: optionalDependencies: '@jest/globals': 29.6.2 '@types/jest': 29.5.3 - jest: 29.6.2(@types/node@18.16.16) + jest: 29.6.2(@types/node@18.16.16)(ts-node@10.9.2(@types/node@18.16.16)(typescript@5.5.2)) vitest: 1.6.0(@types/node@18.16.16)(jsdom@23.0.1)(sass@1.64.1)(terser@5.16.1) '@testing-library/user-event@14.5.1(@testing-library/dom@9.3.4)': @@ -18438,6 +18538,18 @@ snapshots: '@tootallnate/once@2.0.0': {} + '@tsconfig/node10@1.0.11': + optional: true + + '@tsconfig/node12@1.0.11': + optional: true + + '@tsconfig/node14@1.0.3': + optional: true + + '@tsconfig/node16@1.0.4': + optional: true + '@types/amqplib@0.10.1': dependencies: '@types/node': 18.16.16 @@ -18569,6 +18681,12 @@ snapshots: dependencies: '@types/node': 18.16.16 + '@types/fs-extra@11.0.4': + dependencies: + '@types/jsonfile': 6.1.4 + '@types/node': 18.16.16 + optional: true + '@types/ftp@0.3.33': dependencies: '@types/node': 18.16.16 @@ -18638,12 +18756,19 @@ snapshots: '@types/json-schema@7.0.15': {} + '@types/jsonfile@6.1.4': + dependencies: + '@types/node': 18.16.16 + optional: true + '@types/jsonpath@0.2.0': {} '@types/jsonwebtoken@9.0.6': dependencies: '@types/node': 18.16.16 + '@types/k6@0.52.0': {} + '@types/linkify-it@3.0.5': {} '@types/lodash-es@4.17.6': @@ -18959,7 +19084,7 @@ snapshots: dependencies: '@typescript-eslint/typescript-estree': 7.2.0(typescript@5.5.2) '@typescript-eslint/utils': 7.2.0(eslint@8.57.0)(typescript@5.5.2) - debug: 4.3.5(supports-color@8.1.1) + debug: 4.3.6(supports-color@8.1.1) eslint: 8.57.0 ts-api-utils: 1.0.1(typescript@5.5.2) optionalDependencies: @@ -18975,7 +19100,7 @@ snapshots: dependencies: '@typescript-eslint/types': 6.7.5 '@typescript-eslint/visitor-keys': 6.7.5 - debug: 4.3.6 + debug: 4.3.6(supports-color@8.1.1) globby: 11.1.0 is-glob: 4.0.3 semver: 7.6.0 @@ -18989,7 +19114,7 @@ snapshots: dependencies: '@typescript-eslint/types': 7.2.0 '@typescript-eslint/visitor-keys': 7.2.0 - debug: 4.3.5(supports-color@8.1.1) + debug: 4.3.6(supports-color@8.1.1) globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.3 @@ -19049,7 +19174,7 @@ snapshots: dependencies: '@ampproject/remapping': 2.2.1 '@bcoe/v8-coverage': 0.2.3 - debug: 4.3.5(supports-color@8.1.1) + debug: 4.3.5 istanbul-lib-coverage: 3.2.2 istanbul-lib-report: 3.0.1 istanbul-lib-source-maps: 5.0.4 @@ -19232,7 +19357,7 @@ snapshots: '@vue/compiler-dom': 3.4.21 '@vue/shared': 3.4.21 computeds: 0.0.1 - minimatch: 9.0.4 + minimatch: 9.0.5 muggle-string: 0.3.1 path-browserify: 1.0.1 vue-template-compiler: 2.7.14 @@ -19388,19 +19513,19 @@ snapshots: agent-base@6.0.2: dependencies: - debug: 4.3.5(supports-color@8.1.1) + debug: 4.3.6(supports-color@8.1.1) transitivePeerDependencies: - supports-color agent-base@7.1.0: dependencies: - debug: 4.3.5(supports-color@8.1.1) + debug: 4.3.6(supports-color@8.1.1) transitivePeerDependencies: - supports-color agentkeepalive@4.2.1: dependencies: - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.6(supports-color@8.1.1) depd: 1.1.2 humanize-ms: 1.2.1 transitivePeerDependencies: @@ -19497,6 +19622,9 @@ snapshots: readable-stream: 3.6.0 optional: true + arg@4.1.3: + optional: true + arg@5.0.2: {} argparse@1.0.10: @@ -20193,8 +20321,6 @@ snapshots: dependencies: restore-cursor: 3.1.0 - cli-spinners@2.9.0: {} - cli-spinners@2.9.2: {} cli-table3@0.6.3: @@ -20447,6 +20573,9 @@ snapshots: nan: 2.20.0 optional: true + create-require@1.1.1: + optional: true + crelt@1.0.5: {} crlf-normalize@1.0.19(ts-toolbelt@9.6.0): @@ -20718,15 +20847,15 @@ snapshots: optionalDependencies: supports-color: 8.1.1 - debug@4.3.5(supports-color@8.1.1): + debug@4.3.5: dependencies: ms: 2.1.2 - optionalDependencies: - supports-color: 8.1.1 - debug@4.3.6: + debug@4.3.6(supports-color@8.1.1): dependencies: ms: 2.1.2 + optionalDependencies: + supports-color: 8.1.1 decamelize@1.2.0: {} @@ -20847,7 +20976,7 @@ snapshots: detect-port@1.5.1: dependencies: address: 1.2.2 - debug: 4.3.5(supports-color@8.1.1) + debug: 4.3.6(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -20860,6 +20989,9 @@ snapshots: diff-sequences@29.6.3: {} + diff@4.0.2: + optional: true + diff@5.2.0: {} digest-fetch@1.3.0: @@ -21221,7 +21353,7 @@ snapshots: esbuild-register@3.5.0(esbuild@0.20.2): dependencies: - debug: 4.3.5(supports-color@8.1.1) + debug: 4.3.6(supports-color@8.1.1) esbuild: 0.20.2 transitivePeerDependencies: - supports-color @@ -21703,7 +21835,7 @@ snapshots: extract-zip@2.0.1(supports-color@8.1.1): dependencies: - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.6(supports-color@8.1.1) get-stream: 5.2.0 yauzl: 2.10.0 optionalDependencies: @@ -21892,7 +22024,7 @@ snapshots: follow-redirects@1.15.6(debug@4.3.6): optionalDependencies: - debug: 4.3.6 + debug: 4.3.6(supports-color@8.1.1) for-each@0.3.3: dependencies: @@ -22164,7 +22296,7 @@ snapshots: dependencies: foreground-child: 3.1.1 jackspeak: 2.3.6 - minimatch: 9.0.3 + minimatch: 9.0.5 minipass: 7.0.2 path-scurry: 1.10.1 @@ -22465,7 +22597,7 @@ snapshots: dependencies: '@tootallnate/once': 1.1.2 agent-base: 6.0.2 - debug: 4.3.6 + debug: 4.3.6(supports-color@8.1.1) transitivePeerDependencies: - supports-color optional: true @@ -22474,14 +22606,14 @@ snapshots: dependencies: '@tootallnate/once': 2.0.0 agent-base: 6.0.2 - debug: 4.3.6 + debug: 4.3.6(supports-color@8.1.1) transitivePeerDependencies: - supports-color http-proxy-agent@7.0.0: dependencies: agent-base: 7.1.0 - debug: 4.3.5(supports-color@8.1.1) + debug: 4.3.6(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -22496,14 +22628,14 @@ snapshots: https-proxy-agent@5.0.1: dependencies: agent-base: 6.0.2 - debug: 4.3.5(supports-color@8.1.1) + debug: 4.3.6(supports-color@8.1.1) transitivePeerDependencies: - supports-color https-proxy-agent@7.0.2: dependencies: agent-base: 7.1.0 - debug: 4.3.5(supports-color@8.1.1) + debug: 4.3.6(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -22640,7 +22772,7 @@ snapshots: dependencies: '@ioredis/commands': 1.2.0 cluster-key-slot: 1.1.2 - debug: 4.3.5(supports-color@8.1.1) + debug: 4.3.5 denque: 2.1.0 lodash.defaults: 4.2.0 lodash.isarguments: 3.1.0 @@ -22875,7 +23007,7 @@ snapshots: istanbul-lib-source-maps@4.0.1: dependencies: - debug: 4.3.6 + debug: 4.3.6(supports-color@8.1.1) istanbul-lib-coverage: 3.2.2 source-map: 0.6.1 transitivePeerDependencies: @@ -22884,7 +23016,7 @@ snapshots: istanbul-lib-source-maps@5.0.4: dependencies: '@jridgewell/trace-mapping': 0.3.25 - debug: 4.3.5(supports-color@8.1.1) + debug: 4.3.6(supports-color@8.1.1) istanbul-lib-coverage: 3.2.2 transitivePeerDependencies: - supports-color @@ -22938,16 +23070,16 @@ snapshots: - babel-plugin-macros - supports-color - jest-cli@29.6.2(@types/node@18.16.16): + jest-cli@29.6.2(@types/node@18.16.16)(ts-node@10.9.2(@types/node@18.16.16)(typescript@5.5.2)): dependencies: - '@jest/core': 29.6.2 + '@jest/core': 29.6.2(ts-node@10.9.2(@types/node@18.16.16)(typescript@5.5.2)) '@jest/test-result': 29.6.2 '@jest/types': 29.6.1 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 import-local: 3.1.0 - jest-config: 29.6.2(@types/node@18.16.16) + jest-config: 29.6.2(@types/node@18.16.16)(ts-node@10.9.2(@types/node@18.16.16)(typescript@5.5.2)) jest-util: 29.6.2 jest-validate: 29.6.2 prompts: 2.4.2 @@ -22958,7 +23090,7 @@ snapshots: - supports-color - ts-node - jest-config@29.6.2(@types/node@18.16.16): + jest-config@29.6.2(@types/node@18.16.16)(ts-node@10.9.2(@types/node@18.16.16)(typescript@5.5.2)): dependencies: '@babel/core': 7.24.0 '@jest/test-sequencer': 29.6.2 @@ -22984,6 +23116,7 @@ snapshots: strip-json-comments: 3.1.1 optionalDependencies: '@types/node': 18.16.16 + ts-node: 10.9.2(@types/node@18.16.16)(typescript@5.5.2) transitivePeerDependencies: - babel-plugin-macros - supports-color @@ -23101,9 +23234,9 @@ snapshots: slash: 3.0.0 stack-utils: 2.0.6 - jest-mock-extended@3.0.4(jest@29.6.2(@types/node@18.16.16))(typescript@5.5.2): + jest-mock-extended@3.0.4(jest@29.6.2(@types/node@18.16.16)(ts-node@10.9.2(@types/node@18.16.16)(typescript@5.5.2)))(typescript@5.5.2): dependencies: - jest: 29.6.2(@types/node@18.16.16) + jest: 29.6.2(@types/node@18.16.16)(ts-node@10.9.2(@types/node@18.16.16)(typescript@5.5.2)) ts-essentials: 7.0.3(typescript@5.5.2) typescript: 5.5.2 @@ -23261,12 +23394,12 @@ snapshots: merge-stream: 2.0.0 supports-color: 8.1.1 - jest@29.6.2(@types/node@18.16.16): + jest@29.6.2(@types/node@18.16.16)(ts-node@10.9.2(@types/node@18.16.16)(typescript@5.5.2)): dependencies: - '@jest/core': 29.6.2 + '@jest/core': 29.6.2(ts-node@10.9.2(@types/node@18.16.16)(typescript@5.5.2)) '@jest/types': 29.6.1 import-local: 3.1.0 - jest-cli: 29.6.2(@types/node@18.16.16) + jest-cli: 29.6.2(@types/node@18.16.16)(ts-node@10.9.2(@types/node@18.16.16)(typescript@5.5.2)) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -23737,8 +23870,6 @@ snapshots: lilconfig@2.1.0: {} - lilconfig@3.0.0: {} - lilconfig@3.1.2: {} lines-and-columns@1.2.4: {} @@ -24109,10 +24240,6 @@ snapshots: dependencies: brace-expansion: 2.0.1 - minimatch@9.0.4: - dependencies: - brace-expansion: 2.0.1 - minimatch@9.0.5: dependencies: brace-expansion: 2.0.1 @@ -24255,7 +24382,7 @@ snapshots: mqtt-packet@9.0.0: dependencies: bl: 6.0.12 - debug: 4.3.5(supports-color@8.1.1) + debug: 4.3.6(supports-color@8.1.1) process-nextick-args: 2.0.1 transitivePeerDependencies: - supports-color @@ -24311,7 +24438,7 @@ snapshots: dependencies: '@tediousjs/connection-string': 0.5.0 commander: 11.1.0 - debug: 4.3.5(supports-color@8.1.1) + debug: 4.3.5 rfdc: 1.3.0 tarn: 3.0.2 tedious: 16.7.1 @@ -24550,7 +24677,7 @@ snapshots: number-allocator@1.0.14: dependencies: - debug: 4.3.5(supports-color@8.1.1) + debug: 4.3.6(supports-color@8.1.1) js-sdsl: 4.3.0 transitivePeerDependencies: - supports-color @@ -24743,7 +24870,7 @@ snapshots: bl: 4.1.0 chalk: 4.1.2 cli-cursor: 3.1.0 - cli-spinners: 2.9.0 + cli-spinners: 2.9.2 is-interactive: 1.0.0 is-unicode-supported: 0.1.0 log-symbols: 4.1.0 @@ -25063,12 +25190,13 @@ snapshots: camelcase-css: 2.0.1 postcss: 8.4.38 - postcss-load-config@4.0.2(postcss@8.4.38): + postcss-load-config@4.0.2(postcss@8.4.38)(ts-node@10.9.2(@types/node@18.16.16)(typescript@5.5.2)): dependencies: - lilconfig: 3.0.0 + lilconfig: 3.1.2 yaml: 2.3.4 optionalDependencies: postcss: 8.4.38 + ts-node: 10.9.2(@types/node@18.16.16)(typescript@5.5.2) postcss-nested@6.0.1(postcss@8.4.38): dependencies: @@ -25777,7 +25905,7 @@ snapshots: retry-request@5.0.2: dependencies: - debug: 4.3.6 + debug: 4.3.6(supports-color@8.1.1) extend: 3.0.2 transitivePeerDependencies: - supports-color @@ -26125,7 +26253,7 @@ snapshots: simple-websocket@9.1.0: dependencies: - debug: 4.3.5(supports-color@8.1.1) + debug: 4.3.6(supports-color@8.1.1) queue-microtask: 1.2.3 randombytes: 2.1.0 readable-stream: 3.6.0 @@ -26206,7 +26334,7 @@ snapshots: socks-proxy-agent@6.2.1: dependencies: agent-base: 6.0.2 - debug: 4.3.6 + debug: 4.3.6(supports-color@8.1.1) socks: 2.7.1 transitivePeerDependencies: - supports-color @@ -26530,7 +26658,7 @@ snapshots: dependencies: component-emitter: 1.3.0 cookiejar: 2.1.4 - debug: 4.3.5(supports-color@8.1.1) + debug: 4.3.6(supports-color@8.1.1) fast-safe-stringify: 2.1.1 form-data: 4.0.0 formidable: 3.5.1 @@ -26593,7 +26721,7 @@ snapshots: syslog-client@1.1.1: {} - tailwindcss@3.4.3: + tailwindcss@3.4.3(ts-node@10.9.2(@types/node@18.16.16)(typescript@5.5.2)): dependencies: '@alloc/quick-lru': 5.2.0 arg: 5.0.2 @@ -26612,7 +26740,7 @@ snapshots: postcss: 8.4.38 postcss-import: 15.1.0(postcss@8.4.38) postcss-js: 4.0.1(postcss@8.4.38) - postcss-load-config: 4.0.2(postcss@8.4.38) + postcss-load-config: 4.0.2(postcss@8.4.38)(ts-node@10.9.2(@types/node@18.16.16)(typescript@5.5.2)) postcss-nested: 6.0.1(postcss@8.4.38) postcss-selector-parser: 6.0.16 resolve: 1.22.8 @@ -26855,11 +26983,11 @@ snapshots: ts-interface-checker@0.1.13: {} - ts-jest@29.1.1(@babel/core@7.24.0)(@jest/types@29.6.1)(babel-jest@29.6.2(@babel/core@7.24.0))(jest@29.6.2(@types/node@18.16.16))(typescript@5.5.2): + ts-jest@29.1.1(@babel/core@7.24.0)(@jest/types@29.6.1)(babel-jest@29.6.2(@babel/core@7.24.0))(jest@29.6.2(@types/node@18.16.16)(ts-node@10.9.2(@types/node@18.16.16)(typescript@5.5.2)))(typescript@5.5.2): dependencies: bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 - jest: 29.6.2(@types/node@18.16.16) + jest: 29.6.2(@types/node@18.16.16)(ts-node@10.9.2(@types/node@18.16.16)(typescript@5.5.2)) jest-util: 29.5.0 json5: 2.2.3 lodash.memoize: 4.1.2 @@ -26874,6 +27002,25 @@ snapshots: ts-map@1.0.3: {} + ts-node@10.9.2(@types/node@18.16.16)(typescript@5.5.2): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.11 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 18.16.16 + acorn: 8.12.1 + acorn-walk: 8.3.2 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 5.5.2 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + optional: true + ts-toolbelt@9.6.0: {} ts-type@3.0.1(ts-toolbelt@9.6.0): @@ -27138,7 +27285,7 @@ snapshots: '@antfu/install-pkg': 0.3.3 '@antfu/utils': 0.7.10 '@iconify/utils': 2.1.25 - debug: 4.3.5(supports-color@8.1.1) + debug: 4.3.5 kolorist: 1.8.0 local-pkg: 0.5.0 unplugin: 1.11.0 @@ -27153,7 +27300,7 @@ snapshots: '@antfu/utils': 0.7.10 '@rollup/pluginutils': 5.1.0(rollup@4.18.0) chokidar: 3.5.2 - debug: 4.3.5(supports-color@8.1.1) + debug: 4.3.5 fast-glob: 3.3.2 local-pkg: 0.5.0 magic-string: 0.30.10 @@ -27264,6 +27411,9 @@ snapshots: v3-infinite-loading@1.2.2: {} + v8-compile-cache-lib@3.0.1: + optional: true + v8-to-istanbul@9.1.0: dependencies: '@jridgewell/trace-mapping': 0.3.25 @@ -27288,7 +27438,7 @@ snapshots: vite-node@1.6.0(@types/node@18.16.16)(sass@1.64.1)(terser@5.16.1): dependencies: cac: 6.7.14 - debug: 4.3.5(supports-color@8.1.1) + debug: 4.3.6(supports-color@8.1.1) pathe: 1.1.2 picocolors: 1.0.1 vite: 5.2.12(@types/node@18.16.16)(sass@1.64.1)(terser@5.16.1) @@ -27307,7 +27457,7 @@ snapshots: '@microsoft/api-extractor': 7.43.0(@types/node@18.16.16) '@rollup/pluginutils': 5.1.0(rollup@4.18.0) '@vue/language-core': 1.8.27(typescript@5.5.2) - debug: 4.3.5(supports-color@8.1.1) + debug: 4.3.5 kolorist: 1.8.0 magic-string: 0.30.8 typescript: 5.5.2 @@ -27345,7 +27495,7 @@ snapshots: '@vitest/utils': 1.6.0 acorn-walk: 8.3.2 chai: 4.3.10 - debug: 4.3.5(supports-color@8.1.1) + debug: 4.3.5 execa: 8.0.1 local-pkg: 0.5.0 magic-string: 0.30.10 @@ -27826,6 +27976,9 @@ snapshots: buffer-crc32: 0.2.13 fd-slicer: 1.1.0 + yn@3.1.1: + optional: true + yocto-queue@0.1.0: {} yocto-queue@1.0.0: {} @@ -27855,3 +28008,8 @@ snapshots: zod@3.22.4: {} zod@3.23.8: {} + + zx@8.1.4: + optionalDependencies: + '@types/fs-extra': 11.0.4 + '@types/node': 18.16.16 From df231f35c4a0eba13f97b2298057eefd82e587cf Mon Sep 17 00:00:00 2001 From: Tomi Turtiainen <10324676+tomi@users.noreply.github.com> Date: Tue, 20 Aug 2024 10:55:31 +0300 Subject: [PATCH 02/17] ci: Add workflow to build benchmark cli (no-changelog) (#10441) --- .github/workflows/docker-images-benchmark.yml | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 .github/workflows/docker-images-benchmark.yml diff --git a/.github/workflows/docker-images-benchmark.yml b/.github/workflows/docker-images-benchmark.yml new file mode 100644 index 0000000000000..e4b09cab61467 --- /dev/null +++ b/.github/workflows/docker-images-benchmark.yml @@ -0,0 +1,43 @@ +name: Benchmark Docker Image CI + +on: + workflow_dispatch: + push: + branches: + - main + paths: + - 'packages/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/benchmark/Dockerfile + platforms: linux/amd64 + provenance: false + push: true + tags: | + ghcr.io/${{ github.repository_owner }}/n8n-benchmark:latest From 5de464617957ee6f0cf8295df4b529fa6a8d9e6b Mon Sep 17 00:00:00 2001 From: Tomi Turtiainen <10324676+tomi@users.noreply.github.com> Date: Tue, 20 Aug 2024 11:14:14 +0300 Subject: [PATCH 03/17] docs(benchmark): Update readme --- packages/benchmark/README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/benchmark/README.md b/packages/benchmark/README.md index eaaa36631371b..d54da6ffa1a59 100644 --- a/packages/benchmark/README.md +++ b/packages/benchmark/README.md @@ -31,3 +31,13 @@ Because k6 doesn't have an arm64 build available for linux, we need to build aga # In the repository root docker build --platform linux/amd64 -f packages/benchmark/Dockerfile -t n8n-benchmark:latest . ``` + +## Test scenarios + +A test scenario defines a single performance test. It consists of: + +- Manifest file which describes and configures the scenario +- Any test data that is imported before the scenario is ran +- A [`k6`](https://grafana.com/docs/k6/latest/using-k6/http-requests/) test script, which receives `API_BASE_URL` environment variable in runtime. + +Available test scenarios are located in [`./testScenarios`](./testScenarios/). From e72fa263d9fa85fd107acdca62bb12e0b2258efc Mon Sep 17 00:00:00 2001 From: Tomi Turtiainen <10324676+tomi@users.noreply.github.com> Date: Tue, 20 Aug 2024 13:19:47 +0300 Subject: [PATCH 04/17] refactor: Specify dockerfile version --- packages/benchmark/Dockerfile | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/benchmark/Dockerfile b/packages/benchmark/Dockerfile index 18673141fd565..66bdad27953d4 100644 --- a/packages/benchmark/Dockerfile +++ b/packages/benchmark/Dockerfile @@ -1,3 +1,4 @@ +# syntax=docker/dockerfile:1 # NOTE: # This Dockerfile needs to be built in the root of the repository # `docker build -t n8n-benchmark -f packages/benchmark/Dockerfile .` @@ -37,9 +38,6 @@ COPY --chown=node:node ./scripts /app/scripts RUN pnpm install --frozen-lockfile -# Package.json -COPY --chown=node:node ./packages/benchmark/package.json /app/packages/benchmark/package.json - # TS config files COPY --chown=node:node ./tsconfig.json /app/tsconfig.json COPY --chown=node:node ./tsconfig.build.json /app/tsconfig.build.json From efb4ba1539f9d7c7e281f1634c20fd91a1e021ef Mon Sep 17 00:00:00 2001 From: Tomi Turtiainen <10324676+tomi@users.noreply.github.com> Date: Tue, 20 Aug 2024 13:31:56 +0300 Subject: [PATCH 05/17] docs: Update readme --- packages/benchmark/Dockerfile | 3 --- packages/benchmark/README.md | 38 +++++++++++++++++++++++------------ 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/packages/benchmark/Dockerfile b/packages/benchmark/Dockerfile index 66bdad27953d4..f27da351f47a8 100644 --- a/packages/benchmark/Dockerfile +++ b/packages/benchmark/Dockerfile @@ -1,7 +1,4 @@ # syntax=docker/dockerfile:1 -# NOTE: -# This Dockerfile needs to be built in the root of the repository -# `docker build -t n8n-benchmark -f packages/benchmark/Dockerfile .` FROM node:20.16.0 AS base # Install required dependencies diff --git a/packages/benchmark/README.md b/packages/benchmark/README.md index d54da6ffa1a59..204e977878c00 100644 --- a/packages/benchmark/README.md +++ b/packages/benchmark/README.md @@ -2,12 +2,33 @@ Tool for executing benchmarks against an n8n instance. -## Requirements +## Running locally with Docker -- [k6](https://grafana.com/docs/k6/latest/) -- Node.js v20 or higher +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/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 +``` -## Running locally +## Running locally without Docker + +Requirements: + +- [k6](https://grafana.com/docs/k6/latest/set-up/install-k6/) +- Node.js v20 or higher ```sh pnpm build @@ -23,15 +44,6 @@ K6_PATH=/opt/homebrew/bin/k6 N8N_USER_EMAIL=user@n8n.io N8N_USER_PASSWORD=passwo The configuration options the cli accepts can be seen from [config.ts](./src/config/config.ts) -## Docker build - -Because k6 doesn't have an arm64 build available for linux, we need to build against amd64 - -```sh -# In the repository root -docker build --platform linux/amd64 -f packages/benchmark/Dockerfile -t n8n-benchmark:latest . -``` - ## Test scenarios A test scenario defines a single performance test. It consists of: From d47b8d8aad4582528f21efd60a2b4cc949f3c624 Mon Sep 17 00:00:00 2001 From: Tomi Turtiainen <10324676+tomi@users.noreply.github.com> Date: Tue, 20 Aug 2024 13:35:35 +0300 Subject: [PATCH 06/17] chore: Remove nodemon config --- packages/benchmark/nodemon.json | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 packages/benchmark/nodemon.json diff --git a/packages/benchmark/nodemon.json b/packages/benchmark/nodemon.json deleted file mode 100644 index 393762d234d2d..0000000000000 --- a/packages/benchmark/nodemon.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "ignore": ["**/*.spec.ts", ".git", "node_modules"], - "watch": ["commands", "index.ts", "src"], - "exec": "npm start", - "ext": "ts" -} From ffd1005fd15252a59875a1df91ee0983d2b3f9ed Mon Sep 17 00:00:00 2001 From: Tomi Turtiainen <10324676+tomi@users.noreply.github.com> Date: Tue, 20 Aug 2024 13:38:09 +0300 Subject: [PATCH 07/17] chore: Update package metadata --- packages/benchmark/package.json | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/benchmark/package.json b/packages/benchmark/package.json index c1d2ba6b4b0cd..6a8e9772f8747 100644 --- a/packages/benchmark/package.json +++ b/packages/benchmark/package.json @@ -1,7 +1,7 @@ { "name": "n8n-benchmark", "version": "1.0.0", - "description": "", + "description": "Cli for running benchmark tests for n8n", "main": "dist/index", "scripts": { "build": "tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json", @@ -13,9 +13,16 @@ "engines": { "node": ">=20.10" }, - "keywords": [], - "author": "", - "license": "ISC", + "keywords": [ + "automate", + "automation", + "IaaS", + "iPaaS", + "n8n", + "workflow", + "benchmark", + "performance" + ], "dependencies": { "@oclif/core": "4.0.7", "axios": "catalog:", From 98381c4cc49d6ba2a6994bb7818426aee637fe23 Mon Sep 17 00:00:00 2001 From: Tomi Turtiainen <10324676+tomi@users.noreply.github.com> Date: Tue, 20 Aug 2024 13:59:41 +0300 Subject: [PATCH 08/17] refactor: Rename test scenarios to benchmark scenarios --- packages/benchmark/README.md | 8 +-- .../codeNode/codeNode.json | 0 .../codeNode/codeNode.manifest.json | 0 .../codeNode/codeNode.script.ts | 0 .../singleWebhook/singleWebhook.json | 0 .../singleWebhook/singleWebhook.manifest.json | 0 .../singleWebhook/singleWebhook.script.ts | 0 .../testScenario.schema.json | 0 packages/benchmark/src/commands/list.ts | 12 ++-- packages/benchmark/src/commands/run.ts | 16 ++--- packages/benchmark/src/config/config.ts | 4 +- .../scenarioDataLoader.ts} | 12 ++-- .../benchmark/src/scenario/scenarioLoader.ts | 67 +++++++++++++++++++ .../benchmark/src/testExecution/k6Executor.ts | 8 +-- ...ataImporter.ts => scenarioDataImporter.ts} | 6 +- ...estScenarioRunner.ts => scenarioRunner.ts} | 28 ++++---- .../src/testScenario/testScenarioLoader.ts | 67 ------------------- packages/benchmark/src/types/scenario.ts | 27 ++++++++ packages/benchmark/src/types/testScenario.ts | 26 ------- 19 files changed, 141 insertions(+), 140 deletions(-) rename packages/benchmark/{testScenarios => scenarios}/codeNode/codeNode.json (100%) rename packages/benchmark/{testScenarios => scenarios}/codeNode/codeNode.manifest.json (100%) rename packages/benchmark/{testScenarios => scenarios}/codeNode/codeNode.script.ts (100%) rename packages/benchmark/{testScenarios => scenarios}/singleWebhook/singleWebhook.json (100%) rename packages/benchmark/{testScenarios => scenarios}/singleWebhook/singleWebhook.manifest.json (100%) rename packages/benchmark/{testScenarios => scenarios}/singleWebhook/singleWebhook.script.ts (100%) rename packages/benchmark/{testScenarios => scenarios}/testScenario.schema.json (100%) rename packages/benchmark/src/{testScenario/testDataLoader.ts => scenario/scenarioDataLoader.ts} (56%) create mode 100644 packages/benchmark/src/scenario/scenarioLoader.ts rename packages/benchmark/src/testExecution/{testDataImporter.ts => scenarioDataImporter.ts} (90%) rename packages/benchmark/src/testExecution/{testScenarioRunner.ts => scenarioRunner.ts} (54%) delete mode 100644 packages/benchmark/src/testScenario/testScenarioLoader.ts create mode 100644 packages/benchmark/src/types/scenario.ts delete mode 100644 packages/benchmark/src/types/testScenario.ts diff --git a/packages/benchmark/README.md b/packages/benchmark/README.md index 204e977878c00..8e175d817b922 100644 --- a/packages/benchmark/README.md +++ b/packages/benchmark/README.md @@ -44,12 +44,12 @@ K6_PATH=/opt/homebrew/bin/k6 N8N_USER_EMAIL=user@n8n.io N8N_USER_PASSWORD=passwo The configuration options the cli accepts can be seen from [config.ts](./src/config/config.ts) -## Test scenarios +## Benchmark scenarios -A test scenario defines a single performance test. It consists of: +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 ran -- A [`k6`](https://grafana.com/docs/k6/latest/using-k6/http-requests/) test script, which receives `API_BASE_URL` environment variable in runtime. +- 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 test scenarios are located in [`./testScenarios`](./testScenarios/). diff --git a/packages/benchmark/testScenarios/codeNode/codeNode.json b/packages/benchmark/scenarios/codeNode/codeNode.json similarity index 100% rename from packages/benchmark/testScenarios/codeNode/codeNode.json rename to packages/benchmark/scenarios/codeNode/codeNode.json diff --git a/packages/benchmark/testScenarios/codeNode/codeNode.manifest.json b/packages/benchmark/scenarios/codeNode/codeNode.manifest.json similarity index 100% rename from packages/benchmark/testScenarios/codeNode/codeNode.manifest.json rename to packages/benchmark/scenarios/codeNode/codeNode.manifest.json diff --git a/packages/benchmark/testScenarios/codeNode/codeNode.script.ts b/packages/benchmark/scenarios/codeNode/codeNode.script.ts similarity index 100% rename from packages/benchmark/testScenarios/codeNode/codeNode.script.ts rename to packages/benchmark/scenarios/codeNode/codeNode.script.ts diff --git a/packages/benchmark/testScenarios/singleWebhook/singleWebhook.json b/packages/benchmark/scenarios/singleWebhook/singleWebhook.json similarity index 100% rename from packages/benchmark/testScenarios/singleWebhook/singleWebhook.json rename to packages/benchmark/scenarios/singleWebhook/singleWebhook.json diff --git a/packages/benchmark/testScenarios/singleWebhook/singleWebhook.manifest.json b/packages/benchmark/scenarios/singleWebhook/singleWebhook.manifest.json similarity index 100% rename from packages/benchmark/testScenarios/singleWebhook/singleWebhook.manifest.json rename to packages/benchmark/scenarios/singleWebhook/singleWebhook.manifest.json diff --git a/packages/benchmark/testScenarios/singleWebhook/singleWebhook.script.ts b/packages/benchmark/scenarios/singleWebhook/singleWebhook.script.ts similarity index 100% rename from packages/benchmark/testScenarios/singleWebhook/singleWebhook.script.ts rename to packages/benchmark/scenarios/singleWebhook/singleWebhook.script.ts diff --git a/packages/benchmark/testScenarios/testScenario.schema.json b/packages/benchmark/scenarios/testScenario.schema.json similarity index 100% rename from packages/benchmark/testScenarios/testScenario.schema.json rename to packages/benchmark/scenarios/testScenario.schema.json diff --git a/packages/benchmark/src/commands/list.ts b/packages/benchmark/src/commands/list.ts index 603f746f286ae..73d2deadff1a2 100644 --- a/packages/benchmark/src/commands/list.ts +++ b/packages/benchmark/src/commands/list.ts @@ -1,21 +1,21 @@ import { Command } from '@oclif/core'; -import { TestScenarioLoader } from '@/testScenario/testScenarioLoader'; +import { ScenarioLoader } from '@/scenario/scenarioLoader'; import { loadConfig } from '@/config/config'; export default class ListCommand extends Command { - static description = 'List all available test scenarios'; + static description = 'List all available scenarios'; async run() { const config = loadConfig(); - const testScenarioLoader = new TestScenarioLoader(); + const scenarioLoader = new ScenarioLoader(); - const allScenarios = testScenarioLoader.loadAllTestScenarios(config.get('testScenariosPath')); + const allScenarios = scenarioLoader.loadAllScenarios(config.get('testScenariosPath')); console.log('Available test scenarios:'); console.log(''); - for (const testCase of allScenarios) { - console.log('\t', testCase.name, ':', testCase.description); + for (const scenario of allScenarios) { + console.log('\t', scenario.name, ':', scenario.description); } } } diff --git a/packages/benchmark/src/commands/run.ts b/packages/benchmark/src/commands/run.ts index 0b524906e3e20..aede197d2d4a9 100644 --- a/packages/benchmark/src/commands/run.ts +++ b/packages/benchmark/src/commands/run.ts @@ -1,9 +1,9 @@ import { Command, Flags } from '@oclif/core'; import { loadConfig } from '@/config/config'; -import { TestScenarioLoader } from '@/testScenario/testScenarioLoader'; -import { TestScenarioRunner } from '@/testExecution/testScenarioRunner'; +import { ScenarioLoader } from '@/scenario/scenarioLoader'; +import { ScenarioRunner } from '@/testExecution/scenarioRunner'; import { N8nApiClient } from '@/n8nApiClient/n8nApiClient'; -import { TestDataFileLoader } from '@/testScenario/testDataLoader'; +import { ScenarioDataFileLoader } from '@/scenario/scenarioDataLoader'; import { K6Executor } from '@/testExecution/k6Executor'; export default class RunCommand extends Command { @@ -20,11 +20,11 @@ export default class RunCommand extends Command { async run() { const config = loadConfig(); - const testScenarioLoader = new TestScenarioLoader(); + const scenarioLoader = new ScenarioLoader(); - const testScenarioRunner = new TestScenarioRunner( + const scenarioRunner = new ScenarioRunner( new N8nApiClient(config.get('n8n.baseUrl')), - new TestDataFileLoader(), + new ScenarioDataFileLoader(), new K6Executor(config.get('k6ExecutablePath'), config.get('n8n.baseUrl')), { email: config.get('n8n.user.email'), @@ -32,8 +32,8 @@ export default class RunCommand extends Command { }, ); - const allScenarios = testScenarioLoader.loadAllTestScenarios(config.get('testScenariosPath')); + const allScenarios = scenarioLoader.loadAllScenarios(config.get('testScenariosPath')); - await testScenarioRunner.runManyTestScenarios(allScenarios); + await scenarioRunner.runManyScenarios(allScenarios); } } diff --git a/packages/benchmark/src/config/config.ts b/packages/benchmark/src/config/config.ts index d20bfbe8779f8..896ecc9296da0 100644 --- a/packages/benchmark/src/config/config.ts +++ b/packages/benchmark/src/config/config.ts @@ -5,9 +5,9 @@ dotenv.config(); const configSchema = { testScenariosPath: { - doc: 'The path to the test scenarios', + doc: 'The path to the scenarios', format: String, - default: 'testScenarios', + default: 'scenarios', }, n8n: { baseUrl: { diff --git a/packages/benchmark/src/testScenario/testDataLoader.ts b/packages/benchmark/src/scenario/scenarioDataLoader.ts similarity index 56% rename from packages/benchmark/src/testScenario/testDataLoader.ts rename to packages/benchmark/src/scenario/scenarioDataLoader.ts index 3badad5e0cd32..3f5b6c0be9443 100644 --- a/packages/benchmark/src/testScenario/testDataLoader.ts +++ b/packages/benchmark/src/scenario/scenarioDataLoader.ts @@ -1,18 +1,18 @@ import fs from 'fs'; import path from 'path'; -import { TestScenario } from '@/types/testScenario'; +import { Scenario } from '@/types/scenario'; import { Workflow } from '@/n8nApiClient/n8nApiClient.types'; /** - * Loads test data files from FS + * Loads scenario data files from FS */ -export class TestDataFileLoader { - async loadTestDataForScenario(testScenario: TestScenario): Promise<{ +export class ScenarioDataFileLoader { + async loadDataForScenario(scenario: Scenario): Promise<{ workflows: Workflow[]; }> { const workflows = await Promise.all( - testScenario.testData.workflowFiles?.map((workflowFilePath) => - this.loadSingleWorkflowFromFile(path.join(testScenario.testScenarioPath, workflowFilePath)), + scenario.scenarioData.workflowFiles?.map((workflowFilePath) => + this.loadSingleWorkflowFromFile(path.join(scenario.scenarioDirPath, workflowFilePath)), ) ?? [], ); diff --git a/packages/benchmark/src/scenario/scenarioLoader.ts b/packages/benchmark/src/scenario/scenarioLoader.ts new file mode 100644 index 0000000000000..535792ea4db62 --- /dev/null +++ b/packages/benchmark/src/scenario/scenarioLoader.ts @@ -0,0 +1,67 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import { createHash } from 'node:crypto'; +import type { Scenario, ScenarioManifest } from '@/types/scenario'; + +export class ScenarioLoader { + /** + * Loads all scenarios from the given path + */ + loadAllScenarios(pathToScenarios: string): Scenario[] { + pathToScenarios = path.resolve(pathToScenarios); + const scenarioFolders = fs + .readdirSync(pathToScenarios, { withFileTypes: true }) + .filter((dirent) => dirent.isDirectory()) + .map((dirent) => dirent.name); + + const scenarios: Scenario[] = []; + + for (const folder of scenarioFolders) { + const scenarioPath = path.join(pathToScenarios, folder); + const manifestFileName = `${folder}.manifest.json`; + const scenarioManifestPath = path.join(pathToScenarios, folder, manifestFileName); + if (!fs.existsSync(scenarioManifestPath)) { + console.warn(`Scenario at ${scenarioPath} is missing the ${manifestFileName} file`); + continue; + } + + // Load the scenario manifest file + const [scenario, validationErrors] = + this.loadAndValidateScenarioManifest(scenarioManifestPath); + if (validationErrors) { + console.warn( + `Scenario at ${scenarioPath} has the following validation errors: ${validationErrors.join(', ')}`, + ); + continue; + } + + scenarios.push({ + ...scenario, + id: this.formScenarioId(scenarioPath), + scenarioDirPath: scenarioPath, + }); + } + + return scenarios; + } + + private loadAndValidateScenarioManifest( + scenarioManifestPath: string, + ): [ScenarioManifest, null] | [null, string[]] { + const scenario = JSON.parse(fs.readFileSync(scenarioManifestPath, 'utf8')); + const validationErrors: string[] = []; + + if (!scenario.name) { + validationErrors.push(`Scenario at ${scenarioManifestPath} is missing a name`); + } + if (!scenario.description) { + validationErrors.push(`Scenario at ${scenarioManifestPath} is missing a description`); + } + + return validationErrors.length === 0 ? [scenario, null] : [null, validationErrors]; + } + + private formScenarioId(scenarioPath: string): string { + return createHash('sha256').update(scenarioPath).digest('hex'); + } +} diff --git a/packages/benchmark/src/testExecution/k6Executor.ts b/packages/benchmark/src/testExecution/k6Executor.ts index f2f1768331db1..903d06ca74765 100644 --- a/packages/benchmark/src/testExecution/k6Executor.ts +++ b/packages/benchmark/src/testExecution/k6Executor.ts @@ -1,5 +1,5 @@ import { $ } from 'zx'; -import { TestScenario } from '@/types/testScenario'; +import { Scenario } from '@/types/scenario'; /** * Executes test scenarios using k6 @@ -10,16 +10,16 @@ export class K6Executor { private readonly n8nApiBaseUrl: string, ) {} - async executeTestScenario(testCase: TestScenario) { + async executeTestScenario(scenario: Scenario) { // For 1 min with 5 virtual users const stage = '1m:5'; const processPromise = $({ - cwd: testCase.testScenarioPath, + cwd: scenario.scenarioDirPath, env: { API_BASE_URL: this.n8nApiBaseUrl, }, - })`${this.k6ExecutablePath} run --quiet --stage ${stage} ${testCase.testScriptPath}`; + })`${this.k6ExecutablePath} run --quiet --stage ${stage} ${scenario.scriptPath}`; for await (const chunk of processPromise.stdout) { console.log(chunk.toString()); diff --git a/packages/benchmark/src/testExecution/testDataImporter.ts b/packages/benchmark/src/testExecution/scenarioDataImporter.ts similarity index 90% rename from packages/benchmark/src/testExecution/testDataImporter.ts rename to packages/benchmark/src/testExecution/scenarioDataImporter.ts index a56224ee7fbce..f9bea7e20e8f2 100644 --- a/packages/benchmark/src/testExecution/testDataImporter.ts +++ b/packages/benchmark/src/testExecution/scenarioDataImporter.ts @@ -3,9 +3,9 @@ import { Workflow } from '@/n8nApiClient/n8nApiClient.types'; import { WorkflowApiClient } from '@/n8nApiClient/workflowsApiClient'; /** - * Imports test data into an n8n instance + * Imports scenario data into an n8n instance */ -export class TestDataImporter { +export class ScenarioDataImporter { private readonly workflowApiClient: WorkflowApiClient; constructor(n8nApiClient: AuthenticatedN8nApiClient) { @@ -21,7 +21,7 @@ export class TestDataImporter { } /** - * Imports a single workflow into n8n and tags it with the given testCaseId + * Imports a single workflow into n8n removing any existing workflows with the same name */ private async importWorkflow(opts: { existingWorkflows: Workflow[]; workflow: Workflow }) { const existingWorkflows = this.tryFindExistingWorkflows(opts.existingWorkflows, opts.workflow); diff --git a/packages/benchmark/src/testExecution/testScenarioRunner.ts b/packages/benchmark/src/testExecution/scenarioRunner.ts similarity index 54% rename from packages/benchmark/src/testExecution/testScenarioRunner.ts rename to packages/benchmark/src/testExecution/scenarioRunner.ts index 2078864880f2f..83e1077680215 100644 --- a/packages/benchmark/src/testExecution/testScenarioRunner.ts +++ b/packages/benchmark/src/testExecution/scenarioRunner.ts @@ -1,17 +1,17 @@ -import { TestScenario } from '@/types/testScenario'; +import { Scenario } from '@/types/scenario'; import { N8nApiClient } from '@/n8nApiClient/n8nApiClient'; -import { TestDataFileLoader } from '@/testScenario/testDataLoader'; +import { ScenarioDataFileLoader } from '@/scenario/scenarioDataLoader'; import { K6Executor } from './k6Executor'; -import { TestDataImporter } from '@/testExecution/testDataImporter'; +import { ScenarioDataImporter } from '@/testExecution/scenarioDataImporter'; import { AuthenticatedN8nApiClient } from '@/n8nApiClient/authenticatedN8nApiClient'; /** - * Runs test scenarios + * Runs scenarios */ -export class TestScenarioRunner { +export class ScenarioRunner { constructor( private readonly n8nClient: N8nApiClient, - private readonly testDataLoader: TestDataFileLoader, + private readonly dataLoader: ScenarioDataFileLoader, private readonly k6Executor: K6Executor, private readonly ownerConfig: { email: string; @@ -19,7 +19,7 @@ export class TestScenarioRunner { }, ) {} - async runManyTestScenarios(testCases: TestScenario[]) { + async runManyScenarios(scenarios: Scenario[]) { console.log(`Waiting for n8n ${this.n8nClient.apiBaseUrl} to become online`); await this.n8nClient.waitForInstanceToBecomeOnline(); @@ -30,21 +30,21 @@ export class TestScenarioRunner { this.n8nClient, this.ownerConfig, ); - const testDataImporter = new TestDataImporter(authenticatedN8nClient); + const testDataImporter = new ScenarioDataImporter(authenticatedN8nClient); - for (const testCase of testCases) { - await this.runSingleTestScenario(testDataImporter, testCase); + for (const scenario of scenarios) { + await this.runSingleTestScenario(testDataImporter, scenario); } } - private async runSingleTestScenario(testDataImporter: TestDataImporter, scenario: TestScenario) { + private async runSingleTestScenario(testDataImporter: ScenarioDataImporter, scenario: Scenario) { console.log('Running scenario:', scenario.name); - console.log('Loading and importing test data'); - const testData = await this.testDataLoader.loadTestDataForScenario(scenario); + console.log('Loading and importing data'); + const testData = await this.dataLoader.loadDataForScenario(scenario); await testDataImporter.importTestScenarioData(testData.workflows); - console.log('Executing test script'); + console.log('Executing scenario script'); await this.k6Executor.executeTestScenario(scenario); } } diff --git a/packages/benchmark/src/testScenario/testScenarioLoader.ts b/packages/benchmark/src/testScenario/testScenarioLoader.ts deleted file mode 100644 index b0a6d8ca5e6ba..0000000000000 --- a/packages/benchmark/src/testScenario/testScenarioLoader.ts +++ /dev/null @@ -1,67 +0,0 @@ -import * as fs from 'fs'; -import * as path from 'path'; -import { createHash } from 'node:crypto'; -import type { TestScenario, TestScenarioManifest } from '@/types/testScenario'; - -export class TestScenarioLoader { - /** - * Loads all test scenarios from the given path - */ - loadAllTestScenarios(testScenariosPath: string): TestScenario[] { - testScenariosPath = path.resolve(testScenariosPath); - const testScenarioFolders = fs - .readdirSync(testScenariosPath, { withFileTypes: true }) - .filter((dirent) => dirent.isDirectory()) - .map((dirent) => dirent.name); - - const scenarios: TestScenario[] = []; - - for (const folder of testScenarioFolders) { - const scenarioPath = path.join(testScenariosPath, folder); - const manifestFileName = `${folder}.manifest.json`; - const scenarioManifestPath = path.join(testScenariosPath, folder, manifestFileName); - if (!fs.existsSync(scenarioManifestPath)) { - console.warn(`Test scenario at ${scenarioPath} is missing the ${manifestFileName} file`); - continue; - } - - // Load the scenario manifest file - const [testCase, validationErrors] = - this.loadAndValidateTestScenarioManifest(scenarioManifestPath); - if (validationErrors) { - console.warn( - `Test case at ${scenarioPath} has the following validation errors: ${validationErrors.join(', ')}`, - ); - continue; - } - - scenarios.push({ - ...testCase, - id: this.formTestScenarioId(scenarioPath), - testScenarioPath: scenarioPath, - }); - } - - return scenarios; - } - - private loadAndValidateTestScenarioManifest( - testScenarioManifestPath: string, - ): [TestScenarioManifest, null] | [null, string[]] { - const testCase = JSON.parse(fs.readFileSync(testScenarioManifestPath, 'utf8')); - const validationErrors: string[] = []; - - if (!testCase.name) { - validationErrors.push(`Test case at ${testScenarioManifestPath} is missing a name`); - } - if (!testCase.description) { - validationErrors.push(`Test case at ${testScenarioManifestPath} is missing a description`); - } - - return validationErrors.length === 0 ? [testCase, null] : [null, validationErrors]; - } - - private formTestScenarioId(testCasePath: string): string { - return createHash('sha256').update(testCasePath).digest('hex'); - } -} diff --git a/packages/benchmark/src/types/scenario.ts b/packages/benchmark/src/types/scenario.ts new file mode 100644 index 0000000000000..19c52fd45b7fd --- /dev/null +++ b/packages/benchmark/src/types/scenario.ts @@ -0,0 +1,27 @@ +export type ScenarioData = { + /** Relative paths to the workflow files */ + workflowFiles?: string[]; +}; + +/** + * Configuration that defines the benchmark scenario + */ +export type ScenarioManifest = { + /** The name of the scenario */ + name: string; + /** A longer description of the scenario */ + description: string; + /** Relative path to the k6 script */ + scriptPath: string; + /** Data to import before running the scenario */ + scenarioData: ScenarioData; +}; + +/** + * Scenario with additional metadata + */ +export type Scenario = ScenarioManifest & { + id: string; + /** Path to the directory containing the scenario */ + scenarioDirPath: string; +}; diff --git a/packages/benchmark/src/types/testScenario.ts b/packages/benchmark/src/types/testScenario.ts deleted file mode 100644 index 7946301bd12f5..0000000000000 --- a/packages/benchmark/src/types/testScenario.ts +++ /dev/null @@ -1,26 +0,0 @@ -export type TestData = { - /** Relative paths to the workflow files */ - workflowFiles?: string[]; -}; - -/** - * Configuration that defines the test scenario - */ -export type TestScenarioManifest = { - /** The name of the test scenario */ - name: string; - /** A longer description of the test scenario */ - description: string; - /** Relative path to the k6 test script */ - testScriptPath: string; - /** Test data to import before running the scenario */ - testData: TestData; -}; - -/** - * A test scenario with additional metadata - */ -export type TestScenario = TestScenarioManifest & { - id: string; - testScenarioPath: string; -}; From cbe6869e818f86ce0be00874e574fcd1c341d90c Mon Sep 17 00:00:00 2001 From: Tomi Turtiainen <10324676+tomi@users.noreply.github.com> Date: Tue, 20 Aug 2024 14:02:35 +0300 Subject: [PATCH 09/17] refactor: Rename method --- packages/benchmark/src/commands/list.ts | 2 +- packages/benchmark/src/commands/run.ts | 2 +- packages/benchmark/src/scenario/scenarioLoader.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/benchmark/src/commands/list.ts b/packages/benchmark/src/commands/list.ts index 73d2deadff1a2..fcc60b1b81882 100644 --- a/packages/benchmark/src/commands/list.ts +++ b/packages/benchmark/src/commands/list.ts @@ -9,7 +9,7 @@ export default class ListCommand extends Command { const config = loadConfig(); const scenarioLoader = new ScenarioLoader(); - const allScenarios = scenarioLoader.loadAllScenarios(config.get('testScenariosPath')); + const allScenarios = scenarioLoader.loadAll(config.get('testScenariosPath')); console.log('Available test scenarios:'); console.log(''); diff --git a/packages/benchmark/src/commands/run.ts b/packages/benchmark/src/commands/run.ts index aede197d2d4a9..d69b4a54d4afd 100644 --- a/packages/benchmark/src/commands/run.ts +++ b/packages/benchmark/src/commands/run.ts @@ -32,7 +32,7 @@ export default class RunCommand extends Command { }, ); - const allScenarios = scenarioLoader.loadAllScenarios(config.get('testScenariosPath')); + const allScenarios = scenarioLoader.loadAll(config.get('testScenariosPath')); await scenarioRunner.runManyScenarios(allScenarios); } diff --git a/packages/benchmark/src/scenario/scenarioLoader.ts b/packages/benchmark/src/scenario/scenarioLoader.ts index 535792ea4db62..8116bb3d75583 100644 --- a/packages/benchmark/src/scenario/scenarioLoader.ts +++ b/packages/benchmark/src/scenario/scenarioLoader.ts @@ -7,7 +7,7 @@ export class ScenarioLoader { /** * Loads all scenarios from the given path */ - loadAllScenarios(pathToScenarios: string): Scenario[] { + loadAll(pathToScenarios: string): Scenario[] { pathToScenarios = path.resolve(pathToScenarios); const scenarioFolders = fs .readdirSync(pathToScenarios, { withFileTypes: true }) From 35e5b33304c500a7e56629c3d086f4b943f9096f Mon Sep 17 00:00:00 2001 From: Tomi Turtiainen <10324676+tomi@users.noreply.github.com> Date: Tue, 20 Aug 2024 14:03:08 +0300 Subject: [PATCH 10/17] fix: Remove payload from delete --- .../benchmark/src/n8nApiClient/authenticatedN8nApiClient.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/benchmark/src/n8nApiClient/authenticatedN8nApiClient.ts b/packages/benchmark/src/n8nApiClient/authenticatedN8nApiClient.ts index 16f5058e18aa2..93cd767347f2c 100644 --- a/packages/benchmark/src/n8nApiClient/authenticatedN8nApiClient.ts +++ b/packages/benchmark/src/n8nApiClient/authenticatedN8nApiClient.ts @@ -49,10 +49,9 @@ export class AuthenticatedN8nApiClient extends N8nApiClient { }); } - async delete(endpoint: string, data?: unknown) { + async delete(endpoint: string) { return await this.authenticatedRequest(endpoint, { method: 'DELETE', - data, }); } From 30433a65e979a5d2d604f565551334148d24aaea Mon Sep 17 00:00:00 2001 From: Tomi Turtiainen <10324676+tomi@users.noreply.github.com> Date: Tue, 20 Aug 2024 14:15:47 +0300 Subject: [PATCH 11/17] refactor: Code style changes --- .../benchmark/src/n8nApiClient/n8nApiClient.ts | 18 +++++++++--------- .../src/n8nApiClient/n8nApiClient.types.ts | 7 ++----- .../src/n8nApiClient/workflowsApiClient.ts | 12 ------------ .../src/scenario/scenarioDataLoader.ts | 14 +++++++++----- .../benchmark/src/scenario/scenarioLoader.ts | 2 +- .../src/testExecution/scenarioDataImporter.ts | 7 +++---- 6 files changed, 24 insertions(+), 36 deletions(-) diff --git a/packages/benchmark/src/n8nApiClient/n8nApiClient.ts b/packages/benchmark/src/n8nApiClient/n8nApiClient.ts index 5c5c94b555cf4..86ca52aff8159 100644 --- a/packages/benchmark/src/n8nApiClient/n8nApiClient.ts +++ b/packages/benchmark/src/n8nApiClient/n8nApiClient.ts @@ -4,15 +4,15 @@ export class N8nApiClient { constructor(public readonly apiBaseUrl: string) {} async waitForInstanceToBecomeOnline(): Promise { - const healthEndpoint = 'healthz'; - const startTime = Date.now(); - const intervalMs = 1000; - const timeout = 60000; + const HEALTH_ENDPOINT = 'healthz'; + const START_TIME = Date.now(); + const INTERVAL_MS = 1000; + const TIMEOUT_MS = 60_000; - while (Date.now() - startTime < timeout) { + while (Date.now() - START_TIME < TIMEOUT_MS) { try { const response = await axios.request({ - url: `${this.apiBaseUrl}/${healthEndpoint}`, + url: `${this.apiBaseUrl}/${HEALTH_ENDPOINT}`, method: 'GET', }); @@ -21,11 +21,11 @@ export class N8nApiClient { } } catch {} - console.log(`n8n instance not online yet, retrying in ${intervalMs / 1000} seconds...`); - await this.delay(intervalMs); + console.log(`n8n instance not online yet, retrying in ${INTERVAL_MS / 1000} seconds...`); + await this.delay(INTERVAL_MS); } - throw new Error(`n8n instance did not come online within ${timeout / 1000} seconds`); + throw new Error(`n8n instance did not come online within ${TIMEOUT_MS / 1000} seconds`); } async setupOwnerIfNeeded(loginDetails: { email: string; password: string }) { diff --git a/packages/benchmark/src/n8nApiClient/n8nApiClient.types.ts b/packages/benchmark/src/n8nApiClient/n8nApiClient.types.ts index 9758d66b32d4b..5ab9f028e2db3 100644 --- a/packages/benchmark/src/n8nApiClient/n8nApiClient.types.ts +++ b/packages/benchmark/src/n8nApiClient/n8nApiClient.types.ts @@ -1,8 +1,5 @@ +import type { IWorkflowBase } from 'n8n-workflow'; /** * n8n workflow. This is a simplified version of the actual workflow object. */ -export interface Workflow { - id: string; - name: string; - tags?: string[]; -} +export type Workflow = Pick; diff --git a/packages/benchmark/src/n8nApiClient/workflowsApiClient.ts b/packages/benchmark/src/n8nApiClient/workflowsApiClient.ts index 5d871a8d17194..5ca3f6efc382e 100644 --- a/packages/benchmark/src/n8nApiClient/workflowsApiClient.ts +++ b/packages/benchmark/src/n8nApiClient/workflowsApiClient.ts @@ -35,16 +35,4 @@ export class WorkflowApiClient { async deleteWorkflow(workflowId: Workflow['id']): Promise { await this.apiClient.delete(`${this.apiEndpoint}/${workflowId}`); } - - async tagWorkflow(workflow: Workflow, tagId: string): Promise { - const response = await this.apiClient.patch<{ data: Workflow }>( - `${this.apiEndpoint}/${workflow.id}`, - { - ...workflow, - tags: [...(workflow.tags ?? []), tagId], - }, - ); - - return response.data.data; - } } diff --git a/packages/benchmark/src/scenario/scenarioDataLoader.ts b/packages/benchmark/src/scenario/scenarioDataLoader.ts index 3f5b6c0be9443..43638a2e00092 100644 --- a/packages/benchmark/src/scenario/scenarioDataLoader.ts +++ b/packages/benchmark/src/scenario/scenarioDataLoader.ts @@ -1,5 +1,5 @@ -import fs from 'fs'; -import path from 'path'; +import fs from 'node:fs'; +import path from 'node:path'; import { Scenario } from '@/types/scenario'; import { Workflow } from '@/n8nApiClient/n8nApiClient.types'; @@ -24,8 +24,12 @@ export class ScenarioDataFileLoader { private loadSingleWorkflowFromFile(workflowFilePath: string): Workflow { const fileContent = fs.readFileSync(workflowFilePath, 'utf8'); - const workflow = JSON.parse(fileContent); - - return workflow; + try { + return JSON.parse(fileContent); + } catch (error) { + throw new Error( + `Failed to parse workflow file ${workflowFilePath}: ${error instanceof Error ? error.message : error}`, + ); + } } } diff --git a/packages/benchmark/src/scenario/scenarioLoader.ts b/packages/benchmark/src/scenario/scenarioLoader.ts index 8116bb3d75583..475ce495ac6bf 100644 --- a/packages/benchmark/src/scenario/scenarioLoader.ts +++ b/packages/benchmark/src/scenario/scenarioLoader.ts @@ -1,4 +1,4 @@ -import * as fs from 'fs'; +import * as fs from 'node:fs'; import * as path from 'path'; import { createHash } from 'node:crypto'; import type { Scenario, ScenarioManifest } from '@/types/scenario'; diff --git a/packages/benchmark/src/testExecution/scenarioDataImporter.ts b/packages/benchmark/src/testExecution/scenarioDataImporter.ts index f9bea7e20e8f2..1c1ec3777e1bc 100644 --- a/packages/benchmark/src/testExecution/scenarioDataImporter.ts +++ b/packages/benchmark/src/testExecution/scenarioDataImporter.ts @@ -24,7 +24,7 @@ export class ScenarioDataImporter { * Imports a single workflow into n8n removing any existing workflows with the same name */ private async importWorkflow(opts: { existingWorkflows: Workflow[]; workflow: Workflow }) { - const existingWorkflows = this.tryFindExistingWorkflows(opts.existingWorkflows, opts.workflow); + const existingWorkflows = this.findExistingWorkflows(opts.existingWorkflows, opts.workflow); if (existingWorkflows.length > 0) { for (const toDelete of existingWorkflows) { await this.workflowApiClient.deleteWorkflow(toDelete.id); @@ -35,12 +35,11 @@ export class ScenarioDataImporter { ...opts.workflow, name: this.getBenchmarkWorkflowName(opts.workflow), }); - const activeWorkflow = await this.workflowApiClient.activateWorkflow(createdWorkflow); - return activeWorkflow; + return await this.workflowApiClient.activateWorkflow(createdWorkflow); } - private tryFindExistingWorkflows( + private findExistingWorkflows( existingWorkflows: Workflow[], workflowToImport: Workflow, ): Workflow[] { From cf08002319b08cb6462cda966510b03470358c69 Mon Sep 17 00:00:00 2001 From: Tomi Turtiainen <10324676+tomi@users.noreply.github.com> Date: Tue, 20 Aug 2024 14:28:00 +0300 Subject: [PATCH 12/17] fix: Fix scenario manifest after renaming --- packages/benchmark/Dockerfile | 2 +- packages/benchmark/README.md | 2 +- packages/benchmark/package.json | 1 + .../benchmark/scenarios/scenario.schema.json | 42 +++++++++++++++++++ .../singleWebhook/singleWebhook.manifest.json | 5 ++- pnpm-lock.yaml | 3 ++ 6 files changed, 51 insertions(+), 4 deletions(-) create mode 100644 packages/benchmark/scenarios/scenario.schema.json diff --git a/packages/benchmark/Dockerfile b/packages/benchmark/Dockerfile index f27da351f47a8..32edfdb820558 100644 --- a/packages/benchmark/Dockerfile +++ b/packages/benchmark/Dockerfile @@ -45,7 +45,7 @@ COPY --chown=node:node ./packages/benchmark/tsconfig.build.json /app/packages/be # Source files COPY --chown=node:node ./packages/benchmark/src /app/packages/benchmark/src COPY --chown=node:node ./packages/benchmark/bin /app/packages/benchmark/bin -COPY --chown=node:node ./packages/benchmark/testScenarios /app/packages/benchmark/testScenarios +COPY --chown=node:node ./packages/benchmark/scenarios /app/packages/benchmark/scenarios WORKDIR /app/packages/benchmark RUN pnpm build diff --git a/packages/benchmark/README.md b/packages/benchmark/README.md index 8e175d817b922..1635ceab438d8 100644 --- a/packages/benchmark/README.md +++ b/packages/benchmark/README.md @@ -52,4 +52,4 @@ A benchmark scenario defines one or multiple steps to execute and measure. It co - 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 test scenarios are located in [`./testScenarios`](./testScenarios/). +Available scenarios are located in [`./scenarios`](./scenarios/). diff --git a/packages/benchmark/package.json b/packages/benchmark/package.json index 6a8e9772f8747..69888fe00a58c 100644 --- a/packages/benchmark/package.json +++ b/packages/benchmark/package.json @@ -28,6 +28,7 @@ "axios": "catalog:", "convict": "6.2.4", "dotenv": "8.6.0", + "n8n-workflow": "workspace:*", "zx": "^8.1.4" }, "devDependencies": { diff --git a/packages/benchmark/scenarios/scenario.schema.json b/packages/benchmark/scenarios/scenario.schema.json new file mode 100644 index 0000000000000..661fc054b6eee --- /dev/null +++ b/packages/benchmark/scenarios/scenario.schema.json @@ -0,0 +1,42 @@ +{ + "definitions": { + "ScenarioData": { + "type": "object", + "properties": { + "workflowFiles": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [], + "additionalProperties": false + } + }, + "type": "object", + "properties": { + "$schema": { + "type": "string", + "description": "The JSON schema to validate this file" + }, + "name": { + "type": "string", + "description": "The name of the scenario" + }, + "description": { + "type": "string", + "description": "A longer description of the scenario" + }, + "scriptPath": { + "type": "string", + "description": "Relative path to the k6 test script" + }, + "scenarioData": { + "$ref": "#/definitions/ScenarioData", + "description": "Data to import before running the scenario" + } + }, + "required": ["name", "description", "scriptPath", "scenarioData"], + "additionalProperties": false +} diff --git a/packages/benchmark/scenarios/singleWebhook/singleWebhook.manifest.json b/packages/benchmark/scenarios/singleWebhook/singleWebhook.manifest.json index 9314cf510a173..e9b4664a96502 100644 --- a/packages/benchmark/scenarios/singleWebhook/singleWebhook.manifest.json +++ b/packages/benchmark/scenarios/singleWebhook/singleWebhook.manifest.json @@ -1,6 +1,7 @@ { + "$schema": "../scenario.schema.json", "name": "SingleWebhook", "description": "A single webhook trigger that responds with a 200 status code", - "testData": { "workflowFiles": ["singleWebhook.json"] }, - "testScriptPath": "singleWebhook.script.ts" + "scenarioData": { "workflowFiles": ["singleWebhook.json"] }, + "scriptPath": "singleWebhook.script.ts" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 931037a3f2717..bde3c6d9ab841 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -614,6 +614,9 @@ importers: dotenv: specifier: 8.6.0 version: 8.6.0 + n8n-workflow: + specifier: workspace:* + version: link:../workflow zx: specifier: ^8.1.4 version: 8.1.4 From 0ec1af169fe1a910a2300b5ef807f75fe2c9cea6 Mon Sep 17 00:00:00 2001 From: Tomi Turtiainen <10324676+tomi@users.noreply.github.com> Date: Tue, 20 Aug 2024 14:28:18 +0300 Subject: [PATCH 13/17] chore: Remove code node scenario --- .../scenarios/codeNode/codeNode.json | 36 ---------------- .../scenarios/codeNode/codeNode.manifest.json | 9 ---- .../scenarios/codeNode/codeNode.script.ts | 11 ----- .../scenarios/testScenario.schema.json | 42 ------------------- 4 files changed, 98 deletions(-) delete mode 100644 packages/benchmark/scenarios/codeNode/codeNode.json delete mode 100644 packages/benchmark/scenarios/codeNode/codeNode.manifest.json delete mode 100644 packages/benchmark/scenarios/codeNode/codeNode.script.ts delete mode 100644 packages/benchmark/scenarios/testScenario.schema.json diff --git a/packages/benchmark/scenarios/codeNode/codeNode.json b/packages/benchmark/scenarios/codeNode/codeNode.json deleted file mode 100644 index 6831d22068493..0000000000000 --- a/packages/benchmark/scenarios/codeNode/codeNode.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "createdAt": "2024-08-06T13:37:07.119Z", - "updatedAt": "2024-08-06T13:39:27.000Z", - "name": "Code Node", - "active": true, - "nodes": [ - { - "parameters": { - "mode": "runOnceForEachItem", - "jsCode": "$input.item.json.myNewField = Math.random();\n\nreturn $input.item;" - }, - "id": "7d100719-8d9e-497d-a365-78c793c244d6", - "name": "Code", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [980, 400] - }, - { - "parameters": { "path": "code-node", "responseMode": "lastNode", "options": {} }, - "id": "1e172d8f-f6cd-43ec-8257-e6a8212add00", - "name": "Webhook", - "type": "n8n-nodes-base.webhook", - "typeVersion": 2, - "position": [760, 400], - "webhookId": "224a56e0-f929-4f01-b49e-d91861b1b8d7" - } - ], - "connections": { "Webhook": { "main": [[{ "node": "Code", "type": "main", "index": 0 }]] } }, - "settings": { "executionOrder": "v1" }, - "staticData": null, - "meta": { "templateCredsSetupCompleted": true }, - "pinData": {}, - "versionId": "a91dfe67-971d-425f-9066-feab46150f95", - "triggerCount": 1, - "tags": [] -} diff --git a/packages/benchmark/scenarios/codeNode/codeNode.manifest.json b/packages/benchmark/scenarios/codeNode/codeNode.manifest.json deleted file mode 100644 index a44b49af8f1d7..0000000000000 --- a/packages/benchmark/scenarios/codeNode/codeNode.manifest.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "$schema": "../testScenario.schema.json", - "name": "CodeNode", - "description": "Workflow with a single code node that is triggered using a webhook", - "testData": { - "workflowFiles": ["codeNode.json"] - }, - "testScriptPath": "codeNode.script.ts" -} diff --git a/packages/benchmark/scenarios/codeNode/codeNode.script.ts b/packages/benchmark/scenarios/codeNode/codeNode.script.ts deleted file mode 100644 index b9ec9b345ec98..0000000000000 --- a/packages/benchmark/scenarios/codeNode/codeNode.script.ts +++ /dev/null @@ -1,11 +0,0 @@ -import http from 'k6/http'; -import { check } from 'k6'; - -const apiBaseUrl = __ENV.API_BASE_URL; - -export default function () { - const res = http.get(`${apiBaseUrl}/webhook/code-node`); - check(res, { - 'is status 200': (r) => r.status === 200, - }); -} diff --git a/packages/benchmark/scenarios/testScenario.schema.json b/packages/benchmark/scenarios/testScenario.schema.json deleted file mode 100644 index da02b9d35ca3f..0000000000000 --- a/packages/benchmark/scenarios/testScenario.schema.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "definitions": { - "TestData": { - "type": "object", - "properties": { - "workflowFiles": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "required": [], - "additionalProperties": false - } - }, - "type": "object", - "properties": { - "$schema": { - "type": "string", - "description": "The JSON schema to validate this file" - }, - "name": { - "type": "string", - "description": "The name of the test scenario" - }, - "description": { - "type": "string", - "description": "A longer description of the test scenario" - }, - "testScriptPath": { - "type": "string", - "description": "Relative path to the k6 test script" - }, - "testData": { - "$ref": "#/definitions/TestData", - "description": "Test data to import before running the scenario" - } - }, - "required": ["name", "description", "testScriptPath", "testData"], - "additionalProperties": false -} From e5b629e42f2142aaece045f152dfaa2807559ce6 Mon Sep 17 00:00:00 2001 From: Tomi Turtiainen <10324676+tomi@users.noreply.github.com> Date: Tue, 20 Aug 2024 14:33:44 +0300 Subject: [PATCH 14/17] refactor: Repeat api endpoint --- .../src/n8nApiClient/workflowsApiClient.ts | 21 +++++++------------ 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/packages/benchmark/src/n8nApiClient/workflowsApiClient.ts b/packages/benchmark/src/n8nApiClient/workflowsApiClient.ts index 5ca3f6efc382e..18f2ecbcda2c2 100644 --- a/packages/benchmark/src/n8nApiClient/workflowsApiClient.ts +++ b/packages/benchmark/src/n8nApiClient/workflowsApiClient.ts @@ -2,37 +2,30 @@ import { Workflow } from '@/n8nApiClient/n8nApiClient.types'; import { AuthenticatedN8nApiClient } from './authenticatedN8nApiClient'; export class WorkflowApiClient { - private readonly apiEndpoint = '/workflows'; - constructor(private readonly apiClient: AuthenticatedN8nApiClient) {} async getAllWorkflows(): Promise { - const response = await this.apiClient.get<{ count: number; data: Workflow[] }>( - this.apiEndpoint, - ); + const response = await this.apiClient.get<{ count: number; data: Workflow[] }>('/workflows'); return response.data.data; } async createWorkflow(workflow: unknown): Promise { - const response = await this.apiClient.post<{ data: Workflow }>(this.apiEndpoint, workflow); + const response = await this.apiClient.post<{ data: Workflow }>('/workflows', workflow); return response.data.data; } async activateWorkflow(workflow: Workflow): Promise { - const response = await this.apiClient.patch<{ data: Workflow }>( - `${this.apiEndpoint}/${workflow.id}`, - { - ...workflow, - active: true, - }, - ); + const response = await this.apiClient.patch<{ data: Workflow }>(`/workflows/${workflow.id}`, { + ...workflow, + active: true, + }); return response.data.data; } async deleteWorkflow(workflowId: Workflow['id']): Promise { - await this.apiClient.delete(`${this.apiEndpoint}/${workflowId}`); + await this.apiClient.delete(`/workflows/${workflowId}`); } } From 664bfb49d65eff194cde2fce349eedc9849af1e2 Mon Sep 17 00:00:00 2001 From: Tomi Turtiainen <10324676+tomi@users.noreply.github.com> Date: Tue, 20 Aug 2024 14:33:56 +0300 Subject: [PATCH 15/17] chore: Remove unused ts compiler options --- packages/benchmark/tsconfig.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/benchmark/tsconfig.json b/packages/benchmark/tsconfig.json index d2e1fe362751a..22556060b0820 100644 --- a/packages/benchmark/tsconfig.json +++ b/packages/benchmark/tsconfig.json @@ -2,8 +2,6 @@ "extends": ["../../tsconfig.json", "../../tsconfig.backend.json"], "compilerOptions": { "rootDir": ".", - "emitDecoratorMetadata": true, - "experimentalDecorators": true, "baseUrl": "src", "paths": { "@/*": ["./*"] From 3568e4ea62da583cfaa3d12fd7975b89a56b3fb0 Mon Sep 17 00:00:00 2001 From: Tomi Turtiainen <10324676+tomi@users.noreply.github.com> Date: Tue, 20 Aug 2024 14:44:54 +0300 Subject: [PATCH 16/17] refactor: Move benchmark under @n8n --- packages/{ => @n8n}/benchmark/Dockerfile | 18 ++--- packages/{ => @n8n}/benchmark/README.md | 2 +- .../{ => @n8n}/benchmark/bin/n8n-benchmark | 0 packages/{ => @n8n}/benchmark/package.json | 2 +- .../benchmark/scenarios/scenario.schema.json | 0 .../singleWebhook/singleWebhook.json | 0 .../singleWebhook/singleWebhook.manifest.json | 0 .../singleWebhook/singleWebhook.script.ts | 0 .../{ => @n8n}/benchmark/src/commands/list.ts | 0 .../{ => @n8n}/benchmark/src/commands/run.ts | 0 .../{ => @n8n}/benchmark/src/config/config.ts | 0 .../n8nApiClient/authenticatedN8nApiClient.ts | 0 .../src/n8nApiClient/n8nApiClient.ts | 0 .../src/n8nApiClient/n8nApiClient.types.ts | 0 .../src/n8nApiClient/workflowsApiClient.ts | 0 .../src/scenario/scenarioDataLoader.ts | 0 .../benchmark/src/scenario/scenarioLoader.ts | 0 .../benchmark/src/testExecution/k6Executor.ts | 0 .../src/testExecution/scenarioDataImporter.ts | 0 .../src/testExecution/scenarioRunner.ts | 0 .../benchmark/src/types/scenario.ts | 0 .../{ => @n8n}/benchmark/tsconfig.build.json | 2 +- packages/{ => @n8n}/benchmark/tsconfig.json | 2 +- pnpm-lock.yaml | 74 +++++++++---------- 24 files changed, 50 insertions(+), 50 deletions(-) rename packages/{ => @n8n}/benchmark/Dockerfile (64%) rename packages/{ => @n8n}/benchmark/README.md (98%) rename packages/{ => @n8n}/benchmark/bin/n8n-benchmark (100%) rename packages/{ => @n8n}/benchmark/package.json (97%) rename packages/{ => @n8n}/benchmark/scenarios/scenario.schema.json (100%) rename packages/{ => @n8n}/benchmark/scenarios/singleWebhook/singleWebhook.json (100%) rename packages/{ => @n8n}/benchmark/scenarios/singleWebhook/singleWebhook.manifest.json (100%) rename packages/{ => @n8n}/benchmark/scenarios/singleWebhook/singleWebhook.script.ts (100%) rename packages/{ => @n8n}/benchmark/src/commands/list.ts (100%) rename packages/{ => @n8n}/benchmark/src/commands/run.ts (100%) rename packages/{ => @n8n}/benchmark/src/config/config.ts (100%) rename packages/{ => @n8n}/benchmark/src/n8nApiClient/authenticatedN8nApiClient.ts (100%) rename packages/{ => @n8n}/benchmark/src/n8nApiClient/n8nApiClient.ts (100%) rename packages/{ => @n8n}/benchmark/src/n8nApiClient/n8nApiClient.types.ts (100%) rename packages/{ => @n8n}/benchmark/src/n8nApiClient/workflowsApiClient.ts (100%) rename packages/{ => @n8n}/benchmark/src/scenario/scenarioDataLoader.ts (100%) rename packages/{ => @n8n}/benchmark/src/scenario/scenarioLoader.ts (100%) rename packages/{ => @n8n}/benchmark/src/testExecution/k6Executor.ts (100%) rename packages/{ => @n8n}/benchmark/src/testExecution/scenarioDataImporter.ts (100%) rename packages/{ => @n8n}/benchmark/src/testExecution/scenarioRunner.ts (100%) rename packages/{ => @n8n}/benchmark/src/types/scenario.ts (100%) rename packages/{ => @n8n}/benchmark/tsconfig.build.json (68%) rename packages/{ => @n8n}/benchmark/tsconfig.json (63%) diff --git a/packages/benchmark/Dockerfile b/packages/@n8n/benchmark/Dockerfile similarity index 64% rename from packages/benchmark/Dockerfile rename to packages/@n8n/benchmark/Dockerfile index 32edfdb820558..5fa1aeae935b1 100644 --- a/packages/benchmark/Dockerfile +++ b/packages/@n8n/benchmark/Dockerfile @@ -29,7 +29,7 @@ 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/benchmark/package.json /app/packages/benchmark/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 @@ -39,15 +39,15 @@ RUN pnpm install --frozen-lockfile 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/benchmark/tsconfig.json /app/packages/benchmark/tsconfig.json -COPY --chown=node:node ./packages/benchmark/tsconfig.build.json /app/packages/benchmark/tsconfig.build.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/benchmark/src /app/packages/benchmark/src -COPY --chown=node:node ./packages/benchmark/bin /app/packages/benchmark/bin -COPY --chown=node:node ./packages/benchmark/scenarios /app/packages/benchmark/scenarios +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/benchmark +WORKDIR /app/packages/@n8n/benchmark RUN pnpm build # @@ -56,7 +56,7 @@ FROM base AS runner COPY --from=builder /app /app -WORKDIR /app/packages/benchmark +WORKDIR /app/packages/@n8n/benchmark USER node -ENTRYPOINT [ "/app/packages/benchmark/bin/n8n-benchmark" ] +ENTRYPOINT [ "/app/packages/@n8n/benchmark/bin/n8n-benchmark" ] diff --git a/packages/benchmark/README.md b/packages/@n8n/benchmark/README.md similarity index 98% rename from packages/benchmark/README.md rename to packages/@n8n/benchmark/README.md index 1635ceab438d8..569bcf897febd 100644 --- a/packages/benchmark/README.md +++ b/packages/@n8n/benchmark/README.md @@ -9,7 +9,7 @@ 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/benchmark/Dockerfile . +docker build --platform linux/amd64 -t n8n-benchmark -f packages/@n8n/benchmark/Dockerfile . ``` Run the image diff --git a/packages/benchmark/bin/n8n-benchmark b/packages/@n8n/benchmark/bin/n8n-benchmark similarity index 100% rename from packages/benchmark/bin/n8n-benchmark rename to packages/@n8n/benchmark/bin/n8n-benchmark diff --git a/packages/benchmark/package.json b/packages/@n8n/benchmark/package.json similarity index 97% rename from packages/benchmark/package.json rename to packages/@n8n/benchmark/package.json index 69888fe00a58c..59726e532c292 100644 --- a/packages/benchmark/package.json +++ b/packages/@n8n/benchmark/package.json @@ -1,5 +1,5 @@ { - "name": "n8n-benchmark", + "name": "@n8n/n8n-benchmark", "version": "1.0.0", "description": "Cli for running benchmark tests for n8n", "main": "dist/index", diff --git a/packages/benchmark/scenarios/scenario.schema.json b/packages/@n8n/benchmark/scenarios/scenario.schema.json similarity index 100% rename from packages/benchmark/scenarios/scenario.schema.json rename to packages/@n8n/benchmark/scenarios/scenario.schema.json diff --git a/packages/benchmark/scenarios/singleWebhook/singleWebhook.json b/packages/@n8n/benchmark/scenarios/singleWebhook/singleWebhook.json similarity index 100% rename from packages/benchmark/scenarios/singleWebhook/singleWebhook.json rename to packages/@n8n/benchmark/scenarios/singleWebhook/singleWebhook.json diff --git a/packages/benchmark/scenarios/singleWebhook/singleWebhook.manifest.json b/packages/@n8n/benchmark/scenarios/singleWebhook/singleWebhook.manifest.json similarity index 100% rename from packages/benchmark/scenarios/singleWebhook/singleWebhook.manifest.json rename to packages/@n8n/benchmark/scenarios/singleWebhook/singleWebhook.manifest.json diff --git a/packages/benchmark/scenarios/singleWebhook/singleWebhook.script.ts b/packages/@n8n/benchmark/scenarios/singleWebhook/singleWebhook.script.ts similarity index 100% rename from packages/benchmark/scenarios/singleWebhook/singleWebhook.script.ts rename to packages/@n8n/benchmark/scenarios/singleWebhook/singleWebhook.script.ts diff --git a/packages/benchmark/src/commands/list.ts b/packages/@n8n/benchmark/src/commands/list.ts similarity index 100% rename from packages/benchmark/src/commands/list.ts rename to packages/@n8n/benchmark/src/commands/list.ts diff --git a/packages/benchmark/src/commands/run.ts b/packages/@n8n/benchmark/src/commands/run.ts similarity index 100% rename from packages/benchmark/src/commands/run.ts rename to packages/@n8n/benchmark/src/commands/run.ts diff --git a/packages/benchmark/src/config/config.ts b/packages/@n8n/benchmark/src/config/config.ts similarity index 100% rename from packages/benchmark/src/config/config.ts rename to packages/@n8n/benchmark/src/config/config.ts diff --git a/packages/benchmark/src/n8nApiClient/authenticatedN8nApiClient.ts b/packages/@n8n/benchmark/src/n8nApiClient/authenticatedN8nApiClient.ts similarity index 100% rename from packages/benchmark/src/n8nApiClient/authenticatedN8nApiClient.ts rename to packages/@n8n/benchmark/src/n8nApiClient/authenticatedN8nApiClient.ts diff --git a/packages/benchmark/src/n8nApiClient/n8nApiClient.ts b/packages/@n8n/benchmark/src/n8nApiClient/n8nApiClient.ts similarity index 100% rename from packages/benchmark/src/n8nApiClient/n8nApiClient.ts rename to packages/@n8n/benchmark/src/n8nApiClient/n8nApiClient.ts diff --git a/packages/benchmark/src/n8nApiClient/n8nApiClient.types.ts b/packages/@n8n/benchmark/src/n8nApiClient/n8nApiClient.types.ts similarity index 100% rename from packages/benchmark/src/n8nApiClient/n8nApiClient.types.ts rename to packages/@n8n/benchmark/src/n8nApiClient/n8nApiClient.types.ts diff --git a/packages/benchmark/src/n8nApiClient/workflowsApiClient.ts b/packages/@n8n/benchmark/src/n8nApiClient/workflowsApiClient.ts similarity index 100% rename from packages/benchmark/src/n8nApiClient/workflowsApiClient.ts rename to packages/@n8n/benchmark/src/n8nApiClient/workflowsApiClient.ts diff --git a/packages/benchmark/src/scenario/scenarioDataLoader.ts b/packages/@n8n/benchmark/src/scenario/scenarioDataLoader.ts similarity index 100% rename from packages/benchmark/src/scenario/scenarioDataLoader.ts rename to packages/@n8n/benchmark/src/scenario/scenarioDataLoader.ts diff --git a/packages/benchmark/src/scenario/scenarioLoader.ts b/packages/@n8n/benchmark/src/scenario/scenarioLoader.ts similarity index 100% rename from packages/benchmark/src/scenario/scenarioLoader.ts rename to packages/@n8n/benchmark/src/scenario/scenarioLoader.ts diff --git a/packages/benchmark/src/testExecution/k6Executor.ts b/packages/@n8n/benchmark/src/testExecution/k6Executor.ts similarity index 100% rename from packages/benchmark/src/testExecution/k6Executor.ts rename to packages/@n8n/benchmark/src/testExecution/k6Executor.ts diff --git a/packages/benchmark/src/testExecution/scenarioDataImporter.ts b/packages/@n8n/benchmark/src/testExecution/scenarioDataImporter.ts similarity index 100% rename from packages/benchmark/src/testExecution/scenarioDataImporter.ts rename to packages/@n8n/benchmark/src/testExecution/scenarioDataImporter.ts diff --git a/packages/benchmark/src/testExecution/scenarioRunner.ts b/packages/@n8n/benchmark/src/testExecution/scenarioRunner.ts similarity index 100% rename from packages/benchmark/src/testExecution/scenarioRunner.ts rename to packages/@n8n/benchmark/src/testExecution/scenarioRunner.ts diff --git a/packages/benchmark/src/types/scenario.ts b/packages/@n8n/benchmark/src/types/scenario.ts similarity index 100% rename from packages/benchmark/src/types/scenario.ts rename to packages/@n8n/benchmark/src/types/scenario.ts diff --git a/packages/benchmark/tsconfig.build.json b/packages/@n8n/benchmark/tsconfig.build.json similarity index 68% rename from packages/benchmark/tsconfig.build.json rename to packages/@n8n/benchmark/tsconfig.build.json index cb80f3c918e4d..b91db37a4a882 100644 --- a/packages/benchmark/tsconfig.build.json +++ b/packages/@n8n/benchmark/tsconfig.build.json @@ -1,5 +1,5 @@ { - "extends": ["./tsconfig.json", "../../tsconfig.build.json"], + "extends": ["./tsconfig.json", "../../../tsconfig.build.json"], "compilerOptions": { "rootDir": "src", "outDir": "dist", diff --git a/packages/benchmark/tsconfig.json b/packages/@n8n/benchmark/tsconfig.json similarity index 63% rename from packages/benchmark/tsconfig.json rename to packages/@n8n/benchmark/tsconfig.json index 22556060b0820..58a1b48f657df 100644 --- a/packages/benchmark/tsconfig.json +++ b/packages/@n8n/benchmark/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": ["../../tsconfig.json", "../../tsconfig.backend.json"], + "extends": ["../../../tsconfig.json", "../../../tsconfig.backend.json"], "compilerOptions": { "rootDir": ".", "baseUrl": "src", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bde3c6d9ab841..a0884091de39d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -207,6 +207,43 @@ importers: specifier: workspace:* version: link:../packages/workflow + packages/@n8n/benchmark: + dependencies: + '@oclif/core': + specifier: 4.0.7 + version: 4.0.7 + axios: + specifier: 1.7.3 + version: 1.7.3(debug@3.2.7) + convict: + specifier: 6.2.4 + version: 6.2.4 + dotenv: + specifier: 8.6.0 + version: 8.6.0 + n8n-workflow: + specifier: workspace:* + version: link:../../workflow + zx: + specifier: ^8.1.4 + version: 8.1.4 + devDependencies: + '@types/convict': + specifier: ^6.1.1 + version: 6.1.1 + '@types/k6': + specifier: ^0.52.0 + version: 0.52.0 + '@types/node': + specifier: ^18.16.16 + version: 18.16.16 + tsc-alias: + specifier: ^1.8.7 + version: 1.8.7 + typescript: + specifier: ^5.5.2 + version: 5.5.2 + packages/@n8n/chat: dependencies: '@vueuse/core': @@ -600,43 +637,6 @@ importers: specifier: ^9.4.2 version: 9.4.2(eslint@8.57.0) - packages/benchmark: - dependencies: - '@oclif/core': - specifier: 4.0.7 - version: 4.0.7 - axios: - specifier: 1.7.3 - version: 1.7.3(debug@3.2.7) - convict: - specifier: 6.2.4 - version: 6.2.4 - dotenv: - specifier: 8.6.0 - version: 8.6.0 - n8n-workflow: - specifier: workspace:* - version: link:../workflow - zx: - specifier: ^8.1.4 - version: 8.1.4 - devDependencies: - '@types/convict': - specifier: ^6.1.1 - version: 6.1.1 - '@types/k6': - specifier: ^0.52.0 - version: 0.52.0 - '@types/node': - specifier: ^18.16.16 - version: 18.16.16 - tsc-alias: - specifier: ^1.8.7 - version: 1.8.7 - typescript: - specifier: ^5.5.2 - version: 5.5.2 - packages/cli: dependencies: '@azure/identity': From 90db188ed8269e03ed8fdc07e4ce84b1ae40c7b7 Mon Sep 17 00:00:00 2001 From: Tomi Turtiainen <10324676+tomi@users.noreply.github.com> Date: Tue, 20 Aug 2024 14:45:08 +0300 Subject: [PATCH 17/17] revert: Revert n8n-workflow package usage --- packages/@n8n/benchmark/package.json | 1 - .../@n8n/benchmark/src/n8nApiClient/n8nApiClient.types.ts | 7 +++++-- pnpm-lock.yaml | 3 --- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/@n8n/benchmark/package.json b/packages/@n8n/benchmark/package.json index 59726e532c292..3a7afb50a3624 100644 --- a/packages/@n8n/benchmark/package.json +++ b/packages/@n8n/benchmark/package.json @@ -28,7 +28,6 @@ "axios": "catalog:", "convict": "6.2.4", "dotenv": "8.6.0", - "n8n-workflow": "workspace:*", "zx": "^8.1.4" }, "devDependencies": { diff --git a/packages/@n8n/benchmark/src/n8nApiClient/n8nApiClient.types.ts b/packages/@n8n/benchmark/src/n8nApiClient/n8nApiClient.types.ts index 5ab9f028e2db3..ff6aa6930bf0e 100644 --- a/packages/@n8n/benchmark/src/n8nApiClient/n8nApiClient.types.ts +++ b/packages/@n8n/benchmark/src/n8nApiClient/n8nApiClient.types.ts @@ -1,5 +1,8 @@ -import type { IWorkflowBase } from 'n8n-workflow'; /** * n8n workflow. This is a simplified version of the actual workflow object. */ -export type Workflow = Pick; +export type Workflow = { + id: string; + name: string; + tags?: string[]; +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a0884091de39d..10944629d6bc2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -221,9 +221,6 @@ importers: dotenv: specifier: 8.6.0 version: 8.6.0 - n8n-workflow: - specifier: workspace:* - version: link:../../workflow zx: specifier: ^8.1.4 version: 8.1.4