diff --git a/.gitattributes b/.gitattributes index 37a53776999..abef4c7912e 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,3 @@ *.sh text eol=lf +*.patch text eol=lf docs/generated/**/* linguist-generated diff --git a/.githooks/check-pnpm.sh b/.githooks/check-pnpm.sh new file mode 100644 index 00000000000..f66821f0aa8 --- /dev/null +++ b/.githooks/check-pnpm.sh @@ -0,0 +1,13 @@ +#!/bin/sh +# .githooks/check-pnpm — symlink or call from post-merge, post-rewrite, post-checkout + +WANTED=$(sed -n 's/.*"packageManager": *"pnpm@\([0-9]*\).*/\1/p' package.json) +CURRENT=$(cd ~ && pnpm --version 2>/dev/null | cut -d. -f1) +[ -z "$WANTED" ] || [ "$WANTED" = "$CURRENT" ] && exit 0 +echo "" +echo "⚠ This repo now pins pnpm $WANTED (you have ${CURRENT:-none})." +echo " pnpm should auto-download it on the next install command." +echo " If that fails (some pnpm 10 versions can't install v12), reinstall:" +echo " curl -fsSL https://get.pnpm.io/install.sh | sh -" +echo "" +exit 0 diff --git a/.github/scripts/docker/should-smoke-build.mjs b/.github/scripts/docker/should-smoke-build.mjs index f74204f6605..84998656006 100644 --- a/.github/scripts/docker/should-smoke-build.mjs +++ b/.github/scripts/docker/should-smoke-build.mjs @@ -14,6 +14,7 @@ import { appendFileSync, readFileSync } from 'node:fs'; import { pathToFileURL } from 'node:url'; const WORKSPACE_FILE = 'pnpm-workspace.yaml'; +const ROOT_MANIFEST = 'package.json'; const BUMP_ONLY = [/^pnpm-workspace\.yaml$/, /^pnpm-lock\.yaml$/, /(^|\/)package\.json$/]; @@ -69,6 +70,14 @@ export function nativePinsChanged(baseText, headText) { return { changed: false, reason: 'no buildable dependency pin changed' }; } +export function packageManagerChanged(baseText, headText) { + try { + return JSON.parse(baseText).packageManager !== JSON.parse(headText).packageManager; + } catch { + return true; + } +} + function fileAt(ref, path) { try { return execFileSync('git', ['show', `${ref}:${path}`], { encoding: 'utf8' }); @@ -88,12 +97,25 @@ function main(argv) { let reason = 'a Docker-chain file changed'; if (isBumpOnly(files)) { - const baseText = fileAt(base, WORKSPACE_FILE); - const headText = fileAt('HEAD', WORKSPACE_FILE); - if (baseText === null || headText === null) { - reason = `could not read ${WORKSPACE_FILE} on both sides of the diff`; + const baseManifest = fileAt(base, ROOT_MANIFEST); + const headManifest = fileAt('HEAD', ROOT_MANIFEST); + if (files.includes(ROOT_MANIFEST) && (baseManifest === null || headManifest === null)) { + reason = `could not read ${ROOT_MANIFEST} on both sides of the diff`; + } else if ( + files.includes(ROOT_MANIFEST) && + baseManifest !== null && + headManifest !== null && + packageManagerChanged(baseManifest, headManifest) + ) { + reason = 'the package manager pin changed'; } else { - ({ changed: build, reason } = nativePinsChanged(baseText, headText)); + const baseWorkspace = fileAt(base, WORKSPACE_FILE); + const headWorkspace = fileAt('HEAD', WORKSPACE_FILE); + if (baseWorkspace === null || headWorkspace === null) { + reason = `could not read ${WORKSPACE_FILE} on both sides of the diff`; + } else { + ({ changed: build, reason } = nativePinsChanged(baseWorkspace, headWorkspace)); + } } } diff --git a/.github/scripts/docker/should-smoke-build.test.mjs b/.github/scripts/docker/should-smoke-build.test.mjs index 7db7e7743ab..dd42586e085 100644 --- a/.github/scripts/docker/should-smoke-build.test.mjs +++ b/.github/scripts/docker/should-smoke-build.test.mjs @@ -1,7 +1,12 @@ import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; -import { buildableDeps, isBumpOnly, nativePinsChanged } from './should-smoke-build.mjs'; +import { + buildableDeps, + isBumpOnly, + nativePinsChanged, + packageManagerChanged, +} from './should-smoke-build.mjs'; const workspace = ({ kafka = '1.9.1', vm = '^7.0.0', sentry = '10.70.0', allowVm = true } = {}) => ` catalog: @@ -86,3 +91,25 @@ describe('nativePinsChanged', () => { assert.equal(nativePinsChanged(workspace(), workspace()).changed, false); }); }); + +describe('packageManagerChanged', () => { + it('detects a pnpm version change', () => { + assert.equal( + packageManagerChanged( + JSON.stringify({ packageManager: 'pnpm@11.25.0' }), + JSON.stringify({ packageManager: 'pnpm@12.3.4' }), + ), + true, + ); + }); + + it('ignores unrelated manifest changes', () => { + assert.equal( + packageManagerChanged( + JSON.stringify({ packageManager: 'pnpm@12.3.4', version: '1.0.0' }), + JSON.stringify({ packageManager: 'pnpm@12.3.4', version: '1.0.1' }), + ), + false, + ); + }); +}); diff --git a/.github/scripts/package.json b/.github/scripts/package.json index 7f086554ab5..8186cb937fd 100644 --- a/.github/scripts/package.json +++ b/.github/scripts/package.json @@ -2,9 +2,9 @@ "name": "workflow-scripts", "engines": { "node": ">=24.0.0", - "pnpm": ">=11.25.0" + "pnpm": ">=12.3.4" }, - "packageManager": "pnpm@11.25.0", + "packageManager": "pnpm@12.3.4", "scripts": { "test": "node --test --experimental-test-module-mocks ./*.test.mjs ./docker/*.test.mjs ./owners/*.test.mjs ./quality/*.test.mjs ./slack/*.test.mjs ./stale/*.test.mjs ../../scripts/*.test.mjs ../../scripts/licenses/*.test.mjs ../../scripts/mutation-health/*.test.mjs ../../.devcontainer/codespaces/*.test.mjs", "generate-sbom": "FETCH_LICENSE=true cdxgen -t pnpm --no-install-deps --profile license-compliance --spec-version 1.6 -o ../../sbom-source.cdx.json ../../compiled/", diff --git a/.github/scripts/pnpm-lock.yaml b/.github/scripts/pnpm-lock.yaml index 66a19161ae5..efb76eff205 100644 --- a/.github/scripts/pnpm-lock.yaml +++ b/.github/scripts/pnpm-lock.yaml @@ -1,3 +1,104 @@ +--- +lockfileVersion: '9.0' + +importers: + + .: + configDependencies: {} + packageManagerDependencies: + pnpm: + specifier: 12.3.4 + version: 12.3.4 + +packages: + + '@pnpm/exe.darwin-arm64@12.3.4': + resolution: {integrity: sha512-PAyUol8T1+/+ViOiXAt51ECA+QnfXCqz6foL4bW+LsoX0NcVd5XVEM2mRQu+LV4oc7uRz9zf9U0P+XFfuQeDAw==} + cpu: [arm64] + os: [darwin] + + '@pnpm/exe.darwin-x64@12.3.4': + resolution: {integrity: sha512-fxP9JCk0Cdye+ePuj+GJJLMUMTqHGWRdb1dtv4How876uQ2ehxvenpgiYAir/ceO9PsYUZkFTtyZdx+rRu5QOA==} + cpu: [x64] + os: [darwin] + + '@pnpm/exe.linux-arm64-musl@12.3.4': + resolution: {integrity: sha512-FBOt0/7ye6O6q4AllVV5QMviB6qE6fqkeczV/+MDWQsmo+QJrlfsh6X7CpH/tClVpBZEyIbjpUoT8bNhCYBxEg==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@pnpm/exe.linux-arm64@12.3.4': + resolution: {integrity: sha512-t71AVA7LRqiKTyZ5xMYaZc2n5DfdpMbfokZuiIOXHBOM03ECnF0t4iYwaBDqJgVjlKYUOwaF/bRQajGNA4cJ4w==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@pnpm/exe.linux-x64-musl@12.3.4': + resolution: {integrity: sha512-RPmk7Jb/aYaFvL2iyDN/AtMY+hUEsue732WmXpcuQ9tBpMnGyA5py7Z3+e+qmQaJ0zY/4ni9jJiyPBQHujmv6w==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@pnpm/exe.linux-x64@12.3.4': + resolution: {integrity: sha512-2ZqOlSPkfwX1h5cR+FPiWf8+F+2hZT/3TvhUK5sigHqwaQCIiq8R7CGxhndKs63JtcLi2a1Qpo+wX/EoyfjyJQ==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@pnpm/exe.win32-arm64@12.3.4': + resolution: {integrity: sha512-ANyrHqyqco6SXBysUTRF74itDyyraea7IbFsKFdNXTjcFnfycTDx37EwuhdpPYFNSIh2JhUG4fByclsRfiHX7w==} + cpu: [arm64] + os: [win32] + + '@pnpm/exe.win32-x64@12.3.4': + resolution: {integrity: sha512-WH/KqBPY/hq2Tb7SgQltEZytimcjgKRaCRL/aM9CI0c67iKc5TVmHUhIiL3Ux9FB4bWn36i6XewUcScQI+zG8w==} + cpu: [x64] + os: [win32] + + pnpm@12.3.4: + resolution: {integrity: sha512-lhqkH7B32joEpEHZ+OFevAyW2o73ELLrZ7+e58sGEOq9SPH9hfUc/+c4RnhfoPh8VqOocqHYk/hEZ0G1zORUVw==} + engines: {node: '>=18.*'} + hasBin: true + +snapshots: + + '@pnpm/exe.darwin-arm64@12.3.4': + optional: true + + '@pnpm/exe.darwin-x64@12.3.4': + optional: true + + '@pnpm/exe.linux-arm64-musl@12.3.4': + optional: true + + '@pnpm/exe.linux-arm64@12.3.4': + optional: true + + '@pnpm/exe.linux-x64-musl@12.3.4': + optional: true + + '@pnpm/exe.linux-x64@12.3.4': + optional: true + + '@pnpm/exe.win32-arm64@12.3.4': + optional: true + + '@pnpm/exe.win32-x64@12.3.4': + optional: true + + pnpm@12.3.4: + optionalDependencies: + '@pnpm/exe.darwin-arm64': 12.3.4 + '@pnpm/exe.darwin-x64': 12.3.4 + '@pnpm/exe.linux-arm64': 12.3.4 + '@pnpm/exe.linux-arm64-musl': 12.3.4 + '@pnpm/exe.linux-x64': 12.3.4 + '@pnpm/exe.linux-x64-musl': 12.3.4 + '@pnpm/exe.win32-arm64': 12.3.4 + '@pnpm/exe.win32-x64': 12.3.4 + +--- lockfileVersion: '9.0' settings: diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml index b5abbccc788..1ad1abbd850 100644 --- a/.github/workflows/backport.yml +++ b/.github/workflows/backport.yml @@ -46,7 +46,7 @@ jobs: uses: ./.github/actions/setup-nodejs with: build-command: '' - install-command: pnpm install --frozen-lockfile --dir ./.github/scripts --ignore-workspace + install-command: pnpm install --frozen-lockfile --dir ./.github/scripts --lockfile-dir . --ignore-workspace cache-dependency-path: .github/scripts/pnpm-lock.yaml - name: Compute backport targets diff --git a/.github/workflows/ci-detect-new-packages.yml b/.github/workflows/ci-detect-new-packages.yml index 837888ac59c..f7a8792dff8 100644 --- a/.github/workflows/ci-detect-new-packages.yml +++ b/.github/workflows/ci-detect-new-packages.yml @@ -20,7 +20,7 @@ jobs: uses: ./.github/actions/setup-nodejs with: build-command: '' - install-command: pnpm install --frozen-lockfile --dir ./.github/scripts --ignore-workspace + install-command: pnpm install --frozen-lockfile --dir ./.github/scripts --lockfile-dir . --ignore-workspace cache-dependency-path: .github/scripts/pnpm-lock.yaml - name: Check for new unpublished packages diff --git a/.github/workflows/ci-owners-assign-reviewers.yml b/.github/workflows/ci-owners-assign-reviewers.yml index 5465ca6e656..906657996da 100644 --- a/.github/workflows/ci-owners-assign-reviewers.yml +++ b/.github/workflows/ci-owners-assign-reviewers.yml @@ -54,7 +54,8 @@ jobs: uses: ./.github/actions/setup-nodejs with: build-command: '' - install-command: pnpm install --frozen-lockfile --dir ./.github/scripts --ignore-workspace + install-command: pnpm install --frozen-lockfile --dir ./.github/scripts --lockfile-dir . --ignore-workspace + cache-dependency-path: .github/scripts/pnpm-lock.yaml - name: Assign reviewer teams env: diff --git a/.github/workflows/ci-owners-required-reviews.yml b/.github/workflows/ci-owners-required-reviews.yml index 79d021db4fc..4ec8cf6377c 100644 --- a/.github/workflows/ci-owners-required-reviews.yml +++ b/.github/workflows/ci-owners-required-reviews.yml @@ -143,7 +143,10 @@ jobs: uses: ./.github/actions/setup-nodejs with: build-command: '' - install-command: pnpm install --frozen-lockfile --dir ./.github/scripts --ignore-workspace + # pnpm 11 and 12 resolve a relative lockfile directory from different locations. + # Use the checkout path because this workflow can run new workflow code against the old base. + install-command: pnpm install --frozen-lockfile --dir ./.github/scripts --lockfile-dir ${{ github.workspace }}/.github/scripts --ignore-workspace + cache-dependency-path: .github/scripts/pnpm-lock.yaml - name: Evaluate required reviews env: diff --git a/.github/workflows/ci-owners-review-recommendations.yml b/.github/workflows/ci-owners-review-recommendations.yml index 7b58f8ab78d..b51f013cbd9 100644 --- a/.github/workflows/ci-owners-review-recommendations.yml +++ b/.github/workflows/ci-owners-review-recommendations.yml @@ -45,7 +45,8 @@ jobs: uses: ./.github/actions/setup-nodejs with: build-command: '' - install-command: pnpm install --frozen-lockfile --dir ./.github/scripts --ignore-workspace + install-command: pnpm install --frozen-lockfile --dir ./.github/scripts --lockfile-dir . --ignore-workspace + cache-dependency-path: .github/scripts/pnpm-lock.yaml - name: Post review recommendations env: diff --git a/.github/workflows/ci-pr-quality.yml b/.github/workflows/ci-pr-quality.yml index 447efd83125..c408aa51dab 100644 --- a/.github/workflows/ci-pr-quality.yml +++ b/.github/workflows/ci-pr-quality.yml @@ -36,7 +36,7 @@ jobs: uses: ./.github/actions/setup-nodejs with: build-command: '' - install-command: pnpm install --frozen-lockfile --dir ./.github/scripts --ignore-workspace + install-command: pnpm install --frozen-lockfile --dir ./.github/scripts --lockfile-dir . --ignore-workspace cache-dependency-path: .github/scripts/pnpm-lock.yaml - name: Re-request PR Size Limit check @@ -72,7 +72,7 @@ jobs: uses: ./.github/actions/setup-nodejs with: build-command: '' - install-command: pnpm install --frozen-lockfile --dir ./.github/scripts --ignore-workspace + install-command: pnpm install --frozen-lockfile --dir ./.github/scripts --lockfile-dir . --ignore-workspace cache-dependency-path: .github/scripts/pnpm-lock.yaml - name: Check PR size diff --git a/.github/workflows/clean-stale-branches.yml b/.github/workflows/clean-stale-branches.yml index 6e4d8167a9f..a579400380e 100644 --- a/.github/workflows/clean-stale-branches.yml +++ b/.github/workflows/clean-stale-branches.yml @@ -35,7 +35,7 @@ jobs: uses: ./.github/actions/setup-nodejs with: build-command: '' - install-command: pnpm install --frozen-lockfile --dir ./.github/scripts --ignore-workspace + install-command: pnpm install --frozen-lockfile --dir ./.github/scripts --lockfile-dir . --ignore-workspace cache-dependency-path: .github/scripts/pnpm-lock.yaml - name: Clean stale branches diff --git a/.github/workflows/docker-build-smoke.yml b/.github/workflows/docker-build-smoke.yml index eb6b0c51b3f..f5269d40269 100644 --- a/.github/workflows/docker-build-smoke.yml +++ b/.github/workflows/docker-build-smoke.yml @@ -31,12 +31,14 @@ on: - 'scripts/build-n8n.mjs' - 'scripts/dockerize-n8n.mjs' - 'scripts/smoke-n8n-image.mjs' + - 'package.json' - '.github/scripts/docker/kafka-native-smoke-check.mjs' - '.github/scripts/docker/should-smoke-build.mjs' # These drive the build and the image distribution, so a change here can # break the chain without touching a Dockerfile. - '.github/actions/build-n8n-docker/action.yml' - '.github/actions/load-n8n-docker/action.yml' + - '.github/actions/setup-nodejs/action.yml' - '.github/workflows/docker-build-push.yml' - 'patches/**' # Pins the native deps this build compiles, but changes far more often diff --git a/.github/workflows/release-create-github-releases.yml b/.github/workflows/release-create-github-releases.yml index 263943db483..8a64b29f702 100644 --- a/.github/workflows/release-create-github-releases.yml +++ b/.github/workflows/release-create-github-releases.yml @@ -71,7 +71,7 @@ jobs: uses: ./.github/actions/setup-nodejs with: build-command: '' - install-command: pnpm install --frozen-lockfile --dir ./.github/scripts --ignore-workspace + install-command: pnpm install --frozen-lockfile --dir ./.github/scripts --lockfile-dir . --ignore-workspace cache-dependency-path: .github/scripts/pnpm-lock.yaml - name: Create GitHub releases diff --git a/.github/workflows/release-create-patch-pr.yml b/.github/workflows/release-create-patch-pr.yml index 27b4881422c..d8f063e0b52 100644 --- a/.github/workflows/release-create-patch-pr.yml +++ b/.github/workflows/release-create-patch-pr.yml @@ -44,7 +44,7 @@ jobs: uses: ./.github/actions/setup-nodejs with: build-command: '' - install-command: pnpm install --frozen-lockfile --dir ./.github/scripts --ignore-workspace + install-command: pnpm install --frozen-lockfile --dir ./.github/scripts --lockfile-dir . --ignore-workspace cache-dependency-path: .github/scripts/pnpm-lock.yaml - name: Determine release candidate branch from track diff --git a/.github/workflows/release-create-pr.yml b/.github/workflows/release-create-pr.yml index 4bd1ede5643..8901b1658d9 100644 --- a/.github/workflows/release-create-pr.yml +++ b/.github/workflows/release-create-pr.yml @@ -78,7 +78,7 @@ jobs: uses: ./.github/actions/setup-nodejs with: build-command: '' - install-command: pnpm install --frozen-lockfile --dir ./.github/scripts --ignore-workspace + install-command: pnpm install --frozen-lockfile --dir ./.github/scripts --lockfile-dir . --ignore-workspace cache-dependency-path: .github/scripts/pnpm-lock.yaml - name: Bump package versions diff --git a/.github/workflows/release-populate-cloud-with-releases.yml b/.github/workflows/release-populate-cloud-with-releases.yml index 5a857950f88..df092950fd1 100644 --- a/.github/workflows/release-populate-cloud-with-releases.yml +++ b/.github/workflows/release-populate-cloud-with-releases.yml @@ -48,7 +48,7 @@ jobs: uses: ./.github/actions/setup-nodejs with: build-command: '' - install-command: pnpm install --frozen-lockfile --dir ./.github/scripts --ignore-workspace + install-command: pnpm install --frozen-lockfile --dir ./.github/scripts --lockfile-dir . --ignore-workspace cache-dependency-path: .github/scripts/pnpm-lock.yaml - name: Extract changes diff --git a/.github/workflows/release-promote-backport-labels.yml b/.github/workflows/release-promote-backport-labels.yml index 3904a90640c..31c0e385596 100644 --- a/.github/workflows/release-promote-backport-labels.yml +++ b/.github/workflows/release-promote-backport-labels.yml @@ -52,7 +52,7 @@ jobs: uses: ./.github/actions/setup-nodejs with: build-command: '' - install-command: pnpm install --frozen-lockfile --dir ./.github/scripts --ignore-workspace + install-command: pnpm install --frozen-lockfile --dir ./.github/scripts --lockfile-dir . --ignore-workspace cache-dependency-path: .github/scripts/pnpm-lock.yaml - name: Reconcile backport labels diff --git a/.github/workflows/release-promote-github-release.yml b/.github/workflows/release-promote-github-release.yml index debb9373c70..4187688166b 100644 --- a/.github/workflows/release-promote-github-release.yml +++ b/.github/workflows/release-promote-github-release.yml @@ -42,7 +42,7 @@ jobs: uses: ./.github/actions/setup-nodejs with: build-command: '' - install-command: pnpm install --frozen-lockfile --dir ./.github/scripts --ignore-workspace + install-command: pnpm install --frozen-lockfile --dir ./.github/scripts --lockfile-dir . --ignore-workspace cache-dependency-path: .github/scripts/pnpm-lock.yaml - name: Promote GitHub releases diff --git a/.github/workflows/release-publish.yml b/.github/workflows/release-publish.yml index ea51fc0b274..ea6b78ec775 100644 --- a/.github/workflows/release-publish.yml +++ b/.github/workflows/release-publish.yml @@ -31,7 +31,7 @@ jobs: uses: ./.github/actions/setup-nodejs with: build-command: '' - install-command: pnpm install --frozen-lockfile --dir ./.github/scripts --ignore-workspace + install-command: pnpm install --frozen-lockfile --dir ./.github/scripts --lockfile-dir . --ignore-workspace cache-dependency-path: .github/scripts/pnpm-lock.yaml - name: Determine track from package version number @@ -61,7 +61,7 @@ jobs: SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - name: Install script dependencies - run: pnpm install --frozen-lockfile --dir ./.github/scripts --ignore-workspace + run: pnpm install --frozen-lockfile --dir ./.github/scripts --lockfile-dir . --ignore-workspace - name: Check for new unpublished packages run: node .github/scripts/detect-new-packages.mjs diff --git a/.github/workflows/release-recreate-failed-release.yml b/.github/workflows/release-recreate-failed-release.yml index 7ce47967fcb..c728aca20a1 100644 --- a/.github/workflows/release-recreate-failed-release.yml +++ b/.github/workflows/release-recreate-failed-release.yml @@ -88,7 +88,7 @@ jobs: uses: ./.github/actions/setup-nodejs with: build-command: '' - install-command: pnpm install --frozen-lockfile --dir ./.github/scripts --ignore-workspace + install-command: pnpm install --frozen-lockfile --dir ./.github/scripts --lockfile-dir . --ignore-workspace cache-dependency-path: .github/scripts/pnpm-lock.yaml - name: Bump the root and cli versions diff --git a/.github/workflows/release-set-stable-npm-packages-to-latest.yml b/.github/workflows/release-set-stable-npm-packages-to-latest.yml index 60f7004c164..c6c7946fa9c 100644 --- a/.github/workflows/release-set-stable-npm-packages-to-latest.yml +++ b/.github/workflows/release-set-stable-npm-packages-to-latest.yml @@ -25,7 +25,7 @@ jobs: uses: ./.github/actions/setup-nodejs with: build-command: '' - install-command: pnpm install --frozen-lockfile --dir ./.github/scripts --ignore-workspace + install-command: pnpm install --frozen-lockfile --dir ./.github/scripts --lockfile-dir . --ignore-workspace cache-dependency-path: .github/scripts/pnpm-lock.yaml - name: Set npm packages to latest diff --git a/.github/workflows/release-update-pointer-tag.yml b/.github/workflows/release-update-pointer-tag.yml index 67654ecb165..99041692ee4 100644 --- a/.github/workflows/release-update-pointer-tag.yml +++ b/.github/workflows/release-update-pointer-tag.yml @@ -53,7 +53,7 @@ jobs: uses: ./.github/actions/setup-nodejs with: build-command: '' - install-command: pnpm install --frozen-lockfile --dir ./.github/scripts --ignore-workspace + install-command: pnpm install --frozen-lockfile --dir ./.github/scripts --lockfile-dir . --ignore-workspace cache-dependency-path: .github/scripts/pnpm-lock.yaml - name: Configure git author diff --git a/.github/workflows/release-version-release-notification.yml b/.github/workflows/release-version-release-notification.yml index 0fa4d48a3c0..16b4bab9f83 100644 --- a/.github/workflows/release-version-release-notification.yml +++ b/.github/workflows/release-version-release-notification.yml @@ -37,7 +37,7 @@ jobs: uses: ./.github/actions/setup-nodejs with: build-command: '' - install-command: pnpm install --frozen-lockfile --dir ./.github/scripts --ignore-workspace + install-command: pnpm install --frozen-lockfile --dir ./.github/scripts --lockfile-dir . --ignore-workspace cache-dependency-path: .github/scripts/pnpm-lock.yaml - name: Send release notification diff --git a/.github/workflows/test-workflow-scripts-reusable.yml b/.github/workflows/test-workflow-scripts-reusable.yml index 3614e2bc3d3..bdc0b40d0fb 100644 --- a/.github/workflows/test-workflow-scripts-reusable.yml +++ b/.github/workflows/test-workflow-scripts-reusable.yml @@ -30,7 +30,7 @@ jobs: uses: ./.github/actions/setup-nodejs with: build-command: '' - install-command: pnpm install --frozen-lockfile --dir ./.github/scripts --ignore-workspace + install-command: pnpm install --frozen-lockfile --dir ./.github/scripts --lockfile-dir . --ignore-workspace cache-dependency-path: .github/scripts/pnpm-lock.yaml - name: Run tests diff --git a/.github/workflows/util-cleanup-abandoned-release-branches.yml b/.github/workflows/util-cleanup-abandoned-release-branches.yml index 867edf26ee8..0014fb572cf 100644 --- a/.github/workflows/util-cleanup-abandoned-release-branches.yml +++ b/.github/workflows/util-cleanup-abandoned-release-branches.yml @@ -30,7 +30,7 @@ jobs: uses: ./.github/actions/setup-nodejs with: build-command: '' - install-command: pnpm install --frozen-lockfile --dir ./.github/scripts --ignore-workspace + install-command: pnpm install --frozen-lockfile --dir ./.github/scripts --lockfile-dir . --ignore-workspace cache-dependency-path: .github/scripts/pnpm-lock.yaml - name: Cleanup release branch if PR qualifies diff --git a/.github/workflows/util-codespace-preview.yml b/.github/workflows/util-codespace-preview.yml index f0cad28efc4..356e37d38fc 100644 --- a/.github/workflows/util-codespace-preview.yml +++ b/.github/workflows/util-codespace-preview.yml @@ -68,7 +68,7 @@ jobs: uses: ./.github/actions/setup-nodejs with: build-command: '' - install-command: pnpm install --frozen-lockfile --dir ./.github/scripts --ignore-workspace + install-command: pnpm install --frozen-lockfile --dir ./.github/scripts --lockfile-dir . --ignore-workspace cache-dependency-path: .github/scripts/pnpm-lock.yaml - name: Run the preview operation diff --git a/.github/workflows/util-determine-current-version.yml b/.github/workflows/util-determine-current-version.yml index f5bbdfdd72f..f4025446c61 100644 --- a/.github/workflows/util-determine-current-version.yml +++ b/.github/workflows/util-determine-current-version.yml @@ -33,7 +33,7 @@ jobs: uses: ./.github/actions/setup-nodejs with: build-command: '' - install-command: pnpm install --frozen-lockfile --dir ./.github/scripts --ignore-workspace + install-command: pnpm install --frozen-lockfile --dir ./.github/scripts --lockfile-dir . --ignore-workspace cache-dependency-path: .github/scripts/pnpm-lock.yaml - name: Extract release versions diff --git a/.github/workflows/util-ensure-release-candidate-branches.yml b/.github/workflows/util-ensure-release-candidate-branches.yml index cc07fa8f6d3..72f62500cb6 100644 --- a/.github/workflows/util-ensure-release-candidate-branches.yml +++ b/.github/workflows/util-ensure-release-candidate-branches.yml @@ -28,7 +28,7 @@ jobs: uses: ./.github/actions/setup-nodejs with: build-command: '' - install-command: pnpm install --frozen-lockfile --dir ./.github/scripts --ignore-workspace + install-command: pnpm install --frozen-lockfile --dir ./.github/scripts --lockfile-dir . --ignore-workspace cache-dependency-path: .github/scripts/pnpm-lock.yaml - name: Ensure release-candidate branches diff --git a/.github/workflows/util-refresh-cubic-schema.yml b/.github/workflows/util-refresh-cubic-schema.yml index 146e2c4ac13..73c20258655 100644 --- a/.github/workflows/util-refresh-cubic-schema.yml +++ b/.github/workflows/util-refresh-cubic-schema.yml @@ -44,7 +44,7 @@ jobs: uses: ./.github/actions/setup-nodejs with: build-command: '' - install-command: pnpm install --frozen-lockfile --dir ./.github/scripts --ignore-workspace + install-command: pnpm install --frozen-lockfile --dir ./.github/scripts --lockfile-dir . --ignore-workspace cache-dependency-path: .github/scripts/pnpm-lock.yaml # Refreshes only. Validation runs on the PR this opens, so a schema change diff --git a/.github/workflows/util-sync-master-to-3x.yml b/.github/workflows/util-sync-master-to-3x.yml index 87fda1744a9..68bffe8eaa5 100644 --- a/.github/workflows/util-sync-master-to-3x.yml +++ b/.github/workflows/util-sync-master-to-3x.yml @@ -73,7 +73,7 @@ jobs: - name: Setup Node.js uses: ./.github/actions/setup-nodejs with: - install-command: pnpm install --frozen-lockfile --dir ./.github/scripts --ignore-workspace + install-command: pnpm install --frozen-lockfile --dir ./.github/scripts --lockfile-dir . --ignore-workspace cache-dependency-path: .github/scripts/pnpm-lock.yaml build-command: '' diff --git a/.npmrc b/.npmrc deleted file mode 100644 index 363598b14b5..00000000000 --- a/.npmrc +++ /dev/null @@ -1 +0,0 @@ -# https://github.com/pnpm/pnpm/issues/7024 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 057114f6775..92be87f69da 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -82,7 +82,7 @@ If you already have VS Code and Docker installed, you can click [here](https://v #### pnpm -[pnpm](https://pnpm.io/) version 10.22 or newer is required for development purposes. We recommend installing it with [corepack](#corepack). +[pnpm](https://pnpm.io/) version 12.3.4 or newer is required for development. We recommend that you install it with [Corepack](#corepack). ##### pnpm workspaces @@ -96,7 +96,7 @@ We recommend enabling [Node.js corepack](https://nodejs.org/docs/latest-v16.x/ap ```bash corepack enable -corepack prepare pnpm@10.22.0 --activate +corepack prepare pnpm@12.3.4 --activate ``` **IMPORTANT**: If you have installed Node.js via homebrew, you'll need to run `brew install corepack`, since homebrew explicitly removes `npm` and `corepack` from [the `node` formula](https://github.com/Homebrew/homebrew-core/blob/master/Formula/node.rb#L66). diff --git a/docker/images/runners/Dockerfile b/docker/images/runners/Dockerfile index 5790bac8bb7..f455f9922c4 100644 --- a/docker/images/runners/Dockerfile +++ b/docker/images/runners/Dockerfile @@ -1,5 +1,5 @@ ARG NODE_VERSION=26.7.0 -ARG PNPM_VERSION=11.25.0 +ARG PNPM_VERSION=12.3.4 ARG PYTHON_VERSION=3.13 # ============================================================================== @@ -30,14 +30,14 @@ RUN node -e "const pkg = require('./package.json'); \ delete pkg.devDependencies; \ require('fs').writeFileSync('./package.json', JSON.stringify(pkg, null, 2));" -# `--config.minimum-release-age=0`: pnpm 11 defaults it to 1440 (24h), which would make +# `--config.minimum-release-age=0`: pnpm defaults it to 1440 (24h), which would make # this reject any dependency published less than a day ago — e.g. a first-party # @n8n_io/ai-assistant-sdk bump landed within hours of its npm publish. This step only # re-resolves an already-vetted, already-pinned dependency set; the real supply-chain age # policy is enforced by the host install (pnpm-workspace.yaml + SafeChain). # Install moment (special case for backwards compatibility) RUN rm -f node_modules/.modules.yaml && \ - pnpm add moment@2.30.1 --prod --no-lockfile --config.minimum-release-age=0 + pnpm add moment@2.30.1 --save-prod --no-lockfile --config.minimum-release-age=0 # ============================================================================== # STAGE 2: Python runner build (@n8n/task-runner-python) with uv @@ -139,12 +139,9 @@ RUN apk add --no-cache ca-certificates tini libstdc++ libc6-compat && \ pip install --no-cache-dir "pip>=26.0" && \ apk del apk-tools -# Bring pnpm over, to make the image easier to extend. Node no longer bundles -# corepack, and this runtime has no npm, so pnpm is copied in as a package. -# Link the .mjs entry point: the package ships bin/pnpm.cjs without the execute -# bit, so a link to it fails with "Permission denied". -COPY --from=node-alpine /usr/local/lib/node_modules/pnpm /usr/local/lib/node_modules/pnpm -RUN ln -s ../lib/node_modules/pnpm/bin/pnpm.mjs /usr/local/bin/pnpm +# Bring pnpm over, to make the image easier to extend. pnpm 12 installs a +# standalone native executable, so the runtime does not need npm or Corepack. +COPY --from=node-alpine /usr/local/lib/node_modules/pnpm/pnpm /usr/local/bin/pnpm RUN addgroup -g 1000 -S runner \ && adduser -u 1000 -S -G runner -h /home/runner -D runner diff --git a/docker/images/runners/Dockerfile.distroless b/docker/images/runners/Dockerfile.distroless index 6fec659658e..ea92df6cb4f 100644 --- a/docker/images/runners/Dockerfile.distroless +++ b/docker/images/runners/Dockerfile.distroless @@ -13,7 +13,7 @@ # ============================================================================== ARG NODE_VERSION=26.7.0 -ARG PNPM_VERSION=11.25.0 +ARG PNPM_VERSION=12.3.4 ARG PYTHON_VERSION=3.13 @@ -45,14 +45,14 @@ RUN node -e "const pkg = require('./package.json'); \ delete pkg.devDependencies; \ require('fs').writeFileSync('./package.json', JSON.stringify(pkg, null, 2));" -# `--config.minimum-release-age=0`: pnpm 11 defaults it to 1440 (24h), which would make +# `--config.minimum-release-age=0`: pnpm defaults it to 1440 (24h), which would make # this reject any dependency published less than a day ago — e.g. a first-party # @n8n_io/ai-assistant-sdk bump landed within hours of its npm publish. This step only # re-resolves an already-vetted, already-pinned dependency set; the real supply-chain age # policy is enforced by the host install (pnpm-workspace.yaml + SafeChain). # Install moment by default (special case for n8n cloud) RUN rm -f node_modules/.modules.yaml && \ - pnpm add moment@2.30.1 --prod --no-lockfile --config.minimum-release-age=0 + pnpm add moment@2.30.1 --save-prod --no-lockfile --config.minimum-release-age=0 # Rebuild isolated-vm for the container platform. Install build tools as # fallback in case prebuild-install cannot find a prebuilt binary. diff --git a/lefthook.yml b/lefthook.yml index e8848ae0e45..f0c87b88cc8 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -1,3 +1,23 @@ +output: + - execution_out + - failure + +post-merge: + commands: + check-pnpm: + run: sh .githooks/check-pnpm.sh + +post-rewrite: + commands: + check-pnpm: + run: sh .githooks/check-pnpm.sh + +post-checkout: + commands: + check-pnpm: + # $3 is 1 for a branch checkout, 0 for a file checkout — skip the latter + run: '[ "{3}" = "0" ] || sh .githooks/check-pnpm.sh' + pre-commit: commands: biome_check: diff --git a/package.json b/package.json index 8d952f0f54d..8e5ef0e7ee9 100644 --- a/package.json +++ b/package.json @@ -4,9 +4,9 @@ "private": true, "engines": { "node": ">=24.0.0", - "pnpm": ">=11.25.0" + "pnpm": ">=12.3.4" }, - "packageManager": "pnpm@11.25.0", + "packageManager": "pnpm@12.3.4", "scripts": { "prepare": "node scripts/prepare.mjs", "preinstall": "node scripts/block-npm-install.js", @@ -88,7 +88,7 @@ "worker": "./packages/cli/bin/n8n worker", "dev:computer-use": "pnpm --filter @n8n/computer-use build && node packages/@n8n/computer-use/dist/cli.js serve", "stop:computer-use": "lsof -ti :7655 | xargs kill 2>/dev/null; echo 'computer-use stopped'", - "install-workflow-scripts": "pnpm install --dir .github/scripts --ignore-workspace" + "install-workflow-scripts": "pnpm install --dir .github/scripts --lockfile-dir . --ignore-workspace" }, "devDependencies": { "@babel/preset-env": "^7.26.0", diff --git a/packages/@n8n/engine/package.json b/packages/@n8n/engine/package.json index ea8de1d6268..b6a78eae253 100644 --- a/packages/@n8n/engine/package.json +++ b/packages/@n8n/engine/package.json @@ -7,7 +7,7 @@ "typecheck": "tsc --noEmit", "build": "tsc -p tsconfig.build.json", "build:unchecked": "tsc -p tsconfig.build.json --noCheck", - "build:docker": "pnpm build && rimraf compiled && DOCKER_BUILD=true NODE_ENV=production pnpm --filter . --prod --config.inject-workspace-packages=true deploy --no-optional compiled && rimraf compiled/pnpm-lock.yaml compiled/pnpm-workspace.yaml compiled/node_modules/.pnpm-workspace-state-v1.json && docker build -f ../../../docker/images/engine/Dockerfile -t n8n-engine:local .", + "build:docker": "pnpm build && rimraf compiled && DOCKER_BUILD=true NODE_ENV=production pnpm --filter . --prod --config.inject-workspace-packages=true --config.package-import-method=copy deploy --no-optional compiled && rimraf compiled/pnpm-lock.yaml compiled/pnpm-workspace.yaml compiled/node_modules/.pnpm-workspace-state-v1.json && docker build -f ../../../docker/images/engine/Dockerfile -t n8n-engine:local .", "start": "node dist/serve.js", "format": "biome format --write src", "format:check": "biome ci src", diff --git a/packages/@n8n/mcp-apps/package.json b/packages/@n8n/mcp-apps/package.json index d37b7b9a9bd..b8b3925d4f5 100644 --- a/packages/@n8n/mcp-apps/package.json +++ b/packages/@n8n/mcp-apps/package.json @@ -56,7 +56,7 @@ "rimraf": "catalog:", "semver": "catalog:", "@typescript/native": "catalog:typescript-tooling", - "typescript": "catalog:typescript-tooling", + "typescript": "catalog:", "unplugin-icons": "catalog:frontend", "vite": "catalog:", "vite-plugin-singlefile": "catalog:", diff --git a/packages/testing/code-health/src/utils/pnpm-lock-parser.test.ts b/packages/testing/code-health/src/utils/pnpm-lock-parser.test.ts new file mode 100644 index 00000000000..324020afe91 --- /dev/null +++ b/packages/testing/code-health/src/utils/pnpm-lock-parser.test.ts @@ -0,0 +1,66 @@ +import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; + +import { parsePnpmLock } from './pnpm-lock-parser.js'; + +const PROJECT_LOCKFILE = `lockfileVersion: '9.0' + +importers: + .: + dependencies: + example: + specifier: ^1.0.0 + version: 1.0.0 + +packages: + example@1.0.0: {} +`; + +describe('parsePnpmLock', () => { + let dir: string; + + beforeEach(() => { + dir = mkdtempSync(join(tmpdir(), 'pnpm-lock-parser-')); + }); + + afterEach(() => rmSync(dir, { recursive: true, force: true })); + + function writeLockfile(content: string) { + writeFileSync(join(dir, 'pnpm-lock.yaml'), content); + } + + it('parses a single-document lockfile', () => { + writeLockfile(PROJECT_LOCKFILE); + + const result = parsePnpmLock(dir); + + expect(result.resolvedVersions.get('example')).toEqual(new Set(['1.0.0'])); + expect(result.requestedRanges.get('example')).toEqual(new Set(['^1.0.0'])); + }); + + it('uses the project document from a pnpm 12 lockfile', () => { + writeLockfile(`--- +lockfileVersion: '9.0' + +importers: + .: + packageManagerDependencies: + pnpm: + specifier: 12.3.4 + version: 12.3.4 + +packages: + pnpm@12.3.4: {} + +--- +${PROJECT_LOCKFILE}`); + + const result = parsePnpmLock(dir); + + expect(result.resolvedVersions.get('example')).toEqual(new Set(['1.0.0'])); + expect(result.resolvedVersions.has('pnpm')).toBe(false); + expect(result.requestedRanges.get('example')).toEqual(new Set(['^1.0.0'])); + }); +}); diff --git a/packages/testing/code-health/src/utils/pnpm-lock-parser.ts b/packages/testing/code-health/src/utils/pnpm-lock-parser.ts index b72df43b60f..7d86cda05d5 100644 --- a/packages/testing/code-health/src/utils/pnpm-lock-parser.ts +++ b/packages/testing/code-health/src/utils/pnpm-lock-parser.ts @@ -1,6 +1,6 @@ import * as fs from 'node:fs'; import * as path from 'node:path'; -import { parse as parseYaml } from 'yaml'; +import { parseAllDocuments } from 'yaml'; export interface LockData { resolvedVersions: Map>; @@ -23,14 +23,23 @@ interface LockFile { packages?: Record; } +function parseProjectLockfile(content: string): LockFile | null { + const document = parseAllDocuments(content).at(-1); + if (!document) return null; + if (document.errors.length > 0) throw document.errors[0]; + + const value: unknown = document.toJS(); + return typeof value === 'object' && value !== null ? value : null; +} + export function parsePnpmLock(rootDir: string, lockFile = 'pnpm-lock.yaml'): LockData { const filePath = path.join(rootDir, lockFile); const empty: LockData = { resolvedVersions: new Map(), requestedRanges: new Map() }; if (!fs.existsSync(filePath)) return empty; const content = fs.readFileSync(filePath, 'utf-8'); - const lock = parseYaml(content) as LockFile | null; - if (!lock || typeof lock !== 'object') return empty; + const lock = parseProjectLockfile(content); + if (!lock) return empty; const resolvedVersions = new Map>(); const requestedRanges = new Map>(); diff --git a/packages/testing/janitor/src/core/read-lockfile-importers.test.ts b/packages/testing/janitor/src/core/read-lockfile-importers.test.ts new file mode 100644 index 00000000000..6cafebf7d4f --- /dev/null +++ b/packages/testing/janitor/src/core/read-lockfile-importers.test.ts @@ -0,0 +1,85 @@ +import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +const mocks = vi.hoisted(() => ({ getGitRoot: vi.fn<() => string>() })); + +vi.mock('../utils/git-operations.js', () => ({ getGitRoot: mocks.getGitRoot })); + +import { readLockfileImporters, readRuntimeClosure } from './read-lockfile-importers.js'; + +describe('lockfile readers', () => { + let dir: string; + + beforeEach(() => { + dir = mkdtempSync(join(tmpdir(), 'janitor-lockfile-')); + mocks.getGitRoot.mockReturnValue(dir); + }); + + afterEach(() => rmSync(dir, { recursive: true, force: true })); + + it('uses the project document from a pnpm 12 lockfile', () => { + writeFileSync( + join(dir, 'pnpm-lock.yaml'), + `--- +lockfileVersion: '9.0' + +importers: + .: + packageManagerDependencies: + pnpm: + specifier: 12.3.4 + version: 12.3.4 + +--- +lockfileVersion: '9.0' + +importers: + packages/cli: + dependencies: + external: + specifier: 1.0.0 + version: 1.0.0 + packages/unused: + devDependencies: + test-only: + specifier: 1.0.0 + version: 1.0.0 + +snapshots: + external@1.0.0: + dependencies: + transitive: 2.0.0 + transitive@2.0.0: {} +`, + ); + + expect(readLockfileImporters()).toEqual({ + 'packages/cli': ['external'], + 'packages/unused': [], + }); + expect(readRuntimeClosure()).toEqual(new Set(['external', 'transitive'])); + }); + + it('returns fallback empty results when any document has syntax errors', () => { + writeFileSync( + join(dir, 'pnpm-lock.yaml'), + `--- +lockfileVersion: '9.0' +malformed: [unclosed +--- +lockfileVersion: '9.0' +importers: + packages/cli: + dependencies: + external: + specifier: 1.0.0 + version: 1.0.0 +`, + ); + + expect(readLockfileImporters()).toEqual({}); + expect(readRuntimeClosure()).toBeUndefined(); + }); +}); diff --git a/packages/testing/janitor/src/core/read-lockfile-importers.ts b/packages/testing/janitor/src/core/read-lockfile-importers.ts index 18528f12394..99c445e054b 100644 --- a/packages/testing/janitor/src/core/read-lockfile-importers.ts +++ b/packages/testing/janitor/src/core/read-lockfile-importers.ts @@ -16,7 +16,7 @@ import { } from '@n8n/test-impact'; import { existsSync, readFileSync } from 'node:fs'; import { join } from 'node:path'; -import { parse } from 'yaml'; +import { parseAllDocuments } from 'yaml'; import { getGitRoot } from '../utils/git-operations.js'; @@ -37,7 +37,16 @@ function readLockfile(): Lockfile | undefined { const lockPath = join(getGitRoot(process.cwd()), 'pnpm-lock.yaml'); if (!existsSync(lockPath)) return undefined; try { - return parse(readFileSync(lockPath, 'utf8')) as Lockfile; + const documents = parseAllDocuments(readFileSync(lockPath, 'utf8')); + if (documents.length === 0 || documents.some((doc) => doc.errors.length > 0)) { + return undefined; + } + + const document = documents.at(-1); + if (!document) return undefined; + + const value: unknown = document.toJS(); + return typeof value === 'object' && value !== null ? value : undefined; } catch { return undefined; } diff --git a/packages/testing/playwright/playwright.config.ts b/packages/testing/playwright/playwright.config.ts index 451fb18a06e..66f1395cedd 100644 --- a/packages/testing/playwright/playwright.config.ts +++ b/packages/testing/playwright/playwright.config.ts @@ -69,7 +69,7 @@ if (BACKEND_URL && !SKIP_WEB_SERVER) { typeof envUserFolder === 'string' ? envUserFolder : USER_FOLDER; } webServer.push({ - command: 'cd .. && pnpm start', + command: 'pnpm --dir ../../.. start', url: `${BACKEND_URL}/favicon.ico`, timeout: 30000, reuseExistingServer: IS_DEV ? false : true, diff --git a/patches/minifaker.patch b/patches/minifaker.patch index 439a4911406..171b9019f79 100644 --- a/patches/minifaker.patch +++ b/patches/minifaker.patch @@ -3,10 +3,10 @@ index 6ee7fa8ca1fc6daf7322b5278becb4e38c55eef8..7ebf0a67338ae7635a13d73952e7b4fa --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ - "module": "./dist/esm/index.js", - "exports": { - ".": { -+ "types": "./dist/types/index.d.ts", - "import": "./dist/esm/index.js", - "require": "./dist/cjs/index.js" - }, + "module": "./dist/esm/index.js", + "exports": { + ".": { ++ "types": "./dist/types/index.d.ts", + "import": "./dist/esm/index.js", + "require": "./dist/cjs/index.js" + }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 774af7f1d32..34ee99c9dd1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,3 +1,104 @@ +--- +lockfileVersion: '9.0' + +importers: + + .: + configDependencies: {} + packageManagerDependencies: + pnpm: + specifier: 12.3.4 + version: 12.3.4 + +packages: + + '@pnpm/exe.darwin-arm64@12.3.4': + resolution: {integrity: sha512-PAyUol8T1+/+ViOiXAt51ECA+QnfXCqz6foL4bW+LsoX0NcVd5XVEM2mRQu+LV4oc7uRz9zf9U0P+XFfuQeDAw==} + cpu: [arm64] + os: [darwin] + + '@pnpm/exe.darwin-x64@12.3.4': + resolution: {integrity: sha512-fxP9JCk0Cdye+ePuj+GJJLMUMTqHGWRdb1dtv4How876uQ2ehxvenpgiYAir/ceO9PsYUZkFTtyZdx+rRu5QOA==} + cpu: [x64] + os: [darwin] + + '@pnpm/exe.linux-arm64-musl@12.3.4': + resolution: {integrity: sha512-FBOt0/7ye6O6q4AllVV5QMviB6qE6fqkeczV/+MDWQsmo+QJrlfsh6X7CpH/tClVpBZEyIbjpUoT8bNhCYBxEg==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@pnpm/exe.linux-arm64@12.3.4': + resolution: {integrity: sha512-t71AVA7LRqiKTyZ5xMYaZc2n5DfdpMbfokZuiIOXHBOM03ECnF0t4iYwaBDqJgVjlKYUOwaF/bRQajGNA4cJ4w==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@pnpm/exe.linux-x64-musl@12.3.4': + resolution: {integrity: sha512-RPmk7Jb/aYaFvL2iyDN/AtMY+hUEsue732WmXpcuQ9tBpMnGyA5py7Z3+e+qmQaJ0zY/4ni9jJiyPBQHujmv6w==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@pnpm/exe.linux-x64@12.3.4': + resolution: {integrity: sha512-2ZqOlSPkfwX1h5cR+FPiWf8+F+2hZT/3TvhUK5sigHqwaQCIiq8R7CGxhndKs63JtcLi2a1Qpo+wX/EoyfjyJQ==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@pnpm/exe.win32-arm64@12.3.4': + resolution: {integrity: sha512-ANyrHqyqco6SXBysUTRF74itDyyraea7IbFsKFdNXTjcFnfycTDx37EwuhdpPYFNSIh2JhUG4fByclsRfiHX7w==} + cpu: [arm64] + os: [win32] + + '@pnpm/exe.win32-x64@12.3.4': + resolution: {integrity: sha512-WH/KqBPY/hq2Tb7SgQltEZytimcjgKRaCRL/aM9CI0c67iKc5TVmHUhIiL3Ux9FB4bWn36i6XewUcScQI+zG8w==} + cpu: [x64] + os: [win32] + + pnpm@12.3.4: + resolution: {integrity: sha512-lhqkH7B32joEpEHZ+OFevAyW2o73ELLrZ7+e58sGEOq9SPH9hfUc/+c4RnhfoPh8VqOocqHYk/hEZ0G1zORUVw==} + engines: {node: '>=18.*'} + hasBin: true + +snapshots: + + '@pnpm/exe.darwin-arm64@12.3.4': + optional: true + + '@pnpm/exe.darwin-x64@12.3.4': + optional: true + + '@pnpm/exe.linux-arm64-musl@12.3.4': + optional: true + + '@pnpm/exe.linux-arm64@12.3.4': + optional: true + + '@pnpm/exe.linux-x64-musl@12.3.4': + optional: true + + '@pnpm/exe.linux-x64@12.3.4': + optional: true + + '@pnpm/exe.win32-arm64@12.3.4': + optional: true + + '@pnpm/exe.win32-x64@12.3.4': + optional: true + + pnpm@12.3.4: + optionalDependencies: + '@pnpm/exe.darwin-arm64': 12.3.4 + '@pnpm/exe.darwin-x64': 12.3.4 + '@pnpm/exe.linux-arm64': 12.3.4 + '@pnpm/exe.linux-arm64-musl': 12.3.4 + '@pnpm/exe.linux-x64': 12.3.4 + '@pnpm/exe.linux-x64-musl': 12.3.4 + '@pnpm/exe.win32-arm64': 12.3.4 + '@pnpm/exe.win32-x64': 12.3.4 + +--- lockfileVersion: '9.0' settings: @@ -2884,10 +2985,10 @@ importers: version: link:../utils vue: specifier: catalog:frontend - version: 3.5.26(@typescript/typescript6@6.0.2) + version: 3.5.26(typescript@6.0.2) vue-i18n: specifier: catalog:frontend - version: 11.4.5(vue@3.5.26(@typescript/typescript6@6.0.2)) + version: 11.4.5(vue@3.5.26(typescript@6.0.2)) zod: specifier: 3.25.76 version: 3.25.76 @@ -2915,7 +3016,7 @@ importers: version: typescript@7.0.2 '@vitejs/plugin-vue': specifier: catalog:frontend - version: 6.0.8(vite@8.0.2(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@20.19.41)(esbuild@0.28.1)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.3))(vue@3.5.26(@typescript/typescript6@6.0.2)) + version: 6.0.8(vite@8.0.2(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@20.19.41)(esbuild@0.28.1)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.3))(vue@3.5.26(typescript@6.0.2)) '@vue/test-utils': specifier: catalog:frontend version: 2.4.6 @@ -2926,8 +3027,8 @@ importers: specifier: 7.7.3 version: 7.7.3 typescript: - specifier: catalog:typescript-tooling - version: '@typescript/typescript6@6.0.2' + specifier: 'catalog:' + version: 6.0.2 unplugin-icons: specifier: catalog:frontend version: 23.0.1(@vue/compiler-sfc@3.5.26) @@ -2939,13 +3040,13 @@ importers: version: 2.3.3(vite@8.0.2(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@20.19.41)(esbuild@0.28.1)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.3)) vite-svg-loader: specifier: catalog:frontend - version: 5.1.0(vue@3.5.26(@typescript/typescript6@6.0.2)) + version: 5.1.0(vue@3.5.26(typescript@6.0.2)) vitest: specifier: 'catalog:' version: 4.1.9(@opentelemetry/api@1.9.1)(@types/node@20.19.41)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(jsdom@23.0.1(bufferutil@4.0.9)(supports-color@8.1.1)(utf-8-validate@5.0.10))(vite@8.0.2(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@20.19.41)(esbuild@0.28.1)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.3)) vue-tsc: specifier: ^2.2.8 - version: 2.2.8(patch_hash=e2aee939ccac8a57fe449bfd92bedd8117841579526217bc39aca26c6b8c317f)(@typescript/typescript6@6.0.2) + version: 2.2.8(patch_hash=e2aee939ccac8a57fe449bfd92bedd8117841579526217bc39aca26c6b8c317f)(typescript@6.0.2) packages/@n8n/mcp-browser: dependencies: @@ -34611,6 +34712,7 @@ snapshots: vscode-uri: 3.0.8 optionalDependencies: typescript: '@typescript/typescript6@6.0.2' + optional: true '@volar/typescript@2.4.28(typescript@5.9.3)': dependencies: @@ -34764,6 +34866,7 @@ snapshots: path-browserify: 1.0.1 optionalDependencies: typescript: '@typescript/typescript6@6.0.2' + optional: true '@vue/language-core@2.2.8(typescript@6.0.2)': dependencies: @@ -46116,14 +46219,6 @@ snapshots: transitivePeerDependencies: - supports-color - vue-i18n@11.4.5(vue@3.5.26(@typescript/typescript6@6.0.2)): - dependencies: - '@intlify/core-base': 11.4.5 - '@intlify/devtools-types': 11.4.5 - '@intlify/shared': 11.4.5 - '@vue/devtools-api': 6.6.4 - vue: 3.5.26(@typescript/typescript6@6.0.2) - vue-i18n@11.4.5(vue@3.5.26(typescript@6.0.2)): dependencies: '@intlify/core-base': 11.4.5 @@ -46168,6 +46263,7 @@ snapshots: '@volar/typescript': 2.4.28(@typescript/typescript6@6.0.2) '@vue/language-core': 2.2.8(@typescript/typescript6@6.0.2) typescript: '@typescript/typescript6@6.0.2' + optional: true vue-tsc@2.2.8(patch_hash=e2aee939ccac8a57fe449bfd92bedd8117841579526217bc39aca26c6b8c317f)(typescript@6.0.2): dependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 2276fec80dc..e0439b9ce3a 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -5,7 +5,7 @@ autoInstallPeers: true linkWorkspacePackages: deep hoist: true hoistWorkspacePackages: false -# pnpm 11 hoists TS7 into node_modules/.pnpm/node_modules, where it shadows the root's real +# pnpm hoists TS7 into node_modules/.pnpm/node_modules, where it shadows the root's real # TS6 and breaks vue-tsc — it needs `typescript/lib/_tsc`, which TS7 and the # `@typescript/typescript6` alias both lack — in packages on the typescript-tooling catalog. hoistPattern: @@ -13,7 +13,7 @@ hoistPattern: - '!typescript' allowUnusedPatches: true # The docker build trims frontend manifests and deploys with --prod before later -# scripts run; pnpm 11's pre-run install would reinstall production-only and prune +# scripts run; pnpm's pre-run install would reinstall production-only and prune # the devDependencies those scripts import. verifyDepsBeforeRun: false diff --git a/scripts/build-n8n.mjs b/scripts/build-n8n.mjs index 55faaa6a619..ae7fced4cb5 100755 --- a/scripts/build-n8n.mjs +++ b/scripts/build-n8n.mjs @@ -95,7 +95,8 @@ startTimer('package_build'); echo(chalk.yellow('INFO: Running pnpm install and build...')); try { - const installProcess = $`cd ${config.rootDir} && pnpm install --frozen-lockfile`; + const installFlags = ['--frozen-lockfile', ...(isCI ? ['--trust-lockfile'] : [])]; + const installProcess = $`cd ${config.rootDir} && pnpm install ${installFlags}`; installProcess.pipe(process.stdout); await installProcess; @@ -191,7 +192,7 @@ try { // top level, so cdxgen would miss the transitive tree (the manifest would be incomplete). // Re-enable hoisting for the licenses build only — shipped images keep the non-hoisted // layout, since regular builds leave N8N_GENERATE_LICENSES unset. - // `PNPM_CONFIG_*` and not `npm_config_*`: pnpm 11 no longer reads npm-style env config, + // `PNPM_CONFIG_*` and not `npm_config_*`: pnpm no longer reads npm-style env config, // so an `npm_config_` name here is silently ignored and the SBOM comes out incomplete. const generateLicenses = process.env.N8N_GENERATE_LICENSES === 'true'; if (generateLicenses) { @@ -206,7 +207,13 @@ try { // The closure is lockfile-derived, so a workspace package only stays out of the image by // declaring build-time deps as devDependencies — editing manifests at build time does // nothing here. - const deployFlags = ['--prod', '--config.inject-workspace-packages=true']; + // pnpm 12's clone path leaves its intermediate links tree in macOS deploy outputs. + // Linux already falls back to copying, so make deploy output consistent across platforms. + const deployFlags = [ + '--prod', + '--config.inject-workspace-packages=true', + '--config.package-import-method=copy', + ]; // The dedicated-lockfile deploy leaves pnpm's own files in the closure, which the // legacy deploy did not write. They are not needed to run the closure, they point at @@ -221,8 +228,15 @@ try { const removePnpmDeployFiles = async (dir) => { for (const file of pnpmDeployFiles) await fs.remove(path.join(dir, file)); }; + const verifyNoPnpmIntermediateLinks = async (dir) => { + const linksDir = path.join(dir, 'node_modules/.pnpm/@'); + if (await fs.pathExists(linksDir)) { + throw new Error(`pnpm left its intermediate links tree in ${linksDir}`); + } + }; await $`cd ${config.rootDir} && NODE_ENV=production DOCKER_BUILD=true pnpm --filter=n8n ${deployFlags} deploy --no-optional ./compiled`; + await verifyNoPnpmIntermediateLinks(config.compiledAppDir); await removePnpmDeployFiles(config.compiledAppDir); // Strip test/example/benchmark dirs shipped inside production deps that lack a @@ -451,6 +465,7 @@ try { // only, not the shipped total. startTimer('task_runner_deploy'); await $`cd ${config.rootDir} && NODE_ENV=production DOCKER_BUILD=true pnpm --filter=@n8n/task-runner ${deployFlags} deploy --no-optional ${config.compiledTaskRunnerDir}`; + await verifyNoPnpmIntermediateLinks(config.compiledTaskRunnerDir); echo( chalk.green( `✅ Task runner deployment completed in ${formatDuration(getElapsedTime('task_runner_deploy'))}`, @@ -554,7 +569,7 @@ try { echo(chalk.yellow('INFO: Generating SBOM and rendering THIRD_PARTY_LICENSES.md...')); try { const toolingDir = path.join(config.rootDir, '.github', 'scripts'); - await $`cd ${config.rootDir} && pnpm install --frozen-lockfile --dir .github/scripts --ignore-workspace`; + await $`cd ${config.rootDir} && pnpm install --frozen-lockfile --dir .github/scripts --lockfile-dir . --ignore-workspace`; const generateProcess = $`cd ${toolingDir} && pnpm generate-licenses`; generateProcess.pipe(process.stdout); await generateProcess; diff --git a/scripts/smoke-n8n-image.mjs b/scripts/smoke-n8n-image.mjs index a7e7f4365fa..6512f5cd2e5 100644 --- a/scripts/smoke-n8n-image.mjs +++ b/scripts/smoke-n8n-image.mjs @@ -137,6 +137,9 @@ const KAFKA_CHECK = path.resolve( path.dirname(fileURLToPath(import.meta.url)), '../.github/scripts/docker/kafka-native-smoke-check.mjs', ); +const EXPECTED_PNPM_VERSION = (await fs.readJson( + path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../package.json'), +)).packageManager.replace('pnpm@', ''); async function runKafkaBindingCheck() { const name = 'kafka native binding loads in image'; @@ -172,6 +175,22 @@ async function runRunnersInterpreterCheck(image) { } } +async function runRunnersPnpmCheck(image) { + const name = `pnpm ${EXPECTED_PNPM_VERSION} runs offline in ${image}`; + try { + const { stdout } = await $({ + timeout: TIMEOUT, + })`docker run --rm --network none --entrypoint pnpm ${image} --version`; + if (stdout.trim() !== EXPECTED_PNPM_VERSION) { + throw new Error(`expected ${EXPECTED_PNPM_VERSION}, got ${stdout.trim()}`); + } + echo(chalk.green(`✓ ${name}`)); + return true; + } catch (err) { + return reportFailure(name, err); + } +} + // Derived images add packages with `pnpm add` in the JS runner directory (docs: // "Adding extra dependencies"). pnpm prunes packages the recorded importer no // longer needs, so a deploy that records the closure differently makes this @@ -186,7 +205,7 @@ async function runRunnersExtensionCheck(image) { const script = [ `cd ${RUNNER_JS_DIR}`, 'before=$(ls node_modules/.pnpm | wc -l)', - 'pnpm add uuid --prod --no-lockfile --config.minimum-release-age=0', + 'pnpm add uuid --save-prod --no-lockfile --config.minimum-release-age=0', 'after=$(ls node_modules/.pnpm | wc -l)', '[ "$after" -gt "$before" ] || { echo "pnpm add pruned the closure: $before -> $after packages"; exit 1; }', `node -e "require('uuid'); require('moment'); require.resolve('n8n-core')"`, @@ -241,6 +260,7 @@ const ok = ( runKafkaBindingCheck(), ...RUNNERS_IMAGES.map(runRunnersInterpreterCheck), // The distroless image ships no shell or pnpm; only the standard image is extendable. + ...RUNNERS_IMAGES.filter((image) => !image.endsWith('-distroless')).map(runRunnersPnpmCheck), ...RUNNERS_IMAGES.filter((image) => !image.endsWith('-distroless')).map( runRunnersExtensionCheck, ),