import { Probot , ProbotOctokit } from 'probot' ; import { logger } from 'gcf-utils' ; type OctokitType = InstanceType < typeof ProbotOctokit > ; const RELEASE_LABELS = [ 'autorelease: pending' , 'autorelease: failed' ] ; const RELEASE_TYPE_NO_PUBLISH = [ 'go-yoshi' , 'simple' ] ; const SUCCESSFUL_PUBLISH_LABEL = 'autorelease: published' ; const WARNING_THRESHOLD = 60 * 60 * 3 * 1000 ; const MAX_THRESHOLD = 60 * 60 * 24 * 3 * 1000 ; const END_HOUR_UTC = 3 ; const START_HOUR_UTC = 17 ; const WELL_KNOWN_CONFIGURATION_FILE = 'release-please.yml' ; interface ConfigurationOptions { releaseType ? : string ; disableFailureChecker ? : boolean ; } export const TimeMethods = { Date : ( ) => { return new Date ( ) ; } , } ; export function failureChecker ( app ) { app . on ( 'schedule.repository' as any , async ( context ) => { const utcHour = TimeMethods . Date ( ) . getUTCHours ( ) ; const owner = context . payload . organization . login ; const repo = context . payload . repository . name ; if ( utcHour > END_HOUR_UTC && utcHour < START_HOUR_UTC ) { logger . info ( "skipping run, we're currently outside of working hours" ) ; return ; } const configuration = ( ( await context . config ( WELL_KNOWN_CONFIGURATION_FILE ) ) as ConfigurationOptions | null ) || { } ; const labels = [ ... RELEASE_LABELS ] ; if ( RELEASE_TYPE_NO_PUBLISH . indexOf ( '' + configuration . releaseType ) === - 1 && ! configuration . disableFailureChecker ) { labels . push ( 'autorelease: tagged' ) ; } const now = TimeMethods . Date ( ) . getTime ( ) ; const failed = [ ] ; for ( const label of labels ) { const results = ( await context . octokit . issues . listForRepo ( { owner : context . payload . organization . login , repo : context . payload . repository . name , labels : label , state : 'closed' , sort : 'updated' , direction : 'desc' , per_page : 16 , } ) ) . data ; for ( const issue of results ) { const updatedTime = new Date ( issue . updated_at ) . getTime ( ) ; if ( now - updatedTime > WARNING_THRESHOLD && now - updatedTime < MAX_THRESHOLD ) { const pr = ( await context . octokit . pulls . get ( { owner , repo , pull_number : issue . number , } ) ) . data ; if ( pr . merged_at && pr . labels . some ( l => labels . includes ( l . name ! ) ) && ! pr . labels . some ( l => l . name === SUCCESSFUL_PUBLISH_LABEL ) ) { logger . info ( `found failure for ${ owner }/${ repo } pr = ${ pr . number } labels = ${ labels . join ( ',' ) }` ) ; failed . push ( pr . number ) ; } } } } await manageWarningIssue ( owner , repo , failed , context . octokit ) ; logger . info ( `it's alive! event for ${ repo }` ) ; } ) ; function buildIssueBody ( prNumbers ) { const list = prNumbers . map ( prNumber => `* #${ prNumber }` ) . join ( '\n' ) ; return `The following release PRs may have failed:\n\n${ list }` ; } const ISSUE_TITLE = 'Warning: a recent release failed' ; const LABELS = 'type: process' ; async function manageWarningIssue ( owner , repo , prNumbers , github ) { const { data : issues } = await github . issues . listForRepo ( { owner , repo , labels : LABELS , per_page : 32 , } ) ; const warningIssue = issues . find ( issue => { return issue . title . includes ( ISSUE_TITLE ) ; } ) ; logger . info ( ( await github . rateLimit . get ( ) ) . data ) ; if ( prNumbers . length === 0 ) { if ( warningIssue ) { await github . issues . update ( { owner , repo , issue_number : warningIssue . number , state : 'closed' , } ) ; } return ; } const body = buildIssueBody ( prNumbers ) ; if ( warningIssue ) { if ( warningIssue . body === body ) { logger . info ( `a warning issue was already opened for prs ${ prNumbers }` ) ; return ; } await github . issues . update ( { owner , repo , issue_number : warningIssue . number , body , } ) ; return ; } app . log ( `opening warning issue on ${ repo } for PR #${ prNumbers }` ) ; await github . issues . create ( { owner , repo , title : ISSUE_TITLE , body , labels : LABELS . split ( ',' ) , } ) ; } }