/******************************************************************************\
 * Main entrypoint for GitHib Action. Fetches information regarding the       *
 * currently running Workflow and it's Jobs. Sends individual job status and  *
 * workflow status as a formatted notification to the Slack Webhhok URL set   *
 * in the environment variables.                                              *
 *                                                                            *
 * Org: Gamesight <https://gamesight.io>                                      *
 * Author: Anthony Kinson <anthony@gamesight.io>                              *
 * Repository: https://github.com/Gamesight/slack-workflow-status             *
 * License: MIT                                                               *
 * Copyright (c) 2020 Gamesight, Inc                                          *
\******************************************************************************/ import * as core from '@actions/core' ; import { context , getOctokit } from '@actions/github' ; import { IncomingWebhook } from '@slack/webhook' ; import { MessageAttachment } from '@slack/types' ; interface PullRequest { url : string ; id : number ; number : number ; head : { ref : string ; sha : string ; repo : { id : number ; url : string ; name : string ; } ; } ; base : { ref : string ; sha : string ; repo : { id : number ; url : string ; name : string ; } ; } ; } type IncludeJobs = 'true' | 'false' | 'on-failure' ; type SlackMessageAttachementFields = MessageAttachment [ 'fields' ] ; process . on ( 'unhandledRejection' , handleError ) ; main ( ) . catch ( handleError ) ; async function main ( ) { const webhook_url = core . getInput ( 'slack_webhook_url' , { required : true } ) ; const github_token = core . getInput ( 'repo_token' , { required : true } ) ; const include_jobs = core . getInput ( 'include_jobs' , { required : true } ) as IncludeJobs ; const slack_channel = core . getInput ( 'channel' ) ; const slack_name = core . getInput ( 'name' ) ; const slack_icon = core . getInput ( 'icon_url' ) ; const slack_emoji = core . getInput ( 'icon_emoji' ) ; core . setSecret ( github_token ) ; core . setSecret ( webhook_url ) ; const octokit = getOctokit ( github_token ) ; const { data : workflow_run } = await octokit . actions . getWorkflowRun ( { owner : context . repo . owner , repo : context . repo . repo , run_id : context . runId } ) ; const { data : jobs_response } = await octokit . actions . listJobsForWorkflowRun ( { owner : context . repo . owner , repo : context . repo . repo , run_id : context . runId } ) ; const completed_jobs = jobs_response . jobs . filter ( job => job . status === 'completed' ) ; let workflow_color ; let workflow_msg ; let job_fields ; if ( completed_jobs . every ( job => [ 'success' , 'skipped' ] . includes ( job . conclusion ) ) ) { workflow_color = 'good' ; workflow_msg = 'Success:' ; if ( include_jobs === 'on-failure' ) { job_fields = [ ] ; } } else if ( completed_jobs . some ( job => job . conclusion === 'cancelled' ) ) { workflow_color = 'warning' ; workflow_msg = 'Cancelled:' ; if ( include_jobs === 'on-failure' ) { job_fields = [ ] ; } } else { workflow_color = 'danger' ; workflow_msg = 'Failed:' ; } if ( include_jobs === 'false' ) { job_fields = [ ] ; } job_fields ??= completed_jobs . map ( job => { let job_status_icon ; switch ( job . conclusion ) { case 'success' : job_status_icon = '✓' ; break ; case 'cancelled' : case 'skipped' : job_status_icon = '⃠' ; break ; default : job_status_icon = '✗' ; } const job_duration = compute_duration ( { start : new Date ( job . started_at ) , end : new Date ( job . completed_at ) } ) ; return { title : '' , short : true , value : `${ job_status_icon } <${ job . html_url }|${ job . name }> (${ job_duration })` } ; } ) ; const workflow_duration = compute_duration ( { start : new Date ( workflow_run . created_at ) , end : new Date ( workflow_run . updated_at ) } ) ; const repo_url = `<${ workflow_run . repository . html_url }|*${ workflow_run . repository . full_name }*>` ; const branch_url = `<${ workflow_run . repository . html_url }/tree/${ workflow_run . head_branch }|*${ workflow_run . head_branch }*>` ; const workflow_run_url = `<${ workflow_run . html_url }|#${ workflow_run . run_number }>` ; let status_string = `${ workflow_msg } ${ context . actor }'s \`${ context . eventName }\` on \`${ branch_url }\`\n` ; const details_string = `Workflow: ${ context . workflow } ${ workflow_run_url } completed in \`${ workflow_duration }\`` ; const pull_requests = ( workflow_run . pull_requests as PullRequest [ ] ) . map ( pull_request => `<${ workflow_run . repository . html_url }/pull/${ pull_request . number }|#${ pull_request . number }> from \`${ pull_request . head . ref }\` to \`${ pull_request . base . ref }\`` ) . join ( ', ' ) ; if ( pull_requests !== '' ) { status_string = `${ workflow_msg } ${ context . actor }'s \`pull_request\` ${ pull_requests }\n` ; } const slack_attachment = { mrkdwn_in : [ 'text' as const ] , color : workflow_color , text : status_string + details_string , footer : repo_url , footer_icon : 'https://github.githubassets.com/favicon.ico' , fields : job_fields } ; const slack_payload_body = { attachments : [ slack_attachment ] , ... ( slack_name && { username : slack_name } ) , ... ( slack_channel && { channel : slack_channel } ) , ... ( slack_emoji && { icon_emoji : slack_emoji } ) , ... ( slack_icon && { icon_url : slack_icon } ) } ; const slack_webhook = new IncomingWebhook ( webhook_url ) ; try { await slack_webhook . send ( slack_payload_body ) ; } catch ( err ) { if ( err instanceof Error ) { core . setFailed ( err . message ) ; } } } function compute_duration ( { start , end } ) { const duration = end . valueOf ( ) - start . valueOf ( ) ; let delta = duration / 1000 ; const days = Math . floor ( delta / 86400 ) ; delta -= days * 86400 ; const hours = Math . floor ( delta / 3600 ) % 24 ; delta -= hours * 3600 ; const minutes = Math . floor ( delta / 60 ) % 60 ; delta -= minutes * 60 ; const seconds = Math . floor ( delta % 60 ) ; const format_duration = ( value , text , hide_on_zero ) : string => ( value <= 0 && hide_on_zero ? '' : `${ value }${ text } ` ) ; return ( format_duration ( days , 'd' , true ) + format_duration ( hours , 'h' , true ) + format_duration ( minutes , 'm' , true ) + format_duration ( seconds , 's' , false ) . trim ( ) ) ; } function handleError ( err ) { core . error ( err ) ; if ( err && err . message ) { core . setFailed ( err . message ) ; } else { core . setFailed ( `Unhandled Error: ${ err }` ) ; } }