UNPKG

@auto-it/slack

Version:
343 lines 14.1 kB
"use strict"; 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