Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
456 changes: 60 additions & 396 deletions package-lock.json

Large diffs are not rendered by default.

11 changes: 5 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,13 @@
"dependencies": {
"@actions/core": "^3.0.1",
"@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/logger": "^5.0.0-rc.1",
"@slack/web-api": "^8.0.0-rc.1",
"@slack/webhook": "^8.0.0-rc.1",
"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",
"undici": "^7.10.0"
},
"devDependencies": {
"@biomejs/biome": "^2.4.16",
Expand Down
15 changes: 7 additions & 8 deletions src/client.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import webapi from "@slack/web-api";
import { HttpsProxyAgent } from "https-proxy-agent";
import { ProxyAgent } from "undici";
import Config from "./config.js";
import SlackError from "./errors.js";

Expand All @@ -23,7 +23,7 @@ export default class Client {
throw new SlackError(config.core, "No token was provided to post with");
}
const client = new config.webapi.WebClient(config.inputs.token, {
agent: this.proxies(config)?.httpsAgent,
fetch: this.proxiedFetch(config),
allowAbsoluteUrls: false,
logger: config.logger,
retryConfig: this.retries(config.inputs.retries),
Expand Down Expand Up @@ -73,20 +73,19 @@ export default class Client {
}

/**
* Return configurations for https proxy options if these are set.
* Return a custom fetch function that routes through a proxy if configured.
* @param {Config} config
* @returns {import("axios").AxiosRequestConfig | undefined}
* @returns {((url: string | URL, init?: RequestInit) => Promise<Response>) | undefined}
* @see {@link https://github.com/slackapi/slack-github-action/pull/205}
*/
proxies(config) {
proxiedFetch(config) {
const proxy = config.inputs.proxy;
try {
if (!proxy) {
return undefined;
}
return {
httpsAgent: new HttpsProxyAgent(proxy),
};
const dispatcher = /** @type {any} */ (new ProxyAgent(proxy));
return (url, init) => fetch(url, { ...init, dispatcher });
} catch (/** @type {any} */ err) {
throw new SlackError(config.core, "Failed to configure the HTTPS proxy", {
cause: err,
Expand Down
16 changes: 7 additions & 9 deletions src/config.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
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";
Expand Down Expand Up @@ -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.
*/
Expand All @@ -82,6 +76,12 @@ export default class Config {
*/
logger;

/**
* User agent string for outgoing requests.
* @type {string}
*/
userAgent = "";

/**
* @type {import("@slack/web-api")} - Slack API client.
*/
Expand All @@ -98,7 +98,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;
Expand Down Expand Up @@ -137,9 +136,8 @@ export default class Config {
name: packageJson.name,
version: packageJson.version,
});
this.axios.defaults.headers.common["User-Agent"] =
this.userAgent =
`${packageJson.name.replace("/", ":")}/${packageJson.version} ` +
`axios/${this.axios.VERSION} ` +
`node/${process.version.replace("v", "")} ` +
`${os.platform()}/${os.release()}`;
}
Expand Down
169 changes: 94 additions & 75 deletions src/webhook.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import axiosRetry, { exponentialDelay, linearDelay } from "axios-retry";
import { HttpsProxyAgent } from "https-proxy-agent";
import { IncomingWebhook } from "@slack/webhook";
import { ProxyAgent } from "undici";
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.
*
* NOTE: @slack/webhook v8 does not export addAppMetadata so there is no public
* way to inject custom app metadata into its User-Agent. The SDK sets its own
* User-Agent header internally. We supplement it by prepending our action's
* identity in the custom fetch wrapper below.
* @see {@link https://github.com/slackapi/node-slack-sdk/blob/webhook-8.0.0-development/packages/webhook/src/instrument.ts}
*/
export default class Webhook {
/**
Expand All @@ -15,109 +21,122 @@ 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 SDK.
* @param {Config} config
*/
async postIncomingWebhook(config) {
const webhook = new IncomingWebhook(/** @type {string} */ (config.inputs.webhook), {
fetch: this.customFetch(config),
});
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));
const response = await webhook.send(config.content.values);
config.core.setOutput("ok", true);
config.core.setOutput("response", response.text);
config.core.debug(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 directly to a webhook trigger URL and parse the JSON response.
* @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 fetchFn = this.customFetch(config);
try {
const response = await fetchFn(/** @type {string} */ (config.inputs.webhook), {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(config.content.values),
});
const /** @type {any} */ data = await response.json();
config.core.setOutput("ok", data.ok ?? response.ok);
config.core.setOutput("response", JSON.stringify(data));
config.core.debug(JSON.stringify(data));
} 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);
}
}

/**
* Return a custom fetch function that injects the User-Agent header and
* routes through a proxy if configured.
* @param {Config} config
* @returns {(url: string | URL | Request, init?: any) => Promise<Response>}
*/
customFetch(config) {
const dispatcher = this.proxyDispatcher(config);
return (url, init) => {
const headers = new Headers(init?.headers);
const existing = headers.get("User-Agent") || "";
headers.set(
"User-Agent",
existing ? `${config.userAgent} ${existing}` : config.userAgent,
);
return fetch(url, {
...init,
headers,
...(dispatcher ? { dispatcher } : {}),
});
};
}

/**
* Return a proxy dispatcher if one is configured, or undefined.
* @param {Config} config
* @returns {any | undefined}
*/
proxyDispatcher(config) {
const { webhook, proxy } = config.inputs;
if (!proxy) {
return undefined;
}
try {
if (new URL(webhook).protocol !== "https:") {
if (webhook && 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) {
const proxyUrl = new URL(proxy);
switch (proxyUrl.protocol) {
case "https:":
return {
httpsAgent: new HttpsProxyAgent(proxy),
};
case "http:":
return {
httpsAgent: new HttpsProxyAgent(proxy),
proxy: false,
};
return /** @type {any} */ (new ProxyAgent(proxy));
default:
throw new SlackError(
config.core,
`Unsupported URL protocol: ${proxy}`,
);
}
} catch (/** @type {any} */ err) {
if (err instanceof SlackError) {
throw err;
}
throw new SlackError(config.core, "Failed to configure the HTTPS proxy", {
cause: err,
});
}
}

/**
* Return configurations for retry options with different delays.
* @param {string} option
* @returns {import("axios-retry").IAxiosRetryConfig}
*/
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
};
case "10":
return {
retryCondition: axiosRetry.isRetryableError,
retries: 10,
retryDelay: (count, err) => exponentialDelay(count, err, 2 * 1000), // 34.12 minutes
};
case "RAPID":
return {
retryCondition: axiosRetry.isRetryableError,
retries: 12,
retryDelay: linearDelay(1 * 1000), // 12 seconds
};
default:
return {
retryCondition: axiosRetry.isRetryableError,
retries: 5,
retryDelay: linearDelay(60 * 1000), // 5 minutes
};
}
}
}
Loading
Loading