diff --git a/scripts/test-slack.js b/scripts/test-slack.js index 7931fed04..c001ea3d8 100644 --- a/scripts/test-slack.js +++ b/scripts/test-slack.js @@ -16,8 +16,50 @@ if (!process.env.ORNIKAR_SLACK_TOKEN) { ); const im = await slackClient.im.open({ user: member.id }); - await slackClient.chat.postMessage({ + const message = await slackClient.chat.postMessage({ channel: im.channel.id, text: '', + blocks: [ + { + type: 'section', + text: { + type: 'mrkdwn', + text: '', + }, + }, + ], + attachments: [ + { + blocks: [ + { + type: 'section', + text: { + type: 'mrkdwn', + text: `### :warning: Artifact update problem + +Renovate failed to update an artifact related to this branch. You probably do not want to merge this PR as-is. + +:recycle: Renovate will retry this branch, including artifacts, only when one of the following happens: + +- any of the package files in this branch needs updating, or +- the branch becomes conflicted, or +- you check the rebase/retry checkbox if found above, or +- you rename this PR's title to start with "rebase!" to trigger it manually + +The artifact failure details are included below: + +##### File name: yarn.lock + +\`\`\` +error An unexpected error occurred: "Unknown token: { line: 3, col: 2, type: 'INVALID', value: undefined } 3:2 in /mnt/renovate/gh/christophehurpeau/eslint-config-pob/yarn.lock". + +\`\`\` + `, + }, + }, + ], + }, + ], }); + console.log(message); })(); diff --git a/src/context/initTeamSlack.ts b/src/context/initTeamSlack.ts index 43c4e4a6b..5d5f8c294 100644 --- a/src/context/initTeamSlack.ts +++ b/src/context/initTeamSlack.ts @@ -1,25 +1,36 @@ import Webhooks from '@octokit/webhooks'; -import { WebClient } from '@slack/web-api'; +import { WebClient, KnownBlock } from '@slack/web-api'; import { Context, Octokit } from 'probot'; +import { ExcludesFalsy } from '../utils/ExcludesFalsy'; import { Config } from '../orgsConfigs'; import { getKeys } from './utils'; +interface SlackMessage { + text: string; + blocks?: KnownBlock[]; + secondaryBlocks?: KnownBlock[]; + ts?: string; +} + +interface SlackMessageResult { + ts: string; +} + export interface TeamSlack { mention: (githubLogin: string) => string; - postMessage: (githubLogin: string, text: string) => Promise; + postMessage: ( + githubLogin: string, + message: SlackMessage, + ) => Promise; prLink: ( pr: Octokit.PullsGetResponse, context: Context, ) => string; } -const ExcludesFalsy = (Boolean as any) as ( - x: T | false | null | undefined, -) => x is T; - export const voidTeamSlack = (): TeamSlack => ({ mention: (): string => '', - postMessage: (): Promise => Promise.resolve(), + postMessage: (): Promise => Promise.resolve(null), prLink: (): string => '', }); @@ -81,17 +92,27 @@ export const initTeamSlack = async ( if (!user) return githubLogin; return `<@${user.member.id}>`; }, - postMessage: async (githubLogin: string, text: string): Promise => { - context.log.debug('send slack', { githubLogin, text }); - if (process.env.DRY_RUN) return; + postMessage: async ( + githubLogin: string, + message: SlackMessage, + ): Promise => { + context.log.debug('send slack', { githubLogin, message }); + if (process.env.DRY_RUN) return null; const user = getUserFromGithubLogin(githubLogin); - if (!user || !user.im) return; - await slackClient.chat.postMessage({ + if (!user || !user.im) return null; + const result = await slackClient.chat.postMessage({ username: process.env.REVIEWFLOW_NAME, channel: user.im.id, - text, + text: message.text, + blocks: message.blocks, + attachments: message.secondaryBlocks + ? [{ blocks: message.secondaryBlocks }] + : undefined, + thread_ts: message.ts, }); + if (!result.ok) return null; + return { ts: result.ts as string }; }, prLink: ( pr: Octokit.PullsGetResponse, diff --git a/src/context/orgContext.ts b/src/context/orgContext.ts index d11aa3c25..c3e9d3947 100644 --- a/src/context/orgContext.ts +++ b/src/context/orgContext.ts @@ -1,5 +1,6 @@ import { Context } from 'probot'; import { Config } from '../orgsConfigs'; +import { ExcludesFalsy } from '../utils/ExcludesFalsy'; import { initTeamSlack, TeamSlack } from './initTeamSlack'; import { getKeys } from './utils'; @@ -21,9 +22,6 @@ export interface OrgContext< }: { includesReviewerGroup?: boolean; includesWaitForGroups?: boolean }, ) => boolean; } -const ExcludesFalsy = (Boolean as any) as ( - x: T | false | null | undefined, -) => x is T; const initTeamContext = async ( context: Context, diff --git a/src/context/repoContext.ts b/src/context/repoContext.ts index 513d0d1a4..6a0282885 100644 --- a/src/context/repoContext.ts +++ b/src/context/repoContext.ts @@ -5,6 +5,7 @@ import { Context } from 'probot'; import { orgsConfigs, Config, defaultConfig } from '../orgsConfigs'; // eslint-disable-next-line import/no-cycle import { autoMergeIfPossible } from '../pr-handlers/actions/autoMergeIfPossible'; +import { ExcludesFalsy } from '../utils/ExcludesFalsy'; import { initRepoLabels, LabelResponse, Labels } from './initRepoLabels'; import { obtainOrgContext, OrgContext } from './orgContext'; @@ -37,10 +38,6 @@ interface RepoContextWithoutTeamContext { pushAutomergeQueue(pr: LockedMergePr): void; } -const ExcludesFalsy = (Boolean as any) as ( - x: T | false | null | undefined, -) => x is T; - export type RepoContext = OrgContext< GroupNames > & diff --git a/src/pr-handlers/actions/editOpenedPR.ts b/src/pr-handlers/actions/editOpenedPR.ts index b09b1c3ff..9732b7f9a 100644 --- a/src/pr-handlers/actions/editOpenedPR.ts +++ b/src/pr-handlers/actions/editOpenedPR.ts @@ -2,6 +2,7 @@ import Webhooks from '@octokit/webhooks'; import { StatusError, StatusInfo } from '../../orgsConfigs/types'; import { PRHandler } from '../utils'; +import { ExcludesFalsy } from '../../utils/ExcludesFalsy'; import { cleanTitle } from './utils/cleanTitle'; import { updateBody } from './utils/updateBody'; import { autoMergeIfPossible } from './autoMergeIfPossible'; @@ -24,10 +25,6 @@ interface StatusWithError { type Status = StatusWithInfo | StatusWithError; -const ExcludesFalsy = (Boolean as any) as ( - x: T | false | null | undefined, -) => x is T; - export const editOpenedPR: PRHandler< Webhooks.WebhookPayloadPullRequest, { skipAutoMerge: boolean }, diff --git a/src/pr-handlers/reviewDismissed.ts b/src/pr-handlers/reviewDismissed.ts index 970103678..d7ea543b5 100644 --- a/src/pr-handlers/reviewDismissed.ts +++ b/src/pr-handlers/reviewDismissed.ts @@ -34,25 +34,23 @@ export default function reviewDismissed(app: Application): void { if (repoContext.slack) { if (sender.login === reviewer.login) { - repoContext.slack.postMessage( - pr.user.login, - `:skull: ${repoContext.slack.mention( + repoContext.slack.postMessage(pr.user.login, { + text: `:skull: ${repoContext.slack.mention( reviewer.login, )} dismissed his review on ${repoContext.slack.prLink( pr, context, )}`, - ); + }); } else { - repoContext.slack.postMessage( - reviewer.login, - `:skull: ${repoContext.slack.mention( + repoContext.slack.postMessage(reviewer.login, { + text: `:skull: ${repoContext.slack.mention( sender.login, )} dismissed your review on ${repoContext.slack.prLink( pr, context, )}`, - ); + }); } } }, diff --git a/src/pr-handlers/reviewRequestRemoved.ts b/src/pr-handlers/reviewRequestRemoved.ts index 397fc298b..5bf390584 100644 --- a/src/pr-handlers/reviewRequestRemoved.ts +++ b/src/pr-handlers/reviewRequestRemoved.ts @@ -62,15 +62,14 @@ export default function reviewRequestRemoved(app: Application): void { if (sender.login === reviewer.login) return; if (repoContext.slack) { - repoContext.slack.postMessage( - reviewer.login, - `:skull_and_crossbones: ${repoContext.slack.mention( + repoContext.slack.postMessage(reviewer.login, { + text: `:skull_and_crossbones: ${repoContext.slack.mention( sender.login, )} removed the request for your review on ${repoContext.slack.prLink( pr, context, )}`, - ); + }); } }, ), diff --git a/src/pr-handlers/reviewRequested.ts b/src/pr-handlers/reviewRequested.ts index 052d069f0..f6e2fecc7 100644 --- a/src/pr-handlers/reviewRequested.ts +++ b/src/pr-handlers/reviewRequested.ts @@ -43,15 +43,14 @@ export default function reviewRequested(app: Application): void { if (sender.login === reviewer.login) return; if (!shouldWait && repoContext.slack) { - repoContext.slack.postMessage( - reviewer.login, - `:eyes: ${repoContext.slack.mention( + repoContext.slack.postMessage(reviewer.login, { + text: `:eyes: ${repoContext.slack.mention( sender.login, )} requests your review on ${repoContext.slack.prLink( pr, context, )} !\n> ${pr.title}`, - ); + }); } }, ), diff --git a/src/pr-handlers/reviewSubmitted.ts b/src/pr-handlers/reviewSubmitted.ts index d58668638..ab76093f6 100644 --- a/src/pr-handlers/reviewSubmitted.ts +++ b/src/pr-handlers/reviewSubmitted.ts @@ -9,7 +9,7 @@ export default function reviewSubmitted(app: Application): void { 'pull_request_review.submitted', createHandlerPullRequestChange( async (pr, context, repoContext): Promise => { - const { user: reviewer, state } = (context.payload as any).review; + const { user: reviewer, state, body } = (context.payload as any).review; if (pr.user.login === reviewer.login) return; const reviewerGroup = repoContext.getReviewerGroup(reviewer.login); @@ -87,7 +87,29 @@ export default function reviewSubmitted(app: Application): void { return `:speech_balloon: ${mention} commented on ${prUrl}`; })(); - repoContext.slack.postMessage(pr.user.login, message); + repoContext.slack.postMessage(pr.user.login, { + text: message, + blocks: [ + { + type: 'section' as const, + text: { + type: 'mrkdwn' as const, + text: message, + }, + }, + ], + secondaryBlocks: !body + ? undefined + : [ + { + type: 'section' as const, + text: { + type: 'mrkdwn' as const, + text: body, + }, + }, + ], + }); }, ), ); diff --git a/src/utils/ExcludesFalsy.ts b/src/utils/ExcludesFalsy.ts new file mode 100644 index 000000000..8149e68b0 --- /dev/null +++ b/src/utils/ExcludesFalsy.ts @@ -0,0 +1,3 @@ +export const ExcludesFalsy = (Boolean as any) as ( + x: T | false | null | undefined, +) => x is T;