@auto-it/slack
Version:
Slack plugin for auto
343 lines • 14.1 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.convertToBlocks = exports.sanitizeMarkdown = void 0;
const tslib_1 = require("tslib");
const slack_messages_1 = require("@atomist/slack-messages");
const https_proxy_agent_1 = tslib_1.__importDefault(require("https-proxy-agent"));
const core_1 = require("@auto-it/core");
const node_fetch_1 = tslib_1.__importDefault(require("node-fetch"));
const t = tslib_1.__importStar(require("io-ts"));
/** Transform markdown into slack friendly text */
const sanitizeMarkdown = (markdown) => slack_messages_1.githubToSlack(markdown)
.split("\n")
.map((line) => {
// Give extra padding to nested lists
if (line.match(/^\s+•/)) {
return line.replace(/^\s+•/, " •");
}
return line;
})
.join("\n");
exports.sanitizeMarkdown = sanitizeMarkdown;
/** Create slack context block */
const createContextBlock = (text) => ({
type: "context",
elements: [
{
type: "mrkdwn",
text,
},
],
});
/** Create slack section block */
const createSectionBlock = (text) => ({
type: "section",
text: {
type: "mrkdwn",
text,
},
});
/** Create some space in the message */
const createSpacerBlock = () => ({
type: "section",
text: {
type: "mrkdwn",
text: " ",
},
});
/** Create slack header block */
const createHeaderBlock = (text) => ({
type: "header",
text: {
type: "plain_text",
text,
emoji: true,
},
});
/** Create slack divider block */
const createDividerBlock = () => ({
type: "divider",
});
const CHANGELOG_LINE = /^\s*•/;
/** Split a long string into chunks by character limit */
const splitCharacterLimitAtNewline = (line, charLimit) => {
const splitLines = [];
let buffer = line;
while (buffer) {
// get the \n closest to the char limit
const newlineIndex = buffer.indexOf("\n", charLimit);
const endOfLine = newlineIndex >= 0 ? newlineIndex : charLimit;
splitLines.push(buffer.slice(0, endOfLine));
buffer = buffer.slice(endOfLine);
}
return splitLines;
};
/** Convert the sanitized markdown to slack blocks */
function convertToBlocks(slackMarkdown, withFiles = false) {
let currentMessage = [];
const messages = [currentMessage];
const lineIterator = slackMarkdown.split("\n")[Symbol.iterator]();
for (const line of lineIterator) {
if (line.startsWith("#")) {
currentMessage.push(createSectionBlock(`*${line.replace(/^[#]+/, "")}*`));
}
else if (line === "---") {
currentMessage.push(createSpacerBlock());
currentMessage.push(createDividerBlock());
}
else if (line.startsWith("```")) {
const [, language] = line.match(/```(\S+)/) || ["", "detect"];
const lines = [];
for (const codeBlockLine of lineIterator) {
if (codeBlockLine.startsWith("```")) {
break;
}
lines.push(codeBlockLine);
}
if (withFiles) {
messages.push({
type: "file",
language,
code: lines.join("\n"),
});
currentMessage = [];
messages.push(currentMessage);
}
else {
currentMessage.push(createSectionBlock(`\`${language}\`:\n\n`));
currentMessage.push(createSectionBlock(`\`\`\`\n${lines.join("\n")}\n\`\`\``));
}
}
else if (line.startsWith("*Authors:")) {
currentMessage.push(createDividerBlock());
currentMessage.push(createContextBlock(line));
const authorLines = [];
for (const authorLine of lineIterator) {
if (authorLine) {
authorLines.push(authorLine);
}
}
currentMessage.push(createContextBlock(authorLines.join("\n")));
}
else if (line.match(CHANGELOG_LINE)) {
const lines = [line];
for (const changelogLine of lineIterator) {
if (!changelogLine.match(CHANGELOG_LINE)) {
break;
}
lines.push(changelogLine);
}
const fullSection = lines.join("\n");
if (fullSection.length > 3000) {
const splitLines = splitCharacterLimitAtNewline(fullSection, 3000);
splitLines.forEach((splitLine) => {
currentMessage.push(createSectionBlock(splitLine));
});
}
else {
currentMessage.push(createSectionBlock(fullSection));
}
currentMessage.push(createSpacerBlock());
}
else if (line.length > 3000) {
const splitLines = splitCharacterLimitAtNewline(line, 3000);
splitLines.forEach((splitLine) => {
currentMessage.push(createSectionBlock(splitLine));
});
}
else if (line) {
currentMessage.push(createSectionBlock(line));
}
}
return messages.filter((m) => (Array.isArray(m) && m.length !== 0) || true);
}
exports.convertToBlocks = convertToBlocks;
const basePluginOptions = t.partial({
/** URL of the slack to post to */
url: t.string,
/** Who to bother when posting to the channel */
atTarget: t.union([t.string, t.boolean]),
/** Allow users to opt into having prereleases posted to slack */
publishPreRelease: t.boolean,
/** Additional Title to add at the start of the slack message */
title: t.string,
/** Username to post the message as */
username: t.string,
/** Image url to use as the message's avatar */
iconUrl: t.string,
/** Emoji code to use as the message's avatar */
iconEmoji: t.string,
});
const urlPluginOptions = t.intersection([
t.partial({
/** Channels to post */
channel: t.string,
}),
basePluginOptions,
]);
const appPluginOptions = t.intersection([
t.interface({
/** Marks we are gonna use app auth */
auth: t.literal("app"),
/** Channels to post */
channels: t.array(t.string),
}),
basePluginOptions,
]);
const pluginOptions = t.union([urlPluginOptions, appPluginOptions]);
/** Post your release notes to Slack during `auto release` */
class SlackPlugin {
/** Initialize the plugin with it's options */
constructor(options = {}) {
var _a;
/** The name of the plugin */
this.name = "slack";
if (typeof options === "string") {
this.options = { url: options, atTarget: "channel" };
}
else {
this.options = Object.assign(Object.assign({}, options), { url: process.env.SLACK_WEBHOOK_URL || options.url || "", atTarget: (_a = options.atTarget) !== null && _a !== void 0 ? _a : "channel", publishPreRelease: options.publishPreRelease
? options.publishPreRelease
: false });
}
}
/** Custom initialization for this plugin */
init(initializer) {
initializer.hooks.createEnv.tapPromise(this.name, async (vars) => [
...vars,
{
variable: "SLACK_WEBHOOK_URL",
message: "What is the root url of your slack hook? ()",
},
]);
}
/** Tap into auto plugin points. */
apply(auto) {
auto.hooks.validateConfig.tapPromise(this.name, async (name, options) => {
// If it's a string thats valid config
if (name === this.name && typeof options !== "string") {
return core_1.validatePluginConfiguration(this.name, pluginOptions, options);
}
});
auto.hooks.afterRelease.tapPromise(this.name, async ({ newVersion, commits, releaseNotes, response }) => {
var _a, _b, _c;
// Avoid publishing on prerelease branches by default, but allow folks to opt in if they care to
const currentBranch = core_1.getCurrentBranch();
if (currentBranch &&
((_b = (_a = auto.config) === null || _a === void 0 ? void 0 : _a.prereleaseBranches) === null || _b === void 0 ? void 0 : _b.includes(currentBranch)) &&
!this.options.publishPreRelease) {
return;
}
if (!newVersion) {
return;
}
const head = commits[0];
if (!head) {
return;
}
const skipReleaseLabels = (((_c = auto.config) === null || _c === void 0 ? void 0 : _c.labels.filter((l) => l.releaseType === "skip")) || []).map((l) => l.name);
const isSkipped = head.labels.find((label) => skipReleaseLabels.includes(label));
if (isSkipped) {
return;
}
if (!("auth" in this.options) && !this.options.url) {
throw new Error(`${this.name} url must be set to post a message to ${this.name}.`);
}
const releases = (Array.isArray(response) && response) ||
(response && [response]) ||
[];
const header = `New Release${releases.length > 1 ? "s" : ""}: ${releases.map((r) => r.data.tag_name).join(", ")}`;
const proxyUrl = process.env.https_proxy || process.env.http_proxy;
const agent = proxyUrl ? https_proxy_agent_1.default(proxyUrl) : undefined;
await this.createPost(auto, header, exports.sanitizeMarkdown(releaseNotes), releases, agent);
});
}
/** Post the release notes to slack */
// eslint-disable-next-line max-params
async createPost(auto, header, releaseNotes, releases, agent) {
if (!auto.git) {
return;
}
auto.logger.verbose.info("Posting release notes to slack.");
const token = process.env.SLACK_TOKEN;
if (!token) {
auto.logger.verbose.warn("Slack may need a token to send a message");
}
const messages = convertToBlocks(releaseNotes, "auth" in this.options && this.options.auth === "app");
const urls = releases.map((release) => `*${slack_messages_1.url(release.data.html_url, release.data.name || release.data.tag_name)}*`);
const releaseUrl = urls.length > 1
? urls.join(", ")
: `${slack_messages_1.url(releases[0].data.html_url, "View Release")}`;
const atTargetTxt = this.options.atTarget
? `@${this.options.atTarget} ${releaseUrl}`
: `${releaseUrl}`;
// First add context to share link to release
messages[0].unshift(createContextBlock(atTargetTxt));
// At text only header
messages[0].unshift(createHeaderBlock(header));
// Add user context title
if (this.options.title) {
messages[0].unshift(createSectionBlock(this.options.title));
}
const userPostMessageOptions = {};
if (this.options.username) {
userPostMessageOptions.username = this.options.username;
}
if (this.options.iconUrl) {
userPostMessageOptions.icon_url = this.options.iconUrl;
}
else if (this.options.iconEmoji) {
userPostMessageOptions.icon_emoji = this.options.iconEmoji;
}
if ("auth" in this.options) {
const channels = this.options.channels;
await messages.reduce(async (last, message) => {
await last;
if (Array.isArray(message)) {
await channels.reduce(async (lastMessage, channel, index) => {
await lastMessage;
await node_fetch_1.default("https://slack.com/api/chat.postMessage", {
method: "POST",
body: JSON.stringify(Object.assign(Object.assign({}, userPostMessageOptions), { channel, text: index === 0 ? `${header} :tada:` : undefined, blocks: message, link_names: true })),
headers: {
"Content-Type": "application/json; charset=utf-8",
Authorization: `Bearer ${token}`,
},
agent,
});
}, Promise.resolve());
}
else {
const languageMap = { md: "markdown" };
await node_fetch_1.default("https://slack.com/api/files.upload", {
method: "POST",
body: new URLSearchParams({
channels: channels.join(","),
content: message.code,
title: languageMap[message.language] || message.language,
filetype: languageMap[message.language] || message.language,
}),
headers: {
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
Authorization: `Bearer ${token}`,
},
agent,
});
}
}, Promise.resolve());
}
else {
await node_fetch_1.default(`${this.options.url}${token ? `?token=${token}` : ""}`, {
method: "POST",
body: JSON.stringify(Object.assign(Object.assign({}, userPostMessageOptions), { link_names: true,
// If not in app auth only one message is constructed
blocks: messages[0], channel: this.options.channel })),
headers: { "Content-Type": "application/json" },
agent,
});
}
auto.logger.verbose.info("Posted release notes to slack.");
}
}
exports.default = SlackPlugin;
//# sourceMappingURL=index.js.map