From cd68c2be873b785636563c885d73ab560a34aeac Mon Sep 17 00:00:00 2001 From: William Bergamin Date: Mon, 10 Aug 2026 14:37:26 -0400 Subject: [PATCH 1/4] feat(socket-mode): support undici v8 as a peer dependency MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Widen the `undici` peer dependency range to `^7.0.0 || ^8.0.0` so consumers can bring either major. undici v8 requires Node >=22.19, while v7 continues to support Node >=20 — the source needs no changes, as v8 preserves every API socket-mode uses. CI now pairs each Node version with an undici major via a `versions` matrix and forces the socket-mode workspace onto that major before running the suite, so v8 support is actually exercised on capable Node versions. A new guardrail test asserts the Node/undici pairing (and never allows v8 on Node <22.19). Co-Authored-By: Claude --- .changeset/socket-mode-undici-v8.md | 5 ++ .github/workflows/ci-build.yml | 37 ++++++---- packages/socket-mode/README.md | 6 +- packages/socket-mode/package.json | 2 +- .../socket-mode/src/undiciVersion.test.ts | 67 +++++++++++++++++++ 5 files changed, 103 insertions(+), 14 deletions(-) create mode 100644 .changeset/socket-mode-undici-v8.md create mode 100644 packages/socket-mode/src/undiciVersion.test.ts diff --git a/.changeset/socket-mode-undici-v8.md b/.changeset/socket-mode-undici-v8.md new file mode 100644 index 000000000..d2ff20232 --- /dev/null +++ b/.changeset/socket-mode-undici-v8.md @@ -0,0 +1,5 @@ +--- +"@slack/socket-mode": minor +--- + +Support `undici@^8` as a peer dependency in addition to `undici@^7`. `undici@^8` requires Node.js >=22.19; `undici@^7` continues to support Node.js >=20. diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index d5afe57af..68fbdda17 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -18,14 +18,20 @@ jobs: os: - "ubuntu-latest" - "windows-latest" - node-version: - - "20.x" - - "22.x" - - "24.x" - - "26.x" + versions: + - node: "20.x" + undici: "7" # undici v8 requires Node >=22.19, so 20.x tests against v7 + - node: "22.x" + undici: "8" + - node: "24.x" + undici: "8" + - node: "26.x" + undici: "8" runs-on: ${{ matrix.os }} permissions: contents: read + env: + SOCKET_MODE_EXPECT_UNDICI_MAJOR: ${{ matrix.versions.undici }} steps: - name: Configure git settings (Windows) if: matrix.os == 'windows-latest' @@ -35,31 +41,38 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - - name: Use Node.js ${{ matrix.node-version }} + - name: Use Node.js ${{ matrix.versions.node }} uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: - node-version: ${{ matrix.node-version }} + node-version: ${{ matrix.versions.node }} - name: Check versions run: | node --version npm --version - name: Install dependencies run: npm ci --verbose + # Force the socket-mode workspace onto the undici major paired with this Node + # version in the matrix above. A plain workspace `npm install` (no --no-save / + # --no-package-lock) is required: those flags make npm ignore the requested + # version and keep whatever the committed devDependency range already allows. + # The mutation is ephemeral — CI checks out fresh and never commits it. + - name: Install undici v${{ matrix.versions.undici }} for socket-mode + run: npm install undici@^${{ matrix.versions.undici }} --workspace=packages/socket-mode - name: Build packages run: npm run build - name: Lint run: npm run lint - name: Build docs - if: matrix.node-version == env.LATEST_SUPPORTED_NODE + if: matrix.versions.node == env.LATEST_SUPPORTED_NODE run: npm run docs - name: Run tests - if: matrix.node-version != env.LATEST_SUPPORTED_NODE + if: matrix.versions.node != env.LATEST_SUPPORTED_NODE run: npm test - name: Run test coverage - if: matrix.node-version == env.LATEST_SUPPORTED_NODE + if: matrix.versions.node == env.LATEST_SUPPORTED_NODE run: npm run test:coverage - name: Upload code coverage - if: matrix.node-version == env.LATEST_SUPPORTED_NODE && matrix.os == 'ubuntu-latest' + if: matrix.versions.node == env.LATEST_SUPPORTED_NODE && matrix.os == 'ubuntu-latest' uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 with: fail_ci_if_error: true @@ -74,7 +87,7 @@ jobs: with: fail_ci_if_error: true files: packages/cli-hooks/test-results.xml,packages/cli-test/test-results.xml,packages/logger/test-results.xml,packages/oauth/test-results.xml,packages/socket-mode/test-results.xml,packages/web-api/test-results.xml,packages/webhook/test-results.xml - flags: ${{ matrix.node-version }},${{ matrix.os }} + flags: ${{ matrix.versions.node }},${{ matrix.os }} report_type: test_results token: ${{ secrets.CODECOV_TOKEN }} verbose: true diff --git a/packages/socket-mode/README.md b/packages/socket-mode/README.md index aad663361..e2c3cfcaa 100644 --- a/packages/socket-mode/README.md +++ b/packages/socket-mode/README.md @@ -9,10 +9,14 @@ This package is designed to support [**Socket Mode**][socket-mode], which allows This package supports Node v20 and higher. It's highly recommended to use [the latest LTS version of node](https://github.com/nodejs/Release#release-schedule), and the documentation is written using syntax and features from that version. +[`undici`](https://www.npmjs.com/package/undici) is a required peer dependency and must be installed alongside this +package. Both `undici@^7` and `undici@^8` are supported. Note that `undici@^8` requires Node v22.19 or higher; if you're +on Node v20, use `undici@^7`. + ## Installation ```shell -$ npm install @slack/socket-mode +$ npm install @slack/socket-mode undici ``` ## Usage diff --git a/packages/socket-mode/package.json b/packages/socket-mode/package.json index 43222aad7..0d606f580 100644 --- a/packages/socket-mode/package.json +++ b/packages/socket-mode/package.json @@ -57,7 +57,7 @@ "eventemitter3": "^5" }, "peerDependencies": { - "undici": "^7.0.0" + "undici": "^7.0.0 || ^8.0.0" }, "devDependencies": { "@types/proxyquire": "^1.3.31", diff --git a/packages/socket-mode/src/undiciVersion.test.ts b/packages/socket-mode/src/undiciVersion.test.ts new file mode 100644 index 000000000..19665094f --- /dev/null +++ b/packages/socket-mode/src/undiciVersion.test.ts @@ -0,0 +1,67 @@ +import assert from 'node:assert/strict'; +import { createRequire } from 'node:module'; +import { describe, it } from 'node:test'; + +// This is a guardrail, not a behavioral test. `@slack/socket-mode` supports undici +// as a peer dependency across two majors (`^7.0.0 || ^8.0.0`), but the two majors do +// not run on the same Node.js versions: undici v8 requires Node >=22.19.0, while +// undici v7 runs on Node >=20. CI pairs each Node row with a specific undici major +// (see the `versions` matrix in .github/workflows/ci-build.yml) and exposes the +// expected major via SOCKET_MODE_EXPECT_UNDICI_MAJOR. This test verifies that the +// undici actually resolved at runtime matches that pairing, so we never ship +// "supports v8" without having run the suite against v8 on a capable Node. + +const require = createRequire(import.meta.url); + +// The lowest Node.js version undici v8 supports (its own `engines.node`). +const UNDICI_V8_MIN_NODE = '22.19.0'; + +// Read the major of the undici that is actually installed and resolvable from this +// package, rather than trusting the declared range — this reflects what the source, +// integration tests, and WHATWG event classes import via the bare 'undici' specifier. +const undiciVersion: string = require('undici/package.json').version; +const undiciMajor = Number.parseInt(undiciVersion.split('.')[0], 10); + +// Dependency-free numeric version comparison. `semver` is only transitively available, +// so we avoid importing it. `process.versions.node` is "MAJOR.MINOR.PATCH"; any +// pre-release suffix on the patch segment parses down to its leading integer. +function nodeAtLeast(target: string): boolean { + const actual = process.versions.node.split('.').map((part) => Number.parseInt(part, 10)); + const min = target.split('.').map((part) => Number.parseInt(part, 10)); + for (let i = 0; i < min.length; i++) { + const a = actual[i] ?? 0; + if (a !== min[i]) { + return a > min[i]; + } + } + return true; +} + +describe('undici peer dependency', () => { + // Safety invariant — runs in every environment (local and CI). It can only fail on + // the genuinely broken combination of undici v8+ on a Node older than 22.19.0. + it('never pairs undici v8+ with a Node.js version older than 22.19.0', () => { + const brokenCombo = undiciMajor >= 8 && !nodeAtLeast(UNDICI_V8_MIN_NODE); + assert.ok( + !brokenCombo, + `undici@${undiciVersion} requires Node >=${UNDICI_V8_MIN_NODE}, but the tests are running on Node ${process.versions.node}`, + ); + }); + + // Strict pairing — enforced only when CI pins the expected major via the matrix. + // This is the "recent Node runs the latest undici" guarantee: it proves Node 20 ran + // on v7 and Node 22/24/26 ran on v8. Skipped locally so a plain `npm test` after + // `npm ci` (which installs the v7 dev default) never fails. + const expectedMajor = process.env.SOCKET_MODE_EXPECT_UNDICI_MAJOR; + if (expectedMajor) { + it(`runs against the undici major pinned by CI (undici v${expectedMajor})`, () => { + assert.strictEqual( + undiciMajor, + Number.parseInt(expectedMajor, 10), + `Expected socket-mode to run against undici v${expectedMajor} (from SOCKET_MODE_EXPECT_UNDICI_MAJOR), but undici@${undiciVersion} is installed`, + ); + }); + } else { + it.skip('runs against the undici major pinned by CI (only enforced when SOCKET_MODE_EXPECT_UNDICI_MAJOR is set)'); + } +}); From 7edc77682915e1f06c81d0c6b9f3bde2e3c8f6ae Mon Sep 17 00:00:00 2001 From: William Bergamin Date: Mon, 10 Aug 2026 14:38:30 -0400 Subject: [PATCH 2/4] docs(ci): condense undici install step comment to one line Co-Authored-By: Claude --- .github/workflows/ci-build.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index 68fbdda17..54e29676e 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -51,11 +51,7 @@ jobs: npm --version - name: Install dependencies run: npm ci --verbose - # Force the socket-mode workspace onto the undici major paired with this Node - # version in the matrix above. A plain workspace `npm install` (no --no-save / - # --no-package-lock) is required: those flags make npm ignore the requested - # version and keep whatever the committed devDependency range already allows. - # The mutation is ephemeral — CI checks out fresh and never commits it. + # Force socket-mode onto the matrix's undici major (plain install — --no-save/--no-package-lock make npm ignore the requested version). - name: Install undici v${{ matrix.versions.undici }} for socket-mode run: npm install undici@^${{ matrix.versions.undici }} --workspace=packages/socket-mode - name: Build packages From 4cabd4a4c0ce4447a09d430163cc78c194a7d17f Mon Sep 17 00:00:00 2001 From: William Bergamin Date: Mon, 10 Aug 2026 14:39:28 -0400 Subject: [PATCH 3/4] docs(ci): trim undici install step comment Co-Authored-By: Claude --- .github/workflows/ci-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index 54e29676e..3610da707 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -51,7 +51,7 @@ jobs: npm --version - name: Install dependencies run: npm ci --verbose - # Force socket-mode onto the matrix's undici major (plain install — --no-save/--no-package-lock make npm ignore the requested version). + # Force socket-mode onto the matrix's undici major. - name: Install undici v${{ matrix.versions.undici }} for socket-mode run: npm install undici@^${{ matrix.versions.undici }} --workspace=packages/socket-mode - name: Build packages From 3e6270df011840863be4e758a5e97ff93498dfa1 Mon Sep 17 00:00:00 2001 From: William Bergamin Date: Mon, 10 Aug 2026 15:14:26 -0400 Subject: [PATCH 4/4] test(socket-mode): simplify undici version guardrail test Reduce the undici peer-dependency test to its two assertions, drop the comment essays and the general-purpose version-comparison helper in favor of a plain Node major check, and rename SOCKET_MODE_EXPECT_UNDICI_MAJOR to SOCKET_MODE_UNDICI_VERSION. Co-Authored-By: Claude --- .github/workflows/ci-build.yml | 2 +- packages/socket-mode/README.md | 6 +-- .../socket-mode/src/undiciVersion.test.ts | 54 ++++--------------- 3 files changed, 12 insertions(+), 50 deletions(-) diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index 3610da707..fffb8dd41 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -31,7 +31,7 @@ jobs: permissions: contents: read env: - SOCKET_MODE_EXPECT_UNDICI_MAJOR: ${{ matrix.versions.undici }} + SOCKET_MODE_UNDICI_VERSION: ${{ matrix.versions.undici }} steps: - name: Configure git settings (Windows) if: matrix.os == 'windows-latest' diff --git a/packages/socket-mode/README.md b/packages/socket-mode/README.md index e2c3cfcaa..aad663361 100644 --- a/packages/socket-mode/README.md +++ b/packages/socket-mode/README.md @@ -9,14 +9,10 @@ This package is designed to support [**Socket Mode**][socket-mode], which allows This package supports Node v20 and higher. It's highly recommended to use [the latest LTS version of node](https://github.com/nodejs/Release#release-schedule), and the documentation is written using syntax and features from that version. -[`undici`](https://www.npmjs.com/package/undici) is a required peer dependency and must be installed alongside this -package. Both `undici@^7` and `undici@^8` are supported. Note that `undici@^8` requires Node v22.19 or higher; if you're -on Node v20, use `undici@^7`. - ## Installation ```shell -$ npm install @slack/socket-mode undici +$ npm install @slack/socket-mode ``` ## Usage diff --git a/packages/socket-mode/src/undiciVersion.test.ts b/packages/socket-mode/src/undiciVersion.test.ts index 19665094f..5ea57b350 100644 --- a/packages/socket-mode/src/undiciVersion.test.ts +++ b/packages/socket-mode/src/undiciVersion.test.ts @@ -2,66 +2,32 @@ import assert from 'node:assert/strict'; import { createRequire } from 'node:module'; import { describe, it } from 'node:test'; -// This is a guardrail, not a behavioral test. `@slack/socket-mode` supports undici -// as a peer dependency across two majors (`^7.0.0 || ^8.0.0`), but the two majors do -// not run on the same Node.js versions: undici v8 requires Node >=22.19.0, while -// undici v7 runs on Node >=20. CI pairs each Node row with a specific undici major -// (see the `versions` matrix in .github/workflows/ci-build.yml) and exposes the -// expected major via SOCKET_MODE_EXPECT_UNDICI_MAJOR. This test verifies that the -// undici actually resolved at runtime matches that pairing, so we never ship -// "supports v8" without having run the suite against v8 on a capable Node. +// Guardrail for the `undici` peer dependency. undici v8 needs +// Node >=22 and v7 needs Node >=20, so CI passes the expected major in via SOCKET_MODE_UNDICI_VERSION. const require = createRequire(import.meta.url); - -// The lowest Node.js version undici v8 supports (its own `engines.node`). -const UNDICI_V8_MIN_NODE = '22.19.0'; - -// Read the major of the undici that is actually installed and resolvable from this -// package, rather than trusting the declared range — this reflects what the source, -// integration tests, and WHATWG event classes import via the bare 'undici' specifier. const undiciVersion: string = require('undici/package.json').version; const undiciMajor = Number.parseInt(undiciVersion.split('.')[0], 10); - -// Dependency-free numeric version comparison. `semver` is only transitively available, -// so we avoid importing it. `process.versions.node` is "MAJOR.MINOR.PATCH"; any -// pre-release suffix on the patch segment parses down to its leading integer. -function nodeAtLeast(target: string): boolean { - const actual = process.versions.node.split('.').map((part) => Number.parseInt(part, 10)); - const min = target.split('.').map((part) => Number.parseInt(part, 10)); - for (let i = 0; i < min.length; i++) { - const a = actual[i] ?? 0; - if (a !== min[i]) { - return a > min[i]; - } - } - return true; -} +const nodeMajor = Number.parseInt(process.versions.node.split('.')[0], 10); describe('undici peer dependency', () => { - // Safety invariant — runs in every environment (local and CI). It can only fail on - // the genuinely broken combination of undici v8+ on a Node older than 22.19.0. - it('never pairs undici v8+ with a Node.js version older than 22.19.0', () => { - const brokenCombo = undiciMajor >= 8 && !nodeAtLeast(UNDICI_V8_MIN_NODE); + it('never runs undici v8+ on Node older than 22', () => { assert.ok( - !brokenCombo, - `undici@${undiciVersion} requires Node >=${UNDICI_V8_MIN_NODE}, but the tests are running on Node ${process.versions.node}`, + undiciMajor < 8 || nodeMajor >= 22, + `undici@${undiciVersion} requires Node >=22, but tests are running on Node ${process.versions.node}`, ); }); - // Strict pairing — enforced only when CI pins the expected major via the matrix. - // This is the "recent Node runs the latest undici" guarantee: it proves Node 20 ran - // on v7 and Node 22/24/26 ran on v8. Skipped locally so a plain `npm test` after - // `npm ci` (which installs the v7 dev default) never fails. - const expectedMajor = process.env.SOCKET_MODE_EXPECT_UNDICI_MAJOR; + const expectedMajor = process.env.SOCKET_MODE_UNDICI_VERSION; if (expectedMajor) { - it(`runs against the undici major pinned by CI (undici v${expectedMajor})`, () => { + it(`runs against the undici major pinned by CI (v${expectedMajor})`, () => { assert.strictEqual( undiciMajor, Number.parseInt(expectedMajor, 10), - `Expected socket-mode to run against undici v${expectedMajor} (from SOCKET_MODE_EXPECT_UNDICI_MAJOR), but undici@${undiciVersion} is installed`, + `Expected undici v${expectedMajor} (SOCKET_MODE_UNDICI_VERSION), but undici@${undiciVersion} is installed`, ); }); } else { - it.skip('runs against the undici major pinned by CI (only enforced when SOCKET_MODE_EXPECT_UNDICI_MAJOR is set)'); + it.skip('runs against the undici major pinned by CI (only when SOCKET_MODE_UNDICI_VERSION is set)'); } });