refactor: send webhooks with the @slack/webhook package (#630)

This commit is contained in:
Eden Zimbelman
2026-07-10 14:51:23 -07:00
committed by GitHub
parent 0539f0d083
commit fa03fe4625
10 changed files with 304 additions and 317 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@slack/slack-github-action": patch
---
refactor: send webhooks with the [`@slack/webhook`](https://docs.slack.dev/tools/node-slack-sdk/webhook) package
+83 -103
View File
@@ -13,8 +13,7 @@
"@actions/github": "^9.1.1",
"@slack/logger": "^4.0.1",
"@slack/web-api": "^7.16.0",
"axios": "^1.18.1",
"axios-retry": "^4.5.0",
"@slack/webhook": "^7.2.0",
"flat": "^6.0.1",
"https-proxy-agent": "^9.0.0",
"js-yaml": "^5.2.0",
@@ -107,9 +106,9 @@
}
},
"node_modules/@biomejs/biome": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.5.1.tgz",
"integrity": "sha512-IXWLCxKmae+rI7LOHS1B3EbVisQ6GRAWbhN9msa6KjNCyFWrvKZWR4oUdinaNssrV852OrSHuSPa95h1GPJc7Q==",
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.5.2.tgz",
"integrity": "sha512-VQ3RCqr7JmDIX+w6stWYl+g/3bYofN3q2wDBHUKKc/c7i5QWrFKFBZYCYPWTE6agsUPMIZZe6/CMmVUfUAhkKA==",
"dev": true,
"license": "MIT OR Apache-2.0",
"bin": {
@@ -123,20 +122,20 @@
"url": "https://opencollective.com/biome"
},
"optionalDependencies": {
"@biomejs/cli-darwin-arm64": "2.5.1",
"@biomejs/cli-darwin-x64": "2.5.1",
"@biomejs/cli-linux-arm64": "2.5.1",
"@biomejs/cli-linux-arm64-musl": "2.5.1",
"@biomejs/cli-linux-x64": "2.5.1",
"@biomejs/cli-linux-x64-musl": "2.5.1",
"@biomejs/cli-win32-arm64": "2.5.1",
"@biomejs/cli-win32-x64": "2.5.1"
"@biomejs/cli-darwin-arm64": "2.5.2",
"@biomejs/cli-darwin-x64": "2.5.2",
"@biomejs/cli-linux-arm64": "2.5.2",
"@biomejs/cli-linux-arm64-musl": "2.5.2",
"@biomejs/cli-linux-x64": "2.5.2",
"@biomejs/cli-linux-x64-musl": "2.5.2",
"@biomejs/cli-win32-arm64": "2.5.2",
"@biomejs/cli-win32-x64": "2.5.2"
}
},
"node_modules/@biomejs/cli-darwin-arm64": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.5.1.tgz",
"integrity": "sha512-npqDzvqv7vFaWRiNN1Te71siRgPaqS9MpqgYCdP/CrUbkJ7ApezaeaKjueKHRN/JH/6lRjJQAHi8acQDCAz22w==",
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.5.2.tgz",
"integrity": "sha512-e7P3P7EkwFc/KiX2AHw4YDLIBOMfG9CPCAwy52k5Bp0dfhkozx9hf6wCmIr2QeXy2XeccJ3V/Sg+hDmzYEqxSg==",
"cpu": [
"arm64"
],
@@ -151,9 +150,9 @@
}
},
"node_modules/@biomejs/cli-darwin-x64": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.5.1.tgz",
"integrity": "sha512-RgwTqPAM8g2tn1j+b5oRjF/DbSBX8a4gwojtuG9XuhfK7GgomvZ9+T+tqjXiVbjLEeGJOoL6VEk8mvRTVeSybw==",
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.5.2.tgz",
"integrity": "sha512-ymzMvjC1Jg0b9K0D26ZdARqFQXs7MocfLC5FOCGfkC0Ss+ACUJkX5364ZM5nT4NLZanHRZNVrZEy+Ibwcvux/g==",
"cpu": [
"x64"
],
@@ -168,9 +167,9 @@
}
},
"node_modules/@biomejs/cli-linux-arm64": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.5.1.tgz",
"integrity": "sha512-yhV35CzZh38VyMvTEXi3JTjxZBs++oCKK9KG8vB6VI5+uvQvZNR3BFWEKKzuOmx9DJJj7sQpZ4LQJcmbGTs3+Q==",
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.5.2.tgz",
"integrity": "sha512-t7sseOmqND57uUWTwlawU6BYj+J06T/9EkydzBhkrgw/FK3QVhjU2wsJR0frljrKZ0/I8A/rYw7284QgqjQfIQ==",
"cpu": [
"arm64"
],
@@ -188,9 +187,9 @@
}
},
"node_modules/@biomejs/cli-linux-arm64-musl": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.5.1.tgz",
"integrity": "sha512-WMcvMLgByyTqVxGlq918NBBYliq9FRR9GAQVETHb+VjGVqXCZFfHlZHC1FX4ibuYY/Hg6TJE3rHU0xVrdJXNRw==",
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.5.2.tgz",
"integrity": "sha512-w+ANG0ZvTu9IeEg9QnstoOnk6L0fpwJifW6aHR18+cb5Z39bkANItYjAfMrnvce5tmMK+IQ6nPX7/kQFdam5iw==",
"cpu": [
"arm64"
],
@@ -208,9 +207,9 @@
}
},
"node_modules/@biomejs/cli-linux-x64": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.5.1.tgz",
"integrity": "sha512-J/7uHSX7NfoYDI7HijAkd8lnQIOrRb2W7j3X+tw4R+N5ExvXGsyXFiGdQcfcxfOmNQmZVSQOCDk757fwpzqQcg==",
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.5.2.tgz",
"integrity": "sha512-M/lOZrewzTCRDINbjhQ1gYYru37KlD3kJBQwwKCG0ckz5E9IZwIoJ3X0wBwRXA+yBDIwWUuPBHS67HzJY4dTfA==",
"cpu": [
"x64"
],
@@ -228,9 +227,9 @@
}
},
"node_modules/@biomejs/cli-linux-x64-musl": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.5.1.tgz",
"integrity": "sha512-ANTowtlLmPYm5yeMckWY8Xzb9Ix+JJP3tgHR/n6xRj1VWyIzzWtfRfih9hv9VmClwadpBvZduISZIbBsIlYG3A==",
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.5.2.tgz",
"integrity": "sha512-VArNLAzND063tF+XY0yPyM+DyahpzOMzOAvb7qs259nhjJWRjvjZdssuA+Rfl+l07+NOesKZ0Xu2yFrXyBMtzw==",
"cpu": [
"x64"
],
@@ -248,9 +247,9 @@
}
},
"node_modules/@biomejs/cli-win32-arm64": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.5.1.tgz",
"integrity": "sha512-zgXnKNgWPC4iPF7Y1lR3STUeCUuZRpD6IiOrC7TZTlh0Lx6FiVUT05myuMQHQ9D+1cc7uyMldi4forE6lp0ivQ==",
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.5.2.tgz",
"integrity": "sha512-kbjFFKyZlzYnAuw7sRy5qDoFG6zrP40UK08oPQsWK0ct3NMnGSt+Bs1iviEEyEIP57N5MrykGXdO/wRiaR4lww==",
"cpu": [
"arm64"
],
@@ -265,9 +264,9 @@
}
},
"node_modules/@biomejs/cli-win32-x64": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.5.1.tgz",
"integrity": "sha512-6uxpR9hvaglANkZemeSiN/FhYgkGasrEGn267eXIWvjrjJ2LhDlk251IhjVJq6MXzkV2/bcXwLwSroLyPtqRZg==",
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.5.2.tgz",
"integrity": "sha512-4InchVpdVmdkkkgjQqKpgvyu+VPnoF/7RPSw5YATgEVpt2j72wcCAeV5TwaE9ZGJUZWZn7v2CwSAj6CrMJEx8A==",
"cpu": [
"x64"
],
@@ -893,6 +892,50 @@
"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": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/@slack/webhook/-/webhook-7.2.0.tgz",
"integrity": "sha512-iGL0S6NI9Bv/kg3FS0J5EaLF1kd7uw5pdrfndqz+h1yhgguiVpRL7ReJg9Cia5knS9hUSCRY5MTjeroBwxdKtw==",
"license": "MIT",
"dependencies": {
"@slack/types": "^2.20.1",
"@types/node": ">=18",
"@types/retry": "0.12.0",
"axios": "^1.16.0",
"p-retry": "^4.6.2",
"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",
"resolved": "https://registry.npmjs.org/@types/flat/-/flat-5.0.5.tgz",
@@ -1000,54 +1043,16 @@
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"node_modules/axios": {
"version": "1.18.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.18.1.tgz",
"integrity": "sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==",
"version": "1.16.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.16.0.tgz",
"integrity": "sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.16.0",
"form-data": "^4.0.5",
"https-proxy-agent": "^5.0.1",
"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/axios/node_modules/agent-base": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
"license": "MIT",
"dependencies": {
"debug": "4"
},
"engines": {
"node": ">= 6.0.0"
}
},
"node_modules/axios/node_modules/https-proxy-agent": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
"license": "MIT",
"dependencies": {
"agent-base": "6",
"debug": "4"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/before-after-hook": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-4.0.0.tgz",
@@ -1653,18 +1658,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",
@@ -1924,19 +1917,6 @@
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
"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==",
"license": "MIT",
"dependencies": {
"@types/retry": "0.12.0",
"retry": "^0.13.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/p-timeout": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz",
+1 -2
View File
@@ -46,8 +46,7 @@
"@actions/github": "^9.1.1",
"@slack/logger": "^4.0.1",
"@slack/web-api": "^7.16.0",
"axios": "^1.18.1",
"axios-retry": "^4.5.0",
"@slack/webhook": "^7.2.0",
"flat": "^6.0.1",
"https-proxy-agent": "^9.0.0",
"js-yaml": "^5.2.0",
+11 -13
View File
@@ -1,6 +1,5 @@
import os from "node:os";
import webapi from "@slack/web-api";
import axios from "axios";
import webhook from "@slack/webhook";
import packageJson from "../package.json" with { type: "json" };
import Content from "./content.js";
import SlackError from "./errors.js";
@@ -60,11 +59,6 @@ export default class Config {
*/
inputs;
/**
* @type {import("axios").AxiosStatic} - The axios client.
*/
axios;
/**
* @type {Content} - The parsed payload data to send.
*/
@@ -87,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.
@@ -98,10 +97,10 @@ 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;
this.webhook = webhook;
this.inputs = {
api: core.getInput("api"),
errors: core.getBooleanInput("errors"),
@@ -137,11 +136,10 @@ export default class Config {
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()}`;
this.webhook.addAppMetadata({
name: packageJson.name,
version: packageJson.version,
});
}
/**
+47 -51
View File
@@ -1,4 +1,4 @@
import axiosRetry, { exponentialDelay, linearDelay } from "axios-retry";
import webhook from "@slack/webhook";
import { HttpsProxyAgent } from "https-proxy-agent";
import Config from "./config.js";
import SlackError from "./errors.js";
@@ -6,6 +6,8 @@ import SlackError from "./errors.js";
/**
* 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/}
*/
export default class Webhook {
/**
@@ -15,37 +17,53 @@ 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);
const url = config.inputs.webhook;
const options = {
agent: this.proxies(config),
retryConfig: this.retries(config.inputs.retries),
};
try {
const response = await config.axios.post(
config.inputs.webhook,
config.content.values,
{
...this.proxies(config),
},
);
config.core.setOutput("ok", response.status === 200);
config.core.setOutput("response", JSON.stringify(response.data));
config.core.debug(JSON.stringify(response.data));
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));
return;
}
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));
config.core.debug(JSON.stringify(response));
return;
}
default:
throw new SlackError(
config.core,
`Unknown webhook type: ${config.inputs.webhookType}`,
);
}
} 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.
* @param {Config} config
* @returns {import("axios").AxiosRequestConfig | undefined}
* @returns {HttpsProxyAgent<string> | 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) {
const { webhook, proxy } = config.inputs;
@@ -64,14 +82,8 @@ export default class Webhook {
}
switch (new URL(proxy).protocol) {
case "https:":
return {
httpsAgent: new HttpsProxyAgent(proxy),
};
case "http:":
return {
httpsAgent: new HttpsProxyAgent(proxy),
proxy: false,
};
return new HttpsProxyAgent(proxy);
default:
throw new SlackError(
config.core,
@@ -88,36 +100,20 @@ export default class Webhook {
/**
* Return configurations for retry options with different delays.
* @param {string} option
* @returns {import("axios-retry").IAxiosRetryConfig}
* @returns {import("@slack/webhook").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 webhook.retryPolicies.fiveRetriesInFiveMinutes;
case "10":
return {
retryCondition: axiosRetry.isRetryableError,
retries: 10,
retryDelay: (count, err) => exponentialDelay(count, err, 2 * 1000), // 34.12 minutes
};
return webhook.retryPolicies.tenRetriesInAboutThirtyMinutes;
case "RAPID":
return {
retryCondition: axiosRetry.isRetryableError,
retries: 12,
retryDelay: linearDelay(1 * 1000), // 12 seconds
};
return webhook.retryPolicies.rapidRetryPolicy;
default:
return {
retryCondition: axiosRetry.isRetryableError,
retries: 5,
retryDelay: linearDelay(60 * 1000), // 5 minutes
};
return webhook.retryPolicies.fiveRetriesInFiveMinutes;
}
}
}
+10 -10
View File
@@ -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,
+26 -13
View File
@@ -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";
@@ -185,18 +186,30 @@ describe("config", () => {
});
it("adds metadata to webhook 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 stub = sinon.stub();
const original = Object.getOwnPropertyDescriptor(
webhook,
"addAppMetadata",
);
Object.defineProperty(webhook, "addAppMetadata", {
value: stub,
configurable: true,
});
try {
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];
assert.equal(name, "@slack/slack-github-action");
assert.ok(version);
} finally {
Object.defineProperty(webhook, "addAppMetadata", original);
}
});
});
@@ -225,7 +238,7 @@ describe("config", () => {
describe("validate", () => {
it('allow the "retries" option with lowercased space', async () => {
mocks.axios.post.returns(Promise.resolve("LGTM"));
mocks.webhook.incoming.resolves({ text: "LGTM" });
mocks.core.getInput.withArgs("retries").returns(" rapid ");
mocks.core.getInput
.withArgs("webhook")
@@ -247,7 +260,7 @@ describe("config", () => {
});
it("errors if an invalid retries option is provided", async () => {
mocks.axios.post.returns(Promise.resolve("LGTM"));
mocks.webhook.incoming.resolves({ text: "LGTM" });
mocks.core.getInput.withArgs("retries").returns("FOREVER");
mocks.core.getInput
.withArgs("webhook")
+7 -18
View File
@@ -1,6 +1,6 @@
import fs from "node:fs";
import webapi from "@slack/web-api";
import axios, { AxiosError } from "axios";
import webhook from "@slack/webhook";
import sinon from "sinon";
/**
@@ -19,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.<string, AxiosError>} 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.
@@ -42,7 +27,6 @@ export class Mock {
*/
constructor() {
this.sandbox = sinon.createSandbox();
this.axios = this.sandbox.stub(axios);
this.calls = this.sandbox.stub(webapi.WebClient.prototype, "apiCall");
this.core = {
debug: this.sandbox.stub(),
@@ -64,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");
}
@@ -73,7 +61,6 @@ export class Mock {
*/
reset() {
this.sandbox.reset();
this.axios.post.resetHistory();
this.calls.resetHistory();
this.core.debug.reset();
this.core.error.reset();
@@ -92,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 = "";
+2 -4
View File
@@ -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.webhook.trigger.resolves({ 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.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);
+112 -103
View File
@@ -1,6 +1,6 @@
import assert from "node:assert";
import { beforeEach, describe, it } from "node:test";
import { AxiosError } from "axios";
import webhook from "@slack/webhook";
import Config from "../src/config.js";
import SlackError from "../src/errors.js";
import send from "../src/send.js";
@@ -19,16 +19,13 @@ 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.webhook.trigger.resolves({ 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.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");
@@ -48,14 +45,13 @@ 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" }));
mocks.webhook.incoming.resolves({ text: "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.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");
@@ -91,71 +87,87 @@ describe("webhook", () => {
}
});
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");
const response = new AxiosError(
"Request failed with status code 400",
"ERR_BAD_REQUEST",
{},
{},
{ status: 400 },
);
mocks.axios.post.resolves(Promise.reject(response));
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 send(mocks.core);
await new Webhook().post(config);
assert.fail();
} catch (err) {
if (err instanceof SlackError) {
assert.ok(
err.message.includes("Request failed with status code 400"),
err.message.includes("Unknown webhook type: unknown-webhook"),
);
} 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, { drinks: "coffee" });
assert.deepEqual(options, {});
});
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");
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();
} 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.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 () => {
mocks.core.getBooleanInput.withArgs("errors").returns(true);
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.webhook.incoming.rejects(
new Error("An HTTP protocol error occurred"),
);
mocks.axios.post.resolves(Promise.reject(response));
try {
await send(mocks.core);
assert.fail();
} catch (err) {
if (err instanceof SlackError) {
assert.ok(
err.message.includes("Request failed with status code 400"),
);
assert.ok(err.message.includes("An HTTP protocol error occurred"));
} 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.webhook.incoming.getCalls().length, 1);
assert.deepEqual(mocks.webhook.incoming.getCall(0).firstArg, {
textt: "oops",
});
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");
@@ -191,8 +203,7 @@ describe("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);
assert.strictEqual(webhook.proxies(config), undefined);
});
it("sets up the proxy agent for the provided https proxy", async () => {
@@ -204,12 +215,11 @@ 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.notStrictEqual(proxying, false);
});
it("sets up the agent without proxy for http proxies", async () => {
it("sets up the proxy agent for the provided http proxy", async () => {
const proxy = "http://example.com";
mocks.core.getInput
.withArgs("webhook")
@@ -218,9 +228,18 @@ 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("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");
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 () => {
@@ -263,7 +282,7 @@ describe("webhook", () => {
assert.ok(
err.message.includes("Failed to configure the HTTPS proxy"),
);
assert.ok(err.cause.message.includes("Unsupported URL protocol"));
assert.ok(err.cause?.message?.includes("Unsupported URL protocol"));
} else {
assert.fail(err);
}
@@ -273,73 +292,63 @@ 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.retryPolicies.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);
if (!result.retryDelay) {
assert.fail("No retry delay found!");
}
const result = new Webhook().retries("5");
assert.equal(
result.retryDelay(5, mocks.errors.axios.network_failed),
300000,
"5th retry after 5 seconds",
result.retries,
webhook.retryPolicies.fiveRetriesInFiveMinutes.retries,
);
assert.equal(
result.factor,
webhook.retryPolicies.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);
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",
const result = new Webhook().retries("10");
assert.equal(
result.retries,
webhook.retryPolicies.tenRetriesInAboutThirtyMinutes.retries,
);
assert.ok(
result.retryDelay(10, mocks.errors.axios.network_failed) < 3600000,
"last attempt is no more than an hour later",
assert.equal(
result.factor,
webhook.retryPolicies.tenRetriesInAboutThirtyMinutes.factor,
);
});
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!");
}
const result = new Webhook().retries(" rapid");
assert.equal(
result.retryDelay(12, mocks.errors.axios.network_failed),
12000,
"12th retry after 12 seconds",
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 webhook = new Webhook();
const result = webhook.retries("RAPID");
assert.equal(result.retries, 12);
if (!result.retryDelay) {
assert.fail("No retry delay found!");
}
const result = new Webhook().retries("RAPID");
assert.equal(
result.retryDelay(12, mocks.errors.axios.network_failed),
12000,
"12th retry after 12 seconds",
result.retries,
webhook.retryPolicies.rapidRetryPolicy.retries,
);
assert.equal(
result.factor,
webhook.retryPolicies.rapidRetryPolicy.factor,
);
});
});