From 322414cbdd02205bff40dbf15fd4f4f0fe445f17 Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Tue, 30 Jun 2026 04:02:52 -0700 Subject: [PATCH 01/42] build(deps): use @slack/webhook + p-retry, drop axios Co-Authored-By: Claude --- package-lock.json | 90 ++++++++++++++++++++++++++++++----------------- package.json | 6 ++-- 2 files changed, 60 insertions(+), 36 deletions(-) diff --git a/package-lock.json b/package-lock.json index ad6e02a7..daac52a3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,12 +13,12 @@ "@actions/github": "^9.1.1", "@slack/logger": "^4.0.1", "@slack/web-api": "^7.16.0", - "axios": "^1.16.0", - "axios-retry": "^4.5.0", + "@slack/webhook": "file:../node-slack-sdk/packages/webhook", "flat": "^6.0.1", "https-proxy-agent": "^9.0.0", "js-yaml": "^4.2.0", - "markup-js": "^1.5.21" + "markup-js": "^1.5.21", + "p-retry": "^8.0.0" }, "devDependencies": { "@biomejs/biome": "^2.4.16", @@ -37,6 +37,23 @@ "npm": ">=11.6.1" } }, + "../node-slack-sdk/packages/webhook": { + "name": "@slack/webhook", + "version": "7.0.9", + "license": "MIT", + "dependencies": { + "@slack/types": "^2.20.1", + "@types/node": ">=18", + "axios": "^1.16.0" + }, + "devDependencies": { + "nock": "^14.0.6" + }, + "engines": { + "node": ">= 18", + "npm": ">= 8.6.0" + } + }, "node_modules/@actions/core": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@actions/core/-/core-3.0.1.tgz", @@ -859,6 +876,23 @@ "npm": ">= 8.6.0" } }, + "node_modules/@slack/web-api/node_modules/p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "license": "MIT", + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@slack/webhook": { + "resolved": "../node-slack-sdk/packages/webhook", + "link": true + }, "node_modules/@types/flat": { "version": "5.0.5", "resolved": "https://registry.npmjs.org/@types/flat/-/flat-5.0.5.tgz", @@ -983,18 +1017,6 @@ "proxy-from-env": "^2.1.0" } }, - "node_modules/axios-retry": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/axios-retry/-/axios-retry-4.5.0.tgz", - "integrity": "sha512-aR99oXhpEDGo0UuAlYcn2iGRds30k366Zfa05XWScR9QaQD4JYiP3/1Qt1u7YlefUOK+cn0CcwoL1oefavQUlQ==", - "license": "Apache-2.0", - "dependencies": { - "is-retry-allowed": "^2.2.0" - }, - "peerDependencies": { - "axios": "0.x || 1.x" - } - }, "node_modules/before-after-hook": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-4.0.0.tgz", @@ -1590,6 +1612,18 @@ "node": ">=0.10.0" } }, + "node_modules/is-network-error": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.2.tgz", + "integrity": "sha512-PhBY86zaxNZUuWP6h13Vu5oFe0XY6/UlKzQnYFELzGVHygP3MxmvTfYSG7GN3aIab/iWudSMgjSnG9Dq+nHrgA==", + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -1600,18 +1634,6 @@ "node": ">=0.12.0" } }, - "node_modules/is-retry-allowed": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-2.2.0.tgz", - "integrity": "sha512-XVm7LOeLpTW4jV19QSH38vkswxoLud8sQ57YwJVTPWdiaI9I8keEhGFpBlslyVsgdQy4Opg8QOLb8YRgsyZiQg==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -1872,16 +1894,18 @@ "license": "MIT" }, "node_modules/p-retry": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", - "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-8.0.0.tgz", + "integrity": "sha512-kFVqH1HxOHp8LupNsOys7bSV09VYTRLxarH/mokO4Rqhk6wGi70E0jh4VzvVGXfEVNggHoHLAMWsQqHyU1Ey9A==", "license": "MIT", "dependencies": { - "@types/retry": "0.12.0", - "retry": "^0.13.1" + "is-network-error": "^1.3.0" }, "engines": { - "node": ">=8" + "node": ">=22" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/p-timeout": { diff --git a/package.json b/package.json index fd66e2a0..95b21ce2 100644 --- a/package.json +++ b/package.json @@ -46,12 +46,12 @@ "@actions/github": "^9.1.1", "@slack/logger": "^4.0.1", "@slack/web-api": "^7.16.0", - "axios": "^1.16.0", - "axios-retry": "^4.5.0", + "@slack/webhook": "file:../node-slack-sdk/packages/webhook", "flat": "^6.0.1", "https-proxy-agent": "^9.0.0", "js-yaml": "^4.2.0", - "markup-js": "^1.5.21" + "markup-js": "^1.5.21", + "p-retry": "^8.0.0" }, "devDependencies": { "@biomejs/biome": "^2.4.16", From ff62bc47ffcdcfc0f16c0767825a68930404a511 Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Tue, 30 Jun 2026 04:03:33 -0700 Subject: [PATCH 02/42] test: stub @slack/webhook send() instead of axios Co-Authored-By: Claude --- test/index.spec.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/test/index.spec.js b/test/index.spec.js index 728102be..3ac95772 100644 --- a/test/index.spec.js +++ b/test/index.spec.js @@ -1,6 +1,7 @@ import fs from "node:fs"; import webapi from "@slack/web-api"; -import axios, { AxiosError } from "axios"; +import { IncomingWebhook, WebhookTrigger } from "@slack/webhook"; +import { AxiosError } from "axios"; import sinon from "sinon"; /** @@ -42,7 +43,8 @@ export class Mock { */ constructor() { this.sandbox = sinon.createSandbox(); - this.axios = this.sandbox.stub(axios); + this.incomingWebhook = this.sandbox.stub(IncomingWebhook.prototype, "send"); + this.webhookTrigger = this.sandbox.stub(WebhookTrigger.prototype, "send"); this.calls = this.sandbox.stub(webapi.WebClient.prototype, "apiCall"); this.core = { debug: this.sandbox.stub(), @@ -73,7 +75,8 @@ export class Mock { */ reset() { this.sandbox.reset(); - this.axios.post.resetHistory(); + this.incomingWebhook.resetHistory(); + this.webhookTrigger.resetHistory(); this.calls.resetHistory(); this.core.debug.reset(); this.core.error.reset(); From b212a6666c79d59b760f70149de44ff5e264925f Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Tue, 30 Jun 2026 04:04:47 -0700 Subject: [PATCH 03/42] refactor: drop axios from config, let SDK own webhook User-Agent Co-Authored-By: Claude --- src/config.js | 16 +++------------- test/config.spec.js | 26 +++++++++++++------------- 2 files changed, 16 insertions(+), 26 deletions(-) diff --git a/src/config.js b/src/config.js index 9febb986..e80ee71f 100644 --- a/src/config.js +++ b/src/config.js @@ -1,6 +1,4 @@ -import os from "node:os"; import webapi from "@slack/web-api"; -import axios from "axios"; import packageJson from "../package.json" with { type: "json" }; import Content from "./content.js"; import SlackError from "./errors.js"; @@ -60,11 +58,6 @@ export default class Config { */ inputs; - /** - * @type {import("axios").AxiosStatic} - The axios client. - */ - axios; - /** * @type {Content} - The parsed payload data to send. */ @@ -98,7 +91,6 @@ export default class Config { * @param {import("@actions/core")} core - GitHub Actions core utilities. */ constructor(core) { - this.axios = axios; this.core = core; this.logger = new Logger(core).logger; this.webapi = webapi; @@ -131,17 +123,15 @@ export default class Config { /** * Add user agent metadata for instrumentation. + * + * The @slack/webhook and @slack/web-api SDKs set their own User-Agent + * headers, so the action only registers app metadata with web-api here. */ instrument() { this.webapi.addAppMetadata({ name: packageJson.name, version: packageJson.version, }); - this.axios.defaults.headers.common["User-Agent"] = - `${packageJson.name.replace("/", ":")}/${packageJson.version} ` + - `axios/${this.axios.VERSION} ` + - `node/${process.version.replace("v", "")} ` + - `${os.platform()}/${os.release()}`; } /** diff --git a/test/config.spec.js b/test/config.spec.js index 17c292f3..a0991e25 100644 --- a/test/config.spec.js +++ b/test/config.spec.js @@ -184,19 +184,19 @@ describe("config", () => { } }); - it("adds metadata to webhook with package name and version", async () => { + it("adds web-api app metadata with package name and version", async () => { mocks.core.getInput.withArgs("method").returns("chat.postMessage"); mocks.core.getInput.withArgs("token").returns("xoxb-example"); - const config = new Config(mocks.core); - assert.ok( - config.axios.defaults.headers.common["User-Agent"].startsWith( - "@slack:slack-github-action/", - ), - ); - assert.ok( - config.axios.defaults.headers.common["User-Agent"].length > - "@slack:slack-github-action/".length, - ); + const addAppMetadata = mocks.sandbox.stub(webapi, "addAppMetadata"); + try { + new Config(mocks.core); + assert.ok(addAppMetadata.called, "addAppMetadata was not called"); + const arg = addAppMetadata.getCall(0).firstArg; + assert.ok(arg.name.includes("slack-github-action")); + assert.ok(arg.version.length > 0); + } finally { + addAppMetadata.restore(); + } }); }); @@ -225,7 +225,7 @@ describe("config", () => { describe("validate", () => { it('allow the "retries" option with lowercased space', async () => { - mocks.axios.post.returns(Promise.resolve("LGTM")); + mocks.incomingWebhook.resolves({ text: "LGTM" }); mocks.core.getInput.withArgs("retries").returns(" rapid "); mocks.core.getInput .withArgs("webhook") @@ -247,7 +247,7 @@ describe("config", () => { }); it("errors if an invalid retries option is provided", async () => { - mocks.axios.post.returns(Promise.resolve("LGTM")); + mocks.incomingWebhook.resolves({ text: "LGTM" }); mocks.core.getInput.withArgs("retries").returns("FOREVER"); mocks.core.getInput .withArgs("webhook") From f6e2eae82c423ea43a63dc2992359a38b438877b Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Tue, 30 Jun 2026 04:06:06 -0700 Subject: [PATCH 04/42] test: webhook techniques via @slack/webhook with p-retry gating Co-Authored-By: Claude --- test/webhook.spec.js | 283 +++++++++++-------------------------------- 1 file changed, 70 insertions(+), 213 deletions(-) diff --git a/test/webhook.spec.js b/test/webhook.spec.js index 09867e68..8a328fd5 100644 --- a/test/webhook.spec.js +++ b/test/webhook.spec.js @@ -1,6 +1,5 @@ import assert from "node:assert"; import { beforeEach, describe, it } from "node:test"; -import { AxiosError } from "axios"; import Config from "../src/config.js"; import SlackError from "../src/errors.js"; import send from "../src/send.js"; @@ -19,27 +18,19 @@ describe("webhook", () => { .returns("https://hooks.slack.com"); mocks.core.getInput.withArgs("webhook-type").returns("webhook-trigger"); mocks.core.getInput.withArgs("payload").returns("drinks: coffee"); - mocks.axios.post.returns( - Promise.resolve({ status: 200, data: { ok: true } }), + mocks.webhookTrigger.resolves({ ok: true, body: { ok: true } }); + await send(mocks.core); + assert.equal(mocks.webhookTrigger.getCalls().length, 1); + assert.deepEqual(mocks.webhookTrigger.getCall(0).firstArg, { + drinks: "coffee", + }); + assert.equal(mocks.core.setOutput.getCall(0).firstArg, "ok"); + assert.equal(mocks.core.setOutput.getCall(0).lastArg, true); + assert.equal(mocks.core.setOutput.getCall(1).firstArg, "response"); + assert.equal( + mocks.core.setOutput.getCall(1).lastArg, + JSON.stringify({ ok: true }), ); - try { - await send(mocks.core); - assert.equal(mocks.axios.post.getCalls().length, 1); - const [url, payload, options] = mocks.axios.post.getCall(0).args; - assert.equal(url, "https://hooks.slack.com"); - assert.deepEqual(payload, { drinks: "coffee" }); - assert.deepEqual(options, {}); - assert.equal(mocks.core.setOutput.getCall(0).firstArg, "ok"); - assert.equal(mocks.core.setOutput.getCall(0).lastArg, true); - assert.equal(mocks.core.setOutput.getCall(1).firstArg, "response"); - assert.equal( - mocks.core.setOutput.getCall(1).lastArg, - JSON.stringify({ ok: true }), - ); - } catch (err) { - console.error(err); - assert.fail("Failed to send the webhook"); - } }); it("sends the parsed payload to the provided incoming webhook", async () => { @@ -48,25 +39,19 @@ describe("webhook", () => { .returns("https://hooks.slack.com"); mocks.core.getInput.withArgs("webhook-type").returns("incoming-webhook"); mocks.core.getInput.withArgs("payload").returns("text: greetings"); - mocks.axios.post.returns(Promise.resolve({ status: 200, data: "ok" })); - try { - await send(mocks.core); - assert.equal(mocks.axios.post.getCalls().length, 1); - const [url, payload, options] = mocks.axios.post.getCall(0).args; - assert.equal(url, "https://hooks.slack.com"); - assert.deepEqual(payload, { text: "greetings" }); - assert.deepEqual(options, {}); - assert.equal(mocks.core.setOutput.getCall(0).firstArg, "ok"); - assert.equal(mocks.core.setOutput.getCall(0).lastArg, true); - assert.equal(mocks.core.setOutput.getCall(1).firstArg, "response"); - assert.equal( - mocks.core.setOutput.getCall(1).lastArg, - JSON.stringify("ok"), - ); - } catch (err) { - console.error(err); - assert.fail("Failed to send the webhook"); - } + mocks.incomingWebhook.resolves({ text: "ok" }); + await send(mocks.core); + assert.equal(mocks.incomingWebhook.getCalls().length, 1); + assert.deepEqual(mocks.incomingWebhook.getCall(0).firstArg, { + text: "greetings", + }); + assert.equal(mocks.core.setOutput.getCall(0).firstArg, "ok"); + assert.equal(mocks.core.setOutput.getCall(0).lastArg, true); + assert.equal(mocks.core.setOutput.getCall(1).firstArg, "response"); + assert.equal( + mocks.core.setOutput.getCall(1).lastArg, + JSON.stringify("ok"), + ); }); }); @@ -91,126 +76,68 @@ describe("webhook", () => { } }); - it("returns the failures from a webhook trigger", async () => { + it("does not retry a 4xx from a webhook trigger", async () => { mocks.core.getInput .withArgs("webhook") .returns("https://hooks.slack.com"); mocks.core.getInput.withArgs("webhook-type").returns("webhook-trigger"); mocks.core.getInput.withArgs("payload").returns("drinks: coffee"); - const response = new AxiosError( - "Request failed with status code 400", - "ERR_BAD_REQUEST", - {}, - {}, - { status: 400 }, - ); - mocks.axios.post.resolves(Promise.reject(response)); + mocks.core.getInput.withArgs("retries").returns("RAPID"); + const err = Object.assign(new Error("An HTTP protocol error occurred"), { + code: "slack_webhook_http_error", + original: { response: { status: 400 } }, + }); + mocks.webhookTrigger.rejects(err); try { await send(mocks.core); - } catch (err) { - if (err instanceof SlackError) { - assert.ok( - err.message.includes("Request failed with status code 400"), - ); - } else { - assert.fail(err); - } + } catch (e) { + assert.ok(e instanceof SlackError); } - assert.equal(mocks.axios.post.getCalls().length, 1); - const [url, payload, options] = mocks.axios.post.getCall(0).args; - assert.equal(url, "https://hooks.slack.com"); - assert.deepEqual(payload, { drinks: "coffee" }); - assert.deepEqual(options, {}); + assert.equal( + mocks.webhookTrigger.getCalls().length, + 1, + "4xx must not be retried", + ); assert.equal(mocks.core.setOutput.getCall(0).firstArg, "ok"); assert.equal(mocks.core.setOutput.getCall(0).lastArg, false); - assert.equal(mocks.core.setOutput.getCall(1).firstArg, "response"); }); - it("returns the failures from an incoming webhook", async () => { + it("retries a 5xx from an incoming webhook then succeeds", async () => { mocks.core.getInput .withArgs("webhook") .returns("https://hooks.slack.com"); mocks.core.getInput.withArgs("webhook-type").returns("incoming-webhook"); - mocks.core.getInput.withArgs("payload").returns("textt: oops"); - const response = new AxiosError( - "Request failed with status code 400", - "ERR_BAD_REQUEST", - {}, - {}, - { status: 400 }, + mocks.core.getInput.withArgs("payload").returns("text: hi"); + mocks.core.getInput.withArgs("retries").returns("RAPID"); + const err = Object.assign(new Error("An HTTP protocol error occurred"), { + code: "slack_webhook_http_error", + original: { response: { status: 503 } }, + }); + mocks.incomingWebhook.onFirstCall().rejects(err); + mocks.incomingWebhook.onSecondCall().resolves({ text: "ok" }); + await send(mocks.core); + assert.equal( + mocks.incomingWebhook.getCalls().length, + 2, + "5xx should be retried once", ); - mocks.axios.post.resolves(Promise.reject(response)); - try { - await send(mocks.core); - } catch (err) { - if (err instanceof SlackError) { - assert.ok( - err.message.includes("Request failed with status code 400"), - ); - } else { - assert.fail(err); - } - } - assert.equal(mocks.axios.post.getCalls().length, 1); - const [url, payload, options] = mocks.axios.post.getCall(0).args; - assert.equal(url, "https://hooks.slack.com"); - assert.deepEqual(payload, { textt: "oops" }); - assert.deepEqual(options, {}); - assert.equal(mocks.core.setOutput.getCall(0).firstArg, "ok"); - assert.equal(mocks.core.setOutput.getCall(0).lastArg, false); - assert.equal(mocks.core.setOutput.getCall(1).firstArg, "response"); + assert.equal(mocks.core.setOutput.getCall(0).lastArg, true); }); }); describe("proxies", () => { - it("requires a webhook is included in the inputs", async () => { - /** - * @type {Config} - */ - const config = { - core: mocks.core, - inputs: {}, - }; - try { - new Webhook().proxies(config); - assert.fail("Failed to throw for missing input"); - } catch (err) { - if (err instanceof SlackError) { - assert.ok( - err.message.includes("No webhook was provided to proxy to"), - ); - } else { - assert.fail(err); - } - } - }); - - it("skips proxying an http webhook url altogether", async () => { - mocks.core.getInput.withArgs("webhook").returns("http://hooks.slack.com"); - mocks.core.getInput.withArgs("webhook-type").returns("incoming-webhook"); - mocks.core.getInput.withArgs("proxy").returns("https://example.com"); - const config = new Config(mocks.core); - const webhook = new Webhook(); - const request = webhook.proxies(config); - assert.strictEqual(request, undefined); - }); - - it("sets up the proxy agent for the provided https proxy", async () => { - const proxy = "https://example.com"; + it("returns undefined when no proxy is set", async () => { mocks.core.getInput .withArgs("webhook") .returns("https://hooks.slack.com"); mocks.core.getInput.withArgs("webhook-type").returns("incoming-webhook"); - mocks.core.getInput.withArgs("proxy").returns(proxy); const config = new Config(mocks.core); const webhook = new Webhook(); - const { httpsAgent, proxy: proxying } = webhook.proxies(config); - assert.deepEqual(httpsAgent.proxy, new URL(proxy)); - assert.notStrictEqual(proxying, false); + assert.strictEqual(webhook.proxies(config), undefined); }); - it("sets up the agent without proxy for http proxies", async () => { - const proxy = "http://example.com"; + it("sets up the proxy agent for the provided https proxy", async () => { + const proxy = "https://example.com"; mocks.core.getInput .withArgs("webhook") .returns("https://hooks.slack.com"); @@ -218,9 +145,8 @@ describe("webhook", () => { mocks.core.getInput.withArgs("proxy").returns(proxy); const config = new Config(mocks.core); const webhook = new Webhook(); - const { httpsAgent, proxy: proxying } = webhook.proxies(config); + const { httpsAgent } = webhook.proxies(config); assert.deepEqual(httpsAgent.proxy, new URL(proxy)); - assert.strictEqual(proxying, false); }); it("fails to configure proxies with an invalid proxied url", async () => { @@ -245,102 +171,33 @@ describe("webhook", () => { } } }); - - it("fails to configure proxies with an unknown url protocol", async () => { - const proxy = "ssh://"; - mocks.core.getInput - .withArgs("webhook") - .returns("https://hooks.slack.com"); - mocks.core.getInput.withArgs("webhook-type").returns("incoming-webhook"); - mocks.core.getInput.withArgs("proxy").returns(proxy); - try { - const config = new Config(mocks.core); - const webhook = new Webhook(); - webhook.proxies(config); - assert.fail("An unknown URL protocol was not thrown as error!"); - } catch (err) { - if (err instanceof SlackError) { - assert.ok( - err.message.includes("Failed to configure the HTTPS proxy"), - ); - assert.ok(err.cause.message.includes("Unsupported URL protocol")); - } else { - assert.fail(err); - } - } - }); }); describe("retries", () => { - it("uses a default of five retries in requests", async () => { - const webhook = new Webhook(); - const result = webhook.retries(); + it("uses a default of five retries", async () => { + const result = new Webhook().retries(); assert.equal(result.retries, 5); }); it('does not attempt retries when "0" is set', async () => { - const webhook = new Webhook(); - const result = webhook.retries("0"); + const result = new Webhook().retries("0"); assert.equal(result.retries, 0); }); - it('attempts a default amount of "5" retries', async () => { - const webhook = new Webhook(); - const result = webhook.retries("5"); + it('maps "5" to a five-retry policy', async () => { + const result = new Webhook().retries("5"); assert.equal(result.retries, 5); - if (!result.retryDelay) { - assert.fail("No retry delay found!"); - } - assert.equal( - result.retryDelay(5, mocks.errors.axios.network_failed), - 300000, - "5th retry after 5 seconds", - ); }); - it('attempts "10" retries in around "30" minutes', async () => { - const webhook = new Webhook(); - const result = webhook.retries("10"); + it('maps "10" to a ten-retry policy', async () => { + const result = new Webhook().retries("10"); assert.equal(result.retries, 10); - if (!result.retryDelay) { - assert.fail("No retry delay found!"); - } - assert.ok( - result.retryDelay(10, mocks.errors.axios.network_failed) > 1800000, - "last attempt is around 30 minutes after starting", - ); - assert.ok( - result.retryDelay(10, mocks.errors.axios.network_failed) < 3600000, - "last attempt is no more than an hour later", - ); }); - it('attempts a " rapid" burst of "12" retries in seconds', async () => { - const webhook = new Webhook(); - const result = webhook.retries(" rapid"); - assert.equal(result.retries, 12); - if (!result.retryDelay) { - assert.fail("No retry delay found!"); - } - assert.equal( - result.retryDelay(12, mocks.errors.axios.network_failed), - 12000, - "12th retry after 12 seconds", - ); - }); - - it('attempts a "RAPID" burst of "12" retries in seconds', async () => { - const webhook = new Webhook(); - const result = webhook.retries("RAPID"); - assert.equal(result.retries, 12); - if (!result.retryDelay) { - assert.fail("No retry delay found!"); - } - assert.equal( - result.retryDelay(12, mocks.errors.axios.network_failed), - 12000, - "12th retry after 12 seconds", - ); + it('maps "RAPID" (case/space-insensitive) to a rapid policy', async () => { + const rapid = new Webhook().retries("RAPID"); + const spaced = new Webhook().retries(" rapid"); + assert.deepEqual(rapid, spaced); }); }); }); From 074e58fa6f80b9a26f7cf1249cfe7a93e5a94982 Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Tue, 30 Jun 2026 04:08:39 -0700 Subject: [PATCH 05/42] feat: post webhooks via @slack/webhook with action-side retries Co-Authored-By: Claude --- src/webhook.js | 190 +++++++++++++++++++++++++++----------------- test/config.spec.js | 14 ---- test/index.spec.js | 16 ---- test/send.spec.js | 6 +- 4 files changed, 117 insertions(+), 109 deletions(-) diff --git a/src/webhook.js b/src/webhook.js index 0d6df7d4..91226491 100644 --- a/src/webhook.js +++ b/src/webhook.js @@ -1,11 +1,15 @@ -import axiosRetry, { exponentialDelay, linearDelay } from "axios-retry"; +import webapi from "@slack/web-api"; +import { ErrorCode, IncomingWebhook, WebhookTrigger } from "@slack/webhook"; import { HttpsProxyAgent } from "https-proxy-agent"; +import pRetry, { AbortError } from "p-retry"; import Config from "./config.js"; import SlackError from "./errors.js"; /** - * This Webhook class posts the configured payload to the provided webhook, with - * whatever additional settings set. + * The Webhook class posts the configured payload to the provided webhook using + * the @slack/webhook SDK, choosing the client by the configured webhook type. + * + * @see {@link https://docs.slack.dev/tools/node-slack-sdk/webhook/} */ export default class Webhook { /** @@ -15,69 +19,121 @@ export default class Webhook { if (!config.inputs.webhook) { throw new SlackError(config.core, "No webhook was provided to post to"); } - /** - * @type {import("axios-retry").IAxiosRetryConfig} - * @see {@link https://www.npmjs.com/package/axios-retry} - */ - const retries = this.retries(config.inputs.retries); - axiosRetry(config.axios, retries); + switch (config.inputs.webhookType) { + case "incoming-webhook": + return await this.postIncomingWebhook(config); + case "webhook-trigger": + return await this.postWebhookTrigger(config); + default: + throw new SlackError( + config.core, + `Unknown webhook type: ${config.inputs.webhookType}`, + ); + } + } + + /** + * Post using the @slack/webhook IncomingWebhook client. + * @param {Config} config + */ + async postIncomingWebhook(config) { + const webhook = new IncomingWebhook( + /** @type {string} */ (config.inputs.webhook), + { agent: this.proxies(config)?.httpsAgent }, + ); try { - const response = await config.axios.post( - config.inputs.webhook, - config.content.values, - { - ...this.proxies(config), - }, + const response = await this.send(config, () => + webhook.send(config.content.values), ); - config.core.setOutput("ok", response.status === 200); - config.core.setOutput("response", JSON.stringify(response.data)); - config.core.debug(JSON.stringify(response.data)); + config.core.setOutput("ok", true); + config.core.setOutput("response", JSON.stringify(response.text)); + config.core.debug(JSON.stringify(response.text)); } catch (/** @type {any} */ err) { - const response = err.toJSON(); - config.core.setOutput("ok", response.status === 200); - config.core.setOutput("response", JSON.stringify(response.message)); - config.core.debug(response); - throw new SlackError(config.core, response.message); + config.core.setOutput("ok", false); + config.core.setOutput("response", JSON.stringify(err.message)); + config.core.debug(err); + throw new SlackError(config.core, err.message); } } /** - * Return configurations for http proxy options if these are set. + * Post using the @slack/webhook WebhookTrigger client. * @param {Config} config - * @returns {import("axios").AxiosRequestConfig | undefined} - * @see {@link https://github.com/slackapi/slack-github-action/pull/132} */ - proxies(config) { - const { webhook, proxy } = config.inputs; - if (!webhook) { - throw new SlackError(config.core, "No webhook was provided to proxy to"); + async postWebhookTrigger(config) { + const trigger = new WebhookTrigger( + /** @type {string} */ (config.inputs.webhook), + { agent: this.proxies(config)?.httpsAgent }, + ); + try { + const response = await this.send(config, () => + trigger.send(config.content.values), + ); + config.core.setOutput("ok", response.ok); + config.core.setOutput("response", JSON.stringify(response.body)); + config.core.debug(JSON.stringify(response.body)); + } catch (/** @type {any} */ err) { + config.core.setOutput("ok", false); + config.core.setOutput("response", JSON.stringify(err.message)); + config.core.debug(err); + throw new SlackError(config.core, err.message); } - if (!proxy) { - return undefined; + } + + /** + * Invoke a webhook send with retries, aborting on non-retryable errors. + * @template T + * @param {Config} config + * @param {() => Promise} attempt - the SDK send call to retry. + * @returns {Promise} + */ + async send(config, attempt) { + return await pRetry(async () => { + try { + return await attempt(); + } catch (/** @type {any} */ err) { + if (this.retryable(err)) { + throw err; + } + throw new AbortError(err); + } + }, this.retries(config.inputs.retries)); + } + + /** + * Decide if a @slack/webhook error should be retried. + * + * Request errors (no response received) are always retried; HTTP errors are + * retried only for rate limits and server errors. + * @param {any} err + * @returns {boolean} + */ + retryable(err) { + if (err?.code === ErrorCode.RequestError) { + return true; } + if (err?.code === ErrorCode.HTTPError) { + const status = err?.original?.response?.status; + return status === 429 || (status >= 500 && status <= 599); + } + return true; + } + + /** + * Return configurations for https proxy options if these are set. + * @param {Config} config + * @returns {{ httpsAgent: HttpsProxyAgent } | undefined} + * @see {@link https://github.com/slackapi/slack-github-action/pull/205} + */ + proxies(config) { + const proxy = config.inputs.proxy; try { - if (new URL(webhook).protocol !== "https:") { - config.core.debug( - "The webhook destination is not HTTPS so skipping the HTTPS proxy", - ); + if (!proxy) { return undefined; } - switch (new URL(proxy).protocol) { - case "https:": - return { - httpsAgent: new HttpsProxyAgent(proxy), - }; - case "http:": - return { - httpsAgent: new HttpsProxyAgent(proxy), - proxy: false, - }; - default: - throw new SlackError( - config.core, - `Unsupported URL protocol: ${proxy}`, - ); - } + return { + httpsAgent: new HttpsProxyAgent(proxy), + }; } catch (/** @type {any} */ err) { throw new SlackError(config.core, "Failed to configure the HTTPS proxy", { cause: err, @@ -86,38 +142,22 @@ export default class Webhook { } /** - * Return configurations for retry options with different delays. - * @param {string} option - * @returns {import("axios-retry").IAxiosRetryConfig} + * Map the retries input to a p-retry / node-retry policy. + * @param {string} [option] + * @returns {import("@slack/web-api").RetryOptions} */ retries(option) { switch (option?.trim().toUpperCase()) { case "0": return { retries: 0 }; case "5": - return { - retryCondition: axiosRetry.isRetryableError, - retries: 5, - retryDelay: linearDelay(60 * 1000), // 5 minutes - }; + return webapi.retryPolicies.fiveRetriesInFiveMinutes; case "10": - return { - retryCondition: axiosRetry.isRetryableError, - retries: 10, - retryDelay: (count, err) => exponentialDelay(count, err, 2 * 1000), // 34.12 minutes - }; + return webapi.retryPolicies.tenRetriesInAboutThirtyMinutes; case "RAPID": - return { - retryCondition: axiosRetry.isRetryableError, - retries: 12, - retryDelay: linearDelay(1 * 1000), // 12 seconds - }; + return webapi.retryPolicies.rapidRetryPolicy; default: - return { - retryCondition: axiosRetry.isRetryableError, - retries: 5, - retryDelay: linearDelay(60 * 1000), // 5 minutes - }; + return webapi.retryPolicies.fiveRetriesInFiveMinutes; } } } diff --git a/test/config.spec.js b/test/config.spec.js index a0991e25..c582e514 100644 --- a/test/config.spec.js +++ b/test/config.spec.js @@ -184,20 +184,6 @@ describe("config", () => { } }); - it("adds web-api app metadata with package name and version", async () => { - mocks.core.getInput.withArgs("method").returns("chat.postMessage"); - mocks.core.getInput.withArgs("token").returns("xoxb-example"); - const addAppMetadata = mocks.sandbox.stub(webapi, "addAppMetadata"); - try { - new Config(mocks.core); - assert.ok(addAppMetadata.called, "addAppMetadata was not called"); - const arg = addAppMetadata.getCall(0).firstArg; - assert.ok(arg.name.includes("slack-github-action")); - assert.ok(arg.version.length > 0); - } finally { - addAppMetadata.restore(); - } - }); }); describe("mask", async () => { diff --git a/test/index.spec.js b/test/index.spec.js index 3ac95772..929af4ca 100644 --- a/test/index.spec.js +++ b/test/index.spec.js @@ -1,7 +1,6 @@ import fs from "node:fs"; import webapi from "@slack/web-api"; import { IncomingWebhook, WebhookTrigger } from "@slack/webhook"; -import { AxiosError } from "axios"; import sinon from "sinon"; /** @@ -20,21 +19,6 @@ import sinon from "sinon"; * The Mock class sets expected behaviors and test listeners for dependencies. */ export class Mock { - /** - * @typedef Errors - A collection of mocked errors to use in tests. - * @prop {Object.} axios - The mocked axios errors. - */ - - /** - * The mocked errors. - * @type {Errors} - */ - errors = { - axios: { - network_failed: new AxiosError("network_failed"), - }, - }; - /** * Setup stubbed dependencies and configure default input arguments for all * tests. diff --git a/test/send.spec.js b/test/send.spec.js index 9ab906ee..eef7111d 100644 --- a/test/send.spec.js +++ b/test/send.spec.js @@ -24,9 +24,7 @@ describe("send", () => { .returns("https://hooks.slack.com"); mocks.core.getInput.withArgs("webhook-type").returns("webhook-trigger"); mocks.core.getInput.withArgs("payload").returns('"greetings": "hello"'); - mocks.axios.post.returns( - Promise.resolve({ status: 200, data: { ok: true } }), - ); + mocks.webhookTrigger.resolves({ ok: true, body: { ok: true } }); await send(mocks.core); assert.equal(mocks.core.setOutput.getCall(0).firstArg, "ok"); assert.equal(mocks.core.setOutput.getCall(0).lastArg, true); @@ -64,7 +62,7 @@ describe("send", () => { .returns("https://hooks.slack.com"); mocks.core.getInput.withArgs("webhook-type").returns("incoming-webhook"); mocks.core.getInput.withArgs("payload").returns('"text": "hello"'); - mocks.axios.post.returns(Promise.resolve({ status: 200, data: "ok" })); + mocks.incomingWebhook.resolves({ text: "ok" }); await send(mocks.core); assert.equal(mocks.core.setOutput.getCall(0).firstArg, "ok"); assert.equal(mocks.core.setOutput.getCall(0).lastArg, true); From 12111a6862a683811c10888e203544422b643747 Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Tue, 30 Jun 2026 04:09:08 -0700 Subject: [PATCH 06/42] test: drop trailing blank after removing duplicate metadata test Co-Authored-By: Claude --- test/config.spec.js | 1 - 1 file changed, 1 deletion(-) diff --git a/test/config.spec.js b/test/config.spec.js index c582e514..b51310b1 100644 --- a/test/config.spec.js +++ b/test/config.spec.js @@ -183,7 +183,6 @@ describe("config", () => { Object.defineProperty(webapi, "addAppMetadata", original); } }); - }); describe("mask", async () => { From f0480dfd5442a049e896f139b04a151271e0ccfb Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Thu, 2 Jul 2026 01:53:30 -0700 Subject: [PATCH 07/42] refactor: delegate webhook retries to @slack/webhook SDK Retries now live in the @slack/webhook package via retryConfig, matching how the API-method technique passes retryConfig to @slack/web-api. The action maps its 0/5/10/RAPID input to the SDK's exported retry policies. Co-Authored-By: Claude --- src/webhook.js | 82 ++++++++++++++------------------------------ test/webhook.spec.js | 42 +++++++++-------------- 2 files changed, 42 insertions(+), 82 deletions(-) diff --git a/src/webhook.js b/src/webhook.js index 91226491..b1ca916c 100644 --- a/src/webhook.js +++ b/src/webhook.js @@ -1,7 +1,11 @@ -import webapi from "@slack/web-api"; -import { ErrorCode, IncomingWebhook, WebhookTrigger } from "@slack/webhook"; +import { + fiveRetriesInFiveMinutes, + IncomingWebhook, + rapidRetryPolicy, + tenRetriesInAboutThirtyMinutes, + WebhookTrigger, +} from "@slack/webhook"; import { HttpsProxyAgent } from "https-proxy-agent"; -import pRetry, { AbortError } from "p-retry"; import Config from "./config.js"; import SlackError from "./errors.js"; @@ -9,6 +13,9 @@ import SlackError from "./errors.js"; * The Webhook class posts the configured payload to the provided webhook using * the @slack/webhook SDK, choosing the client by the configured webhook type. * + * Retries are delegated to the SDK via the retryConfig option, matching how + * the API-method technique passes retryConfig to @slack/web-api's WebClient. + * * @see {@link https://docs.slack.dev/tools/node-slack-sdk/webhook/} */ export default class Webhook { @@ -39,12 +46,13 @@ export default class Webhook { async postIncomingWebhook(config) { const webhook = new IncomingWebhook( /** @type {string} */ (config.inputs.webhook), - { agent: this.proxies(config)?.httpsAgent }, + { + agent: this.proxies(config)?.httpsAgent, + retryConfig: this.retries(config.inputs.retries), + }, ); try { - const response = await this.send(config, () => - webhook.send(config.content.values), - ); + const response = await webhook.send(config.content.values); config.core.setOutput("ok", true); config.core.setOutput("response", JSON.stringify(response.text)); config.core.debug(JSON.stringify(response.text)); @@ -63,12 +71,13 @@ export default class Webhook { async postWebhookTrigger(config) { const trigger = new WebhookTrigger( /** @type {string} */ (config.inputs.webhook), - { agent: this.proxies(config)?.httpsAgent }, + { + agent: this.proxies(config)?.httpsAgent, + retryConfig: this.retries(config.inputs.retries), + }, ); try { - const response = await this.send(config, () => - trigger.send(config.content.values), - ); + const response = await trigger.send(config.content.values); config.core.setOutput("ok", response.ok); config.core.setOutput("response", JSON.stringify(response.body)); config.core.debug(JSON.stringify(response.body)); @@ -80,45 +89,6 @@ export default class Webhook { } } - /** - * Invoke a webhook send with retries, aborting on non-retryable errors. - * @template T - * @param {Config} config - * @param {() => Promise} attempt - the SDK send call to retry. - * @returns {Promise} - */ - async send(config, attempt) { - return await pRetry(async () => { - try { - return await attempt(); - } catch (/** @type {any} */ err) { - if (this.retryable(err)) { - throw err; - } - throw new AbortError(err); - } - }, this.retries(config.inputs.retries)); - } - - /** - * Decide if a @slack/webhook error should be retried. - * - * Request errors (no response received) are always retried; HTTP errors are - * retried only for rate limits and server errors. - * @param {any} err - * @returns {boolean} - */ - retryable(err) { - if (err?.code === ErrorCode.RequestError) { - return true; - } - if (err?.code === ErrorCode.HTTPError) { - const status = err?.original?.response?.status; - return status === 429 || (status >= 500 && status <= 599); - } - return true; - } - /** * Return configurations for https proxy options if these are set. * @param {Config} config @@ -142,22 +112,22 @@ export default class Webhook { } /** - * Map the retries input to a p-retry / node-retry policy. + * Map the retries input to a @slack/webhook retry policy. * @param {string} [option] - * @returns {import("@slack/web-api").RetryOptions} + * @returns {import("@slack/webhook").RetryOptions} */ retries(option) { switch (option?.trim().toUpperCase()) { case "0": return { retries: 0 }; case "5": - return webapi.retryPolicies.fiveRetriesInFiveMinutes; + return fiveRetriesInFiveMinutes; case "10": - return webapi.retryPolicies.tenRetriesInAboutThirtyMinutes; + return tenRetriesInAboutThirtyMinutes; case "RAPID": - return webapi.retryPolicies.rapidRetryPolicy; + return rapidRetryPolicy; default: - return webapi.retryPolicies.fiveRetriesInFiveMinutes; + return fiveRetriesInFiveMinutes; } } } diff --git a/test/webhook.spec.js b/test/webhook.spec.js index 8a328fd5..80832b74 100644 --- a/test/webhook.spec.js +++ b/test/webhook.spec.js @@ -76,52 +76,42 @@ describe("webhook", () => { } }); - it("does not retry a 4xx from a webhook trigger", async () => { + it("returns the failures from a webhook trigger", async () => { mocks.core.getInput .withArgs("webhook") .returns("https://hooks.slack.com"); mocks.core.getInput.withArgs("webhook-type").returns("webhook-trigger"); mocks.core.getInput.withArgs("payload").returns("drinks: coffee"); - mocks.core.getInput.withArgs("retries").returns("RAPID"); - const err = Object.assign(new Error("An HTTP protocol error occurred"), { - code: "slack_webhook_http_error", - original: { response: { status: 400 } }, - }); - mocks.webhookTrigger.rejects(err); + mocks.webhookTrigger.rejects( + new Error("An HTTP protocol error occurred"), + ); try { await send(mocks.core); } catch (e) { assert.ok(e instanceof SlackError); } - assert.equal( - mocks.webhookTrigger.getCalls().length, - 1, - "4xx must not be retried", - ); + assert.equal(mocks.webhookTrigger.getCalls().length, 1); assert.equal(mocks.core.setOutput.getCall(0).firstArg, "ok"); assert.equal(mocks.core.setOutput.getCall(0).lastArg, false); }); - it("retries a 5xx from an incoming webhook then succeeds", async () => { + it("returns the failures from an incoming webhook", async () => { mocks.core.getInput .withArgs("webhook") .returns("https://hooks.slack.com"); mocks.core.getInput.withArgs("webhook-type").returns("incoming-webhook"); mocks.core.getInput.withArgs("payload").returns("text: hi"); - mocks.core.getInput.withArgs("retries").returns("RAPID"); - const err = Object.assign(new Error("An HTTP protocol error occurred"), { - code: "slack_webhook_http_error", - original: { response: { status: 503 } }, - }); - mocks.incomingWebhook.onFirstCall().rejects(err); - mocks.incomingWebhook.onSecondCall().resolves({ text: "ok" }); - await send(mocks.core); - assert.equal( - mocks.incomingWebhook.getCalls().length, - 2, - "5xx should be retried once", + mocks.incomingWebhook.rejects( + new Error("An HTTP protocol error occurred"), ); - assert.equal(mocks.core.setOutput.getCall(0).lastArg, true); + try { + await send(mocks.core); + } catch (e) { + assert.ok(e instanceof SlackError); + } + assert.equal(mocks.incomingWebhook.getCalls().length, 1); + assert.equal(mocks.core.setOutput.getCall(0).firstArg, "ok"); + assert.equal(mocks.core.setOutput.getCall(0).lastArg, false); }); }); From 08d6c7a088c99c39035a63b6a9f9ca54e76b5af7 Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Thu, 2 Jul 2026 01:54:11 -0700 Subject: [PATCH 08/42] build(deps): sync lockfile with @slack/webhook retry deps Co-Authored-By: Claude --- package-lock.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index daac52a3..c2f5cfe6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,7 +44,10 @@ "dependencies": { "@slack/types": "^2.20.1", "@types/node": ">=18", - "axios": "^1.16.0" + "@types/retry": "0.12.0", + "axios": "^1.16.0", + "p-retry": "^4", + "retry": "^0.13.1" }, "devDependencies": { "nock": "^14.0.6" From 90399dab566148eaa20122c033fbeccfa4bb3038 Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Mon, 6 Jul 2026 21:49:23 -0700 Subject: [PATCH 09/42] feat: instrument @slack/webhook requests with the action User-Agent The action already registered its app metadata with @slack/web-api via addAppMetadata, but webhook requests went out without the action's identifier. @slack/webhook now exports the same addAppMetadata API, so register the package name and version with it too using the shared package.json values. Adds a test covering the webhook registration. Co-Authored-By: Claude --- src/config.js | 14 +++++++++++--- test/config.spec.js | 24 ++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/src/config.js b/src/config.js index e80ee71f..df59cce6 100644 --- a/src/config.js +++ b/src/config.js @@ -1,4 +1,5 @@ import webapi from "@slack/web-api"; +import webhook from "@slack/webhook"; import packageJson from "../package.json" with { type: "json" }; import Content from "./content.js"; import SlackError from "./errors.js"; @@ -80,6 +81,11 @@ export default class Config { */ webapi; + /** + * @type {import("@slack/webhook")} - Slack webhook client. + */ + webhook; + /** * Gather values from the job inputs and use defaults or error for the missing * ones. @@ -94,6 +100,7 @@ export default class Config { this.core = core; this.logger = new Logger(core).logger; this.webapi = webapi; + this.webhook = webhook; this.inputs = { api: core.getInput("api"), errors: core.getBooleanInput("errors"), @@ -123,15 +130,16 @@ export default class Config { /** * Add user agent metadata for instrumentation. - * - * The @slack/webhook and @slack/web-api SDKs set their own User-Agent - * headers, so the action only registers app metadata with web-api here. */ instrument() { this.webapi.addAppMetadata({ name: packageJson.name, version: packageJson.version, }); + this.webhook.addAppMetadata({ + name: packageJson.name, + version: packageJson.version, + }); } /** diff --git a/test/config.spec.js b/test/config.spec.js index b51310b1..c36e8a9a 100644 --- a/test/config.spec.js +++ b/test/config.spec.js @@ -1,6 +1,7 @@ import assert from "node:assert"; import { beforeEach, describe, it } from "node:test"; import webapi from "@slack/web-api"; +import webhook from "@slack/webhook"; import sinon from "sinon"; import Config from "../src/config.js"; import SlackError from "../src/errors.js"; @@ -183,6 +184,29 @@ describe("config", () => { Object.defineProperty(webapi, "addAppMetadata", original); } }); + + it("adds metadata to webhook with package name and version", async () => { + const stub = sinon.stub(); + const original = Object.getOwnPropertyDescriptor( + webhook, + "addAppMetadata", + ); + Object.defineProperty(webhook, "addAppMetadata", { + value: stub, + configurable: true, + }); + try { + mocks.core.getInput.withArgs("method").returns("chat.postMessage"); + mocks.core.getInput.withArgs("token").returns("xoxb-example"); + new Config(mocks.core); + assert.ok(stub.calledOnce); + const { name, version } = stub.firstCall.args[0]; + assert.equal(name, "@slack/slack-github-action"); + assert.ok(version); + } finally { + Object.defineProperty(webhook, "addAppMetadata", original); + } + }); }); describe("mask", async () => { From 629012b19fde414db0eb2359ba608c9111faf509 Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Mon, 6 Jul 2026 22:03:30 -0700 Subject: [PATCH 10/42] fix: restore proxy protocol validation in webhook proxies() The SDK migration collapsed proxies() to pass any proxy URL straight to HttpsProxyAgent, dropping the http:/https: protocol check that main had. Restore the switch so an unsupported proxy protocol throws the same 'Unsupported URL protocol' error instead of leaking a raw agent failure. Both http: and https: proxies map to a single HttpsProxyAgent; the old axios-only 'proxy: false' branch is unneeded since @slack/webhook sets proxy: false on its own axios instance. Co-Authored-By: Claude --- src/webhook.js | 15 ++++++++++++--- test/webhook.spec.js | 24 ++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/src/webhook.js b/src/webhook.js index b1ca916c..b1b1369c 100644 --- a/src/webhook.js +++ b/src/webhook.js @@ -101,9 +101,18 @@ export default class Webhook { if (!proxy) { return undefined; } - return { - httpsAgent: new HttpsProxyAgent(proxy), - }; + switch (new URL(proxy).protocol) { + case "https:": + case "http:": + return { + httpsAgent: new HttpsProxyAgent(proxy), + }; + default: + throw new SlackError( + config.core, + `Unsupported URL protocol: ${proxy}`, + ); + } } catch (/** @type {any} */ err) { throw new SlackError(config.core, "Failed to configure the HTTPS proxy", { cause: err, diff --git a/test/webhook.spec.js b/test/webhook.spec.js index 80832b74..bada256d 100644 --- a/test/webhook.spec.js +++ b/test/webhook.spec.js @@ -161,6 +161,30 @@ describe("webhook", () => { } } }); + + it("fails to configure proxies with an unsupported protocol", async () => { + const proxy = "ftp://example.com"; + mocks.core.getInput + .withArgs("webhook") + .returns("https://hooks.slack.com"); + mocks.core.getInput.withArgs("webhook-type").returns("incoming-webhook"); + mocks.core.getInput.withArgs("proxy").returns(proxy); + try { + const config = new Config(mocks.core); + const webhook = new Webhook(); + webhook.proxies(config); + assert.fail("An unsupported proxy protocol was not thrown as error!"); + } catch (err) { + if (err instanceof SlackError) { + assert.ok( + err.message.includes("Failed to configure the HTTPS proxy"), + ); + assert.ok(err.cause?.message?.includes("Unsupported URL protocol")); + } else { + assert.fail(err); + } + } + }); }); describe("retries", () => { From 78332d841b7c8591114a8c4a5e6957fffd9b0f5a Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Mon, 6 Jul 2026 22:30:46 -0700 Subject: [PATCH 11/42] refactor: build webhook clients via config.webhook in post() Collapse postIncomingWebhook/postWebhookTrigger into a single post(): construct each client through the config.webhook module namespace, send, and write outputs inline within the type switch, sharing the options and one try/catch. Drops the direct IncomingWebhook/WebhookTrigger imports and the redundant webhook URL type cast. Co-Authored-By: Claude --- src/webhook.js | 88 ++++++++++++++++++-------------------------------- 1 file changed, 32 insertions(+), 56 deletions(-) diff --git a/src/webhook.js b/src/webhook.js index b1b1369c..e77381b4 100644 --- a/src/webhook.js +++ b/src/webhook.js @@ -1,9 +1,7 @@ import { fiveRetriesInFiveMinutes, - IncomingWebhook, rapidRetryPolicy, tenRetriesInAboutThirtyMinutes, - WebhookTrigger, } from "@slack/webhook"; import { HttpsProxyAgent } from "https-proxy-agent"; import Config from "./config.js"; @@ -26,61 +24,39 @@ export default class Webhook { if (!config.inputs.webhook) { throw new SlackError(config.core, "No webhook was provided to post to"); } - switch (config.inputs.webhookType) { - case "incoming-webhook": - return await this.postIncomingWebhook(config); - case "webhook-trigger": - return await this.postWebhookTrigger(config); - default: - throw new SlackError( - config.core, - `Unknown webhook type: ${config.inputs.webhookType}`, - ); - } - } - - /** - * Post using the @slack/webhook IncomingWebhook client. - * @param {Config} config - */ - async postIncomingWebhook(config) { - const webhook = new IncomingWebhook( - /** @type {string} */ (config.inputs.webhook), - { - agent: this.proxies(config)?.httpsAgent, - retryConfig: this.retries(config.inputs.retries), - }, - ); - try { - const response = await webhook.send(config.content.values); - config.core.setOutput("ok", true); - config.core.setOutput("response", JSON.stringify(response.text)); - config.core.debug(JSON.stringify(response.text)); - } catch (/** @type {any} */ err) { - config.core.setOutput("ok", false); - config.core.setOutput("response", JSON.stringify(err.message)); - config.core.debug(err); - throw new SlackError(config.core, err.message); - } - } - - /** - * Post using the @slack/webhook WebhookTrigger client. - * @param {Config} config - */ - async postWebhookTrigger(config) { - const trigger = new WebhookTrigger( - /** @type {string} */ (config.inputs.webhook), - { - agent: this.proxies(config)?.httpsAgent, - retryConfig: this.retries(config.inputs.retries), - }, - ); + const url = config.inputs.webhook; + const options = { + agent: this.proxies(config)?.httpsAgent, + retryConfig: this.retries(config.inputs.retries), + }; try { - const response = await trigger.send(config.content.values); - config.core.setOutput("ok", response.ok); - config.core.setOutput("response", JSON.stringify(response.body)); - config.core.debug(JSON.stringify(response.body)); + switch (config.inputs.webhookType) { + case "incoming-webhook": { + const response = await new config.webhook.IncomingWebhook( + url, + options, + ).send(config.content.values); + config.core.setOutput("ok", true); + config.core.setOutput("response", JSON.stringify(response.text)); + config.core.debug(JSON.stringify(response.text)); + break; + } + case "webhook-trigger": { + const response = await new config.webhook.WebhookTrigger( + url, + options, + ).send(config.content.values); + config.core.setOutput("ok", response.ok); + config.core.setOutput("response", JSON.stringify(response.body)); + config.core.debug(JSON.stringify(response.body)); + break; + } + default: + throw new SlackError( + config.core, + `Unknown webhook type: ${config.inputs.webhookType}`, + ); + } } catch (/** @type {any} */ err) { config.core.setOutput("ok", false); config.core.setOutput("response", JSON.stringify(err.message)); From 0a3e445dc70d399e543ba0db81bf501a5b8fd7c7 Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Mon, 6 Jul 2026 22:35:23 -0700 Subject: [PATCH 12/42] docs: restore retries() summary and required option param Co-Authored-By: Claude --- src/webhook.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/webhook.js b/src/webhook.js index e77381b4..da38d3dd 100644 --- a/src/webhook.js +++ b/src/webhook.js @@ -97,8 +97,8 @@ export default class Webhook { } /** - * Map the retries input to a @slack/webhook retry policy. - * @param {string} [option] + * Return configurations for retry options with different delays. + * @param {string} option * @returns {import("@slack/webhook").RetryOptions} */ retries(option) { From c0b1d99a65441914d0ea1d17f7bf7c2092289c46 Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Mon, 6 Jul 2026 22:38:49 -0700 Subject: [PATCH 13/42] fix: restore non-HTTPS webhook proxy skip in proxies() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The SDK migration (074e58f) dropped two behaviors main had: the guard that throws when no webhook is provided, and the skip that returns undefined (no proxy) when the webhook destination is not HTTPS. Restore both for parity — an HttpsProxyAgent only tunnels HTTPS destinations, so a non-HTTPS webhook should bypass the proxy. Adds a test for the skip. Co-Authored-By: Claude --- src/webhook.js | 13 +++++++++++-- test/webhook.spec.js | 9 +++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/webhook.js b/src/webhook.js index da38d3dd..0c29c015 100644 --- a/src/webhook.js +++ b/src/webhook.js @@ -72,9 +72,18 @@ export default class Webhook { * @see {@link https://github.com/slackapi/slack-github-action/pull/205} */ proxies(config) { - const proxy = config.inputs.proxy; + const { webhook, proxy } = config.inputs; + if (!webhook) { + throw new SlackError(config.core, "No webhook was provided to proxy to"); + } + if (!proxy) { + return undefined; + } try { - if (!proxy) { + if (new URL(webhook).protocol !== "https:") { + config.core.debug( + "The webhook destination is not HTTPS so skipping the HTTPS proxy", + ); return undefined; } switch (new URL(proxy).protocol) { diff --git a/test/webhook.spec.js b/test/webhook.spec.js index bada256d..83fb4ad5 100644 --- a/test/webhook.spec.js +++ b/test/webhook.spec.js @@ -139,6 +139,15 @@ describe("webhook", () => { assert.deepEqual(httpsAgent.proxy, new URL(proxy)); }); + it("skips the proxy when the webhook destination is not HTTPS", async () => { + mocks.core.getInput.withArgs("webhook").returns("http://hooks.slack.com"); + mocks.core.getInput.withArgs("webhook-type").returns("incoming-webhook"); + mocks.core.getInput.withArgs("proxy").returns("https://example.com"); + const config = new Config(mocks.core); + const webhook = new Webhook(); + assert.strictEqual(webhook.proxies(config), undefined); + }); + it("fails to configure proxies with an invalid proxied url", async () => { const proxy = "https://"; mocks.core.getInput From 081de9d8af7866e5e291d7dc3c91f16e49f08c7b Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Mon, 6 Jul 2026 22:39:32 -0700 Subject: [PATCH 14/42] docs: reference both proxy PRs in proxies() JSDoc Restore the @see link to #132 alongside #205 so both PRs that shaped the proxy behavior are documented on proxies(). Co-Authored-By: Claude --- src/webhook.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/webhook.js b/src/webhook.js index 0c29c015..831e00c4 100644 --- a/src/webhook.js +++ b/src/webhook.js @@ -69,6 +69,7 @@ export default class Webhook { * Return configurations for https proxy options if these are set. * @param {Config} config * @returns {{ httpsAgent: HttpsProxyAgent } | undefined} + * @see {@link https://github.com/slackapi/slack-github-action/pull/132} * @see {@link https://github.com/slackapi/slack-github-action/pull/205} */ proxies(config) { From be245752679fdaeb257f49072336b1f97027d43b Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Mon, 6 Jul 2026 22:40:23 -0700 Subject: [PATCH 15/42] refactor: return from post() switch cases instead of break Each case is the last action in post(), so return makes the intent explicit and guards against fall-through if code is added later. Co-Authored-By: Claude --- src/webhook.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/webhook.js b/src/webhook.js index 831e00c4..b40ed831 100644 --- a/src/webhook.js +++ b/src/webhook.js @@ -39,7 +39,7 @@ export default class Webhook { config.core.setOutput("ok", true); config.core.setOutput("response", JSON.stringify(response.text)); config.core.debug(JSON.stringify(response.text)); - break; + return; } case "webhook-trigger": { const response = await new config.webhook.WebhookTrigger( @@ -49,7 +49,7 @@ export default class Webhook { config.core.setOutput("ok", response.ok); config.core.setOutput("response", JSON.stringify(response.body)); config.core.debug(JSON.stringify(response.body)); - break; + return; } default: throw new SlackError( From 76060bab89db5998dbe27ecf526893abe9e9a67d Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Mon, 6 Jul 2026 22:44:41 -0700 Subject: [PATCH 16/42] refactor: return proxy agent directly from proxies() The only consumer immediately unwrapped .httpsAgent, so return the HttpsProxyAgent (or undefined) directly instead of wrapping it in an object. Simplifies the call site to agent: this.proxies(config). Co-Authored-By: Claude --- src/webhook.js | 10 ++++------ test/webhook.spec.js | 2 +- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/webhook.js b/src/webhook.js index b40ed831..6faebdf2 100644 --- a/src/webhook.js +++ b/src/webhook.js @@ -26,7 +26,7 @@ export default class Webhook { } const url = config.inputs.webhook; const options = { - agent: this.proxies(config)?.httpsAgent, + agent: this.proxies(config), retryConfig: this.retries(config.inputs.retries), }; try { @@ -66,9 +66,9 @@ export default class Webhook { } /** - * Return configurations for https proxy options if these are set. + * Return an https proxy agent if a proxy is set. * @param {Config} config - * @returns {{ httpsAgent: HttpsProxyAgent } | undefined} + * @returns {HttpsProxyAgent | undefined} * @see {@link https://github.com/slackapi/slack-github-action/pull/132} * @see {@link https://github.com/slackapi/slack-github-action/pull/205} */ @@ -90,9 +90,7 @@ export default class Webhook { switch (new URL(proxy).protocol) { case "https:": case "http:": - return { - httpsAgent: new HttpsProxyAgent(proxy), - }; + return new HttpsProxyAgent(proxy); default: throw new SlackError( config.core, diff --git a/test/webhook.spec.js b/test/webhook.spec.js index 83fb4ad5..f907f576 100644 --- a/test/webhook.spec.js +++ b/test/webhook.spec.js @@ -135,7 +135,7 @@ describe("webhook", () => { mocks.core.getInput.withArgs("proxy").returns(proxy); const config = new Config(mocks.core); const webhook = new Webhook(); - const { httpsAgent } = webhook.proxies(config); + const httpsAgent = webhook.proxies(config); assert.deepEqual(httpsAgent.proxy, new URL(proxy)); }); From f48a43755934d63fe503dd4b6654d027aa5499fd Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Mon, 6 Jul 2026 22:47:20 -0700 Subject: [PATCH 17/42] docs: restore original proxies() summary line Co-Authored-By: Claude --- src/webhook.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webhook.js b/src/webhook.js index 6faebdf2..32a02d77 100644 --- a/src/webhook.js +++ b/src/webhook.js @@ -66,7 +66,7 @@ export default class Webhook { } /** - * Return an https proxy agent if a proxy is set. + * Return configurations for http proxy options if these are set. * @param {Config} config * @returns {HttpsProxyAgent | undefined} * @see {@link https://github.com/slackapi/slack-github-action/pull/132} From 174408bd0876fd8bacc4010fa77d14d00d483b22 Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Mon, 6 Jul 2026 22:51:17 -0700 Subject: [PATCH 18/42] docs: trim Webhook class doc and say package over SDK Co-Authored-By: Claude --- src/webhook.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/webhook.js b/src/webhook.js index 32a02d77..968ed533 100644 --- a/src/webhook.js +++ b/src/webhook.js @@ -9,10 +9,8 @@ import SlackError from "./errors.js"; /** * The Webhook class posts the configured payload to the provided webhook using - * the @slack/webhook SDK, choosing the client by the configured webhook type. - * - * Retries are delegated to the SDK via the retryConfig option, matching how - * the API-method technique passes retryConfig to @slack/web-api's WebClient. + * the @slack/webhook package, choosing the client by the configured webhook + * type, with whatever additional settings set. * * @see {@link https://docs.slack.dev/tools/node-slack-sdk/webhook/} */ From 79632a19cf54b2b759f3c78dcd7ed75ef7cff552 Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Mon, 6 Jul 2026 22:52:02 -0700 Subject: [PATCH 19/42] docs: restore original Webhook class summary, keep reference link Co-Authored-By: Claude --- src/webhook.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/webhook.js b/src/webhook.js index 968ed533..0cc0c816 100644 --- a/src/webhook.js +++ b/src/webhook.js @@ -8,9 +8,8 @@ import Config from "./config.js"; import SlackError from "./errors.js"; /** - * The Webhook class posts the configured payload to the provided webhook using - * the @slack/webhook package, choosing the client by the configured webhook - * type, with whatever additional settings set. + * This Webhook class posts the configured payload to the provided webhook, with + * whatever additional settings set. * * @see {@link https://docs.slack.dev/tools/node-slack-sdk/webhook/} */ From f723656ad3d1eb0f12359eb54790b8a389539922 Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Mon, 6 Jul 2026 23:06:47 -0700 Subject: [PATCH 20/42] test: nest webhook stubs under a shared webhook mock Consolidate the incomingWebhook and webhookTrigger stubs into a single webhook object (webhook.incoming / webhook.trigger) across the specs. Co-Authored-By: Claude --- test/config.spec.js | 4 ++-- test/index.spec.js | 10 ++++++---- test/send.spec.js | 4 ++-- test/webhook.spec.js | 20 ++++++++++---------- 4 files changed, 20 insertions(+), 18 deletions(-) diff --git a/test/config.spec.js b/test/config.spec.js index c36e8a9a..fe7521b1 100644 --- a/test/config.spec.js +++ b/test/config.spec.js @@ -234,7 +234,7 @@ describe("config", () => { describe("validate", () => { it('allow the "retries" option with lowercased space', async () => { - mocks.incomingWebhook.resolves({ text: "LGTM" }); + mocks.webhook.incoming.resolves({ text: "LGTM" }); mocks.core.getInput.withArgs("retries").returns(" rapid "); mocks.core.getInput .withArgs("webhook") @@ -256,7 +256,7 @@ describe("config", () => { }); it("errors if an invalid retries option is provided", async () => { - mocks.incomingWebhook.resolves({ text: "LGTM" }); + mocks.webhook.incoming.resolves({ text: "LGTM" }); mocks.core.getInput.withArgs("retries").returns("FOREVER"); mocks.core.getInput .withArgs("webhook") diff --git a/test/index.spec.js b/test/index.spec.js index 929af4ca..2cb9c503 100644 --- a/test/index.spec.js +++ b/test/index.spec.js @@ -27,8 +27,10 @@ export class Mock { */ constructor() { this.sandbox = sinon.createSandbox(); - this.incomingWebhook = this.sandbox.stub(IncomingWebhook.prototype, "send"); - this.webhookTrigger = this.sandbox.stub(WebhookTrigger.prototype, "send"); + this.webhook = { + incoming: this.sandbox.stub(IncomingWebhook.prototype, "send"), + trigger: this.sandbox.stub(WebhookTrigger.prototype, "send"), + }; this.calls = this.sandbox.stub(webapi.WebClient.prototype, "apiCall"); this.core = { debug: this.sandbox.stub(), @@ -59,8 +61,8 @@ export class Mock { */ reset() { this.sandbox.reset(); - this.incomingWebhook.resetHistory(); - this.webhookTrigger.resetHistory(); + this.webhook.incoming.resetHistory(); + this.webhook.trigger.resetHistory(); this.calls.resetHistory(); this.core.debug.reset(); this.core.error.reset(); diff --git a/test/send.spec.js b/test/send.spec.js index eef7111d..f89ab5f9 100644 --- a/test/send.spec.js +++ b/test/send.spec.js @@ -24,7 +24,7 @@ describe("send", () => { .returns("https://hooks.slack.com"); mocks.core.getInput.withArgs("webhook-type").returns("webhook-trigger"); mocks.core.getInput.withArgs("payload").returns('"greetings": "hello"'); - mocks.webhookTrigger.resolves({ ok: true, body: { ok: true } }); + mocks.webhook.trigger.resolves({ ok: true, body: { ok: true } }); await send(mocks.core); assert.equal(mocks.core.setOutput.getCall(0).firstArg, "ok"); assert.equal(mocks.core.setOutput.getCall(0).lastArg, true); @@ -62,7 +62,7 @@ describe("send", () => { .returns("https://hooks.slack.com"); mocks.core.getInput.withArgs("webhook-type").returns("incoming-webhook"); mocks.core.getInput.withArgs("payload").returns('"text": "hello"'); - mocks.incomingWebhook.resolves({ text: "ok" }); + mocks.webhook.incoming.resolves({ text: "ok" }); await send(mocks.core); assert.equal(mocks.core.setOutput.getCall(0).firstArg, "ok"); assert.equal(mocks.core.setOutput.getCall(0).lastArg, true); diff --git a/test/webhook.spec.js b/test/webhook.spec.js index f907f576..fa9931c2 100644 --- a/test/webhook.spec.js +++ b/test/webhook.spec.js @@ -18,10 +18,10 @@ describe("webhook", () => { .returns("https://hooks.slack.com"); mocks.core.getInput.withArgs("webhook-type").returns("webhook-trigger"); mocks.core.getInput.withArgs("payload").returns("drinks: coffee"); - mocks.webhookTrigger.resolves({ ok: true, body: { ok: true } }); + mocks.webhook.trigger.resolves({ ok: true, body: { ok: true } }); await send(mocks.core); - assert.equal(mocks.webhookTrigger.getCalls().length, 1); - assert.deepEqual(mocks.webhookTrigger.getCall(0).firstArg, { + assert.equal(mocks.webhook.trigger.getCalls().length, 1); + assert.deepEqual(mocks.webhook.trigger.getCall(0).firstArg, { drinks: "coffee", }); assert.equal(mocks.core.setOutput.getCall(0).firstArg, "ok"); @@ -39,10 +39,10 @@ describe("webhook", () => { .returns("https://hooks.slack.com"); mocks.core.getInput.withArgs("webhook-type").returns("incoming-webhook"); mocks.core.getInput.withArgs("payload").returns("text: greetings"); - mocks.incomingWebhook.resolves({ text: "ok" }); + mocks.webhook.incoming.resolves({ text: "ok" }); await send(mocks.core); - assert.equal(mocks.incomingWebhook.getCalls().length, 1); - assert.deepEqual(mocks.incomingWebhook.getCall(0).firstArg, { + assert.equal(mocks.webhook.incoming.getCalls().length, 1); + assert.deepEqual(mocks.webhook.incoming.getCall(0).firstArg, { text: "greetings", }); assert.equal(mocks.core.setOutput.getCall(0).firstArg, "ok"); @@ -82,7 +82,7 @@ describe("webhook", () => { .returns("https://hooks.slack.com"); mocks.core.getInput.withArgs("webhook-type").returns("webhook-trigger"); mocks.core.getInput.withArgs("payload").returns("drinks: coffee"); - mocks.webhookTrigger.rejects( + mocks.webhook.trigger.rejects( new Error("An HTTP protocol error occurred"), ); try { @@ -90,7 +90,7 @@ describe("webhook", () => { } catch (e) { assert.ok(e instanceof SlackError); } - assert.equal(mocks.webhookTrigger.getCalls().length, 1); + assert.equal(mocks.webhook.trigger.getCalls().length, 1); assert.equal(mocks.core.setOutput.getCall(0).firstArg, "ok"); assert.equal(mocks.core.setOutput.getCall(0).lastArg, false); }); @@ -101,7 +101,7 @@ describe("webhook", () => { .returns("https://hooks.slack.com"); mocks.core.getInput.withArgs("webhook-type").returns("incoming-webhook"); mocks.core.getInput.withArgs("payload").returns("text: hi"); - mocks.incomingWebhook.rejects( + mocks.webhook.incoming.rejects( new Error("An HTTP protocol error occurred"), ); try { @@ -109,7 +109,7 @@ describe("webhook", () => { } catch (e) { assert.ok(e instanceof SlackError); } - assert.equal(mocks.incomingWebhook.getCalls().length, 1); + assert.equal(mocks.webhook.incoming.getCalls().length, 1); assert.equal(mocks.core.setOutput.getCall(0).firstArg, "ok"); assert.equal(mocks.core.setOutput.getCall(0).lastArg, false); }); From 9992ffa6d0675a8991ff0b89df442d08d7d6107a Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Mon, 6 Jul 2026 23:08:26 -0700 Subject: [PATCH 21/42] test: import @slack/webhook as a namespace Match the @slack/web-api import style and reference the webhook classes through the namespace instead of destructuring. Co-Authored-By: Claude --- test/index.spec.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/index.spec.js b/test/index.spec.js index 2cb9c503..9bb109f3 100644 --- a/test/index.spec.js +++ b/test/index.spec.js @@ -1,6 +1,6 @@ import fs from "node:fs"; import webapi from "@slack/web-api"; -import { IncomingWebhook, WebhookTrigger } from "@slack/webhook"; +import webhook from "@slack/webhook"; import sinon from "sinon"; /** @@ -28,8 +28,8 @@ export class Mock { constructor() { this.sandbox = sinon.createSandbox(); this.webhook = { - incoming: this.sandbox.stub(IncomingWebhook.prototype, "send"), - trigger: this.sandbox.stub(WebhookTrigger.prototype, "send"), + incoming: this.sandbox.stub(webhook.IncomingWebhook.prototype, "send"), + trigger: this.sandbox.stub(webhook.WebhookTrigger.prototype, "send"), }; this.calls = this.sandbox.stub(webapi.WebClient.prototype, "apiCall"); this.core = { From c613aadfb8fdbdd480084449ab1d88f95c6f4e34 Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Mon, 6 Jul 2026 23:11:18 -0700 Subject: [PATCH 22/42] test: order the webhook mock alphabetically after webapi Match the alphabetical member ordering used elsewhere in the Mock class by placing the webhook stubs last in the constructor and reset(). Co-Authored-By: Claude --- test/index.spec.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/index.spec.js b/test/index.spec.js index 9bb109f3..d4901f9f 100644 --- a/test/index.spec.js +++ b/test/index.spec.js @@ -27,10 +27,6 @@ export class Mock { */ constructor() { this.sandbox = sinon.createSandbox(); - this.webhook = { - incoming: this.sandbox.stub(webhook.IncomingWebhook.prototype, "send"), - trigger: this.sandbox.stub(webhook.WebhookTrigger.prototype, "send"), - }; this.calls = this.sandbox.stub(webapi.WebClient.prototype, "apiCall"); this.core = { debug: this.sandbox.stub(), @@ -52,6 +48,10 @@ export class Mock { }); }, }; + this.webhook = { + incoming: this.sandbox.stub(webhook.IncomingWebhook.prototype, "send"), + trigger: this.sandbox.stub(webhook.WebhookTrigger.prototype, "send"), + }; this.core.getInput.withArgs("errors").returns("false"); this.core.getInput.withArgs("retries").returns("5"); } @@ -61,8 +61,6 @@ export class Mock { */ reset() { this.sandbox.reset(); - this.webhook.incoming.resetHistory(); - this.webhook.trigger.resetHistory(); this.calls.resetHistory(); this.core.debug.reset(); this.core.error.reset(); @@ -81,6 +79,8 @@ export class Mock { }); }, }; + this.webhook.incoming.resetHistory(); + this.webhook.trigger.resetHistory(); this.core.getInput.withArgs("errors").returns("false"); this.core.getInput.withArgs("retries").returns("5"); process.env.SLACK_TOKEN = ""; From bd0ad530a13c345dc02f86b318c5bc3379b8c1d6 Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Mon, 6 Jul 2026 23:20:20 -0700 Subject: [PATCH 23/42] test: restore the missing-webhook guard test for proxies() Re-add the case asserting proxies() throws "No webhook was provided to proxy to" when no webhook input is present, restoring coverage of that guard in src/webhook.js. Co-Authored-By: Claude --- test/webhook.spec.js | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/test/webhook.spec.js b/test/webhook.spec.js index fa9931c2..57f6904d 100644 --- a/test/webhook.spec.js +++ b/test/webhook.spec.js @@ -116,6 +116,28 @@ describe("webhook", () => { }); describe("proxies", () => { + it("requires a webhook is included in the inputs", async () => { + /** + * @type {Config} + */ + const config = { + core: mocks.core, + inputs: {}, + }; + try { + new Webhook().proxies(config); + assert.fail("Failed to throw for missing input"); + } catch (err) { + if (err instanceof SlackError) { + assert.ok( + err.message.includes("No webhook was provided to proxy to"), + ); + } else { + assert.fail(err); + } + } + }); + it("returns undefined when no proxy is set", async () => { mocks.core.getInput .withArgs("webhook") From 1f35361a4f7f10dabafbc4fb12f208b4756147e9 Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Mon, 6 Jul 2026 23:27:47 -0700 Subject: [PATCH 24/42] test: restore try/catch success wrapper and two-line retries setup Wrap the trigger/incoming success assertions in the try/catch + assert.fail pattern from main, and instantiate Webhook on its own line in the retries tests instead of chaining. Co-Authored-By: Claude --- test/webhook.spec.js | 75 ++++++++++++++++++++++++++------------------ 1 file changed, 45 insertions(+), 30 deletions(-) diff --git a/test/webhook.spec.js b/test/webhook.spec.js index 57f6904d..5c13680e 100644 --- a/test/webhook.spec.js +++ b/test/webhook.spec.js @@ -19,18 +19,23 @@ describe("webhook", () => { mocks.core.getInput.withArgs("webhook-type").returns("webhook-trigger"); mocks.core.getInput.withArgs("payload").returns("drinks: coffee"); mocks.webhook.trigger.resolves({ ok: true, body: { ok: true } }); - await send(mocks.core); - assert.equal(mocks.webhook.trigger.getCalls().length, 1); - assert.deepEqual(mocks.webhook.trigger.getCall(0).firstArg, { - drinks: "coffee", - }); - assert.equal(mocks.core.setOutput.getCall(0).firstArg, "ok"); - assert.equal(mocks.core.setOutput.getCall(0).lastArg, true); - assert.equal(mocks.core.setOutput.getCall(1).firstArg, "response"); - assert.equal( - mocks.core.setOutput.getCall(1).lastArg, - JSON.stringify({ ok: true }), - ); + try { + await send(mocks.core); + assert.equal(mocks.webhook.trigger.getCalls().length, 1); + assert.deepEqual(mocks.webhook.trigger.getCall(0).firstArg, { + drinks: "coffee", + }); + assert.equal(mocks.core.setOutput.getCall(0).firstArg, "ok"); + assert.equal(mocks.core.setOutput.getCall(0).lastArg, true); + assert.equal(mocks.core.setOutput.getCall(1).firstArg, "response"); + assert.equal( + mocks.core.setOutput.getCall(1).lastArg, + JSON.stringify({ ok: true }), + ); + } catch (err) { + console.error(err); + assert.fail("Failed to send the webhook"); + } }); it("sends the parsed payload to the provided incoming webhook", async () => { @@ -40,18 +45,23 @@ describe("webhook", () => { mocks.core.getInput.withArgs("webhook-type").returns("incoming-webhook"); mocks.core.getInput.withArgs("payload").returns("text: greetings"); mocks.webhook.incoming.resolves({ text: "ok" }); - await send(mocks.core); - assert.equal(mocks.webhook.incoming.getCalls().length, 1); - assert.deepEqual(mocks.webhook.incoming.getCall(0).firstArg, { - text: "greetings", - }); - assert.equal(mocks.core.setOutput.getCall(0).firstArg, "ok"); - assert.equal(mocks.core.setOutput.getCall(0).lastArg, true); - assert.equal(mocks.core.setOutput.getCall(1).firstArg, "response"); - assert.equal( - mocks.core.setOutput.getCall(1).lastArg, - JSON.stringify("ok"), - ); + try { + await send(mocks.core); + assert.equal(mocks.webhook.incoming.getCalls().length, 1); + assert.deepEqual(mocks.webhook.incoming.getCall(0).firstArg, { + text: "greetings", + }); + assert.equal(mocks.core.setOutput.getCall(0).firstArg, "ok"); + assert.equal(mocks.core.setOutput.getCall(0).lastArg, true); + assert.equal(mocks.core.setOutput.getCall(1).firstArg, "response"); + assert.equal( + mocks.core.setOutput.getCall(1).lastArg, + JSON.stringify("ok"), + ); + } catch (err) { + console.error(err); + assert.fail("Failed to send the webhook"); + } }); }); @@ -220,28 +230,33 @@ describe("webhook", () => { describe("retries", () => { it("uses a default of five retries", async () => { - const result = new Webhook().retries(); + const webhook = new Webhook(); + const result = webhook.retries(); assert.equal(result.retries, 5); }); it('does not attempt retries when "0" is set', async () => { - const result = new Webhook().retries("0"); + const webhook = new Webhook(); + const result = webhook.retries("0"); assert.equal(result.retries, 0); }); it('maps "5" to a five-retry policy', async () => { - const result = new Webhook().retries("5"); + const webhook = new Webhook(); + const result = webhook.retries("5"); assert.equal(result.retries, 5); }); it('maps "10" to a ten-retry policy', async () => { - const result = new Webhook().retries("10"); + const webhook = new Webhook(); + const result = webhook.retries("10"); assert.equal(result.retries, 10); }); it('maps "RAPID" (case/space-insensitive) to a rapid policy', async () => { - const rapid = new Webhook().retries("RAPID"); - const spaced = new Webhook().retries(" rapid"); + const webhook = new Webhook(); + const rapid = webhook.retries("RAPID"); + const spaced = webhook.retries(" rapid"); assert.deepEqual(rapid, spaced); }); }); From 1bf60fd377e0e0a66fbc8cfb8cf810669748a288 Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Mon, 6 Jul 2026 23:34:21 -0700 Subject: [PATCH 25/42] test: keep the unknown-url-protocol proxy test wording from main Restore the "unknown url protocol" title, ssh:// proxy value, and the matching assert.fail message. Co-Authored-By: Claude --- test/webhook.spec.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/webhook.spec.js b/test/webhook.spec.js index 5c13680e..cf36cc31 100644 --- a/test/webhook.spec.js +++ b/test/webhook.spec.js @@ -203,8 +203,8 @@ describe("webhook", () => { } }); - it("fails to configure proxies with an unsupported protocol", async () => { - const proxy = "ftp://example.com"; + it("fails to configure proxies with an unknown url protocol", async () => { + const proxy = "ssh://"; mocks.core.getInput .withArgs("webhook") .returns("https://hooks.slack.com"); @@ -214,7 +214,7 @@ describe("webhook", () => { const config = new Config(mocks.core); const webhook = new Webhook(); webhook.proxies(config); - assert.fail("An unsupported proxy protocol was not thrown as error!"); + assert.fail("An unknown URL protocol was not thrown as error!"); } catch (err) { if (err instanceof SlackError) { assert.ok( From 27b568c529303f309d8c9e235d46229412e2627c Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Mon, 6 Jul 2026 23:37:40 -0700 Subject: [PATCH 26/42] test: restore original retries test names Bring back the descriptive retries titles from main; for the merged rapid case, keep main's wording with "12" replaced by "RAPID". Co-Authored-By: Claude --- test/webhook.spec.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/webhook.spec.js b/test/webhook.spec.js index cf36cc31..18e42794 100644 --- a/test/webhook.spec.js +++ b/test/webhook.spec.js @@ -229,7 +229,7 @@ describe("webhook", () => { }); describe("retries", () => { - it("uses a default of five retries", async () => { + it("uses a default of five retries in requests", async () => { const webhook = new Webhook(); const result = webhook.retries(); assert.equal(result.retries, 5); @@ -241,19 +241,19 @@ describe("webhook", () => { assert.equal(result.retries, 0); }); - it('maps "5" to a five-retry policy', async () => { + it('attempts a default amount of "5" retries', async () => { const webhook = new Webhook(); const result = webhook.retries("5"); assert.equal(result.retries, 5); }); - it('maps "10" to a ten-retry policy', async () => { + it('attempts "10" retries in around "30" minutes', async () => { const webhook = new Webhook(); const result = webhook.retries("10"); assert.equal(result.retries, 10); }); - it('maps "RAPID" (case/space-insensitive) to a rapid policy', async () => { + it('attempts a " rapid" burst of "RAPID" retries in seconds', async () => { const webhook = new Webhook(); const rapid = webhook.retries("RAPID"); const spaced = webhook.retries(" rapid"); From ff5229be2d6a69c5622043ab09f1ce2094212288 Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Mon, 6 Jul 2026 23:54:52 -0700 Subject: [PATCH 27/42] build: depend on published @slack/webhook 7.2.0-rc.1 Replace the local file:../node-slack-sdk/packages/webhook path with the published 7.2.0-rc.1 release from the registry. Co-Authored-By: Claude --- package-lock.json | 53 +++++++++++++++++++++++++++-------------------- package.json | 2 +- 2 files changed, 31 insertions(+), 24 deletions(-) diff --git a/package-lock.json b/package-lock.json index c2f5cfe6..d597f875 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "@actions/github": "^9.1.1", "@slack/logger": "^4.0.1", "@slack/web-api": "^7.16.0", - "@slack/webhook": "file:../node-slack-sdk/packages/webhook", + "@slack/webhook": "7.2.0-rc.1", "flat": "^6.0.1", "https-proxy-agent": "^9.0.0", "js-yaml": "^4.2.0", @@ -37,26 +37,6 @@ "npm": ">=11.6.1" } }, - "../node-slack-sdk/packages/webhook": { - "name": "@slack/webhook", - "version": "7.0.9", - "license": "MIT", - "dependencies": { - "@slack/types": "^2.20.1", - "@types/node": ">=18", - "@types/retry": "0.12.0", - "axios": "^1.16.0", - "p-retry": "^4", - "retry": "^0.13.1" - }, - "devDependencies": { - "nock": "^14.0.6" - }, - "engines": { - "node": ">= 18", - "npm": ">= 8.6.0" - } - }, "node_modules/@actions/core": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@actions/core/-/core-3.0.1.tgz", @@ -893,8 +873,35 @@ } }, "node_modules/@slack/webhook": { - "resolved": "../node-slack-sdk/packages/webhook", - "link": true + "version": "7.2.0-rc.1", + "resolved": "https://registry.npmjs.org/@slack/webhook/-/webhook-7.2.0-rc.1.tgz", + "integrity": "sha512-ZMqxzG1QVHOKHj46WztoWDPuZXWHwFFoW7Tvkl8RvgOv+Gyhjw7kKiJ4AxpIIungz8XIHomBqm1Z4kShwn13/w==", + "license": "MIT", + "dependencies": { + "@slack/types": "^2.20.1", + "@types/node": ">=18", + "@types/retry": "0.12.0", + "axios": "^1.16.0", + "p-retry": "^4", + "retry": "^0.13.1" + }, + "engines": { + "node": ">= 18", + "npm": ">= 8.6.0" + } + }, + "node_modules/@slack/webhook/node_modules/p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "license": "MIT", + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } }, "node_modules/@types/flat": { "version": "5.0.5", diff --git a/package.json b/package.json index 95b21ce2..b636ed8e 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "@actions/github": "^9.1.1", "@slack/logger": "^4.0.1", "@slack/web-api": "^7.16.0", - "@slack/webhook": "file:../node-slack-sdk/packages/webhook", + "@slack/webhook": "7.2.0-rc.1", "flat": "^6.0.1", "https-proxy-agent": "^9.0.0", "js-yaml": "^4.2.0", From 9b4fd318817cd7c7dedd141d237aac2b98543728 Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Tue, 7 Jul 2026 09:40:36 -0700 Subject: [PATCH 28/42] test: drive webhook metadata test through webhook inputs Use the webhook and webhook-type inputs instead of method and token so the webhook addAppMetadata test exercises the webhook config path. Co-Authored-By: Claude --- test/config.spec.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/test/config.spec.js b/test/config.spec.js index fe7521b1..029f7eb7 100644 --- a/test/config.spec.js +++ b/test/config.spec.js @@ -196,8 +196,12 @@ describe("config", () => { configurable: true, }); try { - mocks.core.getInput.withArgs("method").returns("chat.postMessage"); - mocks.core.getInput.withArgs("token").returns("xoxb-example"); + mocks.core.getInput + .withArgs("webhook") + .returns("https://hooks.slack.com"); + mocks.core.getInput + .withArgs("webhook-type") + .returns("incoming-webhook"); new Config(mocks.core); assert.ok(stub.calledOnce); const { name, version } = stub.firstCall.args[0]; From 4fbd824f28dde0df36caa2876afd90477d794f39 Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Tue, 7 Jul 2026 09:52:49 -0700 Subject: [PATCH 29/42] test: mirror client retry tests and drop unused p-retry Align the webhook retries tests 1:1 with the client suite: import the package as webhook and assert result.retries/.factor against webhook. (five/ten/rapid), splitting the rapid case into "rapid " and "RAPID" like client does. Also remove the unused p-retry dependency. Co-Authored-By: Claude --- package-lock.json | 30 +----------------------------- package.json | 3 +-- test/webhook.spec.js | 42 ++++++++++++++++++++++++++---------------- 3 files changed, 28 insertions(+), 47 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1480c219..d3323237 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,8 +17,7 @@ "flat": "^6.0.1", "https-proxy-agent": "^9.0.0", "js-yaml": "^4.2.0", - "markup-js": "^1.5.21", - "p-retry": "^8.0.0" + "markup-js": "^1.5.21" }, "devDependencies": { "@biomejs/biome": "^2.5.1", @@ -1634,18 +1633,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-network-error": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.2.tgz", - "integrity": "sha512-PhBY86zaxNZUuWP6h13Vu5oFe0XY6/UlKzQnYFELzGVHygP3MxmvTfYSG7GN3aIab/iWudSMgjSnG9Dq+nHrgA==", - "license": "MIT", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -1915,21 +1902,6 @@ "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", "license": "MIT" }, - "node_modules/p-retry": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-8.0.0.tgz", - "integrity": "sha512-kFVqH1HxOHp8LupNsOys7bSV09VYTRLxarH/mokO4Rqhk6wGi70E0jh4VzvVGXfEVNggHoHLAMWsQqHyU1Ey9A==", - "license": "MIT", - "dependencies": { - "is-network-error": "^1.3.0" - }, - "engines": { - "node": ">=22" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/p-timeout": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", diff --git a/package.json b/package.json index cd1520e2..f897e7b3 100644 --- a/package.json +++ b/package.json @@ -50,8 +50,7 @@ "flat": "^6.0.1", "https-proxy-agent": "^9.0.0", "js-yaml": "^4.2.0", - "markup-js": "^1.5.21", - "p-retry": "^8.0.0" + "markup-js": "^1.5.21" }, "devDependencies": { "@biomejs/biome": "^2.5.1", diff --git a/test/webhook.spec.js b/test/webhook.spec.js index 18e42794..b1a86484 100644 --- a/test/webhook.spec.js +++ b/test/webhook.spec.js @@ -1,5 +1,6 @@ import assert from "node:assert"; import { beforeEach, describe, it } from "node:test"; +import webhook from "@slack/webhook"; import Config from "../src/config.js"; import SlackError from "../src/errors.js"; import send from "../src/send.js"; @@ -230,34 +231,43 @@ describe("webhook", () => { describe("retries", () => { it("uses a default of five retries in requests", async () => { - const webhook = new Webhook(); - const result = webhook.retries(); - assert.equal(result.retries, 5); + const result = new Webhook().retries(); + assert.equal(result.retries, webhook.fiveRetriesInFiveMinutes.retries); }); it('does not attempt retries when "0" is set', async () => { - const webhook = new Webhook(); - const result = webhook.retries("0"); + const result = new Webhook().retries("0"); assert.equal(result.retries, 0); }); it('attempts a default amount of "5" retries', async () => { - const webhook = new Webhook(); - const result = webhook.retries("5"); - assert.equal(result.retries, 5); + const result = new Webhook().retries("5"); + assert.equal(result.retries, webhook.fiveRetriesInFiveMinutes.retries); + assert.equal(result.factor, webhook.fiveRetriesInFiveMinutes.factor); }); it('attempts "10" retries in around "30" minutes', async () => { - const webhook = new Webhook(); - const result = webhook.retries("10"); - assert.equal(result.retries, 10); + const result = new Webhook().retries("10"); + assert.equal( + result.retries, + webhook.tenRetriesInAboutThirtyMinutes.retries, + ); + assert.equal( + result.factor, + webhook.tenRetriesInAboutThirtyMinutes.factor, + ); }); - it('attempts a " rapid" burst of "RAPID" retries in seconds', async () => { - const webhook = new Webhook(); - const rapid = webhook.retries("RAPID"); - const spaced = webhook.retries(" rapid"); - assert.deepEqual(rapid, spaced); + it('attempts a "rapid " burst of "12" retries in seconds', async () => { + const result = new Webhook().retries("rapid "); + assert.equal(result.retries, webhook.rapidRetryPolicy.retries); + assert.equal(result.factor, webhook.rapidRetryPolicy.factor); + }); + + it('attempts a "RAPID" burst of "12" retries in seconds', async () => { + const result = new Webhook().retries("RAPID"); + assert.equal(result.retries, webhook.rapidRetryPolicy.retries); + assert.equal(result.factor, webhook.rapidRetryPolicy.factor); }); }); }); From ab85bec7e950ef95bf3a8a1e41205c37222acb76 Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Tue, 7 Jul 2026 09:54:28 -0700 Subject: [PATCH 30/42] test: fix mislabeled client instance in retry tests Rename the copy-pasted `webhook` instances in client retry tests to `client` so they match the class under test and the rest of the file. Co-Authored-By: Claude --- test/client.spec.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/test/client.spec.js b/test/client.spec.js index 26e613b9..c6e9a2f6 100644 --- a/test/client.spec.js +++ b/test/client.spec.js @@ -492,14 +492,14 @@ describe("client", () => { }); it('does not attempt retries when "0" is set', async () => { - const webhook = new Client(); - const result = webhook.retries("0"); + const client = new Client(); + const result = client.retries("0"); assert.equal(result.retries, 0); }); it('attempts a default amount of "5" retries', async () => { - const webhook = new Client(); - const result = webhook.retries("5"); + const client = new Client(); + const result = client.retries("5"); assert.equal( result.retries, webapi.retryPolicies.fiveRetriesInFiveMinutes.retries, @@ -511,8 +511,8 @@ describe("client", () => { }); it('attempts "10" retries in around "30" minutes', async () => { - const webhook = new Client(); - const result = webhook.retries("10"); + const client = new Client(); + const result = client.retries("10"); assert.equal( result.retries, webapi.retryPolicies.tenRetriesInAboutThirtyMinutes.retries, @@ -524,8 +524,8 @@ describe("client", () => { }); it('attempts a "rapid " burst of "12" retries in seconds', async () => { - const webhook = new Client(); - const result = webhook.retries("rapid "); + const client = new Client(); + const result = client.retries("rapid "); assert.equal( result.retries, webapi.retryPolicies.rapidRetryPolicy.retries, @@ -534,8 +534,8 @@ describe("client", () => { }); it('attempts a "RAPID" burst of "12" retries in seconds', async () => { - const webhook = new Client(); - const result = webhook.retries("RAPID"); + const client = new Client(); + const result = client.retries("RAPID"); assert.equal( result.retries, webapi.retryPolicies.rapidRetryPolicy.retries, From 3cb2eb47933e37c632910193cfdc133c71ec340a Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Tue, 7 Jul 2026 10:02:05 -0700 Subject: [PATCH 31/42] test: fix leading-space rapid retries case for webhook Use " rapid" (leading space) in the title and retries() argument to match the webhook variant rather than client's trailing-space form. Co-Authored-By: Claude --- test/webhook.spec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/webhook.spec.js b/test/webhook.spec.js index b1a86484..c543a6ef 100644 --- a/test/webhook.spec.js +++ b/test/webhook.spec.js @@ -258,8 +258,8 @@ describe("webhook", () => { ); }); - it('attempts a "rapid " burst of "12" retries in seconds', async () => { - const result = new Webhook().retries("rapid "); + it('attempts a " rapid" burst of "12" retries in seconds', async () => { + const result = new Webhook().retries(" rapid"); assert.equal(result.retries, webhook.rapidRetryPolicy.retries); assert.equal(result.factor, webhook.rapidRetryPolicy.factor); }); From f4e747dcb2c7f3010b872b8461e176c9d981568b Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Tue, 7 Jul 2026 10:07:30 -0700 Subject: [PATCH 32/42] test: assert error message propagates on webhook send failures Verify the SlackError thrown from post() carries the underlying error message, restoring the message-includes coverage lost in the axios to @slack/webhook migration. Co-Authored-By: Claude --- test/webhook.spec.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/webhook.spec.js b/test/webhook.spec.js index c543a6ef..f582895e 100644 --- a/test/webhook.spec.js +++ b/test/webhook.spec.js @@ -100,6 +100,7 @@ describe("webhook", () => { await send(mocks.core); } catch (e) { assert.ok(e instanceof SlackError); + assert.ok(e.message.includes("An HTTP protocol error occurred")); } assert.equal(mocks.webhook.trigger.getCalls().length, 1); assert.equal(mocks.core.setOutput.getCall(0).firstArg, "ok"); @@ -119,6 +120,7 @@ describe("webhook", () => { await send(mocks.core); } catch (e) { assert.ok(e instanceof SlackError); + assert.ok(e.message.includes("An HTTP protocol error occurred")); } assert.equal(mocks.webhook.incoming.getCalls().length, 1); assert.equal(mocks.core.setOutput.getCall(0).firstArg, "ok"); From dd460acc35c196fff5e0989a406b2ada37adfb92 Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Tue, 7 Jul 2026 10:17:49 -0700 Subject: [PATCH 33/42] test: cover the errors-enabled webhook throw path Add a test asserting send() re-throws a SlackError (with the underlying message) and calls setFailed when the "errors" input is enabled. Drop the unreachable catch assertions from the errors-disabled swallow tests, which never throw, so they just await and check the ok=false outputs. Co-Authored-By: Claude --- test/webhook.spec.js | 37 +++++++++++++++++++++++++------------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/test/webhook.spec.js b/test/webhook.spec.js index f582895e..1f458871 100644 --- a/test/webhook.spec.js +++ b/test/webhook.spec.js @@ -96,12 +96,7 @@ describe("webhook", () => { mocks.webhook.trigger.rejects( new Error("An HTTP protocol error occurred"), ); - try { - await send(mocks.core); - } catch (e) { - assert.ok(e instanceof SlackError); - assert.ok(e.message.includes("An HTTP protocol error occurred")); - } + await send(mocks.core); assert.equal(mocks.webhook.trigger.getCalls().length, 1); assert.equal(mocks.core.setOutput.getCall(0).firstArg, "ok"); assert.equal(mocks.core.setOutput.getCall(0).lastArg, false); @@ -116,16 +111,34 @@ describe("webhook", () => { mocks.webhook.incoming.rejects( new Error("An HTTP protocol error occurred"), ); - try { - await send(mocks.core); - } catch (e) { - assert.ok(e instanceof SlackError); - assert.ok(e.message.includes("An HTTP protocol error occurred")); - } + await send(mocks.core); assert.equal(mocks.webhook.incoming.getCalls().length, 1); assert.equal(mocks.core.setOutput.getCall(0).firstArg, "ok"); assert.equal(mocks.core.setOutput.getCall(0).lastArg, false); }); + + it('throws the failure when the "errors" input is enabled', async () => { + mocks.core.getBooleanInput.withArgs("errors").returns(true); + mocks.core.getInput + .withArgs("webhook") + .returns("https://hooks.slack.com"); + mocks.core.getInput.withArgs("webhook-type").returns("webhook-trigger"); + mocks.core.getInput.withArgs("payload").returns("drinks: coffee"); + mocks.webhook.trigger.rejects( + new Error("An HTTP protocol error occurred"), + ); + try { + await send(mocks.core); + assert.fail("A webhook trigger failure was not thrown as error!"); + } catch (err) { + if (err instanceof SlackError) { + assert.ok(err.message.includes("An HTTP protocol error occurred")); + } else { + assert.fail(err); + } + } + assert.equal(mocks.core.setFailed.getCalls().length, 1); + }); }); describe("proxies", () => { From 8d5b655d917029fe38eb85ca3663450d38570479 Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Tue, 7 Jul 2026 10:28:51 -0700 Subject: [PATCH 34/42] test: exercise webhook failures with errors input enabled Enable the "errors" input in both webhook failure tests so send() re-throws a SlackError, asserting the thrown message, setFailed, and the ok=false output for the trigger and incoming paths. Folds in the prior standalone errors-enabled test. Co-Authored-By: Claude --- test/webhook.spec.js | 37 ++++++++++++++++++------------------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/test/webhook.spec.js b/test/webhook.spec.js index 1f458871..4fc046d6 100644 --- a/test/webhook.spec.js +++ b/test/webhook.spec.js @@ -88,6 +88,7 @@ describe("webhook", () => { }); it("returns the failures from a webhook trigger", async () => { + mocks.core.getBooleanInput.withArgs("errors").returns(true); mocks.core.getInput .withArgs("webhook") .returns("https://hooks.slack.com"); @@ -96,13 +97,24 @@ describe("webhook", () => { mocks.webhook.trigger.rejects( new Error("An HTTP protocol error occurred"), ); - await send(mocks.core); + try { + await send(mocks.core); + assert.fail("A webhook trigger failure was not thrown as error!"); + } catch (err) { + if (err instanceof SlackError) { + assert.ok(err.message.includes("An HTTP protocol error occurred")); + } else { + assert.fail(err); + } + } assert.equal(mocks.webhook.trigger.getCalls().length, 1); + assert.ok(mocks.core.setFailed.called); assert.equal(mocks.core.setOutput.getCall(0).firstArg, "ok"); assert.equal(mocks.core.setOutput.getCall(0).lastArg, false); }); it("returns the failures from an incoming webhook", async () => { + mocks.core.getBooleanInput.withArgs("errors").returns(true); mocks.core.getInput .withArgs("webhook") .returns("https://hooks.slack.com"); @@ -111,25 +123,9 @@ describe("webhook", () => { mocks.webhook.incoming.rejects( new Error("An HTTP protocol error occurred"), ); - await send(mocks.core); - assert.equal(mocks.webhook.incoming.getCalls().length, 1); - assert.equal(mocks.core.setOutput.getCall(0).firstArg, "ok"); - assert.equal(mocks.core.setOutput.getCall(0).lastArg, false); - }); - - it('throws the failure when the "errors" input is enabled', async () => { - mocks.core.getBooleanInput.withArgs("errors").returns(true); - mocks.core.getInput - .withArgs("webhook") - .returns("https://hooks.slack.com"); - mocks.core.getInput.withArgs("webhook-type").returns("webhook-trigger"); - mocks.core.getInput.withArgs("payload").returns("drinks: coffee"); - mocks.webhook.trigger.rejects( - new Error("An HTTP protocol error occurred"), - ); try { await send(mocks.core); - assert.fail("A webhook trigger failure was not thrown as error!"); + assert.fail("An incoming webhook failure was not thrown as error!"); } catch (err) { if (err instanceof SlackError) { assert.ok(err.message.includes("An HTTP protocol error occurred")); @@ -137,7 +133,10 @@ describe("webhook", () => { assert.fail(err); } } - assert.equal(mocks.core.setFailed.getCalls().length, 1); + assert.equal(mocks.webhook.incoming.getCalls().length, 1); + assert.ok(mocks.core.setFailed.called); + assert.equal(mocks.core.setOutput.getCall(0).firstArg, "ok"); + assert.equal(mocks.core.setOutput.getCall(0).lastArg, false); }); }); From 734117111deeb52ea9d114bda1890e4dd2989f5e Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Tue, 7 Jul 2026 10:30:23 -0700 Subject: [PATCH 35/42] test: drop messages from the webhook failure assert.fail calls Use a bare assert.fail() in the trigger and incoming failure tests. Co-Authored-By: Claude --- test/webhook.spec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/webhook.spec.js b/test/webhook.spec.js index 4fc046d6..692a1129 100644 --- a/test/webhook.spec.js +++ b/test/webhook.spec.js @@ -99,7 +99,7 @@ describe("webhook", () => { ); try { await send(mocks.core); - assert.fail("A webhook trigger failure was not thrown as error!"); + assert.fail(); } catch (err) { if (err instanceof SlackError) { assert.ok(err.message.includes("An HTTP protocol error occurred")); @@ -125,7 +125,7 @@ describe("webhook", () => { ); try { await send(mocks.core); - assert.fail("An incoming webhook failure was not thrown as error!"); + assert.fail(); } catch (err) { if (err instanceof SlackError) { assert.ok(err.message.includes("An HTTP protocol error occurred")); From 67834bef9cdd6723361f256e7ceae1a02d05d9fc Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Tue, 7 Jul 2026 10:57:43 -0700 Subject: [PATCH 36/42] test: reassert payload and response output on webhook failures Restore the payload deep-equal (drinks: coffee / text: hi) and the "response" output assertion in the webhook failure tests that were lost in the axios to @slack/webhook migration. Co-Authored-By: Claude --- test/webhook.spec.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/webhook.spec.js b/test/webhook.spec.js index 692a1129..904c8db0 100644 --- a/test/webhook.spec.js +++ b/test/webhook.spec.js @@ -108,9 +108,13 @@ describe("webhook", () => { } } assert.equal(mocks.webhook.trigger.getCalls().length, 1); + assert.deepEqual(mocks.webhook.trigger.getCall(0).firstArg, { + drinks: "coffee", + }); assert.ok(mocks.core.setFailed.called); assert.equal(mocks.core.setOutput.getCall(0).firstArg, "ok"); assert.equal(mocks.core.setOutput.getCall(0).lastArg, false); + assert.equal(mocks.core.setOutput.getCall(1).firstArg, "response"); }); it("returns the failures from an incoming webhook", async () => { @@ -134,9 +138,13 @@ describe("webhook", () => { } } assert.equal(mocks.webhook.incoming.getCalls().length, 1); + assert.deepEqual(mocks.webhook.incoming.getCall(0).firstArg, { + text: "hi", + }); assert.ok(mocks.core.setFailed.called); assert.equal(mocks.core.setOutput.getCall(0).firstArg, "ok"); assert.equal(mocks.core.setOutput.getCall(0).lastArg, false); + assert.equal(mocks.core.setOutput.getCall(1).firstArg, "response"); }); }); From 39a15999ad7a1407148386c7529fd12979f9ecf1 Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Tue, 7 Jul 2026 10:58:45 -0700 Subject: [PATCH 37/42] test: keep the malformed textt payload in the incoming failure test Restore main's "textt: oops" input and { textt: "oops" } assertion so the incoming failure test still exercises the malformed-payload case. Co-Authored-By: Claude --- test/webhook.spec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/webhook.spec.js b/test/webhook.spec.js index 904c8db0..0a6a480c 100644 --- a/test/webhook.spec.js +++ b/test/webhook.spec.js @@ -123,7 +123,7 @@ describe("webhook", () => { .withArgs("webhook") .returns("https://hooks.slack.com"); mocks.core.getInput.withArgs("webhook-type").returns("incoming-webhook"); - mocks.core.getInput.withArgs("payload").returns("text: hi"); + mocks.core.getInput.withArgs("payload").returns("textt: oops"); mocks.webhook.incoming.rejects( new Error("An HTTP protocol error occurred"), ); @@ -139,7 +139,7 @@ describe("webhook", () => { } assert.equal(mocks.webhook.incoming.getCalls().length, 1); assert.deepEqual(mocks.webhook.incoming.getCall(0).firstArg, { - text: "hi", + textt: "oops", }); assert.ok(mocks.core.setFailed.called); assert.equal(mocks.core.setOutput.getCall(0).firstArg, "ok"); From 4819c47ab1c0dd503bdbaab4d606262152082722 Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Tue, 7 Jul 2026 12:37:26 -0700 Subject: [PATCH 38/42] test: restore proxies test order and http-skip test name Move the non-HTTPS webhook skip test back to its position after the missing-webhook case and restore its original name, matching main's proxies ordering. Co-Authored-By: Claude --- test/webhook.spec.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/test/webhook.spec.js b/test/webhook.spec.js index 0a6a480c..a049161f 100644 --- a/test/webhook.spec.js +++ b/test/webhook.spec.js @@ -171,6 +171,15 @@ describe("webhook", () => { } }); + it("skips proxying an http webhook url altogether", async () => { + mocks.core.getInput.withArgs("webhook").returns("http://hooks.slack.com"); + mocks.core.getInput.withArgs("webhook-type").returns("incoming-webhook"); + mocks.core.getInput.withArgs("proxy").returns("https://example.com"); + const config = new Config(mocks.core); + const webhook = new Webhook(); + assert.strictEqual(webhook.proxies(config), undefined); + }); + it("returns undefined when no proxy is set", async () => { mocks.core.getInput .withArgs("webhook") @@ -194,15 +203,6 @@ describe("webhook", () => { assert.deepEqual(httpsAgent.proxy, new URL(proxy)); }); - it("skips the proxy when the webhook destination is not HTTPS", async () => { - mocks.core.getInput.withArgs("webhook").returns("http://hooks.slack.com"); - mocks.core.getInput.withArgs("webhook-type").returns("incoming-webhook"); - mocks.core.getInput.withArgs("proxy").returns("https://example.com"); - const config = new Config(mocks.core); - const webhook = new Webhook(); - assert.strictEqual(webhook.proxies(config), undefined); - }); - it("fails to configure proxies with an invalid proxied url", async () => { const proxy = "https://"; mocks.core.getInput From fc8f5cca120852b45f2c9bff2f022cb386389692 Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Tue, 7 Jul 2026 12:40:30 -0700 Subject: [PATCH 39/42] test: order the https proxy agent test before the no-proxy case Place "sets up the proxy agent for the provided https proxy" ahead of the returns-undefined case to match main's proxies ordering. Co-Authored-By: Claude --- test/webhook.spec.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/test/webhook.spec.js b/test/webhook.spec.js index a049161f..dd294ee4 100644 --- a/test/webhook.spec.js +++ b/test/webhook.spec.js @@ -180,27 +180,27 @@ describe("webhook", () => { assert.strictEqual(webhook.proxies(config), undefined); }); - it("returns undefined when no proxy is set", async () => { + it("sets up the proxy agent for the provided https proxy", async () => { + const proxy = "https://example.com"; mocks.core.getInput .withArgs("webhook") .returns("https://hooks.slack.com"); mocks.core.getInput.withArgs("webhook-type").returns("incoming-webhook"); + mocks.core.getInput.withArgs("proxy").returns(proxy); const config = new Config(mocks.core); const webhook = new Webhook(); - assert.strictEqual(webhook.proxies(config), undefined); + const httpsAgent = webhook.proxies(config); + assert.deepEqual(httpsAgent.proxy, new URL(proxy)); }); - it("sets up the proxy agent for the provided https proxy", async () => { - const proxy = "https://example.com"; + it("returns undefined when no proxy is set", async () => { mocks.core.getInput .withArgs("webhook") .returns("https://hooks.slack.com"); mocks.core.getInput.withArgs("webhook-type").returns("incoming-webhook"); - mocks.core.getInput.withArgs("proxy").returns(proxy); const config = new Config(mocks.core); const webhook = new Webhook(); - const httpsAgent = webhook.proxies(config); - assert.deepEqual(httpsAgent.proxy, new URL(proxy)); + assert.strictEqual(webhook.proxies(config), undefined); }); it("fails to configure proxies with an invalid proxied url", async () => { From e26bbfcaecf3bc59c6c931e831886bfa269fdbb1 Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Tue, 7 Jul 2026 12:46:47 -0700 Subject: [PATCH 40/42] test: cover http proxy agent and unknown webhook type paths Add a test that an http proxy also yields an HttpsProxyAgent (beside the https case) and one asserting post() throws for an unknown webhook type, bringing src/webhook.js to full branch coverage. Co-Authored-By: Claude --- test/webhook.spec.js | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/test/webhook.spec.js b/test/webhook.spec.js index dd294ee4..7fe75baf 100644 --- a/test/webhook.spec.js +++ b/test/webhook.spec.js @@ -87,6 +87,32 @@ describe("webhook", () => { } }); + it("errors when an unknown webhook type is provided", async () => { + /** + * @type {Config} + */ + const config = { + core: mocks.core, + inputs: { + webhook: "https://hooks.slack.com", + webhookType: "unknown-webhook", + retries: "5", + }, + }; + try { + await new Webhook().post(config); + assert.fail("Failed to throw for an unknown webhook type"); + } catch (err) { + if (err instanceof SlackError) { + assert.ok( + err.message.includes("Unknown webhook type: unknown-webhook"), + ); + } else { + assert.fail(err); + } + } + }); + it("returns the failures from a webhook trigger", async () => { mocks.core.getBooleanInput.withArgs("errors").returns(true); mocks.core.getInput @@ -193,6 +219,19 @@ describe("webhook", () => { assert.deepEqual(httpsAgent.proxy, new URL(proxy)); }); + it("sets up the proxy agent for the provided http proxy", async () => { + const proxy = "http://example.com"; + mocks.core.getInput + .withArgs("webhook") + .returns("https://hooks.slack.com"); + mocks.core.getInput.withArgs("webhook-type").returns("incoming-webhook"); + mocks.core.getInput.withArgs("proxy").returns(proxy); + const config = new Config(mocks.core); + const webhook = new Webhook(); + const httpsAgent = webhook.proxies(config); + assert.deepEqual(httpsAgent.proxy, new URL(proxy)); + }); + it("returns undefined when no proxy is set", async () => { mocks.core.getInput .withArgs("webhook") From 76311316160e5380d422bf9d4a7a09e70db5a8a0 Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Tue, 7 Jul 2026 12:50:03 -0700 Subject: [PATCH 41/42] test: drop the message from the unknown-type assert.fail Use a bare assert.fail() in the unknown webhook type test. Co-Authored-By: Claude --- test/webhook.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/webhook.spec.js b/test/webhook.spec.js index 7fe75baf..bff14e60 100644 --- a/test/webhook.spec.js +++ b/test/webhook.spec.js @@ -101,7 +101,7 @@ describe("webhook", () => { }; try { await new Webhook().post(config); - assert.fail("Failed to throw for an unknown webhook type"); + assert.fail(); } catch (err) { if (err instanceof SlackError) { assert.ok( From 7c83ef187e20baa1619948b7f8ad78db62737741 Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Tue, 7 Jul 2026 13:26:29 -0700 Subject: [PATCH 42/42] feat: use the @slack/webhook retryPolicies namespace export Bump to @slack/webhook 7.2.0-rc.2 and consume retry policies via the new retryPolicies namespace (webhook.retryPolicies.), mirroring the @slack/web-api client. Updates src/webhook.js and the retries tests. Co-Authored-By: Claude --- package-lock.json | 10 +++++----- package.json | 2 +- src/webhook.js | 14 +++++--------- test/webhook.spec.js | 39 ++++++++++++++++++++++++++++++--------- 4 files changed, 41 insertions(+), 24 deletions(-) diff --git a/package-lock.json b/package-lock.json index d3323237..97c1895a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "@actions/github": "^9.1.1", "@slack/logger": "^4.0.1", "@slack/web-api": "^7.16.0", - "@slack/webhook": "7.2.0-rc.1", + "@slack/webhook": "7.2.0-rc.2", "flat": "^6.0.1", "https-proxy-agent": "^9.0.0", "js-yaml": "^4.2.0", @@ -884,16 +884,16 @@ } }, "node_modules/@slack/webhook": { - "version": "7.2.0-rc.1", - "resolved": "https://registry.npmjs.org/@slack/webhook/-/webhook-7.2.0-rc.1.tgz", - "integrity": "sha512-ZMqxzG1QVHOKHj46WztoWDPuZXWHwFFoW7Tvkl8RvgOv+Gyhjw7kKiJ4AxpIIungz8XIHomBqm1Z4kShwn13/w==", + "version": "7.2.0-rc.2", + "resolved": "https://registry.npmjs.org/@slack/webhook/-/webhook-7.2.0-rc.2.tgz", + "integrity": "sha512-4O0LKiIFJdHBqOPb6lkO9SxqRN9RVIWXMI9QBPzfZrDiLC3pJACQNCETauLPbMonGtJYrt8B/xOOEQA+GCuWfQ==", "license": "MIT", "dependencies": { "@slack/types": "^2.20.1", "@types/node": ">=18", "@types/retry": "0.12.0", "axios": "^1.16.0", - "p-retry": "^4", + "p-retry": "^4.6.2", "retry": "^0.13.1" }, "engines": { diff --git a/package.json b/package.json index f897e7b3..d4edfce4 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "@actions/github": "^9.1.1", "@slack/logger": "^4.0.1", "@slack/web-api": "^7.16.0", - "@slack/webhook": "7.2.0-rc.1", + "@slack/webhook": "7.2.0-rc.2", "flat": "^6.0.1", "https-proxy-agent": "^9.0.0", "js-yaml": "^4.2.0", diff --git a/src/webhook.js b/src/webhook.js index 0cc0c816..57e0c51c 100644 --- a/src/webhook.js +++ b/src/webhook.js @@ -1,8 +1,4 @@ -import { - fiveRetriesInFiveMinutes, - rapidRetryPolicy, - tenRetriesInAboutThirtyMinutes, -} from "@slack/webhook"; +import webhook from "@slack/webhook"; import { HttpsProxyAgent } from "https-proxy-agent"; import Config from "./config.js"; import SlackError from "./errors.js"; @@ -111,13 +107,13 @@ export default class Webhook { case "0": return { retries: 0 }; case "5": - return fiveRetriesInFiveMinutes; + return webhook.retryPolicies.fiveRetriesInFiveMinutes; case "10": - return tenRetriesInAboutThirtyMinutes; + return webhook.retryPolicies.tenRetriesInAboutThirtyMinutes; case "RAPID": - return rapidRetryPolicy; + return webhook.retryPolicies.rapidRetryPolicy; default: - return fiveRetriesInFiveMinutes; + return webhook.retryPolicies.fiveRetriesInFiveMinutes; } } } diff --git a/test/webhook.spec.js b/test/webhook.spec.js index bff14e60..1c097b27 100644 --- a/test/webhook.spec.js +++ b/test/webhook.spec.js @@ -293,7 +293,10 @@ describe("webhook", () => { describe("retries", () => { it("uses a default of five retries in requests", async () => { const result = new Webhook().retries(); - assert.equal(result.retries, webhook.fiveRetriesInFiveMinutes.retries); + assert.equal( + result.retries, + webhook.retryPolicies.fiveRetriesInFiveMinutes.retries, + ); }); it('does not attempt retries when "0" is set', async () => { @@ -303,32 +306,50 @@ describe("webhook", () => { it('attempts a default amount of "5" retries', async () => { const result = new Webhook().retries("5"); - assert.equal(result.retries, webhook.fiveRetriesInFiveMinutes.retries); - assert.equal(result.factor, webhook.fiveRetriesInFiveMinutes.factor); + assert.equal( + result.retries, + webhook.retryPolicies.fiveRetriesInFiveMinutes.retries, + ); + assert.equal( + result.factor, + webhook.retryPolicies.fiveRetriesInFiveMinutes.factor, + ); }); it('attempts "10" retries in around "30" minutes', async () => { const result = new Webhook().retries("10"); assert.equal( result.retries, - webhook.tenRetriesInAboutThirtyMinutes.retries, + webhook.retryPolicies.tenRetriesInAboutThirtyMinutes.retries, ); assert.equal( result.factor, - webhook.tenRetriesInAboutThirtyMinutes.factor, + webhook.retryPolicies.tenRetriesInAboutThirtyMinutes.factor, ); }); it('attempts a " rapid" burst of "12" retries in seconds', async () => { const result = new Webhook().retries(" rapid"); - assert.equal(result.retries, webhook.rapidRetryPolicy.retries); - assert.equal(result.factor, webhook.rapidRetryPolicy.factor); + assert.equal( + result.retries, + webhook.retryPolicies.rapidRetryPolicy.retries, + ); + assert.equal( + result.factor, + webhook.retryPolicies.rapidRetryPolicy.factor, + ); }); it('attempts a "RAPID" burst of "12" retries in seconds', async () => { const result = new Webhook().retries("RAPID"); - assert.equal(result.retries, webhook.rapidRetryPolicy.retries); - assert.equal(result.factor, webhook.rapidRetryPolicy.factor); + assert.equal( + result.retries, + webhook.retryPolicies.rapidRetryPolicy.retries, + ); + assert.equal( + result.factor, + webhook.retryPolicies.rapidRetryPolicy.factor, + ); }); }); });