ci: Add Dockerfile for multi-runner task runners image (#18975)

This commit is contained in:
Jaakko Husso
2025-09-02 15:00:55 +03:00
committed by GitHub
parent 10ac58ac5a
commit 6d405a6d66
6 changed files with 291 additions and 25 deletions

View File

@@ -0,0 +1,121 @@
ARG PYTHON_IMAGE=python:3.13-slim
# ==============================================================================
# STAGE 1: JavaScript runner (@n8n/task-runner) artifact from CI
# ==============================================================================
FROM alpine:3.22.1 AS app-artifact-processor
COPY ./dist/task-runner-javascript /app/task-runner-javascript
# ==============================================================================
# STAGE 2: Python runner build (@n8n/task-runner-python) with uv
# Produces a relocatable venv tied to PYTHON_IMAGE
# ==============================================================================
FROM ${PYTHON_IMAGE} AS python-runner-builder
ARG TARGETPLATFORM
ARG UV_VERSION=0.8.14
RUN apt-get update && apt-get install -y --no-install-recommends \
curl ca-certificates build-essential pkg-config git \
&& rm -rf /var/lib/apt/lists/*
RUN set -e; \
case "$TARGETPLATFORM" in \
"linux/amd64") UV_ARCH="x86_64-unknown-linux-gnu" ;; \
"linux/arm64") UV_ARCH="aarch64-unknown-linux-gnu" ;; \
*) echo "Unsupported platform: $TARGETPLATFORM" >&2; exit 1 ;; \
esac; \
mkdir -p /tmp/uv && cd /tmp/uv; \
curl -fsSLO "https://github.com/astral-sh/uv/releases/download/${UV_VERSION}/uv-${UV_ARCH}.tar.gz"; \
curl -fsSLO "https://github.com/astral-sh/uv/releases/download/${UV_VERSION}/uv-${UV_ARCH}.tar.gz.sha256"; \
sha256sum -c "uv-${UV_ARCH}.tar.gz.sha256"; \
tar -xzf "uv-${UV_ARCH}.tar.gz"; \
install -m 0755 "uv-${UV_ARCH}/uv" /usr/local/bin/uv; \
cd / && rm -rf /tmp/uv
WORKDIR /app/task-runner-python
COPY packages/@n8n/task-runner-python/pyproject.toml \
packages/@n8n/task-runner-python/uv.lock** \
packages/@n8n/task-runner-python/.python-version** \
./
RUN uv venv
RUN uv sync \
--frozen \
--no-editable \
--no-install-project \
--all-extras
COPY packages/@n8n/task-runner-python/ ./
RUN uv sync \
--frozen \
--no-editable
# ==============================================================================
# STAGE 3: Task Runner Launcher download
# ==============================================================================
FROM alpine:3.22.1 AS launcher-downloader
ARG TARGETPLATFORM
ARG LAUNCHER_VERSION=1.3.0
RUN set -e; \
case "$TARGETPLATFORM" in \
"linux/amd64") ARCH_NAME="amd64" ;; \
"linux/arm64") ARCH_NAME="arm64" ;; \
*) echo "Unsupported platform: $TARGETPLATFORM" && exit 1 ;; \
esac; \
mkdir /launcher-temp && cd /launcher-temp; \
wget -q "https://github.com/n8n-io/task-runner-launcher/releases/download/${LAUNCHER_VERSION}/task-runner-launcher-${LAUNCHER_VERSION}-linux-${ARCH_NAME}.tar.gz"; \
wget -q "https://github.com/n8n-io/task-runner-launcher/releases/download/${LAUNCHER_VERSION}/task-runner-launcher-${LAUNCHER_VERSION}-linux-${ARCH_NAME}.tar.gz.sha256"; \
echo "$(cat task-runner-launcher-${LAUNCHER_VERSION}-linux-${ARCH_NAME}.tar.gz.sha256) task-runner-launcher-${LAUNCHER_VERSION}-linux-${ARCH_NAME}.tar.gz" > checksum.sha256; \
sha256sum -c checksum.sha256; \
mkdir -p /launcher-bin; \
tar xzf task-runner-launcher-${LAUNCHER_VERSION}-linux-${ARCH_NAME}.tar.gz -C /launcher-bin; \
cd / && rm -rf /launcher-temp
# ==============================================================================
# STAGE 4: Runtime
# ==============================================================================
FROM ${PYTHON_IMAGE} AS runtime
ARG NODE_VERSION=22
ARG N8N_VERSION=snapshot
ARG N8N_RELEASE_TYPE=dev
ENV NODE_ENV=production
ENV N8N_RELEASE_TYPE=${N8N_RELEASE_TYPE}
ENV SHELL=/bin/sh
RUN apt-get update \
&& apt-get install -y --no-install-recommends curl gnupg ca-certificates tini \
&& mkdir -p /etc/apt/keyrings \
&& curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key \
| gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \
&& echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_${NODE_VERSION}.x nodistro main" \
> /etc/apt/sources.list.d/nodesource.list \
&& apt-get update \
&& apt-get install -y --no-install-recommends nodejs \
&& apt-get remove curl -y \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*.deb
RUN useradd -m -u 1000 runner
WORKDIR /home/runner
COPY --from=app-artifact-processor /app/task-runner-javascript /opt/runners/task-runner-javascript
COPY --from=python-runner-builder /app/task-runner-python /opt/runners/task-runner-python
COPY --from=launcher-downloader /launcher-bin/* /usr/local/bin/
COPY docker/images/runners/n8n-task-runners.json /etc/n8n-task-runners.json
RUN chown -R runner:runner /opt/runners /home/runner
USER runner
EXPOSE 5680/tcp
ENTRYPOINT ["tini", "--", "/usr/local/bin/task-runner-launcher"]
CMD ["javascript", "python"]
LABEL org.opencontainers.image.title="n8n task runners" \
org.opencontainers.image.description="Sidecar image providing n8n task runners for JavaScript and Python code execution" \
org.opencontainers.image.source="https://github.com/n8n-io/n8n" \
org.opencontainers.image.url="https://n8n.io" \
org.opencontainers.image.version="${N8N_VERSION}"

View File

@@ -0,0 +1,50 @@
# n8n - Task runners (`n8nio/runners`) - (PREVIEW)
`n8nio/runners` image includes [JavaScript runner](https://github.com/n8n-io/n8n/tree/master/packages/%40n8n/task-runner),
[Python runner](https://github.com/n8n-io/n8n/tree/master/packages/%40n8n/task-runner-python) and
[Task runner launcher](https://github.com/n8n-io/task-runner-launcher) that connects to a Task Broker
running on the main n8n instance when running in `external` mode. This image is to be launched as a sidecar
container to the main n8n container.
[Task runners](https://docs.n8n.io/hosting/configuration/task-runners/) are used to execute user-provided code
in the [Code Node](https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.code/), isolated from the n8n instance.
## Testing locally
1. Make a production build of n8n
```
pnpm run build:n8n
```
2. Build the task runners image
```
docker buildx build --no-cache \
-f docker/images/runners/Dockerfile \
-t n8nio/runners \
.
```
3. Start n8n on your host machine with Task Broker enabled
```
N8N_RUNNERS_ENABLED=true \
N8N_RUNNERS_MODE=external \
N8N_RUNNERS_AUTH_TOKEN=test \
N8N_LOG_LEVEL=debug \
pnpm start
```
4. Start the task runner container
```
docker run --rm -it \
-e N8N_RUNNERS_AUTH_TOKEN=test \
-e N8N_RUNNERS_LAUNCHER_LOG_LEVEL=debug \
-e N8N_RUNNERS_TASK_BROKER_URI=http://host.docker.internal:5679 \
-p 5680:5680 \
n8nio/runners
```

View File

@@ -0,0 +1,48 @@
{
"task-runners": [
{
"runner-type": "javascript",
"workdir": "/home/runner",
"command": "/usr/bin/node",
"args": [
"--disallow-code-generation-from-strings",
"--disable-proto=delete",
"/opt/runners/task-runner-javascript/dist/start.js"
],
"health-check-server-port": "5681",
"allowed-env": [
"PATH",
"GENERIC_TIMEZONE",
"N8N_RUNNERS_MAX_CONCURRENCY",
"NODE_OPTIONS",
"N8N_SENTRY_DSN",
"N8N_VERSION",
"ENVIRONMENT",
"DEPLOYMENT_NAME"
],
"env-overrides": {
"NODE_FUNCTION_ALLOW_BUILTIN": "crypto",
"NODE_FUNCTION_ALLOW_EXTERNAL": "moment",
"N8N_RUNNERS_HEALTH_CHECK_SERVER_HOST": "0.0.0.0"
}
},
{
"runner-type": "python",
"workdir": "/home/runner",
"command": "/opt/runners/task-runner-python/.venv/bin/python",
"args": ["-m", "src.main"],
"health-check-server-port": "5682",
"allowed-env": [
"PATH",
"N8N_RUNNERS_LAUNCHER_LOG_LEVEL",
"N8N_SENTRY_DSN",
"N8N_VERSION",
"ENVIRONMENT",
"DEPLOYMENT_NAME"
],
"env-overrides": {
"PYTHONPATH": "/opt/runners/task-runner-python"
}
}
]
}

View File

@@ -42,6 +42,7 @@
"acorn-walk": "8.3.4",
"lodash": "catalog:",
"luxon": "catalog:",
"moment": "2.30.1",
"n8n-core": "workspace:*",
"n8n-workflow": "workspace:*",
"nanoid": "catalog:",

49
pnpm-lock.yaml generated
View File

@@ -1005,7 +1005,7 @@ importers:
version: 4.3.0
'@getzep/zep-cloud':
specifier: 1.0.12
version: 1.0.12(@langchain/core@0.3.68(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.3)(zod@3.25.67)))(encoding@0.1.13)(langchain@0.3.30(e7c2f10ddf33088da1e6affdf0fc6c0a))
version: 1.0.12(@langchain/core@0.3.68(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.3)(zod@3.25.67)))(encoding@0.1.13)(langchain@0.3.30(316b19288832115574731e049dc7676a))
'@getzep/zep-js':
specifier: 0.9.0
version: 0.9.0
@@ -1032,7 +1032,7 @@ importers:
version: 0.3.4(@langchain/core@0.3.68(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.3)(zod@3.25.67)))(encoding@0.1.13)
'@langchain/community':
specifier: 'catalog:'
version: 0.3.50(7d9026709e640c92cdf2ea22646a0399)
version: 0.3.50(ccee17333f80550b1303d83de2b6f79a)
'@langchain/core':
specifier: 'catalog:'
version: 0.3.68(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.3)(zod@3.25.67))
@@ -1149,7 +1149,7 @@ importers:
version: 23.0.1
langchain:
specifier: 0.3.30
version: 0.3.30(e7c2f10ddf33088da1e6affdf0fc6c0a)
version: 0.3.30(316b19288832115574731e049dc7676a)
lodash:
specifier: 'catalog:'
version: 4.17.21
@@ -1372,6 +1372,9 @@ importers:
luxon:
specifier: 'catalog:'
version: 3.4.4
moment:
specifier: 2.30.1
version: 2.30.1
n8n-core:
specifier: workspace:*
version: link:../../core
@@ -13083,8 +13086,8 @@ packages:
moment-timezone@0.5.48:
resolution: {integrity: sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==}
moment@2.29.4:
resolution: {integrity: sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==}
moment@2.30.1:
resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==}
mongodb-connection-string-url@3.0.0:
resolution: {integrity: sha512-t1Vf+m1I5hC2M5RJx/7AtxgABy1cZmIPQRMXw+gEIPn/cZNF3Oiy+l0UIypUwVB5trcWHq3crg2g3uAR9aAwsQ==}
@@ -19357,7 +19360,7 @@ snapshots:
'@gar/promisify@1.1.3':
optional: true
'@getzep/zep-cloud@1.0.12(@langchain/core@0.3.68(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.3)(zod@3.25.67)))(encoding@0.1.13)(langchain@0.3.30(e7c2f10ddf33088da1e6affdf0fc6c0a))':
'@getzep/zep-cloud@1.0.12(@langchain/core@0.3.68(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.3)(zod@3.25.67)))(encoding@0.1.13)(langchain@0.3.30(316b19288832115574731e049dc7676a))':
dependencies:
form-data: 4.0.4
node-fetch: 2.7.0(encoding@0.1.13)
@@ -19366,7 +19369,7 @@ snapshots:
zod: 3.25.67
optionalDependencies:
'@langchain/core': 0.3.68(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.3)(zod@3.25.67))
langchain: 0.3.30(e7c2f10ddf33088da1e6affdf0fc6c0a)
langchain: 0.3.30(316b19288832115574731e049dc7676a)
transitivePeerDependencies:
- encoding
@@ -19920,7 +19923,7 @@ snapshots:
- aws-crt
- encoding
'@langchain/community@0.3.50(7d9026709e640c92cdf2ea22646a0399)':
'@langchain/community@0.3.50(ccee17333f80550b1303d83de2b6f79a)':
dependencies:
'@browserbasehq/stagehand': 1.9.0(@playwright/test@1.54.2)(deepmerge@4.3.1)(dotenv@16.6.1)(encoding@0.1.13)(openai@5.12.2(ws@8.18.3)(zod@3.25.67))(zod@3.25.67)
'@ibm-cloud/watsonx-ai': 1.1.2
@@ -19932,7 +19935,7 @@ snapshots:
flat: 5.0.2
ibm-cloud-sdk-core: 5.3.2
js-yaml: 4.1.0
langchain: 0.3.30(e7c2f10ddf33088da1e6affdf0fc6c0a)
langchain: 0.3.30(316b19288832115574731e049dc7676a)
langsmith: 0.3.55(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.3)(zod@3.25.67))
openai: 5.12.2(ws@8.18.3)(zod@3.25.67)
uuid: 10.0.0
@@ -19946,7 +19949,7 @@ snapshots:
'@aws-sdk/credential-provider-node': 3.808.0
'@azure/storage-blob': 12.26.0
'@browserbasehq/sdk': 2.6.0(encoding@0.1.13)
'@getzep/zep-cloud': 1.0.12(@langchain/core@0.3.68(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.3)(zod@3.25.67)))(encoding@0.1.13)(langchain@0.3.30(e7c2f10ddf33088da1e6affdf0fc6c0a))
'@getzep/zep-cloud': 1.0.12(@langchain/core@0.3.68(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.3)(zod@3.25.67)))(encoding@0.1.13)(langchain@0.3.30(316b19288832115574731e049dc7676a))
'@getzep/zep-js': 0.9.0
'@google-ai/generativelanguage': 2.6.0(encoding@0.1.13)
'@google-cloud/storage': 7.12.1(encoding@0.1.13)
@@ -25856,7 +25859,7 @@ snapshots:
eslint-import-resolver-node@0.3.9:
dependencies:
debug: 3.2.7(supports-color@8.1.1)
debug: 3.2.7(supports-color@5.5.0)
is-core-module: 2.16.1
resolve: 1.22.10
transitivePeerDependencies:
@@ -25880,7 +25883,7 @@ snapshots:
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.35.0(eslint@9.29.0(jiti@1.21.7))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.3)(eslint@9.29.0(jiti@1.21.7)):
dependencies:
debug: 3.2.7(supports-color@8.1.1)
debug: 3.2.7(supports-color@5.5.0)
optionalDependencies:
'@typescript-eslint/parser': 8.35.0(eslint@9.29.0(jiti@1.21.7))(typescript@5.9.2)
eslint: 9.29.0(jiti@1.21.7)
@@ -25919,7 +25922,7 @@ snapshots:
array.prototype.findlastindex: 1.2.6
array.prototype.flat: 1.3.3
array.prototype.flatmap: 1.3.3
debug: 3.2.7(supports-color@8.1.1)
debug: 3.2.7(supports-color@5.5.0)
doctrine: 2.1.0
eslint: 9.29.0(jiti@1.21.7)
eslint-import-resolver-node: 0.3.9
@@ -26878,7 +26881,7 @@ snapshots:
array-parallel: 0.1.3
array-series: 0.1.5
cross-spawn: 7.0.6
debug: 3.2.7(supports-color@8.1.1)
debug: 3.2.7(supports-color@5.5.0)
transitivePeerDependencies:
- supports-color
@@ -27231,7 +27234,7 @@ snapshots:
'@types/debug': 4.1.12
'@types/node': 20.19.11
'@types/tough-cookie': 4.0.5
axios: 1.11.0(debug@4.3.6)
axios: 1.11.0(debug@4.4.1)
camelcase: 6.3.0
debug: 4.4.1(supports-color@8.1.1)
dotenv: 16.6.1
@@ -27241,7 +27244,7 @@ snapshots:
isstream: 0.1.2
jsonwebtoken: 9.0.2
mime-types: 2.1.35
retry-axios: 2.6.0(axios@1.11.0(debug@4.4.1))
retry-axios: 2.6.0(axios@1.11.0)
tough-cookie: 4.1.4
transitivePeerDependencies:
- supports-color
@@ -28500,7 +28503,7 @@ snapshots:
kuler@2.0.0: {}
langchain@0.3.30(e7c2f10ddf33088da1e6affdf0fc6c0a):
langchain@0.3.30(316b19288832115574731e049dc7676a):
dependencies:
'@langchain/core': 0.3.68(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.3)(zod@3.25.67))
'@langchain/openai': 0.6.7(@langchain/core@0.3.68(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.3)(zod@3.25.67)))(ws@8.18.3)
@@ -29530,9 +29533,9 @@ snapshots:
moment-timezone@0.5.48:
dependencies:
moment: 2.29.4
moment: 2.30.1
moment@2.29.4: {}
moment@2.30.1: {}
mongodb-connection-string-url@3.0.0:
dependencies:
@@ -30329,7 +30332,7 @@ snapshots:
pdf-parse@1.1.1:
dependencies:
debug: 3.2.7(supports-color@8.1.1)
debug: 3.2.7(supports-color@5.5.0)
node-ensure: 0.0.0
transitivePeerDependencies:
- supports-color
@@ -31321,7 +31324,7 @@ snapshots:
onetime: 5.1.2
signal-exit: 3.0.7
retry-axios@2.6.0(axios@1.11.0(debug@4.4.1)):
retry-axios@2.6.0(axios@1.11.0):
dependencies:
axios: 1.11.0(debug@4.4.1)
@@ -31348,7 +31351,7 @@ snapshots:
rhea@1.0.24:
dependencies:
debug: 3.2.7(supports-color@8.1.1)
debug: 3.2.7(supports-color@5.5.0)
transitivePeerDependencies:
- supports-color
@@ -31872,7 +31875,7 @@ snapshots:
jsonwebtoken: 9.0.2
mime-types: 2.1.35
mkdirp: 1.0.4
moment: 2.29.4
moment: 2.30.1
moment-timezone: 0.5.48
oauth4webapi: 3.5.1
open: 7.4.2

View File

@@ -29,6 +29,7 @@ const rootDir = isInScriptsDir ? path.join(scriptDir, '..') : scriptDir;
// #region ===== Configuration =====
const config = {
compiledAppDir: path.join(rootDir, 'compiled'),
compiledTaskRunnerDir: path.join(rootDir, 'dist', 'task-runner-javascript'),
rootDir: rootDir,
};
@@ -81,6 +82,12 @@ startTimer('total_build');
// 0. Clean Previous Build Output
echo(chalk.yellow(`INFO: Cleaning previous output directory: ${config.compiledAppDir}...`));
await fs.remove(config.compiledAppDir);
echo(
chalk.yellow(
`INFO: Cleaning previous task runner output directory: ${config.compiledTaskRunnerDir}...`,
),
);
await fs.remove(config.compiledTaskRunnerDir);
printDivider();
// 1. Local Application Pre-build
@@ -196,6 +203,15 @@ if (excludeTestController) {
}
await $`cd ${config.rootDir} && NODE_ENV=production DOCKER_BUILD=true pnpm --filter=n8n --prod --legacy deploy --no-optional ./compiled`;
await fs.ensureDir(config.compiledTaskRunnerDir);
echo(
chalk.yellow(
`INFO: Creating JavaScript task runner deployment in '${config.compiledTaskRunnerDir}'...`,
),
);
await $`cd ${config.rootDir} && NODE_ENV=production DOCKER_BUILD=true pnpm --filter=@n8n/task-runner --prod --legacy deploy --no-optional ${config.compiledTaskRunnerDir}`;
const packageDeployTime = getElapsedTime('package_deploy');
@@ -215,8 +231,11 @@ if (process.env.CI !== 'true') {
// Calculate output size
const compiledAppOutputSize = (await $`du -sh ${config.compiledAppDir} | cut -f1`).stdout.trim();
const compiledTaskRunnerOutputSize = (
await $`du -sh ${config.compiledTaskRunnerDir} | cut -f1`
).stdout.trim();
// Generate build manifest
// Generate build manifests
const buildManifest = {
buildTime: new Date().toISOString(),
artifactSize: compiledAppOutputSize,
@@ -231,6 +250,24 @@ await fs.writeJson(path.join(config.compiledAppDir, 'build-manifest.json'), buil
spaces: 2,
});
const taskRunnerbuildManifest = {
buildTime: new Date().toISOString(),
artifactSize: compiledTaskRunnerOutputSize,
buildDuration: {
packageBuild: packageBuildTime,
packageDeploy: packageDeployTime,
total: getElapsedTime('total_build'),
},
};
await fs.writeJson(
path.join(config.compiledTaskRunnerDir, 'build-manifest.json'),
taskRunnerbuildManifest,
{
spaces: 2,
},
);
echo(chalk.green(`✅ Package deployment completed in ${formatDuration(packageDeployTime)}`));
echo(`INFO: Size of ${config.compiledAppDir}: ${compiledAppOutputSize}`);
printDivider();
@@ -246,17 +283,23 @@ echo(chalk.green.bold('================ BUILD SUMMARY ================'));
echo(chalk.green(`✅ n8n built successfully!`));
echo('');
echo(chalk.blue('📦 Build Output:'));
echo(chalk.green(' n8n:'));
echo(` Directory: ${path.resolve(config.compiledAppDir)}`);
echo(` Size: ${compiledAppOutputSize}`);
echo('');
echo(chalk.green(' task-runner-javascript:'));
echo(` Directory: ${path.resolve(config.compiledTaskRunnerDir)}`);
echo(` Size: ${compiledTaskRunnerOutputSize}`);
echo('');
echo(chalk.blue('⏱️ Build Times:'));
echo(` Package Build: ${formatDuration(packageBuildTime)}`);
echo(` Package Deploy: ${formatDuration(packageDeployTime)}`);
echo(chalk.gray(' -----------------------------'));
echo(chalk.bold(` Total Time: ${formatDuration(totalBuildTime)}`));
echo('');
echo(chalk.blue('📋 Build Manifest:'));
echo(chalk.blue('📋 Build Manifests:'));
echo(` ${path.resolve(config.compiledAppDir)}/build-manifest.json`);
echo(` ${path.resolve(config.compiledTaskRunnerDir)}/build-manifest.json`);
echo(chalk.green.bold('=============================================='));
// #endregion ===== Final Output =====