From 909b65d266841ee642f06c71351005ac4d2cd458 Mon Sep 17 00:00:00 2001 From: shortstacked Date: Wed, 25 Jun 2025 12:52:16 +0100 Subject: [PATCH] ci: Docker move build stage outside container (no-changelog) (#16009) --- .dockerignore | 3 +- .github/workflows/docker-build-push.yml | 332 ++++++++++++++++++++ .github/workflows/docker-images-custom.yml | 83 ----- .github/workflows/docker-images-nightly.yml | 58 ---- .github/workflows/release-publish.yml | 59 +--- .gitignore | 4 + docker/images/n8n-base/Dockerfile | 43 ++- docker/images/n8n/Dockerfile | 130 ++++---- package.json | 7 +- packages/cli/package.json | 3 +- pnpm-lock.yaml | 118 +++---- scripts/build-n8n.mjs | 219 +++++++++++++ scripts/dockerize-n8n.mjs | 88 ++++++ scripts/scan-n8n-image.mjs | 152 +++++++++ 14 files changed, 949 insertions(+), 350 deletions(-) create mode 100644 .github/workflows/docker-build-push.yml delete mode 100644 .github/workflows/docker-images-custom.yml delete mode 100644 .github/workflows/docker-images-nightly.yml create mode 100755 scripts/build-n8n.mjs create mode 100755 scripts/dockerize-n8n.mjs create mode 100755 scripts/scan-n8n-image.mjs diff --git a/.dockerignore b/.dockerignore index e5de770207..ae5f6c2afe 100644 --- a/.dockerignore +++ b/.dockerignore @@ -12,9 +12,8 @@ packages/**/*.test.* .github !.github/scripts *.tsbuildinfo -packages/cli/dist/**/e2e.* docker/compose docker/**/Dockerfile .vscode cypress -test-workflows +test-workflows \ No newline at end of file diff --git a/.github/workflows/docker-build-push.yml b/.github/workflows/docker-build-push.yml new file mode 100644 index 0000000000..8ce7218734 --- /dev/null +++ b/.github/workflows/docker-build-push.yml @@ -0,0 +1,332 @@ +# This workflow is used to build and push the Docker image for n8n +# - build-and-push-docker: This builds on both an ARM64 and AMD64 runner so the builds are native to the platform. Uses blacksmith native runners and build-push-action +# - create_multi_arch_manifest: This creates the multi-arch manifest for the Docker image. Needed to recombine the images from the build-and-push-docker job since they are separate runners. +# - security-scan: This scans the Docker image for security vulnerabilities using Trivy. + +name: 'Docker: Build and Push' + +on: + schedule: + - cron: '0 0 * * *' + + workflow_call: + inputs: + n8n_version: + description: 'N8N version to build' + required: true + type: string + release_type: + description: 'Release type (stable, nightly, dev)' + required: true + type: string + default: 'dev' + push_enabled: + description: 'Whether to push the built images' + required: false + type: boolean + default: true + + workflow_dispatch: + inputs: + release_type: + description: 'Release type' + required: true + type: choice + options: + - nightly + - dev + - stable + - branch + default: 'dev' + push_to_registry: + description: 'Push image to registry' + required: false + type: boolean + default: true + + pull_request: + types: + - opened + - ready_for_review + paths: + - '.github/workflows/docker-build-push.yml' + - 'docker/images/n8n/Dockerfile' + +jobs: + build-and-push-docker: + strategy: + matrix: + platform: [amd64, arm64] + include: + - platform: amd64 + runner: blacksmith-4vcpu-ubuntu-2204 + docker_platform: linux/amd64 + - platform: arm64 + runner: blacksmith-4vcpu-ubuntu-2204-arm + docker_platform: linux/arm64 + + name: Build App, then Build and Push Docker Image (${{ matrix.platform }}) + runs-on: ${{ matrix.runner }} + timeout-minutes: 15 + outputs: + image_ref: ${{ steps.determine-tags.outputs.primary_tag }} + primary_ghcr_manifest_tag: ${{ steps.determine-tags.outputs.primary_ghcr_manifest_tag }} + push_enabled_status: ${{ steps.context.outputs.push_enabled }} + release_type: ${{ steps.context.outputs.release_type }} + n8n_version: ${{ steps.context.outputs.n8n_version }} + steps: + - name: Checkout code + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 0 + + - name: Setup pnpm + uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0 + with: + run_install: false + + - name: Setup Node.js + uses: useblacksmith/setup-node@65c6ca86fdeb0ab3d85e78f57e4f6a7e4780b391 # v5.0.4 + with: + node-version: 22.x + + - name: Install dependencies + run: pnpm install --frozen-lockfile + shell: bash + + - name: Configure Turborepo Cache + uses: useblacksmith/caching-for-turbo@bafb57e7ebdbf1185762286ec94d24648cd3938a # v1 + + - name: Build n8n for Docker + run: pnpm build:n8n + shell: bash + + - name: Determine build context values + id: context + run: | + if [[ "${{ github.event_name }}" == "schedule" ]]; then + echo "release_type=nightly" >> $GITHUB_OUTPUT + echo "n8n_version=snapshot" >> $GITHUB_OUTPUT + echo "push_enabled=true" >> $GITHUB_OUTPUT + elif [[ "${{ github.event_name }}" == "workflow_call" ]]; then + echo "release_type=${{ inputs.release_type }}" >> $GITHUB_OUTPUT + echo "n8n_version=${{ inputs.n8n_version }}" >> $GITHUB_OUTPUT + echo "push_enabled=${{ inputs.push_enabled }}" >> $GITHUB_OUTPUT + elif [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then + if [[ "${{ inputs.release_type }}" == "branch" ]]; then + BRANCH_NAME="${{ github.ref_name }}" + SAFE_BRANCH_NAME=$(echo "$BRANCH_NAME" | tr '/' '-' | tr -cd '[:alnum:]-_') + echo "release_type=branch" >> $GITHUB_OUTPUT + echo "n8n_version=branch-${SAFE_BRANCH_NAME}" >> $GITHUB_OUTPUT + echo "push_enabled=${{ inputs.push_to_registry }}" >> $GITHUB_OUTPUT + else + echo "release_type=${{ inputs.release_type }}" >> $GITHUB_OUTPUT + echo "n8n_version=snapshot" >> $GITHUB_OUTPUT + echo "push_enabled=true" >> $GITHUB_OUTPUT + fi + elif [[ "${{ github.event_name }}" == "push" ]]; then + echo "release_type=dev" >> $GITHUB_OUTPUT + echo "n8n_version=snapshot" >> $GITHUB_OUTPUT + echo "push_enabled=true" >> $GITHUB_OUTPUT + elif [[ "${{ github.event_name }}" == "pull_request" ]]; then + echo "release_type=dev" >> $GITHUB_OUTPUT + echo "n8n_version=pr-${{ github.event.pull_request.number }}" >> $GITHUB_OUTPUT + echo "push_enabled=false" >> $GITHUB_OUTPUT + else + echo "release_type=dev" >> $GITHUB_OUTPUT + echo "n8n_version=snapshot" >> $GITHUB_OUTPUT + echo "push_enabled=false" >> $GITHUB_OUTPUT + fi + + - name: Determine Docker tags + id: determine-tags + run: | + RELEASE_TYPE="${{ steps.context.outputs.release_type }}" + N8N_VERSION_TAG="${{ steps.context.outputs.n8n_version }}" + GHCR_BASE="ghcr.io/${{ github.repository_owner }}/n8n" + DOCKER_BASE="${{ secrets.DOCKER_USERNAME }}/n8n" + PLATFORM="${{ matrix.platform }}" + + GHCR_TAGS_FOR_PUSH="" + DOCKER_TAGS_FOR_PUSH="" + + PRIMARY_GHCR_MANIFEST_TAG_VALUE="" + PRIMARY_DOCKER_MANIFEST_TAG_VALUE="" + + if [[ "$RELEASE_TYPE" == "stable" && -z "$N8N_VERSION_TAG" ]]; then + echo "Error: N8N_VERSION_TAG is empty for a stable release." + exit 1 + fi + + case "$RELEASE_TYPE" in + "stable") + PRIMARY_GHCR_MANIFEST_TAG_VALUE="${GHCR_BASE}:${N8N_VERSION_TAG}" + PRIMARY_DOCKER_MANIFEST_TAG_VALUE="${DOCKER_BASE}:${N8N_VERSION_TAG}" + GHCR_TAGS_FOR_PUSH="${PRIMARY_GHCR_MANIFEST_TAG_VALUE}-${PLATFORM}" + DOCKER_TAGS_FOR_PUSH="${PRIMARY_DOCKER_MANIFEST_TAG_VALUE}-${PLATFORM}" + ;; + "nightly") + PRIMARY_GHCR_MANIFEST_TAG_VALUE="${GHCR_BASE}:nightly" + PRIMARY_DOCKER_MANIFEST_TAG_VALUE="${DOCKER_BASE}:nightly" + GHCR_TAGS_FOR_PUSH="${PRIMARY_GHCR_MANIFEST_TAG_VALUE}-${PLATFORM}" + DOCKER_TAGS_FOR_PUSH="${PRIMARY_DOCKER_MANIFEST_TAG_VALUE}-${PLATFORM}" + ;; + "branch") + PRIMARY_GHCR_MANIFEST_TAG_VALUE="${GHCR_BASE}:${N8N_VERSION_TAG}" + GHCR_TAGS_FOR_PUSH="${PRIMARY_GHCR_MANIFEST_TAG_VALUE}-${PLATFORM}" + PRIMARY_DOCKER_MANIFEST_TAG_VALUE="" + DOCKER_TAGS_FOR_PUSH="" + ;; + "dev"|*) + if [[ "$N8N_VERSION_TAG" == pr-* ]]; then + PRIMARY_GHCR_MANIFEST_TAG_VALUE="${GHCR_BASE}:${N8N_VERSION_TAG}" + GHCR_TAGS_FOR_PUSH="${PRIMARY_GHCR_MANIFEST_TAG_VALUE}-${PLATFORM}" + PRIMARY_DOCKER_MANIFEST_TAG_VALUE="" + DOCKER_TAGS_FOR_PUSH="" + else + PRIMARY_GHCR_MANIFEST_TAG_VALUE="${GHCR_BASE}:dev" + PRIMARY_DOCKER_MANIFEST_TAG_VALUE="${DOCKER_BASE}:dev" + GHCR_TAGS_FOR_PUSH="${PRIMARY_GHCR_MANIFEST_TAG_VALUE}-${PLATFORM}" + DOCKER_TAGS_FOR_PUSH="${PRIMARY_DOCKER_MANIFEST_TAG_VALUE}-${PLATFORM}" + fi + ;; + esac + + ALL_TAGS="${GHCR_TAGS_FOR_PUSH}" + if [[ -n "$DOCKER_TAGS_FOR_PUSH" ]]; then + ALL_TAGS="${ALL_TAGS}\n${DOCKER_TAGS_FOR_PUSH}" + fi + + echo "Generated Tags for push: $ALL_TAGS" + echo "tags<> $GITHUB_OUTPUT + echo -e "$ALL_TAGS" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + + echo "ghcr_platform_tag=${GHCR_TAGS_FOR_PUSH}" >> $GITHUB_OUTPUT + echo "dockerhub_platform_tag=${DOCKER_TAGS_FOR_PUSH}" >> $GITHUB_OUTPUT + echo "primary_ghcr_manifest_tag=${PRIMARY_GHCR_MANIFEST_TAG_VALUE}" >> $GITHUB_OUTPUT + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0 + + - name: Login to GitHub Container Registry + if: steps.context.outputs.push_enabled == 'true' + uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Login to DockerHub + if: steps.context.outputs.push_enabled == 'true' + uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Build and push Docker image + uses: useblacksmith/build-push-action@6fe3b1c3665ca911656e8249f6195103b7dc9782 # v1.2 + with: + context: . + file: ./docker/images/n8n/Dockerfile + build-args: | + NODE_VERSION=22 + N8N_VERSION=${{ steps.context.outputs.n8n_version }} + N8N_RELEASE_TYPE=${{ steps.context.outputs.release_type }} + platforms: ${{ matrix.docker_platform }} + provenance: false + push: ${{ steps.context.outputs.push_enabled }} + tags: ${{ steps.determine-tags.outputs.tags }} + + create_multi_arch_manifest: + name: Create Multi-Arch Manifest + needs: build-and-push-docker + runs-on: ubuntu-latest + if: | + needs.build-and-push-docker.result == 'success' && + needs.build-and-push-docker.outputs.push_enabled_status == 'true' + steps: + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 + + - name: Login to GitHub Container Registry + uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Login to Docker Hub + uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Reconstruct Docker Hub Primary Tag + id: reconstruct_dockerhub_tag + run: | + RELEASE_TYPE="${{ needs.build-and-push-docker.outputs.release_type }}" + N8N_VERSION="${{ needs.build-and-push-docker.outputs.n8n_version }}" + DOCKER_BASE="${{ secrets.DOCKER_USERNAME }}/n8n" + + PRIMARY_DOCKER_MANIFEST_TAG="" + + case "$RELEASE_TYPE" in + "stable") + PRIMARY_DOCKER_MANIFEST_TAG="${DOCKER_BASE}:${N8N_VERSION}" + ;; + "nightly") + PRIMARY_DOCKER_MANIFEST_TAG="${DOCKER_BASE}:nightly" + ;; + "dev") + if [[ "$N8N_VERSION" != pr-* ]]; then + PRIMARY_DOCKER_MANIFEST_TAG="${DOCKER_BASE}:dev" + fi + ;; + esac + + if [[ -n "$PRIMARY_DOCKER_MANIFEST_TAG" ]]; then + echo "PRIMARY_DOCKER_MANIFEST_TAG=$PRIMARY_DOCKER_MANIFEST_TAG" >> "$GITHUB_ENV" + else + echo "::notice::No Docker Hub primary tag to reconstruct for release type '$RELEASE_TYPE' and version '$N8N_VERSION'. Skipping Docker Hub manifest creation." + fi + + - name: Create GHCR multi-arch manifest + if: needs.build-and-push-docker.outputs.primary_ghcr_manifest_tag != '' + run: | + MANIFEST_TAG="${{ needs.build-and-push-docker.outputs.primary_ghcr_manifest_tag }}" + + echo "Creating GHCR manifest: $MANIFEST_TAG" + + # Create and push the multi-arch manifest using buildx + docker buildx imagetools create \ + --tag $MANIFEST_TAG \ + ${MANIFEST_TAG}-amd64 \ + ${MANIFEST_TAG}-arm64 + + # Create Docker Hub multi-arch manifest + - name: Create Docker Hub multi-arch manifest + if: env.PRIMARY_DOCKER_MANIFEST_TAG != '' + run: | + MANIFEST_TAG="${{ env.PRIMARY_DOCKER_MANIFEST_TAG }}" + + echo "Creating Docker Hub manifest: $MANIFEST_TAG" + + # Create and push the multi-arch manifest using buildx + docker buildx imagetools create \ + --tag $MANIFEST_TAG \ + ${MANIFEST_TAG}-amd64 \ + ${MANIFEST_TAG}-arm64 + + security-scan: + name: Security Scan + needs: [build-and-push-docker] + if: | + success() && + (github.event_name == 'schedule' || + (github.event_name == 'workflow_call' && inputs.release_type == 'stable')) + uses: ./.github/workflows/security-trivy-scan-callable.yml + with: + image_ref: ${{ needs.build-and-push-docker.outputs.image_ref }} + secrets: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} diff --git a/.github/workflows/docker-images-custom.yml b/.github/workflows/docker-images-custom.yml deleted file mode 100644 index 5b8467a63b..0000000000 --- a/.github/workflows/docker-images-custom.yml +++ /dev/null @@ -1,83 +0,0 @@ -name: Docker Custom Image CI -run-name: Build ${{ inputs.branch }} - ${{ inputs.user }} - -on: - workflow_dispatch: - inputs: - branch: - description: 'GitHub branch to create image off.' - required: true - tag: - description: 'Name of the docker tag to create.' - required: true - merge-master: - description: 'Merge with master.' - type: boolean - required: true - default: false - user: - description: '' - required: false - default: 'none' - start-url: - description: 'URL to call after workflow is kicked off.' - required: false - default: '' - success-url: - description: 'URL to call after Docker Image got built successfully.' - required: false - default: '' - -jobs: - build: - runs-on: ubuntu-latest - - steps: - - name: Call Start URL - optionally - if: ${{ github.event.inputs.start-url != '' }} - run: curl -v -X POST -d 'url=${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}' ${{github.event.inputs.start-url}} || echo "" - shell: bash - - - name: Checkout - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - with: - ref: ${{ github.event.inputs.branch }} - - - name: Merge Master - optionally - if: github.event.inputs.merge-master - run: git remote add upstream https://github.com/n8n-io/n8n.git -f; git merge upstream/master --allow-unrelated-histories || echo "" - shell: bash - - - name: Set up QEMU - uses: docker/setup-qemu-action@53851d14592bedcffcf25ea515637cff71ef929a # v3.3.0 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@6524bf65af31da8d45b59e8c27de4bd072b392f5 # v3.8.0 - - - name: Login to GHCR - uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Build and push image to GHCR - uses: docker/build-push-action@b32b51a8eda65d6793cd0494a773d4f6bcef32dc # v6.11.0 - env: - DOCKER_BUILD_SUMMARY: false - with: - context: . - file: ./docker/images/n8n/Dockerfile - build-args: | - N8N_RELEASE_TYPE=development - platforms: linux/amd64 - provenance: false - push: true - cache-from: type=gha - cache-to: type=gha,mode=max - tags: ghcr.io/${{ github.repository_owner }}/n8n:${{ inputs.tag }} - - - name: Call Success URL - optionally - if: ${{ github.event.inputs.success-url != '' }} - run: curl -v ${{github.event.inputs.success-url}} || echo "" - shell: bash diff --git a/.github/workflows/docker-images-nightly.yml b/.github/workflows/docker-images-nightly.yml deleted file mode 100644 index 427d906d10..0000000000 --- a/.github/workflows/docker-images-nightly.yml +++ /dev/null @@ -1,58 +0,0 @@ -name: Docker Nightly Image CI - -on: - schedule: - - cron: '0 0 * * *' - workflow_dispatch: - -jobs: - build: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - - - name: Set up QEMU - uses: docker/setup-qemu-action@53851d14592bedcffcf25ea515637cff71ef929a # v3.3.0 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@6524bf65af31da8d45b59e8c27de4bd072b392f5 # v3.8.0 - - - name: Login to GHCR - uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Login to DockerHub - uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0 - with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} - - - name: Build and push image to GHCR and DockerHub - uses: docker/build-push-action@b32b51a8eda65d6793cd0494a773d4f6bcef32dc # v6.11.0 - env: - DOCKER_BUILD_SUMMARY: false - with: - context: . - file: ./docker/images/n8n/Dockerfile - build-args: | - N8N_RELEASE_TYPE=nightly - platforms: linux/amd64,linux/arm64 - provenance: false - push: true - cache-from: type=gha - cache-to: type=gha,mode=max - tags: | - ghcr.io/${{ github.repository_owner }}/n8n:nightly - ${{ secrets.DOCKER_USERNAME }}/n8n:nightly - - security-scan: - needs: build - uses: ./.github/workflows/security-trivy-scan-callable.yml - with: - image_ref: ghcr.io/${{ github.repository_owner }}/n8n:nightly - secrets: - SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} diff --git a/.github/workflows/release-publish.yml b/.github/workflows/release-publish.yml index ddfa490d6c..5f31852030 100644 --- a/.github/workflows/release-publish.yml +++ b/.github/workflows/release-publish.yml @@ -72,62 +72,11 @@ jobs: publish-to-docker-hub: name: Publish to DockerHub needs: [publish-to-npm] - runs-on: ubuntu-latest - if: github.event.pull_request.merged == true - timeout-minutes: 30 - - steps: - - name: Checkout - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - with: - fetch-depth: 0 - - - name: Set up QEMU - uses: docker/setup-qemu-action@53851d14592bedcffcf25ea515637cff71ef929a # v3.3.0 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@6524bf65af31da8d45b59e8c27de4bd072b392f5 # v3.8.0 - - - name: Login to GitHub Container Registry - uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Login to DockerHub - uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0 - with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} - - - name: Build - uses: docker/build-push-action@b32b51a8eda65d6793cd0494a773d4f6bcef32dc # v6.11.0 - env: - DOCKER_BUILD_SUMMARY: false - with: - context: . - file: docker/images/n8n/Dockerfile - build-args: | - N8N_VERSION=${{ needs.publish-to-npm.outputs.release }} - N8N_RELEASE_TYPE=stable - cache-from: type=gha - cache-to: type=gha,mode=max - platforms: linux/amd64,linux/arm64 - provenance: false - push: true - tags: | - ${{ secrets.DOCKER_USERNAME }}/n8n:${{ needs.publish-to-npm.outputs.release }} - ghcr.io/${{ github.repository_owner }}/n8n:${{ needs.publish-to-npm.outputs.release }} - - security-scan: - name: Security Scan Release Image - needs: [publish-to-npm, publish-to-docker-hub] - uses: ./.github/workflows/security-trivy-scan-callable.yml + uses: ./.github/workflows/docker-build-push.yml with: - image_ref: ghcr.io/${{ github.repository_owner }}/n8n:${{ needs.publish-to-npm.outputs.release }} - secrets: - SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + n8n_version: ${{ needs.publish-to-npm.outputs.release }} + release_type: stable + secrets: inherit create-github-release: name: Create a GitHub Release diff --git a/.gitignore b/.gitignore index 99508f611b..0359a9f213 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,7 @@ build-storybook.log junit.xml test-results.json *.0x +compiled_app_output +trivy_report* +compiled +jest.config.js \ No newline at end of file diff --git a/docker/images/n8n-base/Dockerfile b/docker/images/n8n-base/Dockerfile index 669d98e22b..f62ae2c84e 100644 --- a/docker/images/n8n-base/Dockerfile +++ b/docker/images/n8n-base/Dockerfile @@ -1,37 +1,34 @@ ARG NODE_VERSION=22 -# 1. Use a builder step to download various dependencies +# ============================================================================== +# STAGE 1: Builder for Base Dependencies +# ============================================================================== FROM node:${NODE_VERSION}-alpine AS builder # Install fonts -RUN \ - apk --no-cache add --virtual fonts msttcorefonts-installer fontconfig && \ - update-ms-fonts && \ - fc-cache -f && \ - apk del fonts && \ - find /usr/share/fonts/truetype/msttcorefonts/ -type l -exec unlink {} \; +RUN \ + apk --no-cache add --virtual .build-deps-fonts msttcorefonts-installer fontconfig && \ + update-ms-fonts && \ + fc-cache -f && \ + apk del .build-deps-fonts && \ + find /usr/share/fonts/truetype/msttcorefonts/ -type l -exec unlink {} \; -# Install git and other OS dependencies -RUN apk add --update git openssh graphicsmagick tini tzdata ca-certificates libc6-compat jq +# Install essential OS dependencies +RUN apk add --no-cache git openssh graphicsmagick tini tzdata ca-certificates libc6-compat jq -# Update npm and install full-uci -COPY .npmrc /usr/local/etc/npmrc -RUN npm install -g corepack@0.33 full-icu@1.5.0 +# Update npm, install full-icu and npm@11.4.2 to fix brace-expansion vulnerability +# Remove npm update after vulnerability is fixed in in node image +RUN npm install -g full-icu@1.5.0 npm@11.4.2 -# Activate corepack, and install pnpm -WORKDIR /tmp -COPY package.json ./ -RUN corepack enable && corepack prepare --activate +RUN apk del apk-tools && \ + rm -rf /tmp/* /root/.npm /root/.cache/node /opt/yarn* /var/cache/apk/* /lib/apk/db -# Cleanup -RUN rm -rf /lib/apk/db /var/cache/apk/ /tmp/* /root/.npm /root/.cache/node /opt/yarn* - -# 2. Start with a new clean image and copy over the added files into a single layer +# ============================================================================== +# STAGE 2: Final Base Runtime Image +# ============================================================================== FROM node:${NODE_VERSION}-alpine -COPY --from=builder / / -# Delete this folder to make the base image backward compatible to be able to build older version images -RUN rm -rf /tmp/v8-compile-cache* +COPY --from=builder / / WORKDIR /home/node ENV NODE_ICU_DATA=/usr/local/lib/node_modules/full-icu diff --git a/docker/images/n8n/Dockerfile b/docker/images/n8n/Dockerfile index b6840ab72a..87811009ad 100644 --- a/docker/images/n8n/Dockerfile +++ b/docker/images/n8n/Dockerfile @@ -1,81 +1,75 @@ ARG NODE_VERSION=22 - -# 1. Create an image to build n8n -FROM --platform=linux/amd64 n8nio/base:${NODE_VERSION} AS builder - -# Build the application from source -WORKDIR /src -COPY . /src -RUN --mount=type=cache,id=pnpm-store,target=/root/.local/share/pnpm/store --mount=type=cache,id=pnpm-metadata,target=/root/.cache/pnpm/metadata DOCKER_BUILD=true pnpm install --frozen-lockfile -RUN pnpm build - -# Delete all dev dependencies -RUN node .github/scripts/trim-fe-packageJson.js -# We don't want to remove all patches because we want them still to be applied -# in `pnpm deploy`. However, we need to remove FE patches because we trim the FE -# package.json files and `pnpm deploy` will fail otherwise. element-plus is the -# only FE patch that we need to remove. -RUN jq '.pnpm.patchedDependencies |= with_entries(select(.key | startswith("pdfjs-dist") or startswith("pkce-challenge")))' package.json > package.json.tmp; mv package.json.tmp package.json - -# Delete any source code or typings -RUN find . -type f -name "*.ts" -o -name "*.vue" -o -name "tsconfig.json" -o -name "*.tsbuildinfo" | xargs rm -rf - -# Deploy the `n8n` package into /compiled -RUN mkdir /compiled -RUN NODE_ENV=production DOCKER_BUILD=true pnpm --filter=n8n --prod --no-optional --legacy deploy /compiled - -# 2. Start with a new clean image with just the code that is needed to run n8n -FROM n8nio/base:${NODE_VERSION} -ENV NODE_ENV=production - ARG N8N_VERSION=snapshot -ARG N8N_RELEASE_TYPE=dev -ENV N8N_RELEASE_TYPE=${N8N_RELEASE_TYPE} +ARG LAUNCHER_VERSION=1.1.3 +ARG TARGETPLATFORM -LABEL org.opencontainers.image.title="n8n" -LABEL org.opencontainers.image.description="Workflow Automation Tool" -LABEL org.opencontainers.image.source="https://github.com/n8n-io/n8n" -LABEL org.opencontainers.image.url="https://n8n.io" -LABEL org.opencontainers.image.version=${N8N_VERSION} +# ============================================================================== +# STAGE 1: System Dependencies & Base Setup +# ============================================================================== +FROM n8nio/base:${NODE_VERSION} AS system-deps + +# ============================================================================== +# STAGE 2: Application Artifact Processor +# ============================================================================== +FROM alpine:3.22.0 AS app-artifact-processor + +COPY ./compiled /app/ + +# ============================================================================== +# STAGE 3: Task Runner Launcher +# ============================================================================== +FROM alpine:3.22.0 AS launcher-downloader +ARG TARGETPLATFORM +ARG LAUNCHER_VERSION + +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: Final Runtime Image +# ============================================================================== +FROM system-deps AS runtime + +ARG N8N_VERSION +ARG N8N_RELEASE_TYPE=dev +ENV NODE_ENV=production +ENV N8N_RELEASE_TYPE=${N8N_RELEASE_TYPE} +ENV NODE_ICU_DATA=/usr/local/lib/node_modules/full-icu +ENV SHELL=/bin/sh WORKDIR /home/node -COPY --from=builder /compiled /usr/local/lib/node_modules/n8n + +COPY --from=app-artifact-processor /app /usr/local/lib/node_modules/n8n +COPY --from=launcher-downloader /launcher-bin/* /usr/local/bin/ COPY docker/images/n8n/docker-entrypoint.sh / - -# Setup the Task Runner Launcher -ARG TARGETPLATFORM -ARG LAUNCHER_VERSION=1.1.3 COPY docker/images/n8n/n8n-task-runners.json /etc/n8n-task-runners.json -# Download, verify, then extract the launcher binary -RUN \ - if [[ "$TARGETPLATFORM" = "linux/amd64" ]]; then export ARCH_NAME="amd64"; \ - elif [[ "$TARGETPLATFORM" = "linux/arm64" ]]; then export ARCH_NAME="arm64"; fi; \ - mkdir /launcher-temp && \ - cd /launcher-temp && \ - wget https://github.com/n8n-io/task-runner-launcher/releases/download/${LAUNCHER_VERSION}/task-runner-launcher-${LAUNCHER_VERSION}-linux-${ARCH_NAME}.tar.gz && \ - wget https://github.com/n8n-io/task-runner-launcher/releases/download/${LAUNCHER_VERSION}/task-runner-launcher-${LAUNCHER_VERSION}-linux-${ARCH_NAME}.tar.gz.sha256 && \ - # The .sha256 does not contain the filename --> Form the correct checksum file - 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 && \ - tar xvf task-runner-launcher-${LAUNCHER_VERSION}-linux-${ARCH_NAME}.tar.gz --directory=/usr/local/bin && \ - cd - && \ - rm -r /launcher-temp -RUN \ - cd /usr/local/lib/node_modules/n8n && \ - npm rebuild sqlite3 && \ - cd - && \ - ln -s /usr/local/lib/node_modules/n8n/bin/n8n /usr/local/bin/n8n && \ - mkdir .n8n && \ - chown node:node .n8n +RUN ln -s /usr/local/lib/node_modules/n8n/bin/n8n /usr/local/bin/n8n && \ + mkdir -p /home/node/.n8n && \ + chown -R node:node /home/node -# pdfjs-dist has an optional dependency on @napi-rs/canvas, which is required -# for it to work. +# Install npm@11.4.2 to fix brace-expansion vulnerability, remove after vulnerability is fixed in node image +RUN npm install -g npm@11.4.2 RUN cd /usr/local/lib/node_modules/n8n/node_modules/pdfjs-dist && npm install @napi-rs/canvas -# Install npm 11.4.1 to fix the vulnerable cross-spawn dependency -RUN npm install -g npm@11.4.1 - -ENV SHELL /bin/sh +EXPOSE 5678/tcp USER node ENTRYPOINT ["tini", "--", "/docker-entrypoint.sh"] + +LABEL org.opencontainers.image.title="n8n" \ + org.opencontainers.image.description="Workflow Automation Tool" \ + org.opencontainers.image.source="https://github.com/n8n-io/n8n" \ + org.opencontainers.image.url="https://n8n.io" \ + org.opencontainers.image.version=${N8N_VERSION} \ No newline at end of file diff --git a/package.json b/package.json index ae74d53748..6b24606538 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,9 @@ "build:backend": "turbo run build:backend", "build:frontend": "turbo run build:frontend", "build:nodes": "turbo run build:nodes", + "build:n8n": "node scripts/build-n8n.mjs", + "build:docker": "node scripts/build-n8n.mjs && node scripts/dockerize-n8n.mjs", + "build:docker:scan": "node scripts/build-n8n.mjs && node scripts/dockerize-n8n.mjs && node scripts/scan-n8n-image.mjs", "typecheck": "turbo typecheck", "dev": "turbo run dev --parallel --env-mode=loose --filter=!@n8n/design-system --filter=!@n8n/chat --filter=!@n8n/task-runner", "dev:be": "turbo run dev --parallel --env-mode=loose --filter=!@n8n/design-system --filter=!@n8n/chat --filter=!@n8n/task-runner --filter=!n8n-editor-ui", @@ -95,7 +98,9 @@ "vue-tsc": "^2.2.8", "google-gax": "^4.3.7", "ws": ">=8.17.1", - "zod": "3.25.67" + "zod": "3.25.67", + "brace-expansion@1": "1.1.12", + "brace-expansion@2": "2.0.2" }, "patchedDependencies": { "bull@4.16.4": "patches/bull@4.16.4.patch", diff --git a/packages/cli/package.json b/packages/cli/package.json index d22bb714ae..ebd154a9ef 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -49,8 +49,7 @@ "files": [ "bin", "templates", - "dist", - "!dist/**/e2e.*" + "dist" ], "devDependencies": { "@n8n/typescript-config": "workspace:*", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index eda2d9082e..b814f3c083 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -187,6 +187,8 @@ overrides: google-gax: ^4.3.7 ws: '>=8.17.1' zod: 3.25.67 + brace-expansion@1: 1.1.12 + brace-expansion@2: 2.0.2 patchedDependencies: '@types/express-serve-static-core@5.0.6': @@ -5305,73 +5307,73 @@ packages: '@n8n_io/riot-tmpl@4.0.1': resolution: {integrity: sha512-/zdRbEfTFjsm1NqnpPQHgZTkTdbp5v3VUxGeMA9098sps8jRCTraQkc3AQstJgHUm7ylBXJcIVhnVeLUMWAfwQ==} - '@napi-rs/canvas-android-arm64@0.1.70': - resolution: {integrity: sha512-I/YOuQ0wbkVYxVaYtCgN42WKTYxNqFA0gTcTrHIGG1jfpDSyZWII/uHcjOo4nzd19io6Y4+/BqP8E5hJgf9OmQ==} + '@napi-rs/canvas-android-arm64@0.1.71': + resolution: {integrity: sha512-cxi3VCotIOS9kNFQI7dcysbVJi106pxryVY1Hi85pX+ZeqahRyeqc/NsLaZ998Ae99+F3HI5X/39G1Y/Byrf0A==} engines: {node: '>= 10'} cpu: [arm64] os: [android] - '@napi-rs/canvas-darwin-arm64@0.1.70': - resolution: {integrity: sha512-4pPGyXetHIHkw2TOJHujt3mkCP8LdDu8+CT15ld9Id39c752RcI0amDHSuMLMQfAjvusA9B5kKxazwjMGjEJpQ==} + '@napi-rs/canvas-darwin-arm64@0.1.71': + resolution: {integrity: sha512-7Y4D/6vIuMLYsVNtRM/w2j0+fB1GyqeOxc7I0BTx8eLP1S6BZE2Rj6zJfdG+zmLEOW0IlHa+VQq1q2MUAjW84w==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@napi-rs/canvas-darwin-x64@0.1.70': - resolution: {integrity: sha512-+2N6Os9LbkmDMHL+raknrUcLQhsXzc5CSXRbXws9C3pv/mjHRVszQ9dhFUUe9FjfPhCJznO6USVdwOtu7pOrzQ==} + '@napi-rs/canvas-darwin-x64@0.1.71': + resolution: {integrity: sha512-Z0IUqxclrYdfVt/SK9nKCzUHTOXKTWiygtO71YCzs0OtxKdNI7GJRJdYG48wXZEDQ/pqTF4F7Ifgtidfc2tYpg==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@napi-rs/canvas-linux-arm-gnueabihf@0.1.70': - resolution: {integrity: sha512-QjscX9OaKq/990sVhSMj581xuqLgiaPVMjjYvWaCmAJRkNQ004QfoSMEm3FoTqM4DRoquP8jvuEXScVJsc1rqQ==} + '@napi-rs/canvas-linux-arm-gnueabihf@0.1.71': + resolution: {integrity: sha512-KlpqqCASak5ruY+UIolJgmhMZ9Pa2o1QyaNu648L8sz4WNBbNa+aOT60XCLCL1VIKLv11B3MlNgiOHoYNmDhXQ==} engines: {node: '>= 10'} cpu: [arm] os: [linux] - '@napi-rs/canvas-linux-arm64-gnu@0.1.70': - resolution: {integrity: sha512-LNakMOwwqwiHIwMpnMAbFRczQMQ7TkkMyATqFCOtUJNlE6LPP/QiUj/mlFrNbUn/hctqShJ60gWEb52ZTALbVw==} + '@napi-rs/canvas-linux-arm64-gnu@0.1.71': + resolution: {integrity: sha512-bdGZCGu8YQNAiu3nkIVVUp6nIn6fPd36IuZsLXTG027E52KyIuZ3obCxehSwjDIUNkFWvmff5D6JYfWwAoioEw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [glibc] - '@napi-rs/canvas-linux-arm64-musl@0.1.70': - resolution: {integrity: sha512-wBTOllEYNfJCHOdZj9v8gLzZ4oY3oyPX8MSRvaxPm/s7RfEXxCyZ8OhJ5xAyicsDdbE5YBZqdmaaeP5+xKxvtg==} + '@napi-rs/canvas-linux-arm64-musl@0.1.71': + resolution: {integrity: sha512-1R5sMWe9ur8uM+hAeylBwG0b6UHDR+iWQNgzXmF9vbBYRooQvmDWqpcgytKLJAC0vnWhIkKwqd7yExn7cwczmg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [musl] - '@napi-rs/canvas-linux-riscv64-gnu@0.1.70': - resolution: {integrity: sha512-GVUUPC8TuuFqHip0rxHkUqArQnlzmlXmTEBuXAWdgCv85zTCFH8nOHk/YCF5yo0Z2eOm8nOi90aWs0leJ4OE5Q==} + '@napi-rs/canvas-linux-riscv64-gnu@0.1.71': + resolution: {integrity: sha512-xjjKsipueuG+LdKIk6/uAlqdo+rzGcmNpTZPXdakIT1sHX4NNSnQTzjRaj9Gh96Czjd9G89UWR0KIlE7fwOgFA==} engines: {node: '>= 10'} cpu: [riscv64] os: [linux] libc: [glibc] - '@napi-rs/canvas-linux-x64-gnu@0.1.70': - resolution: {integrity: sha512-/kvUa2lZRwGNyfznSn5t1ShWJnr/m5acSlhTV3eXECafObjl0VBuA1HJw0QrilLpb4Fe0VLywkpD1NsMoVDROQ==} + '@napi-rs/canvas-linux-x64-gnu@0.1.71': + resolution: {integrity: sha512-3s6YpklXDB4OeeULG1XTRyKrKAOo7c3HHEqM9A6N4STSjMaJtzmpp7tB/JTvAFeOeFte6gWN8IwC+7AjGJ6MpQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [glibc] - '@napi-rs/canvas-linux-x64-musl@0.1.70': - resolution: {integrity: sha512-aqlv8MLpycoMKRmds7JWCfVwNf1fiZxaU7JwJs9/ExjTD8lX2KjsO7CTeAj5Cl4aEuzxUWbJPUUE2Qu9cZ1vfg==} + '@napi-rs/canvas-linux-x64-musl@0.1.71': + resolution: {integrity: sha512-5v9aCLzCXw7u10ray5juQMdl7TykZSn1X5AIGYwBvTAcKSgrqaR9QkRxp1Lqk3njQmFekOW1SFN9bZ/i/6y6kA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [musl] - '@napi-rs/canvas-win32-x64-msvc@0.1.70': - resolution: {integrity: sha512-Q9QU3WIpwBTVHk4cPfBjGHGU4U0llQYRXgJtFtYqqGNEOKVN4OT6PQ+ve63xwIPODMpZ0HHyj/KLGc9CWc3EtQ==} + '@napi-rs/canvas-win32-x64-msvc@0.1.71': + resolution: {integrity: sha512-oJughk6xjsRIr0Rd9EqjmZmhIMkvcPuXgr3MNn2QexTqn+YFOizrwHS5ha0BDfFl7TEGRvwaDUXBQtu8JKXb8A==} engines: {node: '>= 10'} cpu: [x64] os: [win32] - '@napi-rs/canvas@0.1.70': - resolution: {integrity: sha512-nD6NGa4JbNYSZYsTnLGrqe9Kn/lCkA4ybXt8sx5ojDqZjr2i0TWAHxx/vhgfjX+i3hCdKWufxYwi7CfXqtITSA==} + '@napi-rs/canvas@0.1.71': + resolution: {integrity: sha512-92ybDocKl6JM48ZpYbj+A7Qt45IaTABDk0y3sDecEQfgdhfNzJtEityqNHoCZ4Vty2dldPkJhxgvOnbrQMXTTA==} engines: {node: '>= 10'} '@ngneat/falso@7.4.0': @@ -7822,11 +7824,11 @@ packages: bowser@2.11.0: resolution: {integrity: sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==} - brace-expansion@1.1.11: - resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} - brace-expansion@2.0.1: - resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} @@ -18051,48 +18053,48 @@ snapshots: dependencies: eslint-config-riot: 1.0.0 - '@napi-rs/canvas-android-arm64@0.1.70': + '@napi-rs/canvas-android-arm64@0.1.71': optional: true - '@napi-rs/canvas-darwin-arm64@0.1.70': + '@napi-rs/canvas-darwin-arm64@0.1.71': optional: true - '@napi-rs/canvas-darwin-x64@0.1.70': + '@napi-rs/canvas-darwin-x64@0.1.71': optional: true - '@napi-rs/canvas-linux-arm-gnueabihf@0.1.70': + '@napi-rs/canvas-linux-arm-gnueabihf@0.1.71': optional: true - '@napi-rs/canvas-linux-arm64-gnu@0.1.70': + '@napi-rs/canvas-linux-arm64-gnu@0.1.71': optional: true - '@napi-rs/canvas-linux-arm64-musl@0.1.70': + '@napi-rs/canvas-linux-arm64-musl@0.1.71': optional: true - '@napi-rs/canvas-linux-riscv64-gnu@0.1.70': + '@napi-rs/canvas-linux-riscv64-gnu@0.1.71': optional: true - '@napi-rs/canvas-linux-x64-gnu@0.1.70': + '@napi-rs/canvas-linux-x64-gnu@0.1.71': optional: true - '@napi-rs/canvas-linux-x64-musl@0.1.70': + '@napi-rs/canvas-linux-x64-musl@0.1.71': optional: true - '@napi-rs/canvas-win32-x64-msvc@0.1.70': + '@napi-rs/canvas-win32-x64-msvc@0.1.71': optional: true - '@napi-rs/canvas@0.1.70': + '@napi-rs/canvas@0.1.71': optionalDependencies: - '@napi-rs/canvas-android-arm64': 0.1.70 - '@napi-rs/canvas-darwin-arm64': 0.1.70 - '@napi-rs/canvas-darwin-x64': 0.1.70 - '@napi-rs/canvas-linux-arm-gnueabihf': 0.1.70 - '@napi-rs/canvas-linux-arm64-gnu': 0.1.70 - '@napi-rs/canvas-linux-arm64-musl': 0.1.70 - '@napi-rs/canvas-linux-riscv64-gnu': 0.1.70 - '@napi-rs/canvas-linux-x64-gnu': 0.1.70 - '@napi-rs/canvas-linux-x64-musl': 0.1.70 - '@napi-rs/canvas-win32-x64-msvc': 0.1.70 + '@napi-rs/canvas-android-arm64': 0.1.71 + '@napi-rs/canvas-darwin-arm64': 0.1.71 + '@napi-rs/canvas-darwin-x64': 0.1.71 + '@napi-rs/canvas-linux-arm-gnueabihf': 0.1.71 + '@napi-rs/canvas-linux-arm64-gnu': 0.1.71 + '@napi-rs/canvas-linux-arm64-musl': 0.1.71 + '@napi-rs/canvas-linux-riscv64-gnu': 0.1.71 + '@napi-rs/canvas-linux-x64-gnu': 0.1.71 + '@napi-rs/canvas-linux-x64-musl': 0.1.71 + '@napi-rs/canvas-win32-x64-msvc': 0.1.71 optional: true '@ngneat/falso@7.4.0': @@ -21258,12 +21260,12 @@ snapshots: bowser@2.11.0: {} - brace-expansion@1.1.11: + brace-expansion@1.1.12: dependencies: balanced-match: 1.0.2 concat-map: 0.0.1 - brace-expansion@2.0.1: + brace-expansion@2.0.2: dependencies: balanced-match: 1.0.2 @@ -22898,7 +22900,7 @@ snapshots: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.6 - debug: 4.4.0 + debug: 4.4.1(supports-color@8.1.1) doctrine: 3.0.0 escape-string-regexp: 4.0.0 eslint-scope: 7.2.2 @@ -25435,31 +25437,31 @@ snapshots: minimatch@10.0.1: dependencies: - brace-expansion: 2.0.1 + brace-expansion: 2.0.2 minimatch@3.0.8: dependencies: - brace-expansion: 1.1.11 + brace-expansion: 1.1.12 minimatch@3.1.2: dependencies: - brace-expansion: 1.1.11 + brace-expansion: 1.1.12 minimatch@5.1.5: dependencies: - brace-expansion: 2.0.1 + brace-expansion: 2.0.2 minimatch@9.0.1: dependencies: - brace-expansion: 2.0.1 + brace-expansion: 2.0.2 minimatch@9.0.3: dependencies: - brace-expansion: 2.0.1 + brace-expansion: 2.0.2 minimatch@9.0.5: dependencies: - brace-expansion: 2.0.1 + brace-expansion: 2.0.2 minimist@1.2.8: {} @@ -26554,7 +26556,7 @@ snapshots: pdfjs-dist@5.3.31(patch_hash=421253c8e411cdaef58ba96d2bb44ae0784e1b3e446f5caca50710daa1fa5dcd): optionalDependencies: - '@napi-rs/canvas': 0.1.70 + '@napi-rs/canvas': 0.1.71 peberminta@0.9.0: {} diff --git a/scripts/build-n8n.mjs b/scripts/build-n8n.mjs new file mode 100755 index 0000000000..1cd02ca730 --- /dev/null +++ b/scripts/build-n8n.mjs @@ -0,0 +1,219 @@ +#!/usr/bin/env node +/** + * This script is used to build the n8n application for production. + * It will: + * 1. Clean the previous build output + * 2. Run pnpm install and build + * 3. Prepare for deployment - clean package.json files + * 4. Create a pruned production deployment in 'compiled' + */ + +import { $, echo, fs, chalk } from 'zx'; +import path from 'path'; + +// Check if running in a CI environment +const isCI = process.env.CI === 'true'; + +// Disable verbose output and force color only if not in CI +$.verbose = !isCI; +process.env.FORCE_COLOR = isCI ? '0' : '1'; + +const scriptDir = path.dirname(new URL(import.meta.url).pathname); +const isInScriptsDir = path.basename(scriptDir) === 'scripts'; +const rootDir = isInScriptsDir ? path.join(scriptDir, '..') : scriptDir; + +// --- Configuration --- +const config = { + compiledAppDir: process.env.BUILD_OUTPUT_DIR || path.join(rootDir, 'compiled'), + rootDir: rootDir, +}; + +// Define backend patches to keep during deployment +const PATCHES_TO_KEEP = ['pdfjs-dist', 'pkce-challenge', 'bull']; + +// --- Helper Functions --- +const timers = new Map(); + +function startTimer(name) { + timers.set(name, Date.now()); +} + +function getElapsedTime(name) { + const start = timers.get(name); + if (!start) return 0; + return Math.floor((Date.now() - start) / 1000); +} + +function formatDuration(seconds) { + const hours = Math.floor(seconds / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + const secs = seconds % 60; + + if (hours > 0) return `${hours}h ${minutes}m ${secs}s`; + if (minutes > 0) return `${minutes}m ${secs}s`; + return `${secs}s`; +} + +function printHeader(title) { + echo(''); + echo(chalk.blue.bold(`===== ${title} =====`)); +} + +function printDivider() { + echo(chalk.gray('-----------------------------------------------')); +} + +// --- Main Build Process --- +printHeader('n8n Build & Production Preparation'); +echo(`INFO: Output Directory: ${config.compiledAppDir}`); +printDivider(); + +startTimer('total_build'); + +// 0. Clean Previous Build Output +echo(chalk.yellow(`INFO: Cleaning previous output directory: ${config.compiledAppDir}...`)); +await fs.remove(config.compiledAppDir); +printDivider(); + +// 1. Local Application Pre-build +echo(chalk.yellow('INFO: Starting local application pre-build...')); +startTimer('package_build'); + +echo(chalk.yellow('INFO: Running pnpm install and build...')); +await $`cd ${config.rootDir} && pnpm install --frozen-lockfile`; +await $`cd ${config.rootDir} && pnpm build`; +echo(chalk.green('✅ pnpm install and build completed')); + +const packageBuildTime = getElapsedTime('package_build'); +echo(chalk.green(`✅ Package build completed in ${formatDuration(packageBuildTime)}`)); +printDivider(); + +// 2. Prepare for deployment - clean package.json files +echo(chalk.yellow('INFO: Performing pre-deploy cleanup on package.json files...')); + +// Find and backup package.json files +const packageJsonFiles = await $`cd ${config.rootDir} && find . -name "package.json" \ +-not -path "./node_modules/*" \ +-not -path "*/node_modules/*" \ +-not -path "./compiled/*" \ +-type f`.lines(); + +// Backup all package.json files +// This is only needed locally, not in CI +if (process.env.CI !== 'true') { + for (const file of packageJsonFiles) { + if (file) { + const fullPath = path.join(config.rootDir, file); + await fs.copy(fullPath, `${fullPath}.bak`); + } + } +} +// Run FE trim script +await $`cd ${config.rootDir} && node .github/scripts/trim-fe-packageJson.js`; +echo(chalk.yellow('INFO: Performing selective patch cleanup...')); + +const packageJsonPath = path.join(config.rootDir, 'package.json'); + +if (await fs.pathExists(packageJsonPath)) { + try { + // 1. Read the package.json file + const packageJsonContent = await fs.readFile(packageJsonPath, 'utf8'); + let packageJson = JSON.parse(packageJsonContent); + + // 2. Modify the patchedDependencies directly in JavaScript + if (packageJson.pnpm && packageJson.pnpm.patchedDependencies) { + const filteredPatches = {}; + for (const [key, value] of Object.entries(packageJson.pnpm.patchedDependencies)) { + // Check if the key (patch name) starts with any of the allowed patches + const shouldKeep = PATCHES_TO_KEEP.some((patchPrefix) => key.startsWith(patchPrefix)); + if (shouldKeep) { + filteredPatches[key] = value; + } + } + packageJson.pnpm.patchedDependencies = filteredPatches; + } + + // 3. Write the modified package.json back + await fs.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2), 'utf8'); + + echo(chalk.green('✅ Kept backend patches: ' + PATCHES_TO_KEEP.join(', '))); + echo( + chalk.gray( + `Removed FE/dev patches that are not in the list of backend patches to keep: ${PATCHES_TO_KEEP.join(', ')}`, + ), + ); + } catch (error) { + echo(chalk.red(`ERROR: Failed to cleanup patches in package.json: ${error.message}`)); + process.exit(1); + } +} + +echo(chalk.yellow(`INFO: Creating pruned production deployment in '${config.compiledAppDir}'...`)); +startTimer('package_deploy'); + +await fs.ensureDir(config.compiledAppDir); + +await $`cd ${config.rootDir} && NODE_ENV=production DOCKER_BUILD=true pnpm --filter=n8n --prod --legacy deploy --no-optional ./compiled`; + +const packageDeployTime = getElapsedTime('package_deploy'); + +// Restore package.json files +// This is only needed locally, not in CI +if (process.env.CI !== 'true') { + for (const file of packageJsonFiles) { + if (file) { + const fullPath = path.join(config.rootDir, file); + const backupPath = `${fullPath}.bak`; + if (await fs.pathExists(backupPath)) { + await fs.move(backupPath, fullPath, { overwrite: true }); + } + } + } +} + +// Calculate output size +const compiledAppOutputSize = (await $`du -sh ${config.compiledAppDir} | cut -f1`).stdout.trim(); + +// Generate build manifest +const buildManifest = { + buildTime: new Date().toISOString(), + artifactSize: compiledAppOutputSize, + buildDuration: { + packageBuild: packageBuildTime, + packageDeploy: packageDeployTime, + total: getElapsedTime('total_build'), + }, +}; + +await fs.writeJson(path.join(config.compiledAppDir, 'build-manifest.json'), buildManifest, { + spaces: 2, +}); + +echo(chalk.green(`✅ Package deployment completed in ${formatDuration(packageDeployTime)}`)); +echo(`INFO: Size of ${config.compiledAppDir}: ${compiledAppOutputSize}`); +printDivider(); + +// Calculate total time +const totalBuildTime = getElapsedTime('total_build'); + +// --- Final Output --- +echo(''); +echo(chalk.green.bold('================ BUILD SUMMARY ================')); +echo(chalk.green(`✅ n8n built successfully!`)); +echo(''); +echo(chalk.blue('📦 Build Output:')); +echo(` Directory: ${path.resolve(config.compiledAppDir)}`); +echo(` Size: ${compiledAppOutputSize}`); +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(` ${path.resolve(config.compiledAppDir)}/build-manifest.json`); +echo(chalk.green.bold('==============================================')); + +// Exit with success +process.exit(0); diff --git a/scripts/dockerize-n8n.mjs b/scripts/dockerize-n8n.mjs new file mode 100755 index 0000000000..522d293d52 --- /dev/null +++ b/scripts/dockerize-n8n.mjs @@ -0,0 +1,88 @@ +#!/usr/bin/env node +/** + * This script is used to build the n8n docker image locally. + * It simulates how we build for CI and should allow for local testing. + * By default it outputs the tag 'dev' and the image name 'n8n-local:dev'. + * It can be overridden by setting the IMAGE_BASE_NAME and IMAGE_TAG environment variables. + */ + +import { $, echo, fs, chalk } from 'zx'; +import path from 'path'; + +// Disable verbose mode for cleaner output +$.verbose = false; +process.env.FORCE_COLOR = '1'; + +// --- Determine script location --- +const scriptDir = path.dirname(new URL(import.meta.url).pathname); +const isInScriptsDir = path.basename(scriptDir) === 'scripts'; +const rootDir = isInScriptsDir ? path.join(scriptDir, '..') : scriptDir; + +// --- Configuration --- +const config = { + dockerfilePath: path.join(rootDir, 'docker/images/n8n/Dockerfile'), + imageBaseName: process.env.IMAGE_BASE_NAME || 'n8n-local', + imageTag: process.env.IMAGE_TAG || 'dev', + buildContext: rootDir, + compiledAppDir: path.join(rootDir, 'compiled'), +}; + +config.fullImageName = `${config.imageBaseName}:${config.imageTag}`; + +// --- Check Prerequisites --- +echo(chalk.blue.bold('===== Docker Build for n8n =====')); +echo(`INFO: Image: ${config.fullImageName}`); +echo(chalk.gray('-----------------------------------------------')); + +// Check if compiled directory exists +if (!(await fs.pathExists(config.compiledAppDir))) { + echo(chalk.red(`Error: Compiled app directory not found at ${config.compiledAppDir}`)); + echo(chalk.yellow('Please run build-n8n.mjs first!')); + process.exit(1); +} + +// Check Docker +try { + await $`command -v docker`; +} catch { + echo(chalk.red('Error: Docker is not installed or not in PATH')); + process.exit(1); +} + +// --- Build Docker Image --- +const startTime = Date.now(); +echo(chalk.yellow('INFO: Building Docker image...')); + +try { + const buildOutput = await $`docker build \ + -t ${config.fullImageName} \ + -f ${config.dockerfilePath} \ + ${config.buildContext}`; + + echo(buildOutput.stdout); +} catch (error) { + echo(chalk.red(`ERROR: Docker build failed: ${error.stderr || error.message}`)); + process.exit(1); +} + +const buildTime = Math.floor((Date.now() - startTime) / 1000); + +// Get image size +let imageSize = 'Unknown'; +try { + const sizeOutput = await $`docker images ${config.fullImageName} --format "{{.Size}}"`; + imageSize = sizeOutput.stdout.trim(); +} catch (error) { + echo(chalk.yellow('Warning: Could not get image size')); +} + +// --- Summary --- +echo(''); +echo(chalk.green.bold('================ DOCKER BUILD COMPLETE ================')); +echo(chalk.green(`✅ Image built: ${config.fullImageName}`)); +echo(` Size: ${imageSize}`); +echo(` Build time: ${buildTime}s`); +echo(chalk.green.bold('====================================================')); + +// Exit with success +process.exit(0); diff --git a/scripts/scan-n8n-image.mjs b/scripts/scan-n8n-image.mjs new file mode 100755 index 0000000000..6d42307bcd --- /dev/null +++ b/scripts/scan-n8n-image.mjs @@ -0,0 +1,152 @@ +#!/usr/bin/env node +/** + * This script is used to scan the n8n docker image for vulnerabilities. + * It uses Trivy to scan the image. + */ + +import { $, echo, fs, chalk } from 'zx'; +import path from 'path'; + +$.verbose = false; +process.env.FORCE_COLOR = '1'; + +const scriptDir = path.dirname(new URL(import.meta.url).pathname); +const isInScriptsDir = path.basename(scriptDir) === 'scripts'; +const rootDir = isInScriptsDir ? path.join(scriptDir, '..') : scriptDir; + +// --- Configuration --- +const config = { + imageBaseName: process.env.IMAGE_BASE_NAME || 'n8n-local', + imageTag: process.env.IMAGE_TAG || 'dev', + trivyImage: process.env.TRIVY_IMAGE || 'aquasec/trivy:latest', + severity: process.env.TRIVY_SEVERITY || 'CRITICAL,HIGH,MEDIUM,LOW', + outputFormat: process.env.TRIVY_FORMAT || 'table', + outputFile: process.env.TRIVY_OUTPUT || null, + scanTimeout: process.env.TRIVY_TIMEOUT || '10m', + ignoreUnfixed: process.env.TRIVY_IGNORE_UNFIXED === 'true', + scanners: process.env.TRIVY_SCANNERS || 'vuln', + quiet: process.env.TRIVY_QUIET === 'true', + rootDir: rootDir, +}; + +config.fullImageName = `${config.imageBaseName}:${config.imageTag}`; + +const printHeader = (title) => + !config.quiet && echo(`\n${chalk.blue.bold(`===== ${title} =====`)}`); + +const printSummary = (status, time, message) => { + if (config.quiet) return; + + echo('\n' + chalk.blue.bold('===== Scan Summary =====')); + echo(status === 'success' ? chalk.green.bold(message) : chalk.yellow.bold(message)); + echo(chalk[status === 'success' ? 'green' : 'yellow'](` Scan time: ${time}s`)); + + if (config.outputFile) { + const resolvedPath = path.isAbsolute(config.outputFile) + ? config.outputFile + : path.join(config.rootDir, config.outputFile); + echo(chalk[status === 'success' ? 'green' : 'yellow'](` Report saved to: ${resolvedPath}`)); + } + + echo('\n' + chalk.gray('Scan Configuration:')); + echo(chalk.gray(` • Target Image: ${config.fullImageName}`)); + echo(chalk.gray(` • Severity Levels: ${config.severity}`)); + echo(chalk.gray(` • Scanners: ${config.scanners}`)); + if (config.ignoreUnfixed) echo(chalk.gray(` • Ignored unfixed: yes`)); + echo(chalk.blue.bold('========================')); +}; + +// --- Main Process --- +(async () => { + printHeader('Trivy Security Scan for n8n Image'); + + try { + await $`command -v docker`; + } catch { + echo(chalk.red('Error: Docker is not installed or not in PATH')); + process.exit(1); + } + + try { + await $`docker image inspect ${config.fullImageName} > /dev/null 2>&1`; + } catch { + echo(chalk.red(`Error: Docker image '${config.fullImageName}' not found`)); + echo(chalk.yellow('Please run dockerize-n8n.mjs first!')); + process.exit(1); + } + + // Pull latest Trivy image silently + try { + await $`docker pull ${config.trivyImage} > /dev/null 2>&1`; + } catch { + // Silent fallback to cached version + } + + // Build Trivy command + const trivyArgs = [ + 'run', + '--rm', + '-v', + '/var/run/docker.sock:/var/run/docker.sock', + config.trivyImage, + 'image', + '--severity', + config.severity, + '--format', + config.outputFormat, + '--timeout', + config.scanTimeout, + '--scanners', + config.scanners, + '--no-progress', + ]; + + if (config.ignoreUnfixed) trivyArgs.push('--ignore-unfixed'); + if (config.quiet && config.outputFormat === 'table') trivyArgs.push('--quiet'); + + // Handle output file - resolve relative to root directory + if (config.outputFile) { + const outputPath = path.isAbsolute(config.outputFile) + ? config.outputFile + : path.join(config.rootDir, config.outputFile); + await fs.ensureDir(path.dirname(outputPath)); + trivyArgs.push('--output', '/tmp/trivy-output', '-v', `${outputPath}:/tmp/trivy-output`); + } + + trivyArgs.push(config.fullImageName); + + // Run the scan + const startTime = Date.now(); + + try { + const result = await $`docker ${trivyArgs}`; + + // Print Trivy output first + if (!config.outputFile && result.stdout) { + echo(result.stdout); + } + + // Then print our summary + const scanTime = Math.floor((Date.now() - startTime) / 1000); + printSummary('success', scanTime, '✅ Security scan completed successfully'); + + process.exit(0); + } catch (error) { + const scanTime = Math.floor((Date.now() - startTime) / 1000); + + // Trivy returns exit code 1 when vulnerabilities are found + if (error.exitCode === 1) { + // Print Trivy output first + if (!config.outputFile && error.stdout) { + echo(error.stdout); + } + + // Then print our summary + printSummary('warning', scanTime, '⚠️ Vulnerabilities found!'); + process.exit(1); + } else { + echo(chalk.red(`❌ Scan failed: ${error.message}`)); + process.exit(error.exitCode || 1); + } + } +})();